Implement per-distro installation flow

This commit is contained in:
stoorps 2026-03-19 21:59:18 +00:00
parent caf870d05e
commit b9b60e9b6c
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
21 changed files with 1109 additions and 33 deletions

View file

@ -0,0 +1,36 @@
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
use aim_core::platform::DesktopHelpers;
use std::fs;
use tempfile::tempdir;
#[test]
fn integration_failure_removes_new_payload_and_generated_files() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
let blocking_path = root.path().join("not-a-directory");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
fs::write(&blocking_path, "blocker").unwrap();
let final_payload_path = payload_root.join("bat.AppImage");
let desktop_entry_path = blocking_path.join("aim-bat.desktop");
let error = execute_install(&InstallRequest {
staging_root: &staging_root,
final_payload_path: &final_payload_path,
artifact_bytes: b"\x7fELFAppImage",
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_entry_path,
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: None,
icon_bytes: None,
}),
helpers: DesktopHelpers::default(),
})
.unwrap_err();
assert!(error.to_string().contains("desktop integration failed"));
assert!(!final_payload_path.exists());
assert!(!desktop_entry_path.exists());
}

View file

@ -0,0 +1,127 @@
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
use aim_core::platform::DesktopHelpers;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
#[test]
fn install_writes_desktop_entry_and_reports_refresh_warning_only() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
let desktop_root = root.path().join("applications");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
fs::create_dir(&desktop_root).unwrap();
let outcome = execute_install(&InstallRequest {
staging_root: &staging_root,
final_payload_path: &payload_root.join("bat.AppImage"),
artifact_bytes: b"\x7fELFAppImage",
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: None,
icon_bytes: None,
}),
helpers: DesktopHelpers::default(),
})
.unwrap();
assert!(outcome.desktop_entry_path.unwrap().exists());
assert!(!outcome.warnings.is_empty());
}
#[test]
fn install_executes_refresh_helpers_when_available() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
let desktop_root = root.path().join("applications");
let helper_root = root.path().join("helpers");
let log_path = root.path().join("helpers.log");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
fs::create_dir(&desktop_root).unwrap();
fs::create_dir(&helper_root).unwrap();
let update_helper = helper_root.join("update-desktop-database");
let icon_helper = helper_root.join("gtk-update-icon-cache");
fs::write(
&update_helper,
format!("#!/bin/sh\necho desktop:$1 >> {}\n", log_path.display()),
)
.unwrap();
fs::write(
&icon_helper,
format!("#!/bin/sh\necho icon:$3 >> {}\n", log_path.display()),
)
.unwrap();
fs::set_permissions(&update_helper, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&icon_helper, fs::Permissions::from_mode(0o755)).unwrap();
let icon_root = root.path().join("icons/hicolor/256x256/apps");
fs::create_dir_all(&icon_root).unwrap();
let outcome = execute_install(&InstallRequest {
staging_root: &staging_root,
final_payload_path: &payload_root.join("bat.AppImage"),
artifact_bytes: b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: Some(&icon_root.join("bat.png")),
icon_bytes: None,
}),
helpers: DesktopHelpers {
update_desktop_database: true,
gtk_update_icon_cache: true,
update_desktop_database_path: Some(update_helper),
gtk_update_icon_cache_path: Some(icon_helper),
},
})
.unwrap();
assert!(outcome.warnings.is_empty());
let log = fs::read_to_string(&log_path).unwrap();
assert!(log.contains("desktop:"));
assert!(log.contains("icon:"));
}
#[test]
fn install_extracts_icon_from_appimage_payload_when_icon_path_is_requested() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
let desktop_root = root.path().join("applications");
let icon_root = root.path().join("icons/hicolor/256x256/apps");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
fs::create_dir(&desktop_root).unwrap();
fs::create_dir_all(&icon_root).unwrap();
let outcome = execute_install(&InstallRequest {
staging_root: &staging_root,
final_payload_path: &payload_root.join("bat.AppImage"),
artifact_bytes: b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: Some(&icon_root.join("bat.png")),
icon_bytes: None,
}),
helpers: DesktopHelpers::default(),
})
.unwrap();
let icon_path = outcome.icon_path.unwrap();
assert!(icon_path.exists());
assert!(
fs::read(&icon_path)
.unwrap()
.starts_with(b"\x89PNG\r\n\x1a\n")
);
}

View file

@ -0,0 +1,32 @@
use aim_core::integration::install::stage_and_commit_payload;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
#[test]
fn payload_commit_moves_staged_appimage_into_final_location() {
let root = tempdir().unwrap();
let staging_root = root.path().join("staging");
let payload_root = root.path().join("payloads");
fs::create_dir(&staging_root).unwrap();
fs::create_dir(&payload_root).unwrap();
let final_payload_path = payload_root.join("bat.AppImage");
let outcome =
stage_and_commit_payload(&staging_root, &final_payload_path, b"\x7fELFAppImage").unwrap();
assert_eq!(
outcome
.final_payload_path
.extension()
.and_then(|ext| ext.to_str()),
Some("AppImage")
);
assert!(outcome.final_payload_path.exists());
let mode = fs::metadata(&outcome.final_payload_path)
.unwrap()
.permissions()
.mode();
assert_eq!(mode & 0o111, 0o111);
}

View file

@ -0,0 +1,49 @@
use aim_core::integration::policy::{IntegrationMode, resolve_install_policy};
use aim_core::platform::{DistroFamily, HostCapabilities, InstallScope};
use std::path::Path;
#[test]
fn immutable_system_request_downgrades_to_user_when_allowed() {
let capabilities = HostCapabilities::immutable_user_only();
let policy =
resolve_install_policy(DistroFamily::Immutable, InstallScope::System, &capabilities)
.unwrap();
assert_eq!(policy.scope, InstallScope::User);
assert_eq!(policy.integration_mode, IntegrationMode::Degraded);
assert!(!policy.warnings.is_empty());
}
#[test]
fn nix_system_request_is_denied() {
let error = resolve_install_policy(
DistroFamily::Nix,
InstallScope::System,
&HostCapabilities::default(),
)
.unwrap_err();
assert!(error.contains("not supported on Nix hosts"));
}
#[test]
fn system_policy_uses_managed_payload_and_native_integration_roots() {
let policy = resolve_install_policy(
DistroFamily::Fedora,
InstallScope::System,
&HostCapabilities::default(),
)
.unwrap();
assert_eq!(policy.scope, InstallScope::System);
assert_eq!(policy.payload_root, Path::new("/opt/aim/appimages"));
assert_eq!(
policy.desktop_entry_root,
Path::new("/usr/share/applications")
);
assert_eq!(
policy.icon_root,
Path::new("/usr/share/icons/hicolor/256x256/apps")
);
assert_eq!(policy.integration_mode, IntegrationMode::Full);
}

View file

@ -0,0 +1,52 @@
use aim_core::platform::capabilities::{probe_desktop_helpers, probe_writable_roots};
use aim_core::platform::distro::{DistroFamily, detect_distro_family};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
#[test]
fn detects_fedora_family_from_os_release() {
let distro = detect_distro_family("ID=fedora\nID_LIKE=rhel centos\n");
assert_eq!(distro, DistroFamily::Fedora);
}
#[test]
fn detects_immutable_family_from_variant_id() {
let distro = detect_distro_family("ID=fedora\nVARIANT_ID=silverblue\n");
assert_eq!(distro, DistroFamily::Immutable);
}
#[test]
fn probes_desktop_helpers_from_search_paths() {
let helper_dir = tempdir().unwrap();
let update_desktop_database = helper_dir.path().join("update-desktop-database");
let gtk_update_icon_cache = helper_dir.path().join("gtk-update-icon-cache");
fs::write(&update_desktop_database, "#!/bin/sh\n").unwrap();
fs::write(&gtk_update_icon_cache, "#!/bin/sh\n").unwrap();
fs::set_permissions(&update_desktop_database, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&gtk_update_icon_cache, fs::Permissions::from_mode(0o755)).unwrap();
let helpers = probe_desktop_helpers(&[helper_dir.path()]);
assert!(helpers.update_desktop_database);
assert!(helpers.gtk_update_icon_cache);
}
#[test]
fn probes_writable_roots_from_candidate_directories() {
let root = tempdir().unwrap();
let payload = root.path().join("payload");
let desktop_entries = root.path().join("applications");
let icons = root.path().join("icons");
fs::create_dir(&payload).unwrap();
fs::create_dir(&desktop_entries).unwrap();
fs::create_dir(&icons).unwrap();
let writable = probe_writable_roots(&payload, &desktop_entries, &icons);
assert!(writable.payload);
assert!(writable.desktop_entries);
assert!(writable.icons);
}