feat!(core): add bundle createUpdaterArtifacts configuration (#9883)

* Add updater field

* Don't sign updaters when updater field is false

* Clippy

* Add updater to bundle migration

* Format

* Add updater config to api example

* No warning if update is not enabled

* Build

* Add change file

* We don't generate updater for dmg package

* Warning only for v1 compatible

* clean up

* More clean up

* little bit more

* Apply suggestions from code review

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

* Revert license header change

* Remove option around pubkey and msi args

* More migration tests

* Refactor private_key getter

* Only generate signature for updater for v1 compat

* Format

* Use map_err instead of anyhow context

* Don't generate updater for api example

* Fix misaligned comment

* Rename `updater` to `createUpdaterArtifacts`

* Revert changes in helloworld example

* Add warning for v1 compatible

* Update .changes/separate-updater-field.md

Co-authored-by: Lucas Nogueira <118899497+lucasfernog-crabnebula@users.noreply.github.com>

* update error messages [skip ci]

---------

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
Co-authored-by: Lucas Nogueira <118899497+lucasfernog-crabnebula@users.noreply.github.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
Tony 2024-07-01 19:34:58 +08:00 committed by GitHub
parent e7fd7c60d6
commit 911242f092
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 346 additions and 153 deletions

View File

@ -0,0 +1,8 @@
---
"tauri-utils": patch:breaking
"tauri-bundler": patch:breaking
"tauri-cli": patch:breaking
"@tauri-apps/cli": patch:breaking
---
Move updater target from `bundle > targets` to a separate field `bundle > createUpdaterArtifacts`

View File

@ -72,6 +72,7 @@
"android": { "android": {
"minSdkVersion": 24 "minSdkVersion": 24
}, },
"createUpdaterArtifacts": false,
"iOS": {}, "iOS": {},
"icon": [], "icon": [],
"linux": { "linux": {
@ -1519,7 +1520,7 @@
"type": "boolean" "type": "boolean"
}, },
"targets": { "targets": {
"description": "The bundle targets, currently supports [\"deb\", \"rpm\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".", "description": "The bundle targets, currently supports [\"deb\", \"rpm\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\"] or \"all\".",
"default": "all", "default": "all",
"allOf": [ "allOf": [
{ {
@ -1527,6 +1528,15 @@
} }
] ]
}, },
"createUpdaterArtifacts": {
"description": "Produce updaters and their signatures or not",
"default": false,
"allOf": [
{
"$ref": "#/definitions/Updater"
}
]
},
"publisher": { "publisher": {
"description": "The application's publisher. Defaults to the second element in the identifier string.\n Currently maps to the Manufacturer property of the Windows Installer.", "description": "The application's publisher. Defaults to the second element in the identifier string.\n Currently maps to the Manufacturer property of the Windows Installer.",
"type": [ "type": [
@ -1794,12 +1804,34 @@
"enum": [ "enum": [
"dmg" "dmg"
] ]
}
]
},
"Updater": {
"description": "Updater type",
"anyOf": [
{
"description": "Generates lagacy zipped v1 compatible updaters",
"allOf": [
{
"$ref": "#/definitions/V1Compatible"
}
]
}, },
{ {
"description": "The Tauri updater bundle.", "description": "Produce updaters and their signatures or not",
"type": "boolean"
}
]
},
"V1Compatible": {
"description": "Generates lagacy zipped v1 compatible updaters",
"oneOf": [
{
"description": "Generates lagacy zipped v1 compatible updaters",
"type": "string", "type": "string",
"enum": [ "enum": [
"updater" "v1Compatible"
] ]
} }
] ]

View File

@ -116,8 +116,6 @@ pub enum BundleType {
App, App,
/// The Apple Disk Image bundle (.dmg). /// The Apple Disk Image bundle (.dmg).
Dmg, Dmg,
/// The Tauri updater bundle.
Updater,
} }
impl BundleType { impl BundleType {
@ -131,7 +129,6 @@ impl BundleType {
BundleType::Nsis, BundleType::Nsis,
BundleType::App, BundleType::App,
BundleType::Dmg, BundleType::Dmg,
BundleType::Updater,
] ]
} }
} }
@ -149,7 +146,6 @@ impl Display for BundleType {
Self::Nsis => "nsis", Self::Nsis => "nsis",
Self::App => "app", Self::App => "app",
Self::Dmg => "dmg", Self::Dmg => "dmg",
Self::Updater => "updater",
} }
) )
} }
@ -178,7 +174,6 @@ impl<'de> Deserialize<'de> for BundleType {
"nsis" => Ok(Self::Nsis), "nsis" => Ok(Self::Nsis),
"app" => Ok(Self::App), "app" => Ok(Self::App),
"dmg" => Ok(Self::Dmg), "dmg" => Ok(Self::Dmg),
"updater" => Ok(Self::Updater),
_ => Err(DeError::custom(format!("unknown bundle target '{s}'"))), _ => Err(DeError::custom(format!("unknown bundle target '{s}'"))),
} }
} }
@ -1070,6 +1065,33 @@ impl BundleResources {
} }
} }
/// Updater type
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
pub enum Updater {
/// Generates lagacy zipped v1 compatible updaters
String(V1Compatible),
/// Produce updaters and their signatures or not
// Can't use untagged on enum field here: https://github.com/GREsau/schemars/issues/222
Bool(bool),
}
impl Default for Updater {
fn default() -> Self {
Self::Bool(false)
}
}
/// Generates lagacy zipped v1 compatible updaters
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum V1Compatible {
/// Generates lagacy zipped v1 compatible updaters
V1Compatible,
}
/// Configuration for tauri-bundler. /// Configuration for tauri-bundler.
/// ///
/// See more: <https://tauri.app/v1/api/config#bundleconfig> /// See more: <https://tauri.app/v1/api/config#bundleconfig>
@ -1081,9 +1103,12 @@ pub struct BundleConfig {
/// Whether Tauri should bundle your application or just output the executable. /// Whether Tauri should bundle your application or just output the executable.
#[serde(default)] #[serde(default)]
pub active: bool, pub active: bool,
/// The bundle targets, currently supports ["deb", "rpm", "appimage", "nsis", "msi", "app", "dmg", "updater"] or "all". /// The bundle targets, currently supports ["deb", "rpm", "appimage", "nsis", "msi", "app", "dmg"] or "all".
#[serde(default)] #[serde(default)]
pub targets: BundleTarget, pub targets: BundleTarget,
#[serde(default)]
/// Produce updaters and their signatures or not
pub create_updater_artifacts: Updater,
/// The application's publisher. Defaults to the second element in the identifier string. /// The application's publisher. Defaults to the second element in the identifier string.
/// Currently maps to the Manufacturer property of the Windows Installer. /// Currently maps to the Manufacturer property of the Windows Installer.
pub publisher: Option<String>, pub publisher: Option<String>,
@ -2474,6 +2499,7 @@ mod build {
let icon = vec_lit(&self.icon, str_lit); let icon = vec_lit(&self.icon, str_lit);
let active = self.active; let active = self.active;
let targets = quote!(Default::default()); let targets = quote!(Default::default());
let create_updater_artifacts = quote!(Default::default());
let resources = quote!(None); let resources = quote!(None);
let copyright = quote!(None); let copyright = quote!(None);
let category = quote!(None); let category = quote!(None);
@ -2497,6 +2523,7 @@ mod build {
homepage, homepage,
icon, icon,
targets, targets,
create_updater_artifacts,
resources, resources,
copyright, copyright,
category, category,
@ -2811,6 +2838,7 @@ mod test {
let bundle = BundleConfig { let bundle = BundleConfig {
active: false, active: false,
targets: Default::default(), targets: Default::default(),
create_updater_artifacts: Default::default(),
publisher: None, publisher: None,
homepage: None, homepage: None,
icon: Vec::new(), icon: Vec::new(),

View File

@ -42,7 +42,7 @@ pub struct Bundle {
/// Bundles the project. /// Bundles the project.
/// Returns the list of paths where the bundles can be found. /// Returns the list of paths where the bundles can be found.
pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> { pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
let mut package_types = settings.package_types()?; let mut package_types = settings.package_types()?;
if package_types.is_empty() { if package_types.is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
@ -50,8 +50,6 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
package_types.sort_by_key(|a| a.priority()); package_types.sort_by_key(|a| a.priority());
let mut bundles: Vec<Bundle> = Vec::new();
let target_os = settings let target_os = settings
.target() .target()
.split('-') .split('-')
@ -68,7 +66,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
if settings.can_sign() { if settings.can_sign() {
for bin in settings.binaries() { for bin in settings.binaries() {
let bin_path = settings.binary_path(bin); let bin_path = settings.binary_path(bin);
windows::sign::try_sign(&bin_path, &settings)?; windows::sign::try_sign(&bin_path, settings)?;
} }
// Sign the sidecar binaries // Sign the sidecar binaries
@ -89,7 +87,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
continue; continue;
} }
windows::sign::try_sign(&path, &settings)?; windows::sign::try_sign(&path, settings)?;
} }
} else { } else {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@ -97,6 +95,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
} }
} }
let mut bundles = Vec::<Bundle>::new();
for package_type in &package_types { for package_type in &package_types {
// bundle was already built! e.g. DMG already built .app // bundle was already built! e.g. DMG already built .app
if bundles.iter().any(|b| b.package_type == *package_type) { if bundles.iter().any(|b| b.package_type == *package_type) {
@ -105,13 +104,13 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
let bundle_paths = match package_type { let bundle_paths = match package_type {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
PackageType::MacOsBundle => macos::app::bundle_project(&settings)?, PackageType::MacOsBundle => macos::app::bundle_project(settings)?,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
PackageType::IosBundle => macos::ios::bundle_project(&settings)?, PackageType::IosBundle => macos::ios::bundle_project(settings)?,
// dmg is dependent of MacOsBundle, we send our bundles to prevent rebuilding // dmg is dependent of MacOsBundle, we send our bundles to prevent rebuilding
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
PackageType::Dmg => { PackageType::Dmg => {
let bundled = macos::dmg::bundle_project(&settings, &bundles)?; let bundled = macos::dmg::bundle_project(settings, &bundles)?;
if !bundled.app.is_empty() { if !bundled.app.is_empty() {
bundles.push(Bundle { bundles.push(Bundle {
package_type: PackageType::MacOsBundle, package_type: PackageType::MacOsBundle,
@ -122,33 +121,15 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
PackageType::WindowsMsi => windows::msi::bundle_project(&settings, false)?, PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?,
PackageType::Nsis => windows::nsis::bundle_project(&settings, false)?, PackageType::Nsis => windows::nsis::bundle_project(settings, false)?,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
PackageType::Deb => linux::debian::bundle_project(&settings)?, PackageType::Deb => linux::debian::bundle_project(settings)?,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
PackageType::Rpm => linux::rpm::bundle_project(&settings)?, PackageType::Rpm => linux::rpm::bundle_project(settings)?,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
PackageType::AppImage => linux::appimage::bundle_project(&settings)?, PackageType::AppImage => linux::appimage::bundle_project(settings)?,
// updater is dependent of multiple bundle, we send our bundles to prevent rebuilding
PackageType::Updater => {
if !package_types.iter().any(|p| {
matches!(
p,
PackageType::AppImage
| PackageType::MacOsBundle
| PackageType::Dmg
| PackageType::Nsis
| PackageType::WindowsMsi
)
}) {
log::warn!("The updater bundle target exists but couldn't find any updater-enabled target, so the updater artifacts won't be generated. Please add one of these targets as well: app, appimage, msi, nsis");
continue;
}
updater_bundle::bundle_project(&settings, &bundles)?
}
_ => { _ => {
log::warn!("ignoring {}", package_type.short_name()); log::warn!("ignoring {}", package_type.short_name());
continue; continue;
@ -161,6 +142,33 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
}); });
} }
if let Some(updater) = settings.updater() {
if package_types.iter().any(|package_type| {
if updater.v1_compatible {
matches!(
package_type,
PackageType::AppImage
| PackageType::MacOsBundle
| PackageType::Nsis
| PackageType::WindowsMsi
)
} else {
matches!(package_type, PackageType::MacOsBundle)
}
}) {
let updater_paths = updater_bundle::bundle_project(settings, &bundles)?;
bundles.push(Bundle {
package_type: PackageType::Updater,
bundle_paths: updater_paths,
});
} else if updater.v1_compatible {
log::warn!("The updater bundle target exists but couldn't find any updater-enabled target, so the updater artifacts won't be generated. Please add one of these targets as well: app, appimage, msi, nsis");
}
if updater.v1_compatible {
log::warn!("Legacy v1 compatible updater is deprecated and will be removed in v3, change bundle > createUpdaterArtifacts to true when your users are updated to the version with v2 updater plugin");
}
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
// Clean up .app if only building dmg or updater // Clean up .app if only building dmg or updater
@ -188,12 +196,16 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
} }
} }
if !bundles.is_empty() { if bundles.is_empty() {
return Err(anyhow::anyhow!("No bundles were built").into());
}
let bundles_wo_updater = bundles let bundles_wo_updater = bundles
.iter() .iter()
.filter(|b| b.package_type != PackageType::Updater) .filter(|b| b.package_type != PackageType::Updater)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let pluralised = if bundles_wo_updater.len() == 1 { let finished_bundles = bundles_wo_updater.len();
let pluralised = if finished_bundles == 1 {
"bundle" "bundle"
} else { } else {
"bundles" "bundles"
@ -202,20 +214,19 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
let mut printable_paths = String::new(); let mut printable_paths = String::new();
for bundle in &bundles { for bundle in &bundles {
for path in &bundle.bundle_paths { for path in &bundle.bundle_paths {
let mut note = ""; let note = if bundle.package_type == crate::PackageType::Updater {
if bundle.package_type == crate::PackageType::Updater { " (updater)"
note = " (updater)"; } else {
} ""
writeln!(printable_paths, " {}{}", display_path(path), note).unwrap(); };
let path_display = display_path(path);
writeln!(printable_paths, " {path_display}{note}").unwrap();
} }
} }
log::info!(action = "Finished"; "{} {} at:\n{}", bundles_wo_updater.len(), pluralised, printable_paths); log::info!(action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
Ok(bundles) Ok(bundles)
} else {
Err(anyhow::anyhow!("No bundles were built").into())
}
} }
/// Check to see if there are icons in the settings struct /// Check to see if there are icons in the settings struct

View File

@ -50,7 +50,6 @@ impl From<BundleType> for PackageType {
BundleType::Nsis => Self::Nsis, BundleType::Nsis => Self::Nsis,
BundleType::App => Self::MacOsBundle, BundleType::App => Self::MacOsBundle,
BundleType::Dmg => Self::Dmg, BundleType::Dmg => Self::Dmg,
BundleType::Updater => Self::Updater,
} }
} }
} }
@ -156,10 +155,12 @@ pub struct PackageSettings {
/// The updater settings. /// The updater settings.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct UpdaterSettings { pub struct UpdaterSettings {
/// Should generate v1 compatible zipped updater
pub v1_compatible: bool,
/// Signature public key. /// Signature public key.
pub pubkey: String, pub pubkey: String,
/// Args to pass to `msiexec.exe` to run the updater on Windows. /// Args to pass to `msiexec.exe` to run the updater on Windows.
pub msiexec_args: Option<&'static [&'static str]>, pub msiexec_args: &'static [&'static str],
} }
/// The Linux debian bundle settings. /// The Linux debian bundle settings.
@ -865,7 +866,7 @@ impl Settings {
.unwrap_or(std::env::consts::OS) .unwrap_or(std::env::consts::OS)
.replace("darwin", "macos"); .replace("darwin", "macos");
let mut platform_types = match target_os.as_str() { let platform_types = match target_os.as_str() {
"macos" => vec![PackageType::MacOsBundle, PackageType::Dmg], "macos" => vec![PackageType::MacOsBundle, PackageType::Dmg],
"ios" => vec![PackageType::IosBundle], "ios" => vec![PackageType::IosBundle],
"linux" => vec![PackageType::Deb, PackageType::Rpm, PackageType::AppImage], "linux" => vec![PackageType::Deb, PackageType::Rpm, PackageType::AppImage],
@ -877,10 +878,6 @@ impl Settings {
} }
}; };
if self.is_update_enabled() {
platform_types.push(PackageType::Updater);
}
if let Some(package_types) = &self.package_types { if let Some(package_types) = &self.package_types {
let mut types = vec![]; let mut types = vec![];
for package_type in package_types { for package_type in package_types {
@ -1087,14 +1084,4 @@ impl Settings {
pub fn updater(&self) -> Option<&UpdaterSettings> { pub fn updater(&self) -> Option<&UpdaterSettings> {
self.bundle_settings.updater.as_ref() self.bundle_settings.updater.as_ref()
} }
/// Is update enabled
pub fn is_update_enabled(&self) -> bool {
self
.bundle_settings
.updater
.as_ref()
.map(|u| !u.pubkey.is_empty())
.unwrap_or_default()
}
} }

View File

@ -646,7 +646,7 @@ pub fn build_wix_app_installer(
to_json( to_json(
settings settings
.updater() .updater()
.and_then(|updater| updater.msiexec_args) .map(|updater| updater.msiexec_args)
.map(|args| args.join(" ")) .map(|args| args.join(" "))
.unwrap_or_else(|| "/passive".to_string()), .unwrap_or_else(|| "/passive".to_string()),
), ),

View File

@ -72,6 +72,7 @@
"android": { "android": {
"minSdkVersion": 24 "minSdkVersion": 24
}, },
"createUpdaterArtifacts": false,
"iOS": {}, "iOS": {},
"icon": [], "icon": [],
"linux": { "linux": {
@ -1519,7 +1520,7 @@
"type": "boolean" "type": "boolean"
}, },
"targets": { "targets": {
"description": "The bundle targets, currently supports [\"deb\", \"rpm\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".", "description": "The bundle targets, currently supports [\"deb\", \"rpm\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\"] or \"all\".",
"default": "all", "default": "all",
"allOf": [ "allOf": [
{ {
@ -1527,6 +1528,15 @@
} }
] ]
}, },
"createUpdaterArtifacts": {
"description": "Produce updaters and their signatures or not",
"default": false,
"allOf": [
{
"$ref": "#/definitions/Updater"
}
]
},
"publisher": { "publisher": {
"description": "The application's publisher. Defaults to the second element in the identifier string.\n Currently maps to the Manufacturer property of the Windows Installer.", "description": "The application's publisher. Defaults to the second element in the identifier string.\n Currently maps to the Manufacturer property of the Windows Installer.",
"type": [ "type": [
@ -1794,12 +1804,34 @@
"enum": [ "enum": [
"dmg" "dmg"
] ]
}
]
},
"Updater": {
"description": "Updater type",
"anyOf": [
{
"description": "Generates lagacy zipped v1 compatible updaters",
"allOf": [
{
"$ref": "#/definitions/V1Compatible"
}
]
}, },
{ {
"description": "The Tauri updater bundle.", "description": "Produce updaters and their signatures or not",
"type": "boolean"
}
]
},
"V1Compatible": {
"description": "Generates lagacy zipped v1 compatible updaters",
"oneOf": [
{
"description": "Generates lagacy zipped v1 compatible updaters",
"type": "string", "type": "string",
"enum": [ "enum": [
"updater" "v1Compatible"
] ]
} }
] ]

View File

@ -198,41 +198,57 @@ pub fn bundle<A: AppSettings>(
} }
} }
let bundles = tauri_bundler::bundle_project(settings) let bundles = tauri_bundler::bundle_project(&settings)
.map_err(|e| match e { .map_err(|e| match e {
tauri_bundler::Error::BundlerError(e) => e, tauri_bundler::Error::BundlerError(e) => e,
e => anyhow::anyhow!("{e:#}"), e => anyhow::anyhow!("{e:#}"),
}) })
.with_context(|| "failed to bundle project")?; .with_context(|| "failed to bundle project")?;
sign_updaters(settings, bundles, ci)?;
Ok(())
}
fn sign_updaters(
settings: tauri_bundler::Settings,
bundles: Vec<tauri_bundler::Bundle>,
ci: bool,
) -> crate::Result<()> {
let Some(update_settings) = settings.updater() else {
// Updater not enabled
return Ok(());
};
let update_enabled_bundles: Vec<&tauri_bundler::Bundle> = bundles let update_enabled_bundles: Vec<&tauri_bundler::Bundle> = bundles
.iter() .iter()
.filter(|bundle| { .filter(|bundle| {
if update_settings.v1_compatible {
matches!(bundle.package_type, PackageType::Updater)
} else {
matches!( matches!(
bundle.package_type, bundle.package_type,
PackageType::Updater | PackageType::Nsis | PackageType::WindowsMsi | PackageType::AppImage PackageType::Updater
| PackageType::Nsis
| PackageType::WindowsMsi
| PackageType::AppImage
) )
}
}) })
.collect(); .collect();
// Skip if no updater is active if update_enabled_bundles.is_empty() {
if !update_enabled_bundles.is_empty() { return Ok(());
let updater_pub_key = config }
.plugins
.0
.get("updater")
.and_then(|k| k.get("pubkey"))
.and_then(|v| v.as_str())
.map(|v| v.to_string());
if let Some(pubkey) = updater_pub_key {
// get the public key // get the public key
let pubkey = &update_settings.pubkey;
// check if pubkey points to a file... // check if pubkey points to a file...
let maybe_path = Path::new(&pubkey); let maybe_path = Path::new(pubkey);
let pubkey = if maybe_path.exists() { let pubkey = if maybe_path.exists() {
std::fs::read_to_string(maybe_path)? std::fs::read_to_string(maybe_path)?
} else { } else {
pubkey pubkey.to_string()
}; };
// if no password provided we use an empty string // if no password provided we use an empty string
@ -241,8 +257,8 @@ pub fn bundle<A: AppSettings>(
.or_else(|| if ci { Some("".into()) } else { None }); .or_else(|| if ci { Some("".into()) } else { None });
// get the private key // get the private key
let secret_key = match std::env::var("TAURI_SIGNING_PRIVATE_KEY") { let private_key = std::env::var("TAURI_SIGNING_PRIVATE_KEY")
Ok(private_key) => { .map_err(|_| anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable."))?;
// check if private_key points to a file... // check if private_key points to a file...
let maybe_path = Path::new(&private_key); let maybe_path = Path::new(&private_key);
let private_key = if maybe_path.exists() { let private_key = if maybe_path.exists() {
@ -250,33 +266,27 @@ pub fn bundle<A: AppSettings>(
} else { } else {
private_key private_key
}; };
updater_signature::secret_key(private_key, password) let secret_key = updater_signature::secret_key(private_key, password)?;
}
_ => Err(anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")),
}?;
let pubkey = base64::engine::general_purpose::STANDARD.decode(pubkey)?; let pubkey = base64::engine::general_purpose::STANDARD.decode(pubkey)?;
let pub_key_decoded = String::from_utf8_lossy(&pubkey); let pub_key_decoded = String::from_utf8_lossy(&pubkey);
let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?; let public_key = minisign::PublicKeyBox::from_string(&pub_key_decoded)?.into_public_key()?;
// make sure we have our package built
let mut signed_paths = Vec::new(); let mut signed_paths = Vec::new();
for bundle in update_enabled_bundles { for bundle in update_enabled_bundles {
// we expect to have only one path in the vec but we iter if we add // we expect to have only one path in the vec but we iter if we add
// another type of updater package who require multiple file signature // another type of updater package who require multiple file signature
for path in bundle.bundle_paths.iter() { for path in &bundle.bundle_paths {
// sign our path from environment variables // sign our path from environment variables
let (signature_path, signature) = updater_signature::sign_file(&secret_key, path)?; let (signature_path, signature) = updater_signature::sign_file(&secret_key, path)?;
if signature.keynum() != public_key.keynum() { if signature.keynum() != public_key.keynum() {
log::warn!("The updater secret key from `TAURI_PRIVATE_KEY` does not match the public key from `plugins > updater > pubkey`. If you are not rotating keys, this means your configuration is wrong and won't be accepted at runtime when performing update."); log::warn!("The updater secret key from `TAURI_SIGNING_PRIVATE_KEY` does not match the public key from `plugins > updater > pubkey`. If you are not rotating keys, this means your configuration is wrong and won't be accepted at runtime when performing update.");
} }
signed_paths.push(signature_path); signed_paths.push(signature_path);
} }
} }
print_signed_updater_archive(&signed_paths)?; print_signed_updater_archive(&signed_paths)?;
}
}
Ok(()) Ok(())
} }
@ -284,7 +294,8 @@ pub fn bundle<A: AppSettings>(
fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> { fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
use std::fmt::Write; use std::fmt::Write;
if !output_paths.is_empty() { if !output_paths.is_empty() {
let pluralised = if output_paths.len() == 1 { let finished_bundles = output_paths.len();
let pluralised = if finished_bundles == 1 {
"updater signature" "updater signature"
} else { } else {
"updater signatures" "updater signatures"
@ -297,7 +308,7 @@ fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
tauri_utils::display_path(path) tauri_utils::display_path(path)
)?; )?;
} }
log::info!( action = "Finished"; "{} {} at:\n{}", output_paths.len(), pluralised, printable_paths); log::info!( action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
} }
Ok(()) Ok(())
} }

View File

@ -24,7 +24,7 @@ use tauri_bundler::{
AppCategory, AppImageSettings, BundleBinary, BundleSettings, DebianSettings, DmgSettings, AppCategory, AppImageSettings, BundleBinary, BundleSettings, DebianSettings, DmgSettings,
MacOsSettings, PackageSettings, Position, RpmSettings, Size, UpdaterSettings, WindowsSettings, MacOsSettings, PackageSettings, Position, RpmSettings, Size, UpdaterSettings, WindowsSettings,
}; };
use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol}; use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, Updater};
use super::{AppSettings, DevProcess, ExitReason, Interface}; use super::{AppSettings, DevProcess, ExitReason, Interface};
use crate::{ use crate::{
@ -807,11 +807,23 @@ impl AppSettings for RustAppSettings {
let arch64bits = let arch64bits =
self.target_triple.starts_with("x86_64") || self.target_triple.starts_with("aarch64"); self.target_triple.starts_with("x86_64") || self.target_triple.starts_with("aarch64");
let updater_settings = if let Some(updater_plugin_config) = config.plugins.0.get("updater") { let updater_enabled = config.bundle.create_updater_artifacts != Updater::Bool(false);
let updater: UpdaterConfig = serde_json::from_value(updater_plugin_config.clone())?; let v1_compatible = matches!(config.bundle.create_updater_artifacts, Updater::String(_));
let updater_settings = if updater_enabled {
let updater: UpdaterConfig = serde_json::from_value(
config
.plugins
.0
.get("updater")
.ok_or_else(|| {
anyhow::anyhow!("failed to get updater configuration: plugins > updater doesn't exist")
})?
.clone(),
)?;
Some(UpdaterSettings { Some(UpdaterSettings {
v1_compatible,
pubkey: updater.pubkey, pubkey: updater.pubkey,
msiexec_args: Some(updater.windows.install_mode.msiexec_args()), msiexec_args: updater.windows.install_mode.msiexec_args(),
}) })
} else { } else {
None None

View File

@ -224,6 +224,36 @@ fn process_bundle(config: &mut Map<String, Value>) {
if let Some(license_file) = license_file { if let Some(license_file) = license_file {
bundle_config.insert("licenseFile".into(), license_file); bundle_config.insert("licenseFile".into(), license_file);
} }
// Migrate updater from targets to update field
if let Some(targets) = bundle_config.get_mut("targets") {
let shuold_migrate = if let Some(targets) = targets.as_array_mut() {
// targets: ["updater", ...]
if let Some(index) = targets
.iter()
.position(|target| *target == serde_json::Value::String("updater".to_owned()))
{
targets.remove(index);
true
} else {
false
}
} else if let Some(target) = targets.as_str() {
// targets: "updater"
// targets: "all"
if target == "updater" {
bundle_config.remove("targets");
true
} else {
target == "all"
}
} else {
false
};
if shuold_migrate {
bundle_config.insert("createUpdaterArtifacts".to_owned(), "v1Compatible".into());
}
}
} }
config.insert("bundle".into(), bundle_config); config.insert("bundle".into(), bundle_config);
@ -773,6 +803,48 @@ mod test {
); );
} }
#[test]
fn migrate_updater_target() {
let original = serde_json::json!({
"tauri": {
"bundle": {
"targets": ["nsis", "updater"]
}
}
});
let migrated = migrate(&original);
assert_eq!(migrated["bundle"]["createUpdaterArtifacts"], "v1Compatible");
assert_eq!(
migrated["bundle"]["targets"].as_array(),
Some(&vec!["nsis".into()])
);
let original = serde_json::json!({
"tauri": {
"bundle": {
"targets": "all"
}
}
});
let migrated = migrate(&original);
assert_eq!(migrated["bundle"]["createUpdaterArtifacts"], "v1Compatible");
assert_eq!(migrated["bundle"]["targets"], "all");
let original = serde_json::json!({
"tauri": {
"bundle": {
"targets": "updater"
}
}
});
let migrated = migrate(&original);
assert_eq!(migrated["bundle"]["createUpdaterArtifacts"], "v1Compatible");
assert_eq!(migrated["bundle"].get("targets"), None);
}
#[test] #[test]
fn migrate_csp_object() { fn migrate_csp_object() {
let original = serde_json::json!({ let original = serde_json::json!({