Skip to content

Commit

Permalink
feat(updater): Add .deb Package Support to Linux Updater (#1991)
Browse files Browse the repository at this point in the history
  • Loading branch information
jLynx authored Nov 26, 2024
1 parent c665818 commit f8f2eef
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changes/deb-update-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"updater": "minor"
---

Added support for `.deb` package updates on Linux systems.
6 changes: 6 additions & 0 deletions plugins/updater/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
171 changes: 167 additions & 4 deletions plugins/updater/src/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ impl Update {
}
}

/// Linux (AppImage)
/// Linux (AppImage and Deb)
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
Expand All @@ -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()?;

Expand Down Expand Up @@ -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<dyn FnOnce() -> Option<PathBuf>>,
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<String> {
// 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<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)
.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
Expand Down

0 comments on commit f8f2eef

Please sign in to comment.