use std::path::{Path, PathBuf};
use crate::cmd::pkexec_guix_cmd;
use crate::error::{GuixError, PolkitFailure};
use crate::operation::{spawn_operation_with, ExitClassifier, Operation};
pub const CURRENT_SYSTEM_CONFIG: &str = "/run/current-system/configuration.scm";
pub(crate) const GUIX_PROFILES_ROOT: &str = "/var/guix/profiles";
#[derive(Clone)]
pub struct SystemOps;
impl SystemOps {
pub(crate) fn new() -> Self {
Self
}
pub(crate) fn new_for_tests() -> Self {
Self
}
pub fn current_configuration_path(&self) -> Result<PathBuf, GuixError> {
let p = resolve_config_path();
current_configuration_path_with(&p)
}
pub fn reconfigure(
&self,
config: &Path,
opts: ReconfigureOptions,
) -> Result<Operation, GuixError> {
preflight_auth_agent()?;
let args = build_reconfigure_args(config, &opts);
let c = pkexec_guix_cmd(&args)?;
spawn_operation_with(c, ExitClassifier::Pkexec)
}
}
fn build_reconfigure_args(config: &Path, opts: &ReconfigureOptions) -> Vec<String> {
let mut args: Vec<String> = vec!["system".into(), "reconfigure".into()];
for p in &opts.load_paths {
args.push("-L".into());
args.push(p.to_string_lossy().into_owned());
}
if opts.dry_run {
args.push("--dry-run".into());
}
if opts.allow_downgrades {
args.push("--allow-downgrades".into());
}
args.push(config.to_string_lossy().into_owned());
args
}
#[derive(Debug, Clone, Default)]
pub struct ReconfigureOptions {
pub dry_run: bool,
pub allow_downgrades: bool,
pub load_paths: Vec<PathBuf>,
}
pub(crate) fn preflight_auth_agent() -> Result<(), GuixError> {
if std::env::var_os("LIBGUIX_SKIP_AGENT_CHECK").is_some() {
return Ok(());
}
let present = if std::env::var_os("LIBGUIX_FORCE_NO_AGENT").is_some() {
false
} else {
auth_agent_present()
};
if !present {
return Err(GuixError::Polkit {
kind: PolkitFailure::NoAuthAgent,
code: -1,
stderr_tail: String::new(),
});
}
Ok(())
}
#[cfg(test)]
fn resolve_config_path() -> PathBuf {
std::env::var_os("LIBGUIX_TEST_CONFIG_PATH")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(CURRENT_SYSTEM_CONFIG))
}
#[cfg(not(test))]
fn resolve_config_path() -> PathBuf {
PathBuf::from(CURRENT_SYSTEM_CONFIG)
}
fn current_configuration_path_with(p: &Path) -> Result<PathBuf, GuixError> {
match std::fs::metadata(p) {
Ok(_) => Ok(p.to_path_buf()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(GuixError::NotOnGuixSystem),
Err(e) => Err(GuixError::Spawn(e)),
}
}
#[cfg(target_os = "linux")]
const AGENT_NAMES: &[&str] = &[
"lxqt-policykit-agent",
"polkit-gnome-authentication-agent-1",
"polkit-mate-authentication-agent-1",
"polkit-kde-authentication-agent-1",
"mate-polkit-bin",
"mate-polkit",
"hyprpolkitagent",
"xfce-polkit",
"polkit-efl-auth",
"polkit-1-auth-a",
"polkit-dumb-agent",
];
#[cfg(target_os = "linux")]
const COMM_MAX: usize = 15;
#[cfg(target_os = "linux")]
const fn truncate_comm(s: &str) -> &str {
let bytes = s.as_bytes();
let len = if bytes.len() < COMM_MAX {
bytes.len()
} else {
COMM_MAX
};
let (head, _) = bytes.split_at(len);
match std::str::from_utf8(head) {
Ok(s) => s,
Err(_) => "",
}
}
#[cfg(target_os = "linux")]
fn auth_agent_present() -> bool {
auth_agent_present_in(Path::new("/proc"))
}
#[cfg(not(target_os = "linux"))]
fn auth_agent_present() -> bool {
true
}
#[cfg(target_os = "linux")]
pub(crate) fn auth_agent_present_in(proc_root: &Path) -> bool {
let mut needles = [""; AGENT_NAMES.len()];
let mut i = 0;
while i < AGENT_NAMES.len() {
needles[i] = truncate_comm(AGENT_NAMES[i]);
i += 1;
}
let Ok(entries) = std::fs::read_dir(proc_root) else {
return false;
};
for entry in entries.flatten() {
let path = entry.path();
if !path
.file_name()
.and_then(|s| s.to_str())
.is_some_and(|s| s.bytes().all(|b| b.is_ascii_digit()))
{
continue;
}
let comm_path = path.join("comm");
let Ok(comm) = std::fs::read_to_string(&comm_path) else {
continue;
};
let comm = comm.trim();
for needle in &needles {
if comm == *needle {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_options_are_quiet() {
let o = ReconfigureOptions::default();
assert!(!o.dry_run);
assert!(!o.allow_downgrades);
assert!(o.load_paths.is_empty());
}
#[test]
fn reconfigure_args_bare() {
let cfg = PathBuf::from("/etc/config.scm");
let args = build_reconfigure_args(&cfg, &ReconfigureOptions::default());
assert_eq!(args, vec!["system", "reconfigure", "/etc/config.scm"]);
}
#[test]
fn reconfigure_args_include_load_paths() {
let cfg = PathBuf::from("/home/me/dotfiles/system/framework.scm");
let opts = ReconfigureOptions {
load_paths: vec![
PathBuf::from("/home/me/dotfiles/system"),
PathBuf::from("/home/me/dotfiles/extra"),
],
..Default::default()
};
let args = build_reconfigure_args(&cfg, &opts);
assert_eq!(
args,
vec![
"system",
"reconfigure",
"-L",
"/home/me/dotfiles/system",
"-L",
"/home/me/dotfiles/extra",
"/home/me/dotfiles/system/framework.scm",
],
);
assert_eq!(args[0], "system");
assert_eq!(args[1], "reconfigure");
}
#[test]
fn reconfigure_args_load_paths_with_flags() {
let cfg = PathBuf::from("/etc/config.scm");
let opts = ReconfigureOptions {
dry_run: true,
allow_downgrades: true,
load_paths: vec![PathBuf::from("/srv/cfg")],
};
let args = build_reconfigure_args(&cfg, &opts);
assert_eq!(
args,
vec![
"system",
"reconfigure",
"-L",
"/srv/cfg",
"--dry-run",
"--allow-downgrades",
"/etc/config.scm",
],
);
}
#[test]
fn agent_check_does_not_panic() {
let _ = auth_agent_present();
}
#[cfg(target_os = "linux")]
#[test]
fn truncate_comm_matches_kernel_limit() {
assert_eq!(
truncate_comm("polkit-gnome-authentication-agent-1"),
"polkit-gnome-au"
);
assert_eq!(truncate_comm("lxqt-policykit-agent"), "lxqt-policykit-");
assert_eq!(truncate_comm("mate-polkit"), "mate-polkit");
assert_eq!(truncate_comm("hyprpolkitagent"), "hyprpolkitagent");
}
#[test]
fn current_config_path_missing_returns_not_on_guix() {
let p = PathBuf::from("/tmp/libguix-definitely-does-not-exist-xyz/config.scm");
let err = current_configuration_path_with(&p).expect_err("expected error");
assert!(matches!(err, GuixError::NotOnGuixSystem), "got {err:?}");
}
#[test]
fn current_config_path_existing_returns_ok() {
let tmp = tempfile::tempdir().expect("tempdir");
let p = tmp.path().join("configuration.scm");
std::fs::write(&p, "(operating-system ...)").expect("write");
let got = current_configuration_path_with(&p).expect("ok");
assert_eq!(got, p);
}
#[cfg(unix)]
#[test]
fn current_config_path_permission_denied_is_spawn() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().expect("tempdir");
let unreadable_dir = tmp.path().join("locked");
std::fs::create_dir(&unreadable_dir).expect("mkdir");
let inside = unreadable_dir.join("configuration.scm");
std::fs::write(&inside, "x").expect("write");
std::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o000))
.expect("chmod");
let result = current_configuration_path_with(&inside);
let _ = std::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o755));
match result {
Ok(_) | Err(GuixError::Spawn(_)) => {}
Err(GuixError::NotOnGuixSystem) => {
panic!("permission-denied collapsed to NotOnGuixSystem");
}
Err(other) => panic!("unexpected error: {other:?}"),
}
}
#[cfg(target_os = "linux")]
#[test]
fn auth_agent_present_in_fake_proc_detects_lxqt() {
let tmp = tempfile::tempdir().expect("tempdir");
let pid_dir = tmp.path().join("123");
std::fs::create_dir(&pid_dir).expect("mkdir");
std::fs::write(pid_dir.join("comm"), "lxqt-policykit-\n").expect("write comm");
assert!(auth_agent_present_in(tmp.path()));
}
#[cfg(target_os = "linux")]
#[test]
fn auth_agent_present_in_fake_proc_returns_false_when_absent() {
let tmp = tempfile::tempdir().expect("tempdir");
let pid_dir = tmp.path().join("456");
std::fs::create_dir(&pid_dir).expect("mkdir");
std::fs::write(pid_dir.join("comm"), "bash\n").expect("write comm");
assert!(!auth_agent_present_in(tmp.path()));
let pid_dir2 = tmp.path().join("789");
std::fs::create_dir(&pid_dir2).expect("mkdir");
assert!(!auth_agent_present_in(tmp.path()));
}
#[cfg(target_os = "linux")]
#[test]
fn auth_agent_present_in_rejects_prefix_only_match() {
let tmp = tempfile::tempdir().expect("tempdir");
let pid_dir = tmp.path().join("321");
std::fs::create_dir(&pid_dir).expect("mkdir");
std::fs::write(pid_dir.join("comm"), "xfce-polkit-imp\n").expect("write");
assert!(
!auth_agent_present_in(tmp.path()),
"prefix-only match must not trigger"
);
}
}