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,88 @@
use std::fs::{self, OpenOptions};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DesktopHelpers {
pub update_desktop_database: bool,
pub gtk_update_icon_cache: bool,
pub update_desktop_database_path: Option<PathBuf>,
pub gtk_update_icon_cache_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct WritableRoots {
pub payload: bool,
pub desktop_entries: bool,
pub icons: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HostCapabilities {
pub is_immutable: bool,
pub is_nix: bool,
pub has_desktop_session: bool,
pub helpers: DesktopHelpers,
pub writable_roots: WritableRoots,
}
impl HostCapabilities {
pub fn immutable_user_only() -> Self {
Self {
is_immutable: true,
has_desktop_session: true,
..Self::default()
}
}
}
pub fn probe_desktop_helpers(search_paths: &[&Path]) -> DesktopHelpers {
let update_desktop_database_path = command_path(search_paths, "update-desktop-database");
let gtk_update_icon_cache_path = command_path(search_paths, "gtk-update-icon-cache");
DesktopHelpers {
update_desktop_database: update_desktop_database_path.is_some(),
gtk_update_icon_cache: gtk_update_icon_cache_path.is_some(),
update_desktop_database_path,
gtk_update_icon_cache_path,
}
}
pub fn probe_writable_roots(payload: &Path, desktop_entries: &Path, icons: &Path) -> WritableRoots {
WritableRoots {
payload: is_writable_dir(payload),
desktop_entries: is_writable_dir(desktop_entries),
icons: is_writable_dir(icons),
}
}
fn command_path(search_paths: &[&Path], executable: &str) -> Option<PathBuf> {
search_paths
.iter()
.map(|path| path.join(executable))
.find(|candidate| is_executable_file(candidate))
}
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
fn is_writable_dir(path: &Path) -> bool {
if !path.is_dir() {
return false;
}
let probe_path = path.join(".aim-write-test");
let result = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&probe_path);
match result {
Ok(_) => {
let _ = fs::remove_file(&probe_path);
true
}
Err(_) => false,
}
}

View file

@ -0,0 +1,75 @@
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DistroFamily {
Debian,
Fedora,
Arch,
OpenSuse,
Alpine,
Nix,
Immutable,
Generic,
}
pub fn detect_distro_family(os_release: &str) -> DistroFamily {
let id = lookup_field(os_release, "ID");
let id_like = lookup_field(os_release, "ID_LIKE");
let variant_id = lookup_field(os_release, "VARIANT_ID");
if matches_any(id, id_like, &["nixos"]) {
return DistroFamily::Nix;
}
if matches_field(variant_id, &["silverblue", "kinoite", "coreos", "aurora"])
|| matches_any(
id,
id_like,
&["silverblue", "kinoite", "ublue", "fedora-immutable"],
)
{
return DistroFamily::Immutable;
}
if matches_any(id, id_like, &["fedora", "rhel", "centos"]) {
return DistroFamily::Fedora;
}
if matches_any(id, id_like, &["debian", "ubuntu"]) {
return DistroFamily::Debian;
}
if matches_any(id, id_like, &["arch", "manjaro"]) {
return DistroFamily::Arch;
}
if matches_any(id, id_like, &["opensuse", "suse", "sles"]) {
return DistroFamily::OpenSuse;
}
if matches_any(id, id_like, &["alpine"]) {
return DistroFamily::Alpine;
}
DistroFamily::Generic
}
fn lookup_field<'a>(os_release: &'a str, key: &str) -> Option<&'a str> {
os_release
.lines()
.find_map(|line| line.strip_prefix(&format!("{key}=")))
.map(trim_value)
}
fn trim_value(value: &str) -> &str {
value.trim().trim_matches('"')
}
fn matches_any(id: Option<&str>, id_like: Option<&str>, needles: &[&str]) -> bool {
matches_field(id, needles) || matches_field(id_like, needles)
}
fn matches_field(field: Option<&str>, needles: &[&str]) -> bool {
field
.into_iter()
.flat_map(|value| value.split_ascii_whitespace())
.any(|candidate| needles.contains(&candidate))
}

View file

@ -1,5 +1,18 @@
pub mod capabilities;
pub mod distro;
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
pub use crate::domain::app::InstallScope;
pub use capabilities::{DesktopHelpers, HostCapabilities, WritableRoots};
pub use distro::{DistroFamily, detect_distro_family};
const OS_RELEASE_PATH_ENV: &str = "AIM_OS_RELEASE_PATH";
const HELPER_PATHS_ENV: &str = "AIM_HELPER_PATHS";
pub fn user_managed_appimages_dir(home_dir: &Path) -> PathBuf {
home_dir.join(".local/lib/aim/appimages")
}
@ -23,3 +36,71 @@ pub fn system_applications_dir() -> PathBuf {
pub fn system_icons_dir() -> PathBuf {
PathBuf::from("/usr/share/icons/hicolor/256x256/apps")
}
pub fn probe_live_host(
home_dir: &Path,
requested_scope: InstallScope,
) -> io::Result<(DistroFamily, HostCapabilities)> {
let os_release = load_os_release()?;
let family = detect_distro_family(&os_release);
let helper_paths = helper_search_paths();
let helper_refs = helper_paths
.iter()
.map(PathBuf::as_path)
.collect::<Vec<_>>();
let helpers = capabilities::probe_desktop_helpers(&helper_refs);
let (payload_root, desktop_root, icon_root) = match requested_scope {
InstallScope::User => (
user_managed_appimages_dir(home_dir),
user_applications_dir(home_dir),
user_icons_dir(home_dir),
),
InstallScope::System => (
system_managed_appimages_dir(),
system_applications_dir(),
system_icons_dir(),
),
};
Ok((
family,
HostCapabilities {
is_immutable: family == DistroFamily::Immutable,
is_nix: family == DistroFamily::Nix,
has_desktop_session: env::var_os("DISPLAY").is_some()
|| env::var_os("WAYLAND_DISPLAY").is_some()
|| env::var_os("XDG_CURRENT_DESKTOP").is_some(),
helpers,
writable_roots: capabilities::probe_writable_roots(
&payload_root,
&desktop_root,
&icon_root,
),
},
))
}
fn load_os_release() -> io::Result<String> {
let path = env::var_os(OS_RELEASE_PATH_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/etc/os-release"));
match fs::read_to_string(path) {
Ok(contents) => Ok(contents),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(String::new()),
Err(error) => Err(error),
}
}
fn helper_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(extra_paths) = env::var_os(HELPER_PATHS_ENV) {
paths.extend(env::split_paths(&extra_paths));
}
if let Some(system_paths) = env::var_os("PATH") {
paths.extend(env::split_paths(&system_paths));
}
paths
}