From 384eb82a9f5ab6e7db1e4074ea365aff0266707f Mon Sep 17 00:00:00 2001 From: Robert DeRose Date: Sun, 26 Apr 2026 13:30:20 -0400 Subject: [PATCH 1/2] fix: detect host platform in system-manager init - render the generated system.nix template with the current host architecture instead of always emitting x86_64-linux --- Cargo.lock | 1 + crates/system-manager/Cargo.toml | 3 ++ crates/system-manager/src/main.rs | 72 +++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d754c95..dd707c58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,6 +680,7 @@ dependencies = [ "log", "rpassword", "system-manager-engine", + "tempfile", ] [[package]] diff --git a/crates/system-manager/Cargo.toml b/crates/system-manager/Cargo.toml index 9d91fc76..2f775ae0 100644 --- a/crates/system-manager/Cargo.toml +++ b/crates/system-manager/Cargo.toml @@ -17,3 +17,6 @@ clap.workspace = true env_logger.workspace = true log.workspace = true rpassword.workspace = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/system-manager/src/main.rs b/crates/system-manager/src/main.rs index 910ab327..f80edf55 100644 --- a/crates/system-manager/src/main.rs +++ b/crates/system-manager/src/main.rs @@ -31,6 +31,8 @@ pub const STANDALONE_FLAKE_TEMPLATE: &[u8; 864] = /// network calls when initializing a system-manager configuration from the command line. pub const SYSTEM_MODULE_TEMPLATE: &[u8; 1159] = include_bytes!("../../../templates/system.nix"); +const HOST_PLATFORM_PLACEHOLDER: &str = "x86_64-linux"; + /// Name of the engine binary in the store path const ENGINE_BIN: &str = "system-manager-engine"; @@ -374,8 +376,7 @@ fn go(args: Args) -> Result<()> { path.display() ); - let system_config_filepath = path.join("system.nix"); - init_config_file(&system_config_filepath, SYSTEM_MODULE_TEMPLATE)?; + let host_platform = detect_host_platform(std::env::consts::ARCH)?; let has_flake_support = process::Command::new("nix") .arg("show-config") @@ -386,17 +387,16 @@ fn go(args: Args) -> Result<()> { && out_str.contains("flakes") && out_str.contains("nix-command") }); - if !no_flake && has_flake_support { - let flake_config_filepath = path.join("flake.nix"); - let is_nixos = process::Command::new("nixos-version") - .output() - .is_ok_and(|output| !output.stdout.is_empty()); - if is_nixos { - init_config_file(&flake_config_filepath, NIXOS_FLAKE_TEMPLATE)? - } else { - init_config_file(&flake_config_filepath, STANDALONE_FLAKE_TEMPLATE)? - } - } + let is_nixos = process::Command::new("nixos-version") + .output() + .is_ok_and(|output| !output.stdout.is_empty()); + + init_configuration( + &path, + !no_flake && has_flake_support, + is_nixos, + &host_platform, + )?; log::info!("Configuration '{}' ready for activation!", path.display()); Ok(()) } @@ -474,6 +474,43 @@ fn init_config_file(filepath: &Path, buf: &[u8]) -> Result<()> { Ok(()) } +fn init_configuration( + path: &Path, + include_flake: bool, + is_nixos: bool, + host_platform: &str, +) -> Result<()> { + let system_config_filepath = path.join("system.nix"); + let system_template = render_template(SYSTEM_MODULE_TEMPLATE, host_platform); + init_config_file(&system_config_filepath, system_template.as_bytes())?; + + if include_flake { + let flake_config_filepath = path.join("flake.nix"); + let flake_template = if is_nixos { + render_template(NIXOS_FLAKE_TEMPLATE, host_platform) + } else { + render_template(STANDALONE_FLAKE_TEMPLATE, host_platform) + }; + init_config_file(&flake_config_filepath, flake_template.as_bytes())?; + } + + Ok(()) +} + +fn detect_host_platform(arch: &str) -> Result<&'static str> { + match arch { + "aarch64" => Ok("aarch64-linux"), + "x86_64" => Ok("x86_64-linux"), + _ => bail!( + "system-manager init does not know how to map Rust architecture '{arch}' to a supported Nix system" + ), + } +} + +fn render_template(template: &[u8], host_platform: &str) -> String { + String::from_utf8_lossy(template).replace(HOST_PLATFORM_PLACEHOLDER, host_platform) +} + fn print_store_path>(store_path: SP) -> Result<()> { println!("{}", store_path.as_ref()); Ok(()) @@ -922,6 +959,7 @@ fn handle_toplevel_error(r: Result) -> ExitCode { mod tests { use super::*; use clap::Parser; + use tempfile::tempdir; #[test] fn legacy_use_remote_sudo_flag_is_accepted() { @@ -1031,4 +1069,12 @@ mod tests { assert!(args.ssh_options.is_empty()); } + + #[test] + fn render_template_substitutes_host_platform_in_system_module() { + let rendered = render_template(SYSTEM_MODULE_TEMPLATE, "aarch64-linux"); + + assert!(rendered.contains("nixpkgs.hostPlatform = \"aarch64-linux\";")); + assert!(!rendered.contains("nixpkgs.hostPlatform = \"x86_64-linux\";")); + } } From 6a7990feb58e21e29ee1e43de1e218e2cfdbb0e6 Mon Sep 17 00:00:00 2001 From: Robert DeRose Date: Sun, 26 Apr 2026 13:31:07 -0400 Subject: [PATCH 2/2] fix: clarify SSH failures during remote nix detection - distinguish SSH connection/authentication problems from a successful remote login where nix-store is actually missing - point users at the equivalent ssh command to verify their target host and options first --- crates/system-manager/src/main.rs | 86 ++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/crates/system-manager/src/main.rs b/crates/system-manager/src/main.rs index f80edf55..4cbd400e 100644 --- a/crates/system-manager/src/main.rs +++ b/crates/system-manager/src/main.rs @@ -915,20 +915,27 @@ fn do_copy_closure( } fn ensure_nix_on_target(target_host: &str, ssh_options: &[String]) -> Result<()> { + let probe = "command -v nix-store"; let mut cmd = process::Command::new("ssh"); - cmd.args(ssh_options) - .arg(target_host) - .arg("command -v nix-store") - .stdout(process::Stdio::null()) - .stderr(process::Stdio::null()); - match cmd.status() { - Ok(status) if status.success() => Ok(()), - Ok(_) => anyhow::bail!( - "Nix is not installed on target host '{target_host}' \ - (nix-store not found in PATH). \ - system-manager requires Nix on the target to receive the closure. \ - Install it by running on the target host:\n\ - \n curl -sSfL https://artifacts.nixos.org/nix-installer | sh -s -- install --no-confirm\n" + cmd.args(ssh_options).arg(target_host).arg(probe); + match cmd.output() { + Ok(output) if output.status.success() => Ok(()), + Ok(output) if output.status.code() == Some(255) => anyhow::bail!( + "Failed to connect to target host '{target_host}' over SSH while checking for Nix. \ + This usually means the host is unreachable or the SSH user/authentication/options are incorrect. \ + Verify that this command works first:\n\ + \n ssh {} {target_host}\n{}", + ssh_options.join(" "), + format_probe_stderr(&output.stderr) + ), + Ok(output) => anyhow::bail!( + "Connected to target host '{target_host}', but the remote probe `{probe}` failed with exit status {}. \ + system-manager requires `nix-store` to be available on the target to receive the closure. \ + Make sure Nix is installed on the target host, then verify the remote user and PATH with:\n\ + \n ssh {} {target_host} nix-store --version\n{}", + output.status, + ssh_options.join(" "), + format_probe_stderr(&output.stderr) ), Err(e) => Err(anyhow::Error::from(e).context(format!( "Failed to run ssh to check for Nix on target host '{target_host}'" @@ -936,6 +943,17 @@ fn ensure_nix_on_target(target_host: &str, ssh_options: &[String]) -> Result<()> } } +fn format_probe_stderr(stderr: &[u8]) -> String { + let stderr = String::from_utf8_lossy(stderr); + let stderr = stderr.trim(); + + if stderr.is_empty() { + String::new() + } else { + format!("\n\nssh stderr:\n{stderr}") + } +} + fn store_path_or_active_profile(maybe_store_path: Option) -> PathBuf { maybe_store_path.map_or_else( || { @@ -1077,4 +1095,46 @@ mod tests { assert!(rendered.contains("nixpkgs.hostPlatform = \"aarch64-linux\";")); assert!(!rendered.contains("nixpkgs.hostPlatform = \"x86_64-linux\";")); } + + #[test] + fn detect_host_platform_maps_supported_architectures() { + assert_eq!(detect_host_platform("aarch64").unwrap(), "aarch64-linux"); + assert_eq!(detect_host_platform("x86_64").unwrap(), "x86_64-linux"); + } + + #[test] + fn detect_host_platform_rejects_unsupported_architectures() { + let error = detect_host_platform("arm").expect_err("expected unsupported arch to fail"); + + assert!(error + .to_string() + .contains("does not know how to map Rust architecture 'arm'")); + } + + #[test] + fn init_configuration_writes_aarch64_linux_to_nixos_flake() { + let tempdir = tempdir().expect("failed to create tempdir"); + + init_configuration(tempdir.path(), true, true, "aarch64-linux") + .expect("failed to initialize configuration"); + + let rendered = std::fs::read_to_string(tempdir.path().join("flake.nix")) + .expect("failed to read generated flake.nix"); + + assert!(rendered.contains("system = \"aarch64-linux\";")); + assert!(!rendered.contains("system = \"x86_64-linux\";")); + } + + #[test] + fn init_configuration_writes_x86_64_linux_to_nixos_flake() { + let tempdir = tempdir().expect("failed to create tempdir"); + + init_configuration(tempdir.path(), true, true, "x86_64-linux") + .expect("failed to initialize configuration"); + + let rendered = std::fs::read_to_string(tempdir.path().join("flake.nix")) + .expect("failed to read generated flake.nix"); + + assert!(rendered.contains("system = \"x86_64-linux\";")); + } }