diff --git a/.changes/deb-update-support.md b/.changes/deb-update-support.md new file mode 100644 index 000000000..e71c9e77d --- /dev/null +++ b/.changes/deb-update-support.md @@ -0,0 +1,5 @@ +--- +"updater": "minor" +--- + +Added support for `.deb` package updates on Linux systems. \ No newline at end of file diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs index 1f5d6a15f..d6d9b0ced 100644 --- a/plugins/updater/src/error.rs +++ b/plugins/updater/src/error.rs @@ -63,6 +63,12 @@ pub enum Error { TempDirNotOnSameMountPoint, #[error("binary for the current target not found in the archive")] BinaryNotFoundInArchive, + #[error("failed to create temporary directory")] + TempDirNotFound, + #[error("Authentication failed or was cancelled")] + AuthenticationFailed, + #[error("Failed to install .deb package")] + DebInstallFailed, #[error("invalid updater binary format")] InvalidUpdaterFormat, #[error(transparent)] diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index b1dadd6d2..d66af3dc0 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -748,7 +748,7 @@ impl Update { } } -/// Linux (AppImage) +/// Linux (AppImage and Deb) #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -760,12 +760,19 @@ impl Update { /// ### Expected structure: /// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler /// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage + /// ├── [AppName]_[version]_amd64.deb # Debian package /// └── ... /// - /// We should have an AppImage already installed to be able to copy and install - /// the extract_path is the current AppImage path - /// tmp_dir is where our new AppImage is found 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) + } + } + + fn install_appimage(&self, bytes: &[u8]) -> Result<()> { use std::os::unix::fs::{MetadataExt, PermissionsExt}; let extract_path_metadata = self.extract_path.metadata()?; @@ -835,6 +842,162 @@ 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) { + return Err(Error::InvalidUpdaterFormat); + } + + // Try different temp directories + let tmp_dir_locations = vec![ + Box::new(|| Some(std::env::temp_dir())) as Box Option>, + Box::new(dirs::cache_dir), + Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), + ]; + + // Try writing to multiple temp locations until one succeeds + 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") + .tempdir_in(path) + { + let deb_path = tmp_dir.path().join("package.deb"); + + // Try writing the .deb file + if std::fs::write(&deb_path, bytes).is_ok() { + // If write succeeds, proceed with installation + return self.try_install_with_privileges(&deb_path); + } + // If write fails, continue to next temp location + } + } + } + + // If we get here, all temp locations failed + Err(Error::TempDirNotFound) + } + + fn try_install_with_privileges(&self, deb_path: &Path) -> 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) + .status() + { + if status.success() { + return Ok(()); + } + } + + // 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)? { + return Ok(()); + } + } + + // 3. Final fallback: terminal sudo + let status = std::process::Command::new("sudo") + .arg("dpkg") + .arg("-i") + .arg(deb_path) + .status()?; + + if status.success() { + Ok(()) + } else { + Err(Error::DebInstallFailed) + } + } + + fn get_password_graphically(&self) -> Result { + // Try zenity first + let zenity_result = std::process::Command::new("zenity") + .args([ + "--password", + "--title=Authentication Required", + "--text=Enter your password to install the update:", + ]) + .output(); + + if let Ok(output) = zenity_result { + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + } + + // Fall back to kdialog if zenity fails or isn't available + let kdialog_result = std::process::Command::new("kdialog") + .args(["--password", "Enter your password to install the update:"]) + .output(); + + if let Ok(output) = kdialog_result { + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + } + + Err(Error::AuthenticationFailed) + } + + fn install_with_sudo(&self, deb_path: &Path, password: &str) -> Result { + 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) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + if let Some(mut stdin) = child.stdin.take() { + // Write password to stdin + writeln!(stdin, "{}", password)?; + } + + let status = child.wait()?; + Ok(status.success()) + } } /// MacOS