Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8aa131a
fallback targets
kandrelczyk Feb 20, 2025
597ae9f
Merge branch 'tauri-apps:v2' into feature/fallback_targets
kandrelczyk Feb 21, 2025
6ae53cf
linux test
kandrelczyk Mar 1, 2025
e2e27ac
Merge branch 'feature/fallback_targets' of github.com:kandrelczyk/plu…
kandrelczyk Mar 1, 2025
ae7a2e3
linux ready
kandrelczyk Mar 1, 2025
dc75b76
RPM installation
kandrelczyk Mar 3, 2025
199a52b
small error fix
kandrelczyk Mar 5, 2025
ede0c68
fix windows build
kandrelczyk Mar 7, 2025
d50947c
windows tests
kandrelczyk Mar 7, 2025
ef95298
Merge branch 'feature/fallback_targets' of github.com:kandrelczyk/plu…
kandrelczyk Mar 7, 2025
012f633
add aider files to .gitignore
kandrelczyk Mar 9, 2025
c2877ec
get bundle type out of patched variable
kandrelczyk Mar 18, 2025
c9d0a6c
windows tests
kandrelczyk Mar 18, 2025
896678a
Merge branch 'feature/fallback_targets' of github.com:kandrelczyk/plu…
kandrelczyk Apr 1, 2025
8cb79a3
patch windows binary
kandrelczyk Apr 6, 2025
b1a8781
format
kandrelczyk Apr 7, 2025
0630002
Merge branch 'feature/fallback_targets' of github.com:kandrelczyk/plu…
kandrelczyk Apr 7, 2025
3a43397
fix bundler
kandrelczyk Apr 10, 2025
68564e0
Merge branch 'v2' of github.com:kandrelczyk/plugins-workspace into fe…
kandrelczyk Apr 10, 2025
24504e4
remove local tauri dependency
kandrelczyk Apr 11, 2025
5d12c97
remove print
kandrelczyk Apr 11, 2025
2137583
rever Cargo.lock
kandrelczyk Apr 12, 2025
b80a295
move __TAURI_BUNDLE_TYPE to tauri::utils
kandrelczyk Apr 12, 2025
f75d32b
get_current_bundle_type
kandrelczyk Apr 14, 2025
1ea7522
Merge remote-tracking branch 'origin/v2' into feature/fallback_targets
lucasfernog Jul 7, 2025
940ed70
update tauri
lucasfernog Jul 7, 2025
201a001
fix macos integration test
lucasfernog Jul 7, 2025
80b07ee
Merge branch 'v2' into feature/fallback_targets
kandrelczyk Jul 8, 2025
513376b
Merge branch 'feature/fallback_targets' of github.com:kandrelczyk/plu…
kandrelczyk Jul 8, 2025
9e9d7bc
fix fallback logic
kandrelczyk Jul 9, 2025
f7c10ea
amend! fallback targets
kandrelczyk Jul 10, 2025
2fdda08
reformat
kandrelczyk Jul 14, 2025
4913dbe
fix tests
kandrelczyk Jul 16, 2025
5eae160
reformat
kandrelczyk Jul 16, 2025
5056198
Merge branch 'v2' into feature/fallback_targets
kandrelczyk Jul 21, 2025
5b4c1c1
bump tari versio
kandrelczyk Jul 22, 2025
6c2f563
fix fallback logic
kandrelczyk Jul 23, 2025
c13e583
restore Cargo.lock
kandrelczyk Jul 24, 2025
043d89f
Merge branch 'v2' of https://github.com/tauri-apps/plugins-workspace …
Legend-Master Jul 25, 2025
0a495cc
Bump tauri and add notes
Legend-Master Jul 25, 2025
e67750c
Rename some staffs
Legend-Master Jul 25, 2025
3d7200b
move target logic
kandrelczyk Jul 26, 2025
f563b02
Merge branch 'feature/fallback_targets' of github.com:kandrelczyk/plu…
kandrelczyk Jul 26, 2025
ad8d711
Refactor the target fallback to a function
Legend-Master Jul 26, 2025
c8d57a8
Format and clippy
Legend-Master Jul 26, 2025
fc85f3b
Keep target in `Update` since it's public
Legend-Master Jul 26, 2025
7d14a45
Keep updater/tests/app-updater/src/main.rs lf
Legend-Master Jul 26, 2025
9b3171f
Revert changes in tests/app-updater/src/main.rs
Legend-Master Jul 26, 2025
736223c
Clean up
Legend-Master Jul 26, 2025
d9cd15a
Merge remote-tracking branch 'upstream/v2' into feature/fallback_targets
Legend-Master Jul 26, 2025
feda0b7
changefile
kandrelczyk Jul 26, 2025
f31125a
Bump updater-js as well
Legend-Master Jul 26, 2025
b8c1cb2
update pub fn target docs
kandrelczyk Jul 26, 2025
f2a6b91
update pub fn target docs
kandrelczyk Jul 26, 2025
3101558
Update plugins/updater/src/error.rs
kandrelczyk Jul 28, 2025
0c382a4
Update plugins/updater/src/updater.rs
kandrelczyk Jul 28, 2025
a52b2b9
Update plugins/updater/src/updater.rs
kandrelczyk Jul 28, 2025
b62e132
suggestios
kandrelczyk Jul 28, 2025
cdcc915
Merge branch 'v2' into feature/fallback_targets
kandrelczyk Aug 11, 2025
73a211b
add comment
kandrelczyk Aug 11, 2025
51d4b9f
restore error
kandrelczyk Aug 11, 2025
ed77d90
Revert "Bump tauri and add notes"
Legend-Master Aug 26, 2025
6198160
Merge remote-tracking branch 'upstream/v2' into feature/fallback_targets
Legend-Master Aug 26, 2025
abcee1f
Revert "bump tari versio"
Legend-Master Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ pids
*.sublime*
.idea
debug.log
TODO.md
TODO.md
.aider.*
14 changes: 10 additions & 4 deletions plugins/updater/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub enum Error {
/// Operating system is not supported.
#[error("Unsupported OS, expected one of `linux`, `darwin` or `windows`.")]
UnsupportedOs,
/// Can't determine which type of installer was used for the app
#[error("Couldn't determinet installation method")]
UnknownInstaller,
/// Failed to determine updater package extract path
#[error("Failed to determine updater package extract path.")]
FailedToDetermineExtractPath,
Expand All @@ -39,9 +42,12 @@ pub enum Error {
/// `reqwest` crate errors.
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
/// The platform was not found on the updater JSON response.
#[error("the platform `{0}` was not found on the response `platforms` object")]
/// The platform was not found in the updater JSON response.
#[error("the platform `{0}` was not found in the response `platforms` object")]
TargetNotFound(String),
/// Neither the platform not the fallback platform was not found in the updater JSON response.
#[error("the platform `{0}` and `{1}` were not found in the response `platforms` object")]
TargetsNotFound(String, String),
/// Download failed
#[error("`{0}`")]
Network(String),
Expand All @@ -67,8 +73,8 @@ pub enum Error {
TempDirNotFound,
#[error("Authentication failed or was cancelled")]
AuthenticationFailed,
#[error("Failed to install .deb package")]
DebInstallFailed,
#[error("Failed to install package")]
PackageInstallFailed,
#[error("invalid updater binary format")]
InvalidUpdaterFormat,
#[error(transparent)]
Expand Down
201 changes: 127 additions & 74 deletions plugins/updater/src/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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()));
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),
),
}
}
}
Expand Down Expand Up @@ -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()?);
Expand Down Expand Up @@ -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,
Expand All @@ -342,11 +383,21 @@ pub struct Updater {
}

impl Updater {
fn get_updater_installer(&self) -> Result<Option<Installer>> {
match __TAURI_BUNDLE_TYPE {
"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")]
{
Expand Down Expand Up @@ -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(),
Expand All @@ -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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the installer would return Some even when the corresponding entry does not get used for it's download_url or signature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is correct. We use this information later in install_inner to determine installation method. So for example if the current binary was packaged as Deb but we didn't find specific download_url for deb and used the fallback we still assume that it points to deb package and will try to install it as deb. If we know the current binary was packaged as deb it doesn't make sense to try to install the update as AppImage. Only in case we don't know what the current bundle type is will we fallback to AppImage.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that conflates two things:

  • Detecting information about the current environment.
  • Determining what update to install from remote release information.

It also mixes up responsibilities. Should this function reject the update that's being served when it's for the incorrect installer? Or should the consumer of this function handle that?

tauri_utils::platform::bundle_type() already addresses the environment detection aspect.
Especially when converting that bundle is a pure function.

raw_json: raw_json.unwrap(),
timeout: None,
proxy: self.proxy.clone(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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>>,
Expand All @@ -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
}
Expand All @@ -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() {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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())
Expand Down
Loading
Loading