feat: add file association support, closes #3736 (#4320)

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Co-authored-by: Raphii <iam@raphii.co>
Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Bo 2023-07-17 05:34:43 +08:00 committed by GitHub
parent 84c4159754
commit 3b98141aa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 5566 additions and 163 deletions

View File

@ -0,0 +1,5 @@
---
"tauri-utils": minor:feat
---
Add a configuration object for file associations under `BundleConfig`.

View File

@ -0,0 +1,5 @@
---
"tauri": minor:feat
---
Added support to file associations.

View File

@ -0,0 +1,6 @@
---
"tauri-runtime": minor:feat
"tauri-runtime-wry": minor:feat
---
Added the `Opened` variant to `RunEvent`.

View File

@ -121,7 +121,7 @@ jobs:
steps.covector.outputs.successfulPublish == 'true' &&
contains(steps.covector.outputs.packagesPublished, '@tauri-apps/cli')
run: |
echo '${{ steps.covector.outputs }}' > output.json
echo '${{ toJSON(steps.covector.outputs) }}' > output.json
id=$(jq '.["-tauri-apps-cli-releaseId"]' < output.json)
rm output.json
echo "cliReleaseId=$id" >> "$GITHUB_OUTPUT"

View File

@ -20,6 +20,7 @@ exclude = [
"examples/resources/src-tauri",
"examples/sidecar/src-tauri",
"examples/web/core",
"examples/file-associations/src-tauri",
"examples/workspace",
]

View File

@ -925,6 +925,16 @@
"null"
]
},
"fileAssociations": {
"description": "File associations to application.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/FileAssociation"
}
},
"shortDescription": {
"description": "A short description of your application.",
"type": [
@ -1122,6 +1132,97 @@
}
]
},
"FileAssociation": {
"description": "File association",
"type": "object",
"required": [
"ext"
],
"properties": {
"ext": {
"description": "File extensions to associate with this app. e.g. 'png'",
"type": "array",
"items": {
"$ref": "#/definitions/AssociationExt"
}
},
"name": {
"description": "The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0]",
"type": [
"string",
"null"
]
},
"description": {
"description": "The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.",
"type": [
"string",
"null"
]
},
"role": {
"description": "The apps role with respect to the type. Maps to `CFBundleTypeRole` on macOS.",
"default": "Editor",
"allOf": [
{
"$ref": "#/definitions/BundleTypeRole"
}
]
},
"mimeType": {
"description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"AssociationExt": {
"description": "An extension for a [`FileAssociation`].\n\nA leading `.` is automatically stripped.",
"type": "string"
},
"BundleTypeRole": {
"description": "macOS-only. Corresponds to CFBundleTypeRole",
"oneOf": [
{
"description": "CFBundleTypeRole.Editor. Files can be read and edited.",
"type": "string",
"enum": [
"Editor"
]
},
{
"description": "CFBundleTypeRole.Viewer. Files can be read.",
"type": "string",
"enum": [
"Viewer"
]
},
{
"description": "CFBundleTypeRole.Shell",
"type": "string",
"enum": [
"Shell"
]
},
{
"description": "CFBundleTypeRole.QLGenerator",
"type": "string",
"enum": [
"QLGenerator"
]
},
{
"description": "CFBundleTypeRole.None",
"type": "string",
"enum": [
"None"
]
}
]
},
"AppImageConfig": {
"description": "Configuration for AppImage bundles.\n\nSee more: https://tauri.app/v1/api/config#appimageconfig",
"type": "object",

View File

@ -13,7 +13,7 @@ edition = { workspace = true }
rust-version = { workspace = true }
[dependencies]
wry = { version = "0.28.3", default-features = false, features = [ "file-drop", "protocol" ] }
wry = { version = "0.30", default-features = false, features = [ "file-drop", "protocol" ] }
tauri-runtime = { version = "0.13.0-alpha.6", path = "../tauri-runtime" }
tauri-utils = { version = "2.0.0-alpha.6", path = "../tauri-utils" }
uuid = { version = "1", features = [ "v4" ] }
@ -21,15 +21,15 @@ rand = "0.8"
raw-window-handle = "0.5"
[target."cfg(windows)".dependencies]
webview2-com = "0.22"
webview2-com = "0.25"
[target."cfg(windows)".dependencies.windows]
version = "0.44"
version = "0.48"
features = [ "Win32_Foundation" ]
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
gtk = { version = "0.16", features = [ "v3_24" ] }
webkit2gtk = { version = "0.19.1", features = [ "v2_38" ] }
webkit2gtk = { version = "1.1", features = [ "v2_38" ] }
percent-encoding = "2.1"
[target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies]
@ -48,4 +48,4 @@ macos-private-api = [
"tauri-runtime/macos-private-api"
]
objc-exception = [ "wry/objc-exception" ]
linux-headers = [ "wry/linux-headers", "webkit2gtk/v2_36" ]
linux-headers = [ ]

View File

@ -2952,6 +2952,10 @@ fn handle_event_loop<T: UserEvent>(
);
}
},
#[cfg(target_os = "macos")]
Event::Opened { urls } => {
callback(RunEvent::Opened { urls });
}
_ => (),
}

View File

@ -35,7 +35,7 @@ rand = "0.8"
url = { version = "2" }
[target."cfg(windows)".dependencies.windows]
version = "0.44"
version = "0.48"
features = [ "Win32_Foundation" ]
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
@ -44,6 +44,9 @@ gtk = { version = "0.16", features = [ "v3_24" ] }
[target."cfg(target_os = \"android\")".dependencies]
jni = "0.20"
[target."cfg(target_os = \"macos\")".dependencies]
url = "2"
[features]
devtools = [ ]
system-tray = [ ]

View File

@ -290,6 +290,7 @@ pub trait UserEvent: Debug + Clone + Send + 'static {}
impl<T: Debug + Clone + Send + 'static> UserEvent for T {}
/// Event triggered on the event loop run.
#[derive(Debug)]
#[non_exhaustive]
pub enum RunEvent<T: UserEvent> {
/// Event loop is exiting.
@ -313,6 +314,9 @@ pub enum RunEvent<T: UserEvent> {
///
/// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the “main body” of your event loop.
MainEventsCleared,
/// Emitted when the user wants to open the specified resource with the app.
#[cfg(target_os = "macos")]
Opened { urls: Vec<url::Url> },
/// A custom event defined by the user.
UserEvent(T),
}

View File

@ -43,7 +43,7 @@ dunce = "1"
heck = "0.4"
[target."cfg(windows)".dependencies.windows]
version = "0.44.0"
version = "0.48.0"
features = [
"implement",
"Win32_Foundation",

View File

@ -624,6 +624,78 @@ impl Default for WindowsConfig {
}
}
/// macOS-only. Corresponds to CFBundleTypeRole
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum BundleTypeRole {
/// CFBundleTypeRole.Editor. Files can be read and edited.
#[default]
Editor,
/// CFBundleTypeRole.Viewer. Files can be read.
Viewer,
/// CFBundleTypeRole.Shell
Shell,
/// CFBundleTypeRole.QLGenerator
QLGenerator,
/// CFBundleTypeRole.None
None,
}
impl Display for BundleTypeRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Editor => write!(f, "Editor"),
Self::Viewer => write!(f, "Viewer"),
Self::Shell => write!(f, "Shell"),
Self::QLGenerator => write!(f, "QLGenerator"),
Self::None => write!(f, "None"),
}
}
}
/// An extension for a [`FileAssociation`].
///
/// A leading `.` is automatically stripped.
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct AssociationExt(pub String);
impl fmt::Display for AssociationExt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'d> serde::Deserialize<'d> for AssociationExt {
fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
let ext = String::deserialize(deserializer)?;
if let Some(ext) = ext.strip_prefix('.') {
Ok(AssociationExt(ext.into()))
} else {
Ok(AssociationExt(ext))
}
}
}
/// File association
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FileAssociation {
/// File extensions to associate with this app. e.g. 'png'
pub ext: Vec<AssociationExt>,
/// The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0]
pub name: Option<String>,
/// The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.
pub description: Option<String>,
/// The apps role with respect to the type. Maps to `CFBundleTypeRole` on macOS.
#[serde(default)]
pub role: BundleTypeRole,
/// The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.
#[serde(alias = "mime-type")]
pub mime_type: Option<String>,
}
/// The Updater configuration object.
///
/// See more: https://tauri.app/v1/api/config#updaterconfig
@ -720,6 +792,8 @@ pub struct BundleConfig {
/// Should be one of the following:
/// Business, DeveloperTool, Education, Entertainment, Finance, Game, ActionGame, AdventureGame, ArcadeGame, BoardGame, CardGame, CasinoGame, DiceGame, EducationalGame, FamilyGame, KidsGame, MusicGame, PuzzleGame, RacingGame, RolePlayingGame, SimulationGame, SportsGame, StrategyGame, TriviaGame, WordGame, GraphicsAndDesign, HealthcareAndFitness, Lifestyle, Medical, Music, News, Photography, Productivity, Reference, SocialNetworking, Sports, Travel, Utility, Video, Weather.
pub category: Option<String>,
/// File associations to application.
pub file_associations: Option<Vec<FileAssociation>>,
/// A short description of your application.
#[serde(alias = "short-description")]
pub short_description: Option<String>,
@ -1668,7 +1742,7 @@ fn default_dist_dir() -> AppUrl {
struct PackageVersion(String);
impl<'d> serde::Deserialize<'d> for PackageVersion {
fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<PackageVersion, D::Error> {
fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
struct PackageVersionVisitor;
impl<'d> Visitor<'d> for PackageVersionVisitor {
@ -2291,6 +2365,7 @@ mod build {
let resources = quote!(None);
let copyright = quote!(None);
let category = quote!(None);
let file_associations = quote!(None);
let short_description = quote!(None);
let long_description = quote!(None);
let appimage = quote!(Default::default());
@ -2313,6 +2388,7 @@ mod build {
resources,
copyright,
category,
file_associations,
short_description,
long_description,
appimage,
@ -2616,6 +2692,7 @@ mod test {
resources: None,
copyright: None,
category: None,
file_associations: None,
short_description: None,
long_description: None,
appimage: Default::default(),

View File

@ -70,7 +70,7 @@ ico = { version = "0.2.0", optional = true }
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
gtk = { version = "0.16", features = [ "v3_24" ] }
glib = "0.16"
webkit2gtk = { version = "0.19.1", features = [ "v2_38" ] }
webkit2gtk = { version = "1.1", features = [ "v2_38" ] }
[target."cfg(target_os = \"macos\")".dependencies]
embed_plist = "1.2"
@ -78,10 +78,10 @@ cocoa = "0.24"
objc = "0.2"
[target."cfg(windows)".dependencies]
webview2-com = "0.22"
webview2-com = "0.25"
[target."cfg(windows)".dependencies.windows]
version = "0.44"
version = "0.48"
features = [ "Win32_Foundation" ]
[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]

View File

@ -177,6 +177,12 @@ pub enum RunEvent {
///
/// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the “main body” of your event loop.
MainEventsCleared,
/// Emitted when the user wants to open the specified resource with the app.
#[cfg(target_os = "macos")]
Opened {
/// The URL of the resources that is being open.
urls: Vec<url::Url>,
},
}
impl From<EventLoopMessage> for RunEvent {
@ -1008,6 +1014,7 @@ impl<R: Runtime> Builder<R> {
/// [`State`](crate::State) guard. In particular, if a value of type `T`
/// is managed by Tauri, adding `State<T>` to the list of arguments in a
/// command handler instructs Tauri to retrieve the managed value.
/// Additionally, [`state`](crate::Manager#method.state) can be used to retrieve the value manually.
///
/// # Panics
///
@ -1516,6 +1523,8 @@ fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
RuntimeRunEvent::Resumed => RunEvent::Resumed,
RuntimeRunEvent::MainEventsCleared => RunEvent::MainEventsCleared,
RuntimeRunEvent::UserEvent(t) => t.into(),
#[cfg(target_os = "macos")]
RuntimeRunEvent::Opened { urls } => RunEvent::Opened { urls },
_ => unimplemented!(),
};

View File

@ -680,18 +680,13 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
/// Add `state` to the state managed by the application.
///
/// This method can be called any number of times as long as each call
/// refers to a different `T`.
/// If a state for `T` is already managed, the function returns false and the value is ignored.
/// If the state for the `T` type has previously been set, the state is unchanged and false is returned. Otherwise true is returned.
///
/// Managed state can be retrieved by any command handler via the
/// [`State`](crate::State) guard. In particular, if a value of type `T`
/// is managed by Tauri, adding `State<T>` to the list of arguments in a
/// command handler instructs Tauri to retrieve the managed value.
///
/// # Panics
///
/// Panics if state of type `T` is already being managed.
/// Additionally, [`state`](Self#method.state) can be used to retrieve the value manually.
///
/// # Mutability
///

View File

@ -1054,8 +1054,8 @@ impl<R: Runtime> Window<R> {
/// });
/// }
/// ```
#[cfg(all(feature = "wry"))]
#[cfg_attr(doc_cfg, doc(all(feature = "wry")))]
#[cfg(feature = "wry")]
#[cfg_attr(doc_cfg, doc(feature = "wry"))]
pub fn with_webview<F: FnOnce(PlatformWebview) + Send + 'static>(
&self,
f: F,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
# File Associations Example
This example demonstrates how to make associations between the application and certain file types.
This feature is commonly used for functionality such as previewing or editing files.
## Running the example
```
cargo build --example file-associations
```

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File Associations</title>
</head>
<body>
<h1>File Associations</h1>
<div id="files"></div>
<script>
const d = document.getElementById('files')
d.textContent = window.openedUrls
window.onFileOpen = (files) => {
console.log(files)
d.textContent = files
}
</script>
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"name": "file-associations",
"version": "1.0.0",
"pkg": {
"assets": [
"src/**/*"
]
},
"scripts": {
"tauri": "node ../../tooling/cli/node/tauri.js"
},
"devDependencies": {
}
}

View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
WixTools

View File

@ -0,0 +1,3 @@
// Copyright {20\d{2}(-20\d{2})?} Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
[package]
name = "tauri-file-associations-demo"
version = "0.1.0"
description = "A Tauri application that associate file types"
edition = "2021"
rust-version = "1.57"
[build-dependencies]
tauri-build = { path = "../../../core/tauri-build", features = ["codegen"] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = [ "derive" ] }
tauri = { path = "../../../core/tauri", features = [] }
url = "2"
[features]
default = [ "custom-protocol" ]
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -0,0 +1,21 @@
<!-- Add this file next to your tauri.conf.json file -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<!-- Obviously needs to be replaced with your app's bundle identifier -->
<string>com.tauri.dev-file-associations-demo</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- register the myapp:// and myscheme:// schemes -->
<string>myapp</string>
<string>myscheme</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,7 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,69 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use std::{env, sync::Mutex};
use tauri::Manager;
struct OpenedUrls(Mutex<Option<Vec<url::Url>>>);
fn main() {
tauri::Builder::default()
.manage(OpenedUrls(Default::default()))
.setup(|app| {
#[cfg(any(windows, target_os = "linux"))]
{
// NOTICE: `args` may include URL protocol (`your-app-protocol://`) or arguments (`--`) if app supports them.
let mut urls = Vec::new();
for arg in env::args().skip(1) {
if let Ok(url) = url::Url::parse(&arg) {
urls.push(url);
}
}
if !urls.is_empty() {
app.state::<OpenedUrls>().0.lock().unwrap().replace(urls);
}
}
let opened_urls = if let Some(urls) = &*app.state::<OpenedUrls>().0.lock().unwrap() {
urls
.iter()
.map(|u| u.as_str().replace("\\", "\\\\"))
.collect::<Vec<_>>()
.join(", ")
} else {
"".into()
};
tauri::WindowBuilder::new(app, "main", Default::default())
.initialization_script(&format!("window.openedUrls = `{opened_urls}`"))
.initialization_script(&format!("console.log(`{opened_urls}`)"))
.build()
.unwrap();
Ok(())
})
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app, event| {
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = event {
if let Some(w) = app.get_window("main") {
let urls = urls
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join(",");
let _ = w.eval(&format!("window.onFileOpen(`{urls}`)"));
}
app.state::<OpenedUrls>().0.lock().unwrap().replace(urls);
}
});
}

View File

@ -0,0 +1,58 @@
{
"$schema": "../../../tooling/cli/schema.json",
"build": {
"distDir": [
"../index.html"
],
"devPath": [
"../index.html"
],
"beforeDevCommand": "",
"beforeBuildCommand": ""
},
"tauri": {
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev-file-associations-demo",
"icon": [
"../../.icons/32x32.png",
"../../.icons/128x128.png",
"../../.icons/128x128@2x.png",
"../../.icons/icon.icns",
"../../.icons/icon.ico"
],
"resources": [],
"externalBin": [],
"copyright": "",
"category": "DeveloperTool",
"fileAssociations": [
{
"ext": ["png"],
"mimeType": "image/png"
},
{
"ext": ["jpg", "jpeg"],
"mimeType": "image/jpeg"
},
{
"ext": ["gif"],
"mimeType": "image/gif"
}
],
"shortDescription": "",
"longDescription": "",
"deb": {
"depends": []
},
"macOS": {
"frameworks": [],
"exceptionDomain": ""
}
},
"windows": [],
"security": {
"csp": "default-src 'self'"
}
}
}

View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@ -14,7 +14,7 @@ tauri-build = { path = "../../../../core/tauri-build", features = [] }
[dependencies]
api = { path = "../api" }
tauri = { path = "../../../../core/tauri", features = ["dialog"] }
tauri = { path = "../../../../core/tauri" }
[features]
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -167,8 +167,20 @@ fn generate_desktop_file(settings: &Settings, data_dir: &Path) -> crate::Result<
exec: &'a str,
icon: &'a str,
name: &'a str,
mime_type: Option<String>,
}
let mime_type = if let Some(associations) = settings.file_associations() {
let mime_types: Vec<&str> = associations
.iter()
.filter_map(|association| association.mime_type.as_ref())
.map(|s| s.as_str())
.collect();
Some(mime_types.join(";"))
} else {
None
};
handlebars.render_to_write(
"main.desktop",
&DesktopTemplateParams {
@ -184,6 +196,7 @@ fn generate_desktop_file(settings: &Settings, data_dir: &Path) -> crate::Result<
exec: bin_name,
icon: bin_name,
name: settings.product_name(),
mime_type,
},
file,
)?;

View File

@ -8,3 +8,6 @@ Icon={{icon}}
Name={{name}}
Terminal=false
Type=Application
{{#if mime_type}}
MimeType={{mime_type}}
{{/if}}

View File

@ -31,6 +31,7 @@ use crate::Settings;
use anyhow::Context;
use log::{info, warn};
use tauri_utils::config::BundleTypeRole;
use std::{
fs,
@ -163,6 +164,45 @@ fn create_info_plist(
if let Some(version) = settings.macos().minimum_system_version.clone() {
plist.insert("LSMinimumSystemVersion".into(), version.into());
}
if let Some(associations) = settings.file_associations() {
plist.insert(
"CFBundleDocumentTypes".into(),
plist::Value::Array(
associations
.iter()
.map(|association| {
let mut dict = plist::Dictionary::new();
dict.insert(
"CFBundleTypeExtensions".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|ext| ext.to_string().into())
.collect(),
),
);
dict.insert(
"CFBundleTypeName".into(),
association
.name
.as_ref()
.unwrap_or(&association.ext[0].0)
.to_string()
.into(),
);
dict.insert(
"CFBundleTypeRole".into(),
association.role.to_string().into(),
);
plist::Value::Dictionary(dict)
})
.collect(),
),
);
}
plist.insert("LSRequiresCarbon".into(), true.into());
plist.insert("NSHighResolutionCapable".into(), true.into());
if let Some(copyright) = settings.copyright_string() {

View File

@ -7,7 +7,7 @@ use super::category::AppCategory;
use crate::bundle::{common, platform::target_triple};
pub use tauri_utils::config::WebviewInstallMode;
use tauri_utils::{
config::{BundleType, NSISInstallerMode},
config::{BundleType, FileAssociation, NSISInstallerMode},
resources::{external_binaries, ResourcePaths},
};
@ -362,6 +362,8 @@ pub struct BundleSettings {
pub copyright: Option<String>,
/// the app's category.
pub category: Option<AppCategory>,
/// the file associations
pub file_associations: Option<Vec<FileAssociation>>,
/// the app's short description.
pub short_description: Option<String>,
/// the app's long description.
@ -786,6 +788,11 @@ impl Settings {
self.bundle_settings.category
}
/// Return file associations.
pub fn file_associations(&self) -> &Option<Vec<FileAssociation>> {
&self.bundle_settings.file_associations
}
/// Returns the app's short description.
pub fn short_description(&self) -> &str {
self

View File

@ -646,6 +646,10 @@ pub fn build_wix_app_installer(
}
}
if let Some(file_associations) = &settings.file_associations() {
data.insert("file_associations", to_json(file_associations));
}
if let Some(path) = custom_template_path {
handlebars
.register_template_string("main.wxs", read_to_string(path)?)

View File

@ -301,6 +301,10 @@ fn build_nsis_app_installer(
let binaries = generate_binaries_data(settings)?;
data.insert("binaries", to_json(binaries));
if let Some(file_associations) = &settings.file_associations() {
data.insert("file_associations", to_json(file_associations));
}
let silent_webview2_install = if let WebviewInstallMode::DownloadBootstrapper { silent }
| WebviewInstallMode::EmbedBootstrapper { silent }
| WebviewInstallMode::OfflineInstaller { silent } =
@ -388,6 +392,8 @@ fn build_nsis_app_installer(
}
let mut handlebars = Handlebars::new();
handlebars.register_helper("or", Box::new(handlebars_or));
handlebars.register_helper("association-description", Box::new(association_description));
handlebars.register_escape_fn(|s| {
let mut output = String::new();
for c in s.chars() {
@ -414,6 +420,12 @@ fn build_nsis_app_installer(
.map_err(|e| e.to_string())
.expect("Failed to setup handlebar template");
}
write_ut16_le_with_bom(
&output_path.join("FileAssociation.nsh"),
include_str!("./templates/FileAssociation.nsh"),
)?;
let installer_nsi_path = output_path.join("installer.nsi");
write_ut16_le_with_bom(
&installer_nsi_path,
@ -473,6 +485,42 @@ fn build_nsis_app_installer(
Ok(vec![nsis_installer_path])
}
fn handlebars_or(
h: &handlebars::Helper<'_, '_>,
_: &Handlebars<'_>,
_: &handlebars::Context,
_: &mut handlebars::RenderContext<'_, '_>,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let param1 = h.param(0).unwrap().render();
let param2 = h.param(1).unwrap();
out.write(&if param1.is_empty() {
param2.render()
} else {
param1
})?;
Ok(())
}
fn association_description(
h: &handlebars::Helper<'_, '_>,
_: &Handlebars<'_>,
_: &handlebars::Context,
_: &mut handlebars::RenderContext<'_, '_>,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let description = h.param(0).unwrap().render();
let ext = h.param(1).unwrap();
out.write(&if description.is_empty() {
format!("{} File", ext.render().to_uppercase())
} else {
description
})?;
Ok(())
}
/// BTreeMap<OriginalPath, (ParentOfTargetPath, TargetPath)>
type ResourcesMap = BTreeMap<PathBuf, (String, PathBuf)>;
fn generate_resource_data(settings: &Settings) -> crate::Result<ResourcesMap> {

View File

@ -0,0 +1,135 @@
; from https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
; fileassoc.nsh
; File association helper macros
; Written by Saivert
;
; Improved by Nikku<https://github.com/nikku>.
;
; Features automatic backup system and UPDATEFILEASSOC macro for
; shell change notification.
;
; |> How to use <|
; To associate a file with an application so you can double-click it in explorer, use
; the APP_ASSOCIATE macro like this:
;
; Example:
; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \
; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\""
;
; Never insert the APP_ASSOCIATE macro multiple times, it is only ment
; to associate an application with a single file and using the
; the "open" verb as default. To add more verbs (actions) to a file
; use the APP_ASSOCIATE_ADDVERB macro.
;
; Example:
; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \
; "$INSTDIR\myapp.exe /edit $\"%1$\""
;
; To have access to more options when registering the file association use the
; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the
; standard action (default verb).
;
; Note, that this script takes into account user versus global installs.
; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext.
;
; And finally: To remove the association from the registry use the APP_UNASSOCIATE
; macro. Here is another example just to wrap it up:
; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile"
;
; |> Note <|
; When defining your file class string always use the short form of your application title
; then a period (dot) and the type of file. This keeps the file class sort of unique.
; Examples:
; Winamp.Playlist
; NSIS.Script
; Photoshop.JPEGFile
;
; |> Tech info <|
; The registry key layout for a global file association is:
;
; HKEY_LOCAL_MACHINE\Software\Classes
; <".ext"> = <applicationID>
; <applicationID> = <"description">
; shell
; <verb> = <"menu-item text">
; command = <"command string">
;
;
; The registry key layout for a per-user file association is:
;
; HKEY_CURRENT_USER\Software\Classes
; <".ext"> = <applicationID>
; <applicationID> = <"description">
; shell
; <verb> = <"menu-item text">
; command = <"command string">
;
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
StrCmp "${SHELLNEW}" "0" +2
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
!macroend
!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
!macroend
!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT
ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" ""
!macroend
; !defines for use with SHChangeNotify
!ifdef SHCNE_ASSOCCHANGED
!undef SHCNE_ASSOCCHANGED
!endif
!define SHCNE_ASSOCCHANGED 0x08000000
!ifdef SHCNF_FLUSH
!undef SHCNF_FLUSH
!endif
!define SHCNF_FLUSH 0x1000
!macro UPDATEFILEASSOC
; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we
; can update the shell.
System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)"
!macroend

View File

@ -2,6 +2,7 @@
!include FileFunc.nsh
!include x64.nsh
!include WordFunc.nsh
!include "FileAssociation.nsh"
!define MANUFACTURER "{{manufacturer}}"
!define PRODUCTNAME "{{product_name}}"
@ -527,6 +528,13 @@ Section Install
IntOp $AppSize $AppSize + $0
{{/each}}
; Create file associations
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
!insertmacro APP_ASSOCIATE "{{ext}}" "{{or association.name ext}}" "{{association-description association.description ext}}" "$INSTDIR\${MAINBINARYNAME}.exe,0" "Open with ${PRODUCTNAME}" "$INSTDIR\${MAINBINARYNAME}.exe $\"%1$\""
{{/each}}
{{/each}}
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
@ -623,6 +631,13 @@ Section Uninstall
Delete "$INSTDIR\\{{this}}"
{{/each}}
; Delete app associations
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
!insertmacro APP_UNASSOCIATE "{{ext}}" "{{or association.name ext}}"
{{/each}}
{{/each}}
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"

View File

@ -114,6 +114,15 @@
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
<Extension Id="{{ext}}" Advertise="yes">
<Verb Id="open" Command="Open with {{../../product_name}}" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
{{/each~}}
{{/each~}}
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">

130
tooling/cli/Cargo.lock generated
View File

@ -613,6 +613,15 @@ dependencies = [
"libc",
]
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]]
name = "cpufeatures"
version = "0.2.7"
@ -798,6 +807,12 @@ dependencies = [
"syn 2.0.17",
]
[[package]]
name = "dary_heap"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca"
[[package]]
name = "derive_more"
version = "0.99.17"
@ -1437,6 +1452,15 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -1502,7 +1526,21 @@ checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
dependencies = [
"log",
"mac",
"markup5ever",
"markup5ever 0.10.1",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "html5ever"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
"proc-macro2",
"quote",
"syn 1.0.109",
@ -1679,7 +1717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
"serde",
]
@ -2002,7 +2040,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
dependencies = [
"cssparser",
"html5ever",
"html5ever 0.25.2",
"matches",
"selectors",
]
[[package]]
name = "kuchikiki"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
dependencies = [
"cssparser",
"html5ever 0.26.0",
"indexmap",
"matches",
"selectors",
]
@ -2040,21 +2091,25 @@ checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "libflate"
version = "1.4.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18"
checksum = "9f7d5654ae1795afc7ff76f4365c2c8791b0feb18e8996a96adad8ffd7c3b2bf"
dependencies = [
"adler32",
"core2",
"crc32fast",
"dary_heap",
"libflate_lz77",
]
[[package]]
name = "libflate_lz77"
version = "1.2.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf"
checksum = "be5f52fb8c451576ec6b79d3f4deb327398bc05bbdbd99021a6e77a4c855d524"
dependencies = [
"core2",
"hashbrown 0.13.2",
"rle-decode-fast",
]
@ -2137,7 +2192,21 @@ checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
dependencies = [
"log",
"phf 0.8.0",
"phf_codegen",
"phf_codegen 0.8.0",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
"phf 0.10.1",
"phf_codegen 0.10.0",
"string_cache",
"string_cache_codegen",
"tendril",
@ -2729,6 +2798,16 @@ dependencies = [
"phf_shared 0.8.0",
]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
]
[[package]]
name = "phf_generator"
version = "0.8.0"
@ -3335,7 +3414,7 @@ dependencies = [
"log",
"matches",
"phf 0.8.0",
"phf_codegen",
"phf_codegen 0.8.0",
"precomputed-hash",
"servo_arc",
"smallvec",
@ -3896,7 +3975,7 @@ dependencies = [
"env_logger",
"handlebars 4.3.7",
"heck",
"html5ever",
"html5ever 0.26.0",
"ignore",
"image",
"include_dir",
@ -3907,7 +3986,7 @@ dependencies = [
"jsonrpsee-core",
"jsonrpsee-ws-client",
"jsonschema",
"kuchiki",
"kuchikiki",
"libc",
"local-ip-address",
"log",
@ -4012,7 +4091,7 @@ dependencies = [
"ctor 0.1.26",
"getrandom 0.2.9",
"heck",
"html5ever",
"html5ever 0.25.2",
"infer",
"json-patch",
"json5",
@ -4041,11 +4120,11 @@ dependencies = [
"getrandom 0.2.9",
"glob",
"heck",
"html5ever",
"html5ever 0.26.0",
"infer",
"json-patch",
"json5",
"kuchiki",
"kuchikiki",
"memchr",
"phf 0.10.1",
"schemars",
@ -4058,7 +4137,7 @@ dependencies = [
"toml",
"url",
"walkdir",
"windows 0.44.0",
"windows 0.48.0",
]
[[package]]
@ -4773,23 +4852,14 @@ dependencies = [
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
dependencies = [
"windows-implement 0.44.0",
"windows-interface",
"windows-targets 0.42.2",
]
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-implement 0.48.0",
"windows-interface",
"windows-targets 0.48.0",
]
@ -4805,9 +4875,9 @@ dependencies = [
[[package]]
name = "windows-implement"
version = "0.44.0"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6"
checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c"
dependencies = [
"proc-macro2",
"quote",
@ -4816,9 +4886,9 @@ dependencies = [
[[package]]
name = "windows-interface"
version = "0.44.0"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f"
checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7"
dependencies = [
"proc-macro2",
"quote",

View File

@ -925,6 +925,16 @@
"null"
]
},
"fileAssociations": {
"description": "File associations to application.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/FileAssociation"
}
},
"shortDescription": {
"description": "A short description of your application.",
"type": [
@ -1122,6 +1132,97 @@
}
]
},
"FileAssociation": {
"description": "File association",
"type": "object",
"required": [
"ext"
],
"properties": {
"ext": {
"description": "File extensions to associate with this app. e.g. 'png'",
"type": "array",
"items": {
"$ref": "#/definitions/AssociationExt"
}
},
"name": {
"description": "The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0]",
"type": [
"string",
"null"
]
},
"description": {
"description": "The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.",
"type": [
"string",
"null"
]
},
"role": {
"description": "The apps role with respect to the type. Maps to `CFBundleTypeRole` on macOS.",
"default": "Editor",
"allOf": [
{
"$ref": "#/definitions/BundleTypeRole"
}
]
},
"mimeType": {
"description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"AssociationExt": {
"description": "An extension for a [`FileAssociation`].\n\nA leading `.` is automatically stripped.",
"type": "string"
},
"BundleTypeRole": {
"description": "macOS-only. Corresponds to CFBundleTypeRole",
"oneOf": [
{
"description": "CFBundleTypeRole.Editor. Files can be read and edited.",
"type": "string",
"enum": [
"Editor"
]
},
{
"description": "CFBundleTypeRole.Viewer. Files can be read.",
"type": "string",
"enum": [
"Viewer"
]
},
{
"description": "CFBundleTypeRole.Shell",
"type": "string",
"enum": [
"Shell"
]
},
{
"description": "CFBundleTypeRole.QLGenerator",
"type": "string",
"enum": [
"QLGenerator"
]
},
{
"description": "CFBundleTypeRole.None",
"type": "string",
"enum": [
"None"
]
}
]
},
"AppImageConfig": {
"description": "Configuration for AppImage bundles.\n\nSee more: https://tauri.app/v1/api/config#appimageconfig",
"type": "object",

View File

@ -79,8 +79,6 @@ pub fn command(options: Options) -> Result<()> {
let r = command_internal(options);
if r.is_err() {
kill_before_dev_process();
#[cfg(not(debug_assertions))]
let _ = check_for_updates();
}
r
}
@ -262,8 +260,6 @@ pub fn setup(options: &mut Options, mobile: bool) -> Result<AppInterface> {
let _ = ctrlc::set_handler(move || {
kill_before_dev_process();
#[cfg(not(debug_assertions))]
let _ = check_for_updates();
exit(130);
});
}

View File

@ -1130,6 +1130,7 @@ fn tauri_config_to_bundle_settings(
})?),
None => None,
},
file_associations: config.file_associations,
short_description: config.short_description,
long_description: config.long_description,
external_bin: config.external_bin,