Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/system-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ clap.workspace = true
env_logger.workspace = true
log.workspace = true
rpassword.workspace = true

[dev-dependencies]
tempfile.workspace = true
158 changes: 132 additions & 26 deletions crates/system-manager/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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")
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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<SP: AsRef<StorePath>>(store_path: SP) -> Result<()> {
println!("{}", store_path.as_ref());
Ok(())
Expand Down Expand Up @@ -878,27 +915,45 @@ 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}'"
))),
}
}

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<StorePath>) -> PathBuf {
maybe_store_path.map_or_else(
|| {
Expand All @@ -922,6 +977,7 @@ fn handle_toplevel_error<T>(r: Result<T>) -> ExitCode {
mod tests {
use super::*;
use clap::Parser;
use tempfile::tempdir;

#[test]
fn legacy_use_remote_sudo_flag_is_accepted() {
Expand Down Expand Up @@ -1031,4 +1087,54 @@ 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\";"));
}

#[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\";"));
}
}