//! Builders for the external commands the wizard shells out to. //! //! Kept pure so unit tests can assert the exact argv we'd hand to //! `std::process::Command` without ever exec'ing `cargo` or `systemctl`. use std::path::PathBuf; /// Where to fetch the maestro crates from when running `cargo install`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CargoSource { /// `cargo install --git [--branch ] `. Git { url: String, branch: Option }, /// `cargo install --path -p `. Path(PathBuf), } /// `systemctl --user` verbs the wizard runs. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemctlAction { DaemonReload, Enable, Start, Restart, } impl SystemctlAction { pub fn verb(self) -> &'static str { match self { Self::DaemonReload => "daemon-reload", Self::Enable => "enable", Self::Start => "start", Self::Restart => "restart", } } fn takes_unit_arg(self) -> bool { !matches!(self, Self::DaemonReload) } } /// Build the args for `cargo install ` (excluding the `cargo` and /// `install` words themselves). `locked` adds `--locked` so the install uses /// the committed `Cargo.lock`. /// Build the `cargo install` argument vector. /// /// `features`, when `Some`, restricts the build to an explicit feature set via /// `--no-default-features --features `. pub fn cargo_install_args( source: &CargoSource, crate_name: &str, locked: bool, features: Option<&str>, ) -> Vec { let mut args: Vec = Vec::new(); match source { CargoSource::Git { url, branch } => { args.push("--git".into()); args.push(url.clone()); if let Some(b) = branch { args.push("--branch".into()); args.push(b.clone()); } args.push(crate_name.to_string()); } CargoSource::Path(root) => { args.push("--path".into()); args.push(root.display().to_string()); args.push("-p".into()); args.push(crate_name.to_string()); } } if let Some(list) = features { args.push("--no-default-features".into()); args.push("--features".into()); args.push(list.to_string()); } if locked { args.push("--locked".into()); } args } /// Build the args for `systemctl --user [unit]`. pub fn systemctl_user_args(action: SystemctlAction, unit: &str) -> Vec { let mut args = vec!["--user".to_string(), action.verb().to_string()]; if action.takes_unit_arg() { args.push(unit.to_string()); } args } /// `cargo` binary path. We prefer `$CARGO` (set by `cargo install` itself when /// it invokes child processes, and by `rustup`), then `~/.cargo/bin/cargo`, and /// finally a bare `cargo` so PATH lookup applies. pub fn cargo_bin() -> PathBuf { if let Ok(p) = std::env::var("CARGO") { return PathBuf::from(p); } if let Some(home) = dirs::home_dir() { let in_cargo_home = home.join(".cargo").join("bin").join("cargo"); if in_cargo_home.exists() { return in_cargo_home; } } PathBuf::from("cargo") } #[cfg(test)] mod tests { use super::*; #[test] fn git_args_no_branch() { let src = CargoSource::Git { url: "https://git.naps.pt/naps62/maestro".into(), branch: None, }; let args = cargo_install_args(&src, "maestro", false, None); assert_eq!( args, vec!["--git", "https://git.naps.pt/naps62/maestro", "maestro",] ); } #[test] fn git_args_with_features_omit_defaults() { let src = CargoSource::Git { url: "https://git.naps.pt/naps62/maestro".into(), branch: None, }; let args = cargo_install_args(&src, "maestro", true, Some("cli,daemon,tui")); assert_eq!( args, vec![ "--git", "https://git.naps.pt/naps62/maestro", "maestro", "--no-default-features", "--features", "cli,daemon,tui", "--locked", ] ); } #[test] fn git_args_with_branch_and_locked() { let src = CargoSource::Git { url: "https://git.example/x".into(), branch: Some("main".into()), }; let args = cargo_install_args(&src, "maestro", true, None); assert_eq!( args, vec![ "--git", "https://git.example/x", "--branch", "main", "maestro", "--locked", ] ); } #[test] fn path_args_use_p_flag() { let src = CargoSource::Path(PathBuf::from("/repos/maestro")); let args = cargo_install_args(&src, "maestro", true, None); assert_eq!( args, vec!["--path", "/repos/maestro", "-p", "maestro", "--locked",] ); } #[test] fn systemctl_daemon_reload_omits_unit() { let args = systemctl_user_args(SystemctlAction::DaemonReload, "maestro.service"); assert_eq!(args, vec!["--user", "daemon-reload"]); } #[test] fn systemctl_enable_includes_unit() { let args = systemctl_user_args(SystemctlAction::Enable, "maestro.service"); assert_eq!(args, vec!["--user", "enable", "maestro.service"]); } #[test] fn systemctl_start_includes_unit() { let args = systemctl_user_args(SystemctlAction::Start, "maestro.service"); assert_eq!(args, vec!["--user", "start", "maestro.service"]); } #[test] fn systemctl_restart_includes_unit() { let args = systemctl_user_args(SystemctlAction::Restart, "maestro.service"); assert_eq!(args, vec!["--user", "restart", "maestro.service"]); } #[test] fn cargo_bin_honors_env_var() { // Save & restore $CARGO to keep the test hermetic. let prev = std::env::var("CARGO").ok(); std::env::set_var("CARGO", "/opt/rust/bin/cargo"); assert_eq!(cargo_bin(), PathBuf::from("/opt/rust/bin/cargo")); match prev { Some(v) => std::env::set_var("CARGO", v), None => std::env::remove_var("CARGO"), } } }