Skip to content

Commit

Permalink
feat(packager): add support to deep links
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog-crabnebula committed May 30, 2024
1 parent 93e027d commit 9b38335
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 12 deletions.
6 changes: 6 additions & 0 deletions .changes/deep-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"cargo-packager": minor
"@crabnebula/packager": minor
---

Added deep link support.
43 changes: 43 additions & 0 deletions bindings/packager/nodejs/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@
"$ref": "#/definitions/FileAssociation"
}
},
"deepLinkProtocols": {
"description": "Deep-link protocols.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/DeepLinkProtocol"
}
},
"resources": {
"description": "The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory or an object of `src` and `target` paths. In the case of using an object, the `src` could be either a glob pattern, path to a file, path to a directory, and the `target` is a path inside the final resources folder in the installed package.\n\n## Format-specific:\n\n- **[PackageFormat::Nsis] / [PackageFormat::Wix]**: The resources are placed next to the executable in the root of the packager. - **[PackageFormat::Deb]**: The resources are placed in `usr/lib` of the package.",
"type": [
Expand Down Expand Up @@ -596,6 +606,39 @@
}
]
},
"DeepLinkProtocol": {
"description": "Deep link protocol",
"type": "object",
"required": [
"schemes"
],
"properties": {
"schemes": {
"description": "URL schemes to associate with this app without `://`. For example `my-app`",
"type": "array",
"items": {
"type": "string"
}
},
"name": {
"description": "The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `<bundle-id>.<schemes[0]>`",
"type": [
"string",
"null"
]
},
"role": {
"description": "The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.",
"default": "editor",
"allOf": [
{
"$ref": "#/definitions/BundleTypeRole"
}
]
}
},
"additionalProperties": false
},
"Resource": {
"description": "A path to a resource (with optional glob pattern) or an object of `src` and `target` paths.",
"anyOf": [
Expand Down
21 changes: 21 additions & 0 deletions bindings/packager/nodejs/src-ts/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ export interface Config {
* The file associations
*/
fileAssociations?: FileAssociation[] | null;
/**
* Deep-link protocols.
*/
deepLinkProtocols?: DeepLinkProtocol[] | null;
/**
* The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory or an object of `src` and `target` paths. In the case of using an object, the `src` could be either a glob pattern, path to a file, path to a directory, and the `target` is a path inside the final resources folder in the installed package.
*
Expand Down Expand Up @@ -327,6 +331,23 @@ export interface FileAssociation {
*/
role?: BundleTypeRole & string;
}
/**
* Deep link protocol
*/
export interface DeepLinkProtocol {
/**
* URL schemes to associate with this app without `://`. For example `my-app`
*/
schemes: string[];
/**
* The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `<bundle-id>.<schemes[0]>`
*/
name?: string | null;
/**
* The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.
*/
role?: BundleTypeRole & string;
}
/**
* The Windows configuration.
*/
Expand Down
43 changes: 43 additions & 0 deletions crates/packager/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@
"$ref": "#/definitions/FileAssociation"
}
},
"deepLinkProtocols": {
"description": "Deep-link protocols.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/DeepLinkProtocol"
}
},
"resources": {
"description": "The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory or an object of `src` and `target` paths. In the case of using an object, the `src` could be either a glob pattern, path to a file, path to a directory, and the `target` is a path inside the final resources folder in the installed package.\n\n## Format-specific:\n\n- **[PackageFormat::Nsis] / [PackageFormat::Wix]**: The resources are placed next to the executable in the root of the packager. - **[PackageFormat::Deb]**: The resources are placed in `usr/lib` of the package.",
"type": [
Expand Down Expand Up @@ -596,6 +606,39 @@
}
]
},
"DeepLinkProtocol": {
"description": "Deep link protocol",
"type": "object",
"required": [
"schemes"
],
"properties": {
"schemes": {
"description": "URL schemes to associate with this app without `://`. For example `my-app`",
"type": "array",
"items": {
"type": "string"
}
},
"name": {
"description": "The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `<bundle-id>.<schemes[0]>`",
"type": [
"string",
"null"
]
},
"role": {
"description": "The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.",
"default": "editor",
"allOf": [
{
"$ref": "#/definitions/BundleTypeRole"
}
]
}
},
"additionalProperties": false
},
"Resource": {
"description": "A path to a resource (with optional glob pattern) or an object of `src` and `target` paths.",
"anyOf": [
Expand Down
48 changes: 47 additions & 1 deletion crates/packager/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub struct FileAssociation {
}

impl FileAssociation {
/// Creates a new [`FileAssociation``] using provided extensions.
/// Creates a new [`FileAssociation`] using provided extensions.
pub fn new<I, S>(extensions: I) -> Self
where
I: IntoIterator<Item = S>,
Expand Down Expand Up @@ -132,6 +132,49 @@ impl FileAssociation {
}
}

/// Deep link protocol
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[non_exhaustive]
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,
}

impl DeepLinkProtocol {
/// Creates a new [`DeepLinkProtocol``] using provided schemes.
pub fn new<I, S>(schemes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
schemes: schemes.into_iter().map(Into::into).collect(),
name: None,
role: BundleTypeRole::default(),
}
}

/// Set he name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext`
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name.replace(name.into());
self
}

/// Set he app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS.
/// Defaults to [`BundleTypeRole::Editor`]
pub fn role(mut self, role: BundleTypeRole) -> Self {
self.role = role;
self
}
}

/// The Linux debian configuration.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
Expand Down Expand Up @@ -1510,6 +1553,9 @@ pub struct Config {
/// The file associations
#[serde(alias = "file-associations", alias = "file_associations")]
pub file_associations: Option<Vec<FileAssociation>>,
/// Deep-link protocols.
#[serde(alias = "deep-link-protocols", alias = "deep_link_protocols")]
pub deep_link_protocols: Option<Vec<DeepLinkProtocol>>,
/// The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory
/// or an object of `src` and `target` paths. In the case of using an object,
/// the `src` could be either a glob pattern, path to a file, path to a directory,
Expand Down
38 changes: 38 additions & 0 deletions crates/packager/src/package/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,44 @@ fn create_info_plist(
);
}

if let Some(protocols) = &config.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(
"CFBundleURLName".into(),
protocol
.name
.clone()
.unwrap_or(format!(
"{} {}",
config.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) = &config.copyright {
Expand Down
30 changes: 20 additions & 10 deletions crates/packager/src/package/deb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,26 @@ fn generate_desktop_file(config: &Config, data_dir: &Path) -> crate::Result<()>
mime_type: Option<String>,
}

let mime_type = if let Some(associations) = &config.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) = &config.file_associations {
mime_type.extend(
associations
.iter()
.filter_map(|association| association.mime_type.clone()),
);
}

if let Some(protocols) = &config.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",
Expand Down
23 changes: 23 additions & 0 deletions crates/packager/src/package/nsis/installer.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,14 @@ Section Install
{{/each}}
{{/each}}

; Register deep links
{{#each deep_link_protocols 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"

Expand Down Expand Up @@ -584,6 +592,21 @@ 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 deep links
{{#each deep_link_protocols 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"

Expand Down
8 changes: 8 additions & 0 deletions crates/packager/src/package/nsis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,14 @@ fn build_nsis_app_installer(ctx: &Context, nsis_path: &Path) -> crate::Result<Ve
data.insert("file_associations", to_json(file_associations));
}

if let Some(protocols) = &config.deep_link_protocols {
let schemes = protocols
.iter()
.flat_map(|p| &p.schemes)
.collect::<Vec<_>>();
data.insert("deep_link_protocols", to_json(schemes));
}

let out_file = "nsis-output.exe";
data.insert("out_file", to_json(out_file));

Expand Down
13 changes: 13 additions & 0 deletions crates/packager/src/package/wix/main.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,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"/>
Expand Down
Loading

0 comments on commit 9b38335

Please sign in to comment.