-
Notifications
You must be signed in to change notification settings - Fork 404
feat(updater): support bundle-specific targets #2624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v2
Are you sure you want to change the base?
Changes from 23 commits
8aa131a
597ae9f
6ae53cf
e2e27ac
ae7a2e3
dc75b76
199a52b
ede0c68
d50947c
ef95298
012f633
c2877ec
c9d0a6c
896678a
8cb79a3
b1a8781
0630002
3a43397
68564e0
24504e4
5d12c97
2137583
b80a295
f75d32b
1ea7522
940ed70
201a001
80b07ee
513376b
9e9d7bc
f7c10ea
2fdda08
4913dbe
5eae160
5056198
5b4c1c1
6c2f563
c13e583
043d89f
0a495cc
e67750c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,4 +57,5 @@ pids | |
*.sublime* | ||
.idea | ||
debug.log | ||
TODO.md | ||
TODO.md | ||
.aider.* |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,9 @@ use reqwest::{ | |
}; | ||
use semver::Version; | ||
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; | ||
use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime}; | ||
use tauri::{ | ||
utils::platform::current_exe, utils::__TAURI_BUNDLE_TYPE, AppHandle, Resource, Runtime, | ||
}; | ||
use time::OffsetDateTime; | ||
use url::Url; | ||
|
||
|
@@ -37,6 +39,31 @@ use crate::{ | |
|
||
const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); | ||
|
||
#[derive(Clone)] | ||
pub enum Installer { | ||
AppImage, | ||
Deb, | ||
Rpm, | ||
|
||
App, | ||
|
||
Msi, | ||
Nsis, | ||
} | ||
|
||
impl Installer { | ||
fn suffix(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)] | ||
pub struct ReleaseManifestPlatform { | ||
/// Download URL for the platform | ||
|
@@ -71,26 +98,39 @@ pub struct RemoteRelease { | |
|
||
impl RemoteRelease { | ||
/// The release's download URL for the given target. | ||
pub fn download_url(&self, target: &str) -> Result<&Url> { | ||
pub fn download_url(&self, target: &str, installer: Option<Installer>) -> Result<&Url> { | ||
let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); | ||
Legend-Master marked this conversation as resolved.
Show resolved
Hide resolved
|
||
match self.data { | ||
RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url), | ||
RemoteReleaseInner::Static { ref platforms } => platforms | ||
.get(target) | ||
.map_or(Err(Error::TargetNotFound(target.to_string())), |p| { | ||
Ok(&p.url) | ||
}), | ||
RemoteReleaseInner::Static { ref platforms } => platforms.get(target).map_or_else( | ||
|| match fallback_target { | ||
Some(fallback) => platforms.get(&fallback).map_or( | ||
Err(Error::TargetsNotFound(target.to_string(), fallback)), | ||
|p| Ok(&p.url), | ||
), | ||
None => Err(Error::TargetNotFound(target.to_string())), | ||
}, | ||
|p| Ok(&p.url), | ||
), | ||
} | ||
} | ||
|
||
/// The release's signature for the given target. | ||
pub fn signature(&self, target: &str) -> Result<&String> { | ||
pub fn signature(&self, target: &str, installer: Option<Installer>) -> Result<&String> { | ||
let fallback_target = installer.map(|installer| format!("{target}-{}", installer.suffix())); | ||
|
||
match self.data { | ||
RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature), | ||
RemoteReleaseInner::Static { ref platforms } => platforms | ||
.get(target) | ||
.map_or(Err(Error::TargetNotFound(target.to_string())), |platform| { | ||
Ok(&platform.signature) | ||
}), | ||
RemoteReleaseInner::Static { ref platforms } => platforms.get(target).map_or_else( | ||
|| match fallback_target { | ||
Some(fallback) => platforms.get(&fallback).map_or( | ||
Err(Error::TargetsNotFound(target.to_string(), fallback)), | ||
|p| Ok(&p.signature), | ||
), | ||
None => Err(Error::TargetNotFound(target.to_string())), | ||
}, | ||
|p| Ok(&p.signature), | ||
), | ||
} | ||
} | ||
} | ||
|
@@ -270,7 +310,8 @@ impl UpdaterBuilder { | |
(target.clone(), target) | ||
} else { | ||
let target = get_updater_target().ok_or(Error::UnsupportedOs)?; | ||
(target.to_string(), format!("{target}-{arch}")) | ||
let json_target = format!("{target}-{arch}"); | ||
(target.to_owned(), json_target) | ||
}; | ||
|
||
let executable_path = self.executable_path.clone().unwrap_or(current_exe()?); | ||
|
@@ -327,7 +368,7 @@ pub struct Updater { | |
proxy: Option<Url>, | ||
endpoints: Vec<Url>, | ||
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, | ||
// The value we search if the updater server returns a JSON with the `platforms` object | ||
json_target: String, | ||
|
@@ -342,11 +383,21 @@ pub struct Updater { | |
} | ||
|
||
impl Updater { | ||
fn get_updater_installer(&self) -> Result<Option<Installer>> { | ||
match __TAURI_BUNDLE_TYPE { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do we want to do for Mac, iOS and Android? Or in general case when the binary was not patched and we don't know the bundle type? I think the simplest thing to do would be to return PackageFormat::Unknown in case the binary was not patched and use We have BundleType in tauri::utils::config but if we want to return Unknown I would have to create new type. What would be the best place to put it? tauri::utils::config? some new module? |
||
"DEB_BUNDLE" => Ok(Some(Installer::Deb)), | ||
"RPM_BUNDLE" => Ok(Some(Installer::Rpm)), | ||
"APP_BUNDLE" => Ok(Some(Installer::AppImage)), | ||
"MSI_BUNDLE" => Ok(Some(Installer::Msi)), | ||
"NSS_BUNDLE" => Ok(Some(Installer::Nsis)), | ||
_ => Err(Error::UnknownInstaller), | ||
} | ||
} | ||
|
||
pub async fn check(&self) -> Result<Option<Update>> { | ||
// we want JSON only | ||
let mut headers = self.headers.clone(); | ||
headers.insert("Accept", HeaderValue::from_str("application/json").unwrap()); | ||
|
||
// Set SSL certs for linux if they aren't available. | ||
#[cfg(target_os = "linux")] | ||
{ | ||
|
@@ -464,6 +515,8 @@ impl Updater { | |
None => release.version > self.current_version, | ||
}; | ||
|
||
let installer = self.get_updater_installer()?; | ||
|
||
let update = if should_update { | ||
Some(Update { | ||
run_on_main_thread: self.run_on_main_thread.clone(), | ||
|
@@ -475,9 +528,14 @@ impl Updater { | |
extract_path: self.extract_path.clone(), | ||
version: release.version.to_string(), | ||
date: release.pub_date, | ||
download_url: release.download_url(&self.json_target)?.to_owned(), | ||
signature: release.signature(&self.json_target)?.to_owned(), | ||
body: release.notes, | ||
download_url: release | ||
.download_url(&self.json_target, installer.clone())? | ||
.to_owned(), | ||
body: release.notes.clone(), | ||
signature: release | ||
.signature(&self.json_target, installer.clone())? | ||
.to_owned(), | ||
installer, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the installer would return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is correct. We use this information later in |
||
raw_json: raw_json.unwrap(), | ||
timeout: None, | ||
proxy: self.proxy.clone(), | ||
|
@@ -511,6 +569,8 @@ pub struct Update { | |
pub date: Option<OffsetDateTime>, | ||
/// Target | ||
pub target: String, | ||
/// Current installer | ||
pub installer: Option<Installer>, | ||
/// Download URL announced | ||
pub download_url: Url, | ||
/// Signature announced | ||
|
@@ -843,11 +903,10 @@ impl Update { | |
/// └── ... | ||
/// | ||
fn install_inner(&self, bytes: &[u8]) -> Result<()> { | ||
if self.is_deb_package() { | ||
self.install_deb(bytes) | ||
} else { | ||
// Handle AppImage or other formats | ||
self.install_appimage(bytes) | ||
match self.installer { | ||
Some(Installer::Deb) => self.install_deb(bytes), | ||
Some(Installer::Rpm) => self.install_rpm(bytes), | ||
_ => self.install_appimage(bytes), | ||
} | ||
} | ||
|
||
|
@@ -924,46 +983,25 @@ impl Update { | |
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<()> { | ||
// First verify the bytes are actually a .deb package | ||
if !infer::archive::is_deb(bytes) { | ||
log::warn!("update is not a valid deb package"); | ||
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 | ||
let tmp_dir_locations = vec![ | ||
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>, | ||
|
@@ -975,15 +1013,19 @@ impl Update { | |
for tmp_dir_location in tmp_dir_locations { | ||
if let Some(path) = tmp_dir_location() { | ||
if let Ok(tmp_dir) = tempfile::Builder::new() | ||
.prefix("tauri_deb_update") | ||
.prefix("tauri_rpm_update") | ||
.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 | ||
if std::fs::write(&deb_path, bytes).is_ok() { | ||
if std::fs::write(&pkg_path, bytes).is_ok() { | ||
// 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 | ||
} | ||
|
@@ -994,12 +1036,17 @@ impl Update { | |
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) | ||
if let Ok(status) = std::process::Command::new("pkexec") | ||
.arg("dpkg") | ||
.arg("-i") | ||
.arg(deb_path) | ||
.arg(install_cmd) | ||
.arg(install_arg) | ||
.arg(pkg_path) | ||
.status() | ||
{ | ||
if status.success() { | ||
|
@@ -1010,24 +1057,24 @@ impl Update { | |
|
||
// 2. Try zenity or kdialog for a graphical sudo experience | ||
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"); | ||
return Ok(()); | ||
} | ||
} | ||
|
||
// 3. Final fallback: terminal sudo | ||
let status = std::process::Command::new("sudo") | ||
.arg("dpkg") | ||
.arg("-i") | ||
.arg(deb_path) | ||
.arg(install_cmd) | ||
.arg(install_arg) | ||
.arg(pkg_path) | ||
.status()?; | ||
|
||
if status.success() { | ||
log::debug!("installed deb with sudo"); | ||
Ok(()) | ||
} else { | ||
Err(Error::DebInstallFailed) | ||
Err(Error::PackageInstallFailed) | ||
} | ||
} | ||
|
||
|
@@ -1061,15 +1108,21 @@ impl Update { | |
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::process::{Command, Stdio}; | ||
|
||
let mut child = Command::new("sudo") | ||
.arg("-S") // read password from stdin | ||
.arg("dpkg") | ||
.arg("-i") | ||
.arg(deb_path) | ||
.arg(install_cmd) | ||
.arg(install_arg) | ||
.arg(pkg_path) | ||
.stdin(Stdio::piped()) | ||
.stdout(Stdio::piped()) | ||
.stderr(Stdio::piped()) | ||
|
Uh oh!
There was an error while loading. Please reload this page.