feat: Add support for deep links (#8680)

* initial windows impl

* macos

* adapt windows impl to config changes for macos

* debian

* add missing x-scheme-handler prefix

* bundle xdg-mime

* typo

* revert messed up fmt

* rm pnpm lock

* rm todo

* Update core/tauri-utils/src/config.rs

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>

* Update core/tauri-utils/src/config.rs

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>

* &Option<> -> Option<&>

* DL0 -> R7

---------

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
This commit is contained in:
Fabian-Lars 2024-01-29 17:59:45 +01:00 committed by GitHub
parent 57e3d43d96
commit 38b8e67237
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 177 additions and 24 deletions

View File

@ -1293,7 +1293,7 @@
]
},
"role": {
"description": "The apps role with respect to the type. Maps to `CFBundleTypeRole` on macOS.",
"description": "The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS.",
"default": "Editor",
"allOf": [
{

View File

@ -833,7 +833,7 @@ pub struct FileAssociation {
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.
/// The app's 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.
@ -841,6 +841,20 @@ pub struct FileAssociation {
pub mime_type: Option<String>,
}
/// File association
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeepLinkProtocol {
/// URL schemes to associate with this app without `://`. For example `my-app`
pub schemes: Vec<String>,
/// The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `<bundle-id>.<schemes[0]>`
pub name: Option<String>,
/// The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.
#[serde(default)]
pub role: BundleTypeRole,
}
/// The Updater configuration object.
///
/// See more: <https://tauri.app/v1/api/config#updaterconfig>

View File

@ -125,16 +125,26 @@ pub fn generate_desktop_file(
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
};
let mut mime_type: Vec<String> = Vec::new();
if let Some(associations) = settings.file_associations() {
mime_type.extend(
associations
.iter()
.filter_map(|association| association.mime_type.clone()),
);
}
if let Some(protocols) = settings.deep_link_protocols() {
mime_type.extend(
protocols
.iter()
.flat_map(|protocol| &protocol.schemes)
.map(|s| format!("x-scheme-handler/{s}")),
);
}
let mime_type = (!mime_type.is_empty()).then_some(mime_type.join(";"));
handlebars.render_to_write(
"main.desktop",

View File

@ -28,6 +28,11 @@ if [[ "$APPIMAGE_BUNDLE_XDG_OPEN" != "0" ]] && [[ -f "/usr/bin/xdg-open" ]]; the
cp /usr/bin/xdg-open usr/bin
fi
if [[ "$APPIMAGE_BUNDLE_XDG_MIME" != "0" ]] && [[ -f "/usr/bin/xdg-mime" ]]; then
echo "Copying /usr/bin/xdg-mime"
cp /usr/bin/xdg-mime usr/bin
fi
if [[ "$TAURI_TRAY_LIBRARY_PATH" != "0" ]]; then
echo "Copying appindicator library ${TAURI_TRAY_LIBRARY_PATH}"
cp ${TAURI_TRAY_LIBRARY_PATH} usr/lib

View File

@ -277,6 +277,44 @@ fn create_info_plist(
);
}
if let Some(protocols) = settings.deep_link_protocols() {
plist.insert(
"CFBundleURLTypes".into(),
plist::Value::Array(
protocols
.iter()
.map(|protocol| {
let mut dict = plist::Dictionary::new();
dict.insert(
"CFBundleURLSchemes".into(),
plist::Value::Array(
protocol
.schemes
.iter()
.map(|s| s.to_string().into())
.collect(),
),
);
dict.insert(
"CFBundleTypeName".into(),
protocol
.name
.clone()
.unwrap_or(format!(
"{} {}",
settings.bundle_identifier(),
protocol.schemes[0]
))
.into(),
);
dict.insert("CFBundleTypeRole".into(), protocol.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, FileAssociation, NSISInstallerMode, NsisCompression},
config::{BundleType, DeepLinkProtocol, FileAssociation, NSISInstallerMode, NsisCompression},
resources::{external_binaries, ResourcePaths},
};
@ -478,6 +478,8 @@ pub struct BundleSettings {
/// e.g. `sqlite3-universal-apple-darwin`. See
/// <https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary>
pub external_bin: Option<Vec<String>>,
/// Deep-link protocols.
pub deep_link_protocols: Option<Vec<DeepLinkProtocol>>,
/// Debian-specific settings.
pub deb: DebianSettings,
/// Rpm-specific settings.
@ -900,8 +902,14 @@ impl Settings {
}
/// Return file associations.
pub fn file_associations(&self) -> &Option<Vec<FileAssociation>> {
&self.bundle_settings.file_associations
pub fn file_associations(&self) -> Option<&Vec<FileAssociation>> {
self.bundle_settings.file_associations.as_ref()
}
/// Return the list of deep link protocols to be registered for
/// this bundle.
pub fn deep_link_protocols(&self) -> Option<&Vec<DeepLinkProtocol>> {
self.bundle_settings.deep_link_protocols.as_ref()
}
/// Returns the app's short description.

View File

@ -520,7 +520,7 @@ pub fn build_wix_app_installer(
.unwrap_or_default();
data.insert("product_name", to_json(settings.product_name()));
data.insert("version", to_json(&app_version));
data.insert("version", to_json(app_version));
let bundle_id = settings.bundle_identifier();
let manufacturer = settings
.publisher()
@ -570,7 +570,7 @@ pub fn build_wix_app_installer(
let merge_modules = get_merge_modules(settings)?;
data.insert("merge_modules", to_json(merge_modules));
data.insert("app_exe_source", to_json(&app_exe_source));
data.insert("app_exe_source", to_json(app_exe_source));
// copy icon from `settings.windows().icon_path` folder to resource folder near msi
let icon_path = copy_icon(settings, "icon.ico", &settings.windows().icon_path)?;
@ -618,10 +618,18 @@ pub fn build_wix_app_installer(
}
}
if let Some(file_associations) = &settings.file_associations() {
if let Some(file_associations) = settings.file_associations() {
data.insert("file_associations", to_json(file_associations));
}
if let Some(protocols) = settings.deep_link_protocols() {
let schemes = protocols
.iter()
.flat_map(|p| &p.schemes)
.collect::<Vec<_>>();
data.insert("deep_link_protocols", to_json(schemes));
}
if let Some(path) = custom_template_path {
handlebars
.register_template_string("main.wxs", read_to_string(path)?)

View File

@ -323,10 +323,18 @@ fn build_nsis_app_installer(
let estimated_size = generate_estimated_size(&main_binary_path, &binaries, &resources)?;
data.insert("estimated_size", to_json(estimated_size));
if let Some(file_associations) = &settings.file_associations() {
if let Some(file_associations) = settings.file_associations() {
data.insert("file_associations", to_json(file_associations));
}
if let Some(protocols) = settings.deep_link_protocols() {
let schemes = protocols
.iter()
.flat_map(|p| &p.schemes)
.collect::<Vec<_>>();
data.insert("deep_link_protocols", to_json(schemes));
}
let silent_webview2_install = if let WebviewInstallMode::DownloadBootstrapper { silent }
| WebviewInstallMode::EmbedBootstrapper { silent }
| WebviewInstallMode::OfflineInstaller { silent } =

View File

@ -94,7 +94,7 @@ pub fn verify(path: &Path) -> crate::Result<bool> {
// Construct SignTool command
let signtool = locate_signtool()?;
let mut cmd = Command::new(&signtool);
let mut cmd = Command::new(signtool);
cmd.arg("verify");
cmd.arg("/pa");
cmd.arg(path);

View File

@ -560,6 +560,14 @@ Section Install
{{/each}}
{{/each}}
; Register deep links
{{#each deep_link_protocol as |protocol| ~}}
WriteRegStr SHCTX "Software\Classes\{{protocol}}" "URL Protocol" ""
WriteRegStr SHCTX "Software\Classes\{{protocol}}" "" "URL:${BUNDLEID} protocol"
WriteRegStr SHCTX "Software\Classes\{{protocol}}\DefaultIcon" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0"
WriteRegStr SHCTX "Software\Classes\{{protocol}}\shell\open\command" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\""
{{/each}}
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
@ -652,6 +660,15 @@ Section Uninstall
{{/each}}
{{/each}}
; Delete deep links
{{#each deep_link_protocol as |protocol| ~}}
ReadRegStr $R7 SHCTX "Software\Classes\{{protocol}}\shell\open\command" ""
!if $R7 == "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\""
DeleteRegKey SHCTX "Software\Classes\{{protocol}}"
!endif
{{/each}}
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"

View File

@ -111,6 +111,19 @@
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
<!-- Change the Root to HKCU for perUser installations -->
{{#each deep_link_protocols as |protocol| ~}}
<RegistryKey Root="HKLM" Key="Software\\Classes\\{{protocol}}">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:{{bundle_id}} protocol"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="&quot;[!Path]&quot;,0" />
</RegistryKey>
<RegistryKey Key="shell\\open\\command">
<RegistryValue Type="string" Value="&quot;[!Path]&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>

View File

@ -1293,7 +1293,7 @@
]
},
"role": {
"description": "The apps role with respect to the type. Maps to `CFBundleTypeRole` on macOS.",
"description": "The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS.",
"default": "Editor",
"allOf": [
{

View File

@ -156,6 +156,16 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
if config_.tauri.bundle.appimage.bundle_media_framework {
std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1");
}
if let Some(open) = config_.plugins.0.get("shell").and_then(|v| v.get("open")) {
if open.as_bool().is_some_and(|x| x) || open.is_string() {
std::env::set_var("APPIMAGE_BUNDLE_XDG_OPEN", "1");
}
}
if settings.deep_link_protocols().is_some() {
std::env::set_var("APPIMAGE_BUNDLE_XDG_MIME", "1");
}
}
let bundles = bundle_project(settings)

View File

@ -25,7 +25,7 @@ use tauri_bundler::{
AppCategory, BundleBinary, BundleSettings, DebianSettings, DmgSettings, MacOsSettings,
PackageSettings, Position, RpmSettings, Size, UpdaterSettings, WindowsSettings,
};
use tauri_utils::config::parse::is_configuration_file;
use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol};
use super::{AppSettings, DevProcess, ExitReason, Interface};
use crate::helpers::{
@ -681,6 +681,13 @@ pub struct RustAppSettings {
target: Target,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum DesktopDeepLinks {
One(DeepLinkProtocol),
List(Vec<DeepLinkProtocol>),
}
impl AppSettings for RustAppSettings {
fn get_package_settings(&self) -> PackageSettings {
self.package_settings.clone()
@ -694,12 +701,27 @@ impl AppSettings for RustAppSettings {
let arch64bits =
self.target_triple.starts_with("x86_64") || self.target_triple.starts_with("aarch64");
tauri_config_to_bundle_settings(
let mut settings = tauri_config_to_bundle_settings(
&self.manifest,
features,
config.tauri.bundle.clone(),
arch64bits,
)
)?;
if let Some(plugin_config) = config
.plugins
.0
.get("deep-link")
.and_then(|c| c.get("desktop").cloned())
{
let protocols: DesktopDeepLinks = serde_json::from_value(plugin_config.clone())?;
settings.deep_link_protocols = Some(match protocols {
DesktopDeepLinks::One(p) => vec![p],
DesktopDeepLinks::List(p) => p,
});
}
Ok(settings)
}
fn app_binary_path(&self, options: &Options) -> crate::Result<PathBuf> {