Skip to content

Commit 90f1231

Browse files
jLynxgezihuzi
authored andcommitted
feat(updater): Add .deb Package Support to Linux Updater (tauri-apps#1991)
1 parent 8795c01 commit 90f1231

File tree

3 files changed

+178
-4
lines changed

3 files changed

+178
-4
lines changed

.changes/deb-update-support.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"updater": "minor"
3+
---
4+
5+
Added support for `.deb` package updates on Linux systems.

plugins/updater/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ pub enum Error {
6363
TempDirNotOnSameMountPoint,
6464
#[error("binary for the current target not found in the archive")]
6565
BinaryNotFoundInArchive,
66+
#[error("failed to create temporary directory")]
67+
TempDirNotFound,
68+
#[error("Authentication failed or was cancelled")]
69+
AuthenticationFailed,
70+
#[error("Failed to install .deb package")]
71+
DebInstallFailed,
6672
#[error("invalid updater binary format")]
6773
InvalidUpdaterFormat,
6874
#[error(transparent)]

plugins/updater/src/updater.rs

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ impl Update {
748748
}
749749
}
750750

751-
/// Linux (AppImage)
751+
/// Linux (AppImage and Deb)
752752
#[cfg(any(
753753
target_os = "linux",
754754
target_os = "dragonfly",
@@ -760,12 +760,19 @@ impl Update {
760760
/// ### Expected structure:
761761
/// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
762762
/// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
763+
/// ├── [AppName]_[version]_amd64.deb # Debian package
763764
/// └── ...
764765
///
765-
/// We should have an AppImage already installed to be able to copy and install
766-
/// the extract_path is the current AppImage path
767-
/// tmp_dir is where our new AppImage is found
768766
fn install_inner(&self, bytes: &[u8]) -> Result<()> {
767+
if self.is_deb_package() {
768+
self.install_deb(bytes)
769+
} else {
770+
// Handle AppImage or other formats
771+
self.install_appimage(bytes)
772+
}
773+
}
774+
775+
fn install_appimage(&self, bytes: &[u8]) -> Result<()> {
769776
use std::os::unix::fs::{MetadataExt, PermissionsExt};
770777
let extract_path_metadata = self.extract_path.metadata()?;
771778

@@ -835,6 +842,162 @@ impl Update {
835842

836843
Err(Error::TempDirNotOnSameMountPoint)
837844
}
845+
846+
fn is_deb_package(&self) -> bool {
847+
// First check if we're in a typical Debian installation path
848+
let in_system_path = self
849+
.extract_path
850+
.to_str()
851+
.map(|p| p.starts_with("/usr"))
852+
.unwrap_or(false);
853+
854+
if !in_system_path {
855+
return false;
856+
}
857+
858+
// Then verify it's actually a Debian-based system by checking for dpkg
859+
let dpkg_exists = std::path::Path::new("/var/lib/dpkg").exists();
860+
let apt_exists = std::path::Path::new("/etc/apt").exists();
861+
862+
// Additional check for the package in dpkg database
863+
let package_in_dpkg = if let Ok(output) = std::process::Command::new("dpkg")
864+
.args(["-S", &self.extract_path.to_string_lossy()])
865+
.output()
866+
{
867+
output.status.success()
868+
} else {
869+
false
870+
};
871+
872+
// Consider it a deb package only if:
873+
// 1. We're in a system path AND
874+
// 2. We have Debian package management tools AND
875+
// 3. The binary is tracked by dpkg
876+
dpkg_exists && apt_exists && package_in_dpkg
877+
}
878+
879+
fn install_deb(&self, bytes: &[u8]) -> Result<()> {
880+
// First verify the bytes are actually a .deb package
881+
if !infer::archive::is_deb(bytes) {
882+
return Err(Error::InvalidUpdaterFormat);
883+
}
884+
885+
// Try different temp directories
886+
let tmp_dir_locations = vec![
887+
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
888+
Box::new(dirs::cache_dir),
889+
Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
890+
];
891+
892+
// Try writing to multiple temp locations until one succeeds
893+
for tmp_dir_location in tmp_dir_locations {
894+
if let Some(path) = tmp_dir_location() {
895+
if let Ok(tmp_dir) = tempfile::Builder::new()
896+
.prefix("tauri_deb_update")
897+
.tempdir_in(path)
898+
{
899+
let deb_path = tmp_dir.path().join("package.deb");
900+
901+
// Try writing the .deb file
902+
if std::fs::write(&deb_path, bytes).is_ok() {
903+
// If write succeeds, proceed with installation
904+
return self.try_install_with_privileges(&deb_path);
905+
}
906+
// If write fails, continue to next temp location
907+
}
908+
}
909+
}
910+
911+
// If we get here, all temp locations failed
912+
Err(Error::TempDirNotFound)
913+
}
914+
915+
fn try_install_with_privileges(&self, deb_path: &Path) -> Result<()> {
916+
// 1. First try using pkexec (graphical sudo prompt)
917+
if let Ok(status) = std::process::Command::new("pkexec")
918+
.arg("dpkg")
919+
.arg("-i")
920+
.arg(deb_path)
921+
.status()
922+
{
923+
if status.success() {
924+
return Ok(());
925+
}
926+
}
927+
928+
// 2. Try zenity or kdialog for a graphical sudo experience
929+
if let Ok(password) = self.get_password_graphically() {
930+
if self.install_with_sudo(deb_path, &password)? {
931+
return Ok(());
932+
}
933+
}
934+
935+
// 3. Final fallback: terminal sudo
936+
let status = std::process::Command::new("sudo")
937+
.arg("dpkg")
938+
.arg("-i")
939+
.arg(deb_path)
940+
.status()?;
941+
942+
if status.success() {
943+
Ok(())
944+
} else {
945+
Err(Error::DebInstallFailed)
946+
}
947+
}
948+
949+
fn get_password_graphically(&self) -> Result<String> {
950+
// Try zenity first
951+
let zenity_result = std::process::Command::new("zenity")
952+
.args([
953+
"--password",
954+
"--title=Authentication Required",
955+
"--text=Enter your password to install the update:",
956+
])
957+
.output();
958+
959+
if let Ok(output) = zenity_result {
960+
if output.status.success() {
961+
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
962+
}
963+
}
964+
965+
// Fall back to kdialog if zenity fails or isn't available
966+
let kdialog_result = std::process::Command::new("kdialog")
967+
.args(["--password", "Enter your password to install the update:"])
968+
.output();
969+
970+
if let Ok(output) = kdialog_result {
971+
if output.status.success() {
972+
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
973+
}
974+
}
975+
976+
Err(Error::AuthenticationFailed)
977+
}
978+
979+
fn install_with_sudo(&self, deb_path: &Path, password: &str) -> Result<bool> {
980+
use std::io::Write;
981+
use std::process::{Command, Stdio};
982+
983+
let mut child = Command::new("sudo")
984+
.arg("-S") // read password from stdin
985+
.arg("dpkg")
986+
.arg("-i")
987+
.arg(deb_path)
988+
.stdin(Stdio::piped())
989+
.stdout(Stdio::piped())
990+
.stderr(Stdio::piped())
991+
.spawn()?;
992+
993+
if let Some(mut stdin) = child.stdin.take() {
994+
// Write password to stdin
995+
writeln!(stdin, "{}", password)?;
996+
}
997+
998+
let status = child.wait()?;
999+
Ok(status.success())
1000+
}
8381001
}
8391002

8401003
/// MacOS

0 commit comments

Comments
 (0)