use crate::{Config, system::System};
use anyhow::{Result, bail};
use log::{debug, info};
use std::{
env::{current_exe, var},
fs::{self, create_dir_all},
process::Command,
};
pub struct Nix;
impl Nix {
pub const DIR: &'static str = "nix";
const NIX_ENV: &'static str = "IN_NIX";
pub fn bootstrap(config: Config) -> Result<()> {
debug!("Nix environment not found, bootstrapping one");
let dir = config.root().join(Self::DIR);
if !dir.exists() {
create_dir_all(&dir)?;
fs::write(
dir.join("flake.nix"),
include_str!("../nix/runtime-flake.nix")
.replace("KUBERNIX_SYSTEM", Self::nix_system()?),
)?;
fs::write(dir.join("flake.lock"), include_str!("../flake.lock"))?;
let packages = &config.packages().join(" ");
debug!("Adding additional packages: {:?}", config.packages());
fs::write(
dir.join("packages.nix"),
include_str!("../nix/packages.nix").replace("/* PACKAGES */", packages),
)?;
let target_overlay = dir.join("overlay.nix");
match config.overlay() {
Some(overlay) => {
info!("Using custom overlay '{}'", overlay.display());
fs::copy(overlay, target_overlay)?;
}
None => {
debug!("Using default overlay");
fs::write(target_overlay, include_str!("../nix/overlay.nix"))?;
}
}
Self::git_init(&dir)?;
}
let exe = format!("{}", current_exe()?.display());
let root = format!("{}", config.root().display());
let log_level = format!("{}", config.log_level());
let log_format = format!("{}", config.log_format());
let cidr = format!("{}", config.cidr());
let nodes = format!("{}", config.nodes());
let container_runtime = config.container_runtime();
let shell_val: String = config.shell().unwrap_or_default().to_owned();
let overlay_val = config.overlay().map(|o| format!("{}", o.display()));
let mut args = vec![
exe.as_str(),
"--root",
root.as_str(),
"--log-level",
log_level.as_str(),
"--log-format",
log_format.as_str(),
"--cidr",
cidr.as_str(),
"--nodes",
nodes.as_str(),
"--container-runtime",
container_runtime,
];
if let Some(ref overlay) = overlay_val {
args.push("--overlay");
args.push(overlay.as_str());
}
let dockerfile_val = config.dockerfile().map(|d| format!("{}", d.display()));
if let Some(ref dockerfile) = dockerfile_val {
args.push("--dockerfile");
args.push(dockerfile.as_str());
}
for pkg in config.packages() {
args.push("--packages");
args.push(pkg.as_str());
}
for addon in config.addons() {
args.push("--addons");
args.push(addon.as_str());
}
if config.no_shell() {
args.push("--no-shell");
} else if !shell_val.is_empty() {
args.push("--shell");
args.push(&shell_val);
}
Self::run(&config, &args)
}
pub fn run(config: &Config, args: &[&str]) -> Result<()> {
let nix_dir = config.root().join(Self::DIR);
let flake_ref = format!("path:{}", nix_dir.display());
let status = Command::new(System::find_executable("nix")?)
.env(Nix::NIX_ENV, "true")
.arg("develop")
.arg(&flake_ref)
.arg("--no-update-lock-file")
.arg("--log-format")
.arg("raw")
.arg("--command")
.arg("bash")
.arg("-c")
.arg(args.join(" "))
.status()?;
if !status.success() {
bail!("nix develop exited with status {}", status);
}
Ok(())
}
fn git_init(dir: &std::path::Path) -> Result<()> {
let git = System::find_executable("git")?;
let run = |args: &[&str]| -> Result<()> {
let status = Command::new(&git).arg("-C").arg(dir).args(args).status()?;
if !status.success() {
bail!("git {} failed with status {}", args.join(" "), status,);
}
Ok(())
};
run(&["init", "-q"])?;
run(&["config", "user.email", "kubernix@localhost"])?;
run(&["config", "user.name", "kubernix"])?;
run(&["config", "commit.gpgsign", "false"])?;
run(&["add", "."])?;
run(&["commit", "-q", "-m", "init", "--allow-empty"])?;
Ok(())
}
fn nix_system() -> Result<&'static str> {
match std::env::consts::ARCH {
"x86_64" => Ok("x86_64-linux"),
"aarch64" => Ok("aarch64-linux"),
arch => bail!(
"unsupported architecture '{}' (only x86_64 and aarch64 Linux are supported)",
arch,
),
}
}
pub fn is_active() -> bool {
var(Nix::NIX_ENV).is_ok()
}
#[cfg(test)]
pub fn set_env_marker() {
unsafe { std::env::set_var(Self::NIX_ENV, "true") };
}
#[cfg(test)]
pub fn remove_env_marker() {
unsafe { std::env::remove_var(Self::NIX_ENV) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use tempfile::tempdir;
#[test]
fn nix_system_success() {
let system = Nix::nix_system().unwrap();
assert!(
system == "x86_64-linux" || system == "aarch64-linux",
"unexpected system: {}",
system,
);
assert!(system.ends_with("-linux"));
}
#[test]
fn git_init_success() {
let dir = tempdir().unwrap().keep();
std::fs::write(dir.join("test.txt"), "hello").unwrap();
Nix::git_init(&dir).unwrap();
assert!(dir.join(".git").exists());
}
#[test]
fn git_init_invalid_dir() {
let dir = Path::new("/nonexistent/path");
assert!(Nix::git_init(dir).is_err());
}
#[test]
fn runtime_flake_template_contains_placeholder() {
let template = include_str!("../nix/runtime-flake.nix");
assert!(template.contains("KUBERNIX_SYSTEM"));
}
#[test]
fn runtime_flake_system_replacement() {
let template = include_str!("../nix/runtime-flake.nix");
let result = template.replace("KUBERNIX_SYSTEM", "x86_64-linux");
assert!(result.contains("x86_64-linux"));
assert!(!result.contains("KUBERNIX_SYSTEM"));
}
#[test]
fn packages_template_contains_placeholder() {
let template = include_str!("../nix/packages.nix");
assert!(template.contains("/* PACKAGES */"));
}
#[test]
fn packages_replacement() {
let template = include_str!("../nix/packages.nix");
let result = template.replace("/* PACKAGES */", "hello world");
assert!(result.contains("hello world"));
assert!(!result.contains("/* PACKAGES */"));
}
#[test]
fn flake_lock_is_valid_json() {
let lock = include_str!("../flake.lock");
let parsed: serde_json::Value = serde_json::from_str(lock).unwrap();
assert!(parsed["nodes"]["nixpkgs"]["locked"]["rev"].is_string());
}
#[test]
fn is_active_toggle() {
Nix::remove_env_marker();
assert!(!Nix::is_active());
Nix::set_env_marker();
assert!(Nix::is_active());
Nix::remove_env_marker();
assert!(!Nix::is_active());
}
}