use std::ffi::OsString; use std::path::Path; use std::process::Command; use anyhow::{Context, Result, bail}; use crate::package_installer::{PackageInstaller, PackageRequest}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Invocation { program: OsString, arguments: Vec, } impl Invocation { pub fn new( program: impl Into, arguments: impl IntoIterator>, ) -> Self { Self { program: program.into(), arguments: arguments.into_iter().map(Into::into).collect(), } } pub fn program(&self) -> &OsString { &self.program } pub fn arguments(&self) -> &[OsString] { &self.arguments } } pub trait CommandRunner { fn run(&self, invocation: Invocation) -> Result<()>; } #[derive(Debug, Default, Clone, Copy)] pub struct ProcessRunner; impl CommandRunner for ProcessRunner { fn run(&self, invocation: Invocation) -> Result<()> { let status = Command::new(&invocation.program) .args(&invocation.arguments) .status() .with_context(|| format!("run {}", invocation.program.to_string_lossy()))?; if !status.success() { bail!( "{} exited with {status}", invocation.program.to_string_lossy() ); } Ok(()) } } /// Runs Hasher commands under the configured non-root Hasher account. /// Root retains ownership of native rootfs finalization and packaging stages. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SudoUserRunner { user: OsString, } impl SudoUserRunner { pub fn new(user: impl Into) -> Self { Self { user: user.into() } } pub fn wrap(&self, invocation: Invocation) -> Invocation { let mut arguments = vec![ OsString::from("-n"), OsString::from("-u"), self.user.clone(), ]; arguments.push(invocation.program); arguments.extend(invocation.arguments); Invocation::new("sudo", arguments) } pub fn prepare_workdir(&self, workdir: &Path) -> Invocation { Invocation::new( "sudo", [ "-n".into(), "install".into(), "-d".into(), "-o".into(), self.user.clone(), "-g".into(), self.user.clone(), workdir.as_os_str().to_owned(), ], ) } } impl CommandRunner for SudoUserRunner { fn run(&self, invocation: Invocation) -> Result<()> { if let Some(workdir) = invocation .arguments() .windows(2) .find_map(|pair| (pair[0] == "--workdir").then(|| Path::new(&pair[1]))) { ProcessRunner.run(self.prepare_workdir(workdir))?; } ProcessRunner.run(self.wrap(invocation)) } } #[derive(Debug)] pub struct HasherInstaller { runner: R, } impl HasherInstaller { pub fn new(runner: R) -> Self { Self { runner } } pub fn runner(&self) -> &R { &self.runner } } impl PackageInstaller for HasherInstaller { fn install(&self, request: &PackageRequest) -> Result<()> { self.runner.run(Invocation::new( "hsh", [ "--mountpoints=/proc".into(), "--initroot-only".into(), "--apt-config".into(), request.apt_config().as_path().as_os_str().to_owned(), "--workdir".into(), request.workdir().as_os_str().to_owned(), ], ))?; let mut arguments = vec![ OsString::from("--mountpoints=/proc"), OsString::from("--workdir"), request.workdir().as_os_str().to_owned(), ]; arguments.extend(request.selectors().iter().map(OsString::from)); arguments.extend(request.packages().iter().map(OsString::from)); self.runner.run(Invocation::new("hsh-install", arguments)) } }