Skip to content

Feature/fallback 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

Open
wants to merge 24 commits into
base: v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
205 changes: 131 additions & 74 deletions plugins/updater/src/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ 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::{
config::{get_current_bundle_type, PackageType},
platform::current_exe,
},
AppHandle, Resource, Runtime,
};
use time::OffsetDateTime;
use url::Url;

Expand All @@ -37,6 +43,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 +102,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 +314,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 +372,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 +387,21 @@ pub struct Updater {
}

impl Updater {
fn get_updater_installer(&self) -> Result<Option<Installer>> {
match get_current_bundle_type() {
PackageType::Deb => Ok(Some(Installer::Deb)),
PackageType::Rpm => Ok(Some(Installer::Rpm)),
PackageType::AppImage => Ok(Some(Installer::AppImage)),
PackageType::Msi => Ok(Some(Installer::Msi)),
PackageType::Nsis => 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 +519,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 +532,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,
raw_json: raw_json.unwrap(),
timeout: None,
proxy: self.proxy.clone(),
Expand Down Expand Up @@ -511,6 +573,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 +907,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 +987,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 +1017,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 +1040,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 +1061,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 +1112,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