feat: support message dialogs with 3 buttons (#2641)

* feat: support message dialogs with 3 buttons

* change file

* From<String>

* untagged & YesNoCancel

* revert package.json

* Update plugins/dialog/src/desktop.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* no optional

* Update desktop.rs

* Update plugins/dialog/src/models.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* change to an enum

* convert back into union

* regen

* update @since

* map buttons for linux

* enhance type

* Add examples

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Tony <legendmastertony@gmail.com>

Committed via a GitHub action: https://github.com/tauri-apps/plugins-workspace/actions/runs/17266975220

Co-authored-by: lucasfernog <lucasfernog@users.noreply.github.com>
This commit is contained in:
Amr Bashir
2025-08-27 12:41:57 +00:00
committed by tauri-bot
parent 8d72cddfd9
commit 0f6717b8e9
4 changed files with 323 additions and 132 deletions

View File

@@ -39,9 +39,14 @@ pub enum Error {
/// `reqwest` crate errors. /// `reqwest` crate errors.
#[error(transparent)] #[error(transparent)]
Reqwest(#[from] reqwest::Error), Reqwest(#[from] reqwest::Error),
/// The platform was not found on the updater JSON response. /// The platform was not found in the updater JSON response.
#[error("the platform `{0}` was not found on the response `platforms` object")] #[error("the platform `{0}` was not found in the response `platforms` object")]
TargetNotFound(String), TargetNotFound(String),
/// Neither the platform nor the fallback platform was found in the updater JSON response.
#[error(
"None of the fallback platforms `{0:?}` were found in the response `platforms` object"
)]
TargetsNotFound(Vec<String>),
/// Download failed /// Download failed
#[error("`{0}`")] #[error("`{0}`")]
Network(String), Network(String),
@@ -69,6 +74,8 @@ pub enum Error {
AuthenticationFailed, AuthenticationFailed,
#[error("Failed to install .deb package")] #[error("Failed to install .deb package")]
DebInstallFailed, DebInstallFailed,
#[error("Failed to install package")]
PackageInstallFailed,
#[error("invalid updater binary format")] #[error("invalid updater binary format")]
InvalidUpdaterFormat, InvalidUpdaterFormat,
#[error(transparent)] #[error(transparent)]

View File

@@ -26,7 +26,13 @@ use reqwest::{
}; };
use semver::Version; use semver::Version;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime}; use tauri::{
utils::{
config::BundleType,
platform::{bundle_type, current_exe},
},
AppHandle, Resource, Runtime,
};
use time::OffsetDateTime; use time::OffsetDateTime;
use url::Url; use url::Url;
@@ -37,6 +43,31 @@ use crate::{
const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
#[derive(Copy, Clone)]
pub enum Installer {
AppImage,
Deb,
Rpm,
App,
Msi,
Nsis,
}
impl Installer {
fn name(self) -> &'static str {
match self {
Self::AppImage => "appimage",
Self::Deb => "deb",
Self::Rpm => "rpm",
Self::App => "app",
Self::Msi => "msi",
Self::Nsis => "nsis",
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ReleaseManifestPlatform { pub struct ReleaseManifestPlatform {
/// Download URL for the platform /// Download URL for the platform
@@ -265,13 +296,7 @@ impl UpdaterBuilder {
return Err(Error::EmptyEndpoints); return Err(Error::EmptyEndpoints);
}; };
let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?; let arch = updater_arch().ok_or(Error::UnsupportedArch)?;
let (target, json_target) = if let Some(target) = self.target {
(target.clone(), target)
} else {
let target = get_updater_target().ok_or(Error::UnsupportedOs)?;
(target.to_string(), format!("{target}-{arch}"))
};
let executable_path = self.executable_path.clone().unwrap_or(current_exe()?); let executable_path = self.executable_path.clone().unwrap_or(current_exe()?);
@@ -294,8 +319,7 @@ impl UpdaterBuilder {
installer_args: self.installer_args, installer_args: self.installer_args,
current_exe_args: self.current_exe_args, current_exe_args: self.current_exe_args,
arch, arch,
target, target: self.target,
json_target,
headers: self.headers, headers: self.headers,
extract_path, extract_path,
on_before_exit: self.on_before_exit, on_before_exit: self.on_before_exit,
@@ -327,10 +351,9 @@ pub struct Updater {
proxy: Option<Url>, proxy: Option<Url>,
endpoints: Vec<Url>, endpoints: Vec<Url>,
arch: &'static str, arch: &'static str,
// The `{{target}}` variable we replace in the endpoint // The `{{target}}` variable we replace in the endpoint and serach for in the JSON,
target: String, // this is either the user provided target or the current operating system by default
// The value we search if the updater server returns a JSON with the `platforms` object target: Option<String>,
json_target: String,
headers: HeaderMap, headers: HeaderMap,
extract_path: PathBuf, extract_path: PathBuf,
on_before_exit: Option<OnBeforeExit>, on_before_exit: Option<OnBeforeExit>,
@@ -359,6 +382,11 @@ impl Updater {
std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
} }
} }
let target = if let Some(target) = &self.target {
target
} else {
updater_os().ok_or(Error::UnsupportedOs)?
};
let mut remote_release: Option<RemoteRelease> = None; let mut remote_release: Option<RemoteRelease> = None;
let mut raw_json: Option<serde_json::Value> = None; let mut raw_json: Option<serde_json::Value> = None;
@@ -381,11 +409,11 @@ impl Updater {
.to_string() .to_string()
// url::Url automatically url-encodes the path components // url::Url automatically url-encodes the path components
.replace("%7B%7Bcurrent_version%7D%7D", &encoded_version) .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version)
.replace("%7B%7Btarget%7D%7D", &self.target) .replace("%7B%7Btarget%7D%7D", target)
.replace("%7B%7Barch%7D%7D", self.arch) .replace("%7B%7Barch%7D%7D", self.arch)
// but not query parameters // but not query parameters
.replace("{{current_version}}", &encoded_version) .replace("{{current_version}}", &encoded_version)
.replace("{{target}}", &self.target) .replace("{{target}}", target)
.replace("{{arch}}", self.arch) .replace("{{arch}}", self.arch)
.parse()?; .parse()?;
@@ -466,6 +494,9 @@ impl Updater {
None => release.version > self.current_version, None => release.version > self.current_version,
}; };
let installer = installer_for_bundle_type(bundle_type());
let (download_url, signature) = self.get_urls(&release, &installer)?;
let update = if should_update { let update = if should_update {
Some(Update { Some(Update {
run_on_main_thread: self.run_on_main_thread.clone(), run_on_main_thread: self.run_on_main_thread.clone(),
@@ -473,12 +504,12 @@ impl Updater {
on_before_exit: self.on_before_exit.clone(), on_before_exit: self.on_before_exit.clone(),
app_name: self.app_name.clone(), app_name: self.app_name.clone(),
current_version: self.current_version.to_string(), current_version: self.current_version.to_string(),
target: self.target.clone(), target: target.to_owned(),
extract_path: self.extract_path.clone(), extract_path: self.extract_path.clone(),
version: release.version.to_string(), version: release.version.to_string(),
date: release.pub_date, date: release.pub_date,
download_url: release.download_url(&self.json_target)?.to_owned(), download_url: download_url.clone(),
signature: release.signature(&self.json_target)?.to_owned(), signature: signature.to_owned(),
body: release.notes, body: release.notes,
raw_json: raw_json.unwrap(), raw_json: raw_json.unwrap(),
timeout: None, timeout: None,
@@ -494,6 +525,38 @@ impl Updater {
Ok(update) Ok(update)
} }
fn get_urls<'a>(
&self,
release: &'a RemoteRelease,
installer: &Option<Installer>,
) -> Result<(&'a Url, &'a String)> {
// Use the user provided target
if let Some(target) = &self.target {
return Ok((release.download_url(target)?, release.signature(target)?));
}
// Or else we search for [`{os}-{arch}-{installer}`, `{os}-{arch}`] in order
let os = updater_os().ok_or(Error::UnsupportedOs)?;
let arch = self.arch;
let mut targets = Vec::new();
if let Some(installer) = installer {
let installer = installer.name();
targets.push(format!("{os}-{arch}-{installer}"));
}
targets.push(format!("{os}-{arch}"));
for target in &targets {
log::debug!("Searching for updater target '{target}' in release data");
if let (Ok(download_url), Ok(signature)) =
(release.download_url(target), release.signature(target))
{
return Ok((download_url, signature));
};
}
Err(Error::TargetsNotFound(targets))
}
} }
#[derive(Clone)] #[derive(Clone)]
@@ -511,7 +574,8 @@ pub struct Update {
pub version: String, pub version: String,
/// Update publish date /// Update publish date
pub date: Option<OffsetDateTime>, pub date: Option<OffsetDateTime>,
/// Target /// The `{{target}}` variable we replace in the endpoint and search for in the JSON,
/// this is either the user provided target or the current operating system by default
pub target: String, pub target: String,
/// Download URL announced /// Download URL announced
pub download_url: Url, pub download_url: Url,
@@ -852,11 +916,10 @@ impl Update {
/// └── ... /// └── ...
/// ///
fn install_inner(&self, bytes: &[u8]) -> Result<()> { fn install_inner(&self, bytes: &[u8]) -> Result<()> {
if self.is_deb_package() { match installer_for_bundle_type(bundle_type()) {
self.install_deb(bytes) Some(Installer::Deb) => self.install_deb(bytes),
} else { Some(Installer::Rpm) => self.install_rpm(bytes),
// Handle AppImage or other formats _ => self.install_appimage(bytes),
self.install_appimage(bytes)
} }
} }
@@ -933,39 +996,6 @@ impl Update {
Err(Error::TempDirNotOnSameMountPoint) Err(Error::TempDirNotOnSameMountPoint)
} }
fn is_deb_package(&self) -> bool {
// First check if we're in a typical Debian installation path
let in_system_path = self
.extract_path
.to_str()
.map(|p| p.starts_with("/usr"))
.unwrap_or(false);
if !in_system_path {
return false;
}
// Then verify it's actually a Debian-based system by checking for dpkg
let dpkg_exists = std::path::Path::new("/var/lib/dpkg").exists();
let apt_exists = std::path::Path::new("/etc/apt").exists();
// Additional check for the package in dpkg database
let package_in_dpkg = if let Ok(output) = std::process::Command::new("dpkg")
.args(["-S", &self.extract_path.to_string_lossy()])
.output()
{
output.status.success()
} else {
false
};
// Consider it a deb package only if:
// 1. We're in a system path AND
// 2. We have Debian package management tools AND
// 3. The binary is tracked by dpkg
dpkg_exists && apt_exists && package_in_dpkg
}
fn install_deb(&self, bytes: &[u8]) -> Result<()> { fn install_deb(&self, bytes: &[u8]) -> Result<()> {
// First verify the bytes are actually a .deb package // First verify the bytes are actually a .deb package
if !infer::archive::is_deb(bytes) { if !infer::archive::is_deb(bytes) {
@@ -973,6 +1003,18 @@ impl Update {
return Err(Error::InvalidUpdaterFormat); return Err(Error::InvalidUpdaterFormat);
} }
self.try_tmp_locations(bytes, "dpkg", "-i")
}
fn install_rpm(&self, bytes: &[u8]) -> Result<()> {
// First verify the bytes are actually a .rpm package
if !infer::archive::is_rpm(bytes) {
return Err(Error::InvalidUpdaterFormat);
}
self.try_tmp_locations(bytes, "rpm", "-U")
}
fn try_tmp_locations(&self, bytes: &[u8], install_cmd: &str, install_arg: &str) -> Result<()> {
// Try different temp directories // Try different temp directories
let tmp_dir_locations = vec![ let tmp_dir_locations = vec![
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>, Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
@@ -984,15 +1026,19 @@ impl Update {
for tmp_dir_location in tmp_dir_locations { for tmp_dir_location in tmp_dir_locations {
if let Some(path) = tmp_dir_location() { if let Some(path) = tmp_dir_location() {
if let Ok(tmp_dir) = tempfile::Builder::new() if let Ok(tmp_dir) = tempfile::Builder::new()
.prefix("tauri_deb_update") .prefix("tauri_rpm_update")
.tempdir_in(path) .tempdir_in(path)
{ {
let deb_path = tmp_dir.path().join("package.deb"); let pkg_path = tmp_dir.path().join("package.rpm");
// Try writing the .deb file // Try writing the .deb file
if std::fs::write(&deb_path, bytes).is_ok() { if std::fs::write(&pkg_path, bytes).is_ok() {
// If write succeeds, proceed with installation // If write succeeds, proceed with installation
return self.try_install_with_privileges(&deb_path); return self.try_install_with_privileges(
&pkg_path,
install_cmd,
install_arg,
);
} }
// If write fails, continue to next temp location // If write fails, continue to next temp location
} }
@@ -1003,12 +1049,17 @@ impl Update {
Err(Error::TempDirNotFound) Err(Error::TempDirNotFound)
} }
fn try_install_with_privileges(&self, deb_path: &Path) -> Result<()> { fn try_install_with_privileges(
&self,
pkg_path: &Path,
install_cmd: &str,
install_arg: &str,
) -> Result<()> {
// 1. First try using pkexec (graphical sudo prompt) // 1. First try using pkexec (graphical sudo prompt)
if let Ok(status) = std::process::Command::new("pkexec") if let Ok(status) = std::process::Command::new("pkexec")
.arg("dpkg") .arg(install_cmd)
.arg("-i") .arg(install_arg)
.arg(deb_path) .arg(pkg_path)
.status() .status()
{ {
if status.success() { if status.success() {
@@ -1019,7 +1070,7 @@ impl Update {
// 2. Try zenity or kdialog for a graphical sudo experience // 2. Try zenity or kdialog for a graphical sudo experience
if let Ok(password) = self.get_password_graphically() { if let Ok(password) = self.get_password_graphically() {
if self.install_with_sudo(deb_path, &password)? { if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? {
log::debug!("installed deb with GUI sudo"); log::debug!("installed deb with GUI sudo");
return Ok(()); return Ok(());
} }
@@ -1027,16 +1078,16 @@ impl Update {
// 3. Final fallback: terminal sudo // 3. Final fallback: terminal sudo
let status = std::process::Command::new("sudo") let status = std::process::Command::new("sudo")
.arg("dpkg") .arg(install_cmd)
.arg("-i") .arg(install_arg)
.arg(deb_path) .arg(pkg_path)
.status()?; .status()?;
if status.success() { if status.success() {
log::debug!("installed deb with sudo"); log::debug!("installed deb with sudo");
Ok(()) Ok(())
} else { } else {
Err(Error::DebInstallFailed) Err(Error::PackageInstallFailed)
} }
} }
@@ -1070,15 +1121,21 @@ impl Update {
Err(Error::AuthenticationFailed) Err(Error::AuthenticationFailed)
} }
fn install_with_sudo(&self, deb_path: &Path, password: &str) -> Result<bool> { fn install_with_sudo(
&self,
pkg_path: &Path,
password: &str,
install_cmd: &str,
install_arg: &str,
) -> Result<bool> {
use std::io::Write; use std::io::Write;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
let mut child = Command::new("sudo") let mut child = Command::new("sudo")
.arg("-S") // read password from stdin .arg("-S") // read password from stdin
.arg("dpkg") .arg(install_cmd)
.arg("-i") .arg(install_arg)
.arg(deb_path) .arg(pkg_path)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@@ -1086,7 +1143,7 @@ impl Update {
if let Some(mut stdin) = child.stdin.take() { if let Some(mut stdin) = child.stdin.take() {
// Write password to stdin // Write password to stdin
writeln!(stdin, "{}", password)?; writeln!(stdin, "{password}")?;
} }
let status = child.wait()?; let status = child.wait()?;
@@ -1199,16 +1256,18 @@ impl Update {
} }
} }
/// Gets the target string used on the updater. /// Gets the base target string used by the updater. If bundle type is available it
/// will be added to this string when selecting the download URL and signature.
/// `tauri::utils::platform::bundle_type` method is used to obtain current bundle type.
pub fn target() -> Option<String> { pub fn target() -> Option<String> {
if let (Some(target), Some(arch)) = (get_updater_target(), get_updater_arch()) { if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) {
Some(format!("{target}-{arch}")) Some(format!("{target}-{arch}"))
} else { } else {
None None
} }
} }
pub(crate) fn get_updater_target() -> Option<&'static str> { fn updater_os() -> Option<&'static str> {
if cfg!(target_os = "linux") { if cfg!(target_os = "linux") {
Some("linux") Some("linux")
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
@@ -1221,7 +1280,7 @@ pub(crate) fn get_updater_target() -> Option<&'static str> {
} }
} }
pub(crate) fn get_updater_arch() -> Option<&'static str> { fn updater_arch() -> Option<&'static str> {
if cfg!(target_arch = "x86") { if cfg!(target_arch = "x86") {
Some("i686") Some("i686")
} else if cfg!(target_arch = "x86_64") { } else if cfg!(target_arch = "x86_64") {
@@ -1315,6 +1374,18 @@ impl<'de> Deserialize<'de> for RemoteRelease {
} }
} }
fn installer_for_bundle_type(bundle: Option<BundleType>) -> Option<Installer> {
match bundle? {
BundleType::Deb => Some(Installer::Deb),
BundleType::Rpm => Some(Installer::Rpm),
BundleType::AppImage => Some(Installer::AppImage),
BundleType::Msi => Some(Installer::Msi),
BundleType::Nsis => Some(Installer::Nsis),
BundleType::App => Some(Installer::App), // App is also returned for Dmg type
_ => None,
}
}
fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error> fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,

View File

@@ -2,6 +2,7 @@
"identifier": "com.tauri.updater", "identifier": "com.tauri.updater",
"plugins": { "plugins": {
"updater": { "updater": {
"dangerousInsecureTransportProtocol": true,
"endpoints": ["http://localhost:3007"], "endpoints": ["http://localhost:3007"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK",
"windows": { "windows": {

View File

@@ -17,6 +17,7 @@ use tauri::utils::config::{Updater, V1Compatible};
const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg=="; const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg==";
const UPDATED_EXIT_CODE: i32 = 0; const UPDATED_EXIT_CODE: i32 = 0;
const ERROR_EXIT_CODE: i32 = 1;
const UP_TO_DATE_EXIT_CODE: i32 = 2; const UP_TO_DATE_EXIT_CODE: i32 = 2;
#[derive(Serialize)] #[derive(Serialize)]
@@ -48,7 +49,7 @@ struct Update {
fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) { fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) {
let mut command = Command::new("cargo"); let mut command = Command::new("cargo");
command command
.args(["tauri", "build", "--debug", "--verbose"]) .args(["tauri", "build", "--verbose"])
.arg("--config") .arg("--config")
.arg(serde_json::to_string(config).unwrap()) .arg(serde_json::to_string(config).unwrap())
.env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY) .env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
@@ -80,6 +81,8 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
enum BundleTarget { enum BundleTarget {
AppImage, AppImage,
Deb,
Rpm,
App, App,
@@ -91,6 +94,8 @@ impl BundleTarget {
fn name(self) -> &'static str { fn name(self) -> &'static str {
match self { match self {
Self::AppImage => "appimage", Self::AppImage => "appimage",
Self::Deb => "deb",
Self::Rpm => "rpm",
Self::App => "app", Self::App => "app",
Self::Msi => "msi", Self::Msi => "msi",
Self::Nsis => "nsis", Self::Nsis => "nsis",
@@ -109,57 +114,168 @@ impl Default for BundleTarget {
} }
} }
fn target_to_platforms(
update_platform: Option<String>,
signature: String,
) -> HashMap<String, PlatformUpdate> {
let mut platforms = HashMap::new();
if let Some(platform) = update_platform {
platforms.insert(
platform,
PlatformUpdate {
signature,
url: "http://localhost:3007/download",
with_elevated_task: false,
},
);
}
platforms
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { fn test_cases(
vec![( root_dir: &Path,
BundleTarget::AppImage, version: &str,
root_dir.join(format!( target: String,
"target/debug/bundle/appimage/app-updater_{version}_amd64.AppImage" ) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
)), vec![
)] // update using fallback
(
BundleTarget::AppImage,
root_dir.join(format!(
"target/release/bundle/appimage/app-updater_{version}_amd64.AppImage"
)),
Some(target.clone()),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// update using full name
(
BundleTarget::AppImage,
root_dir.join(format!(
"target/release/bundle/appimage/app-updater_{version}_amd64.AppImage"
)),
Some(format!("{target}-{}", BundleTarget::AppImage.name())),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// no update
(
BundleTarget::AppImage,
root_dir.join(format!(
"target/release/bundle/appimage/app-updater_{version}_amd64.AppImage"
)),
None,
vec![ERROR_EXIT_CODE],
),
]
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { fn test_cases(
vec![( root_dir: &Path,
BundleTarget::App, _version: &str,
root_dir.join("target/debug/bundle/macos/app-updater.app"), target: String,
)] ) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
vec![
(
BundleTarget::App,
root_dir.join("target/release/bundle/macos/app-updater.app"),
Some(target.clone()),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// update with installer
(
BundleTarget::App,
root_dir.join("target/release/bundle/macos/app-updater.app"),
Some(format!("{target}-{}", BundleTarget::App.name())),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// no update
(
BundleTarget::App,
root_dir.join("target/release/bundle/macos/app-updater.app"),
None,
vec![ERROR_EXIT_CODE],
),
]
} }
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { fn bundle_paths(
root_dir: &Path,
_version: &str,
v1compatible: bool,
) -> Vec<(BundleTarget, PathBuf)> {
vec![( vec![(
BundleTarget::App, BundleTarget::App,
root_dir.join("target/debug/bundle/ios/app-updater.ipa"), root_dir.join("target/release/bundle/ios/app-updater.ipa"),
)] )]
} }
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf { fn bundle_path(root_dir: &Path, _version: &str, v1compatible: bool) -> PathBuf {
root_dir.join("target/debug/bundle/android/app-updater.apk") root_dir.join("target/release/bundle/android/app-updater.apk")
} }
#[cfg(windows)] #[cfg(windows)]
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { fn test_cases(
root_dir: &Path,
version: &str,
target: String,
) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
vec![ vec![
( (
BundleTarget::Nsis, BundleTarget::Nsis,
root_dir.join(format!( root_dir.join(format!(
"target/debug/bundle/nsis/app-updater_{version}_x64-setup.exe" "target/release/bundle/nsis/app-updater_{version}_x64-setup.exe"
)), )),
Some(target.clone()),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Nsis,
root_dir.join(format!(
"target/release/bundle/nsis/app-updater_{version}_x64-setup.exe"
)),
Some(format!("{target}-{}", BundleTarget::Nsis.name())),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Nsis,
root_dir.join(format!(
"target/release/bundle/nsis/app-updater_{version}_x64-setup.exe"
)),
None,
vec![ERROR_EXIT_CODE],
), ),
( (
BundleTarget::Msi, BundleTarget::Msi,
root_dir.join(format!( root_dir.join(format!(
"target/debug/bundle/msi/app-updater_{version}_x64_en-US.msi" "target/release/bundle/msi/app-updater_{version}_x64_en-US.msi"
)), )),
Some(target.clone()),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Msi,
root_dir.join(format!(
"target/release/bundle/msi/app-updater_{version}_x64_en-US.msi"
)),
Some(format!("{target}-{}", BundleTarget::Msi.name())),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Msi,
root_dir.join(format!(
"target/release/bundle/msi/app-updater_{version}_x64_en-US.msi"
)),
None,
vec![ERROR_EXIT_CODE],
), ),
] ]
} }
#[test] #[test]
#[ignore]
fn update_app() { fn update_app() {
let target = let target =
tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); tauri_plugin_updater::target().expect("running updater test in an unsupported platform");
@@ -185,9 +301,6 @@ fn update_app() {
Updater::String(V1Compatible::V1Compatible) Updater::String(V1Compatible::V1Compatible)
); );
// bundle app update
build_app(&manifest_dir, &config, true, Default::default());
let updater_zip_ext = if v1_compatible { let updater_zip_ext = if v1_compatible {
if cfg!(windows) { if cfg!(windows) {
Some("zip") Some("zip")
@@ -200,7 +313,13 @@ fn update_app() {
None None
}; };
for (bundle_target, out_bundle_path) in bundle_paths(&root_dir, "1.0.0") { for (bundle_target, out_bundle_path, update_platform, status_checks) in
test_cases(&root_dir, "1.0.0", target.clone())
{
// bundle app update
config.version = "1.0.0";
build_app(&manifest_dir, &config, true, BundleTarget::default());
let bundle_updater_ext = if v1_compatible { let bundle_updater_ext = if v1_compatible {
out_bundle_path out_bundle_path
.extension() .extension()
@@ -228,13 +347,11 @@ fn update_app() {
}); });
let out_updater_path = out_bundle_path.with_extension(updater_extension); let out_updater_path = out_bundle_path.with_extension(updater_extension);
let updater_path = root_dir.join(format!( let updater_path = root_dir.join(format!(
"target/debug/{}", "target/release/{}",
out_updater_path.file_name().unwrap().to_str().unwrap() out_updater_path.file_name().unwrap().to_str().unwrap()
)); ));
std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
let target = target.clone();
// start the updater server // start the updater server
let server = Arc::new( let server = Arc::new(
tiny_http::Server::http("localhost:3007").expect("failed to start updater server"), tiny_http::Server::http("localhost:3007").expect("failed to start updater server"),
@@ -245,16 +362,9 @@ fn update_app() {
for request in server_.incoming_requests() { for request in server_.incoming_requests() {
match request.url() { match request.url() {
"/" => { "/" => {
let mut platforms = HashMap::new(); let platforms =
target_to_platforms(update_platform.clone(), signature.clone());
platforms.insert(
target.clone(),
PlatformUpdate {
signature: signature.clone(),
url: "http://localhost:3007/download",
with_elevated_task: false,
},
);
let body = serde_json::to_vec(&Update { let body = serde_json::to_vec(&Update {
version: "1.0.0", version: "1.0.0",
date: time::OffsetDateTime::now_utc() date: time::OffsetDateTime::now_utc()
@@ -293,19 +403,12 @@ fn update_app() {
// bundle initial app version // bundle initial app version
build_app(&manifest_dir, &config, false, bundle_target); build_app(&manifest_dir, &config, false, bundle_target);
let status_checks = if matches!(bundle_target, BundleTarget::Msi) {
// for msi we can't really check if the app was updated, because we can't change the install path
vec![UPDATED_EXIT_CODE]
} else {
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE]
};
for expected_exit_code in status_checks { for expected_exit_code in status_checks {
let mut binary_cmd = if cfg!(windows) { let mut binary_cmd = if cfg!(windows) {
Command::new(root_dir.join("target/debug/app-updater.exe")) Command::new(root_dir.join("target/release/app-updater.exe"))
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
Command::new( Command::new(
bundle_paths(&root_dir, "0.1.0") test_cases(&root_dir, "0.1.0", target.clone())
.first() .first()
.unwrap() .unwrap()
.1 .1
@@ -313,11 +416,20 @@ fn update_app() {
) )
} else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() { } else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
let mut c = Command::new("xvfb-run"); let mut c = Command::new("xvfb-run");
c.arg("--auto-servernum") c.arg("--auto-servernum").arg(
.arg(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1); &test_cases(&root_dir, "0.1.0", target.clone())
.first()
.unwrap()
.1,
);
c c
} else { } else {
Command::new(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1) Command::new(
&test_cases(&root_dir, "0.1.0", target.clone())
.first()
.unwrap()
.1,
)
}; };
binary_cmd.env("TARGET", bundle_target.name()); binary_cmd.env("TARGET", bundle_target.name());
@@ -327,7 +439,7 @@ fn update_app() {
if code != expected_exit_code { if code != expected_exit_code {
panic!( panic!(
"failed to run app, expected exit code {expected_exit_code}, got {code}" "failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", bundle_target.name()
); );
} }
#[cfg(windows)] #[cfg(windows)]