Add system-wide installation via pkexec with polkit policy

This commit is contained in:
lashman
2026-02-28 00:11:57 +02:00
parent c622057830
commit 27eb9f259d
4 changed files with 231 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>Driftwood</vendor>
<vendor_url>https://github.com/driftwood-appimage</vendor_url>
<action id="app.driftwood.Driftwood.system-install">
<description>Install AppImage system-wide</description>
<message>Authentication is required to install an AppImage for all users.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>

View File

@@ -1778,6 +1778,14 @@ impl Database {
Ok(()) Ok(())
} }
pub fn set_system_wide(&self, id: i64, system_wide: bool) -> SqlResult<()> {
self.conn.execute(
"UPDATE appimages SET system_wide = ?2 WHERE id = ?1",
params![id, system_wide as i32],
)?;
Ok(())
}
// --- Launch statistics --- // --- Launch statistics ---
pub fn get_top_launched(&self, limit: i32) -> SqlResult<Vec<(String, u64)>> { pub fn get_top_launched(&self, limit: i32) -> SqlResult<Vec<(String, u64)>> {

View File

@@ -535,6 +535,160 @@ pub fn set_default_app(
Ok(()) Ok(())
} }
/// Install an AppImage system-wide via pkexec.
/// Copies the binary to /opt/driftwood-apps/, desktop file to
/// /usr/share/applications/, and icon to /usr/share/icons/hicolor/.
pub fn install_system_wide(
record: &AppImageRecord,
db: &Database,
) -> Result<(), String> {
let app_name = record
.app_name
.as_deref()
.or(Some(&record.filename))
.ok_or("No app name")?;
let app_id = make_app_id(app_name);
let dest_dir = "/opt/driftwood-apps";
let dest_binary = format!("{}/{}", dest_dir, record.filename);
let desktop_filename = format!("driftwood-{}.desktop", app_id);
let system_desktop = format!("/usr/share/applications/{}", desktop_filename);
let icon_id = format!("driftwood-{}", app_id);
// Create destination directory
let status = Command::new("pkexec")
.args(["mkdir", "-p", dest_dir])
.status()
.map_err(|e| format!("pkexec failed: {}", e))?;
if !status.success() {
return Err("Failed to create system directory".to_string());
}
// Copy AppImage binary
let status = Command::new("pkexec")
.args(["cp", &record.path, &dest_binary])
.status()
.map_err(|e| format!("pkexec cp failed: {}", e))?;
if !status.success() {
return Err("Failed to copy AppImage to system directory".to_string());
}
// Make executable
let status = Command::new("pkexec")
.args(["chmod", "+x", &dest_binary])
.status()
.map_err(|e| format!("pkexec chmod failed: {}", e))?;
if !status.success() {
return Err("Failed to set execute permission".to_string());
}
db.register_modification(record.id, "system_binary", &dest_binary, None).ok();
// Generate system desktop file content
let categories = record.categories.as_deref().unwrap_or("");
let comment = record.description.as_deref().unwrap_or("");
let mut desktop_content = format!("\
[Desktop Entry]
Type=Application
Name={name}
Exec=\"{exec}\" %U
Icon={icon}
Categories={categories}
Comment={comment}
Terminal=false
X-AppImage-Managed-By=Driftwood
",
name = app_name,
exec = escape_exec_arg(&dest_binary),
icon = icon_id,
categories = categories,
comment = comment,
);
let mime_types = record.mime_types.as_deref().unwrap_or("");
let wm_class = record.startup_wm_class.as_deref().unwrap_or("");
if !mime_types.is_empty() {
desktop_content.push_str(&format!("MimeType={}\n", mime_types));
}
if !wm_class.is_empty() {
desktop_content.push_str(&format!("StartupWMClass={}\n", wm_class));
}
// Write desktop file via temp file + pkexec mv
let tmp = std::env::temp_dir().join(&desktop_filename);
fs::write(&tmp, &desktop_content)
.map_err(|e| format!("Failed to write temp desktop file: {}", e))?;
let status = Command::new("pkexec")
.args(["mv", &tmp.to_string_lossy(), &system_desktop])
.status()
.map_err(|e| format!("pkexec mv failed: {}", e))?;
if !status.success() {
return Err("Failed to install system desktop file".to_string());
}
db.register_modification(record.id, "system_desktop", &system_desktop, None).ok();
// Copy icon if available
if let Some(ref cached_icon) = record.icon_path {
let cached = Path::new(cached_icon);
if cached.exists() {
let ext = cached.extension().and_then(|e| e.to_str()).unwrap_or("png");
let (subdir, icon_filename) = if ext == "svg" {
("scalable/apps", format!("{}.svg", icon_id))
} else {
("256x256/apps", format!("{}.png", icon_id))
};
let system_icon_dir = format!("/usr/share/icons/hicolor/{}", subdir);
let system_icon = format!("{}/{}", system_icon_dir, icon_filename);
Command::new("pkexec")
.args(["mkdir", "-p", &system_icon_dir])
.status()
.ok();
let status = Command::new("pkexec")
.args(["cp", &cached.to_string_lossy(), &system_icon])
.status();
if let Ok(s) = status {
if s.success() {
db.register_modification(record.id, "system_icon", &system_icon, None).ok();
}
}
}
}
db.set_system_wide(record.id, true).ok();
Ok(())
}
/// Remove a system-wide installation.
pub fn remove_system_wide(
db: &Database,
appimage_id: i64,
) -> Result<(), String> {
let mods = db.get_modifications(appimage_id).unwrap_or_default();
for m in &mods {
match m.mod_type.as_str() {
"system_binary" | "system_desktop" | "system_icon" => {
let status = Command::new("pkexec")
.args(["rm", "-f", &m.file_path])
.status();
if let Ok(s) = status {
if s.success() {
db.remove_modification(m.id).ok();
}
}
}
_ => {}
}
}
db.set_system_wide(appimage_id, false).ok();
Ok(())
}
fn update_desktop_database() { fn update_desktop_database() {
let apps_dir = applications_dir(); let apps_dir = applications_dir();
Command::new("update-desktop-database") Command::new("update-desktop-database")

View File

@@ -1017,6 +1017,55 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &
}); });
integration_group.add(&wm_class_row); integration_group.add(&wm_class_row);
// System-wide install toggle
if record.integrated {
let syswide_row = adw::SwitchRow::builder()
.title("Install system-wide")
.subtitle(if record.system_wide {
"Installed for all users in /opt/driftwood-apps/"
} else {
"Make available to all users on this computer"
})
.active(record.system_wide)
.tooltip_text(
"Copies the AppImage and its shortcut to system directories \
so all users on this computer can access it. Requires \
administrator privileges."
)
.build();
let record_sw = record.clone();
let db_sw = db.clone();
let toast_sw = toast_overlay.clone();
let record_id_sw = record.id;
syswide_row.connect_active_notify(move |row| {
if row.is_active() {
match integrator::install_system_wide(&record_sw, &db_sw) {
Ok(()) => {
toast_sw.add_toast(adw::Toast::new("Installed system-wide"));
}
Err(e) => {
log::error!("System-wide install failed: {}", e);
toast_sw.add_toast(adw::Toast::new("System-wide install failed"));
row.set_active(false);
}
}
} else {
match integrator::remove_system_wide(&db_sw, record_id_sw) {
Ok(()) => {
toast_sw.add_toast(adw::Toast::new("System-wide install removed"));
}
Err(e) => {
log::error!("Failed to remove system-wide install: {}", e);
toast_sw.add_toast(adw::Toast::new("Failed to remove system-wide install"));
row.set_active(true);
}
}
}
});
integration_group.add(&syswide_row);
}
inner.append(&integration_group); inner.append(&integration_group);
// Version Rollback group // Version Rollback group