use std::path::Path;
use crate::handlers::run_once::RunOnceCommand;
use crate::handlers::{ExecutionPhase, HANDLER_NIX};
const WRAPPER_EXPR_TEMPLATE: &str = r#"let
raw = import @PATH@;
m = if builtins.isFunction raw then raw {} else raw;
in
if builtins.isList m then m
else if builtins.isAttrs m && (m.type or null) == "derivation" then [ m ]
else if builtins.isAttrs m then builtins.attrValues m
else throw "packages.nix at ${@PATH@} evaluates to an unsupported shape (must be a list of derivations, a bare derivation, or an attribute set of derivations)""#;
const EXTRA_FEATURES_FLAG: &str = "--extra-experimental-features";
const EXTRA_FEATURES_VALUE: &str = "nix-command flakes";
pub struct NixCommand;
impl RunOnceCommand for NixCommand {
fn handler_name(&self) -> &str {
HANDLER_NIX
}
fn phase(&self) -> ExecutionPhase {
ExecutionPhase::Provision
}
fn command_for(&self, path: &Path) -> (String, Vec<String>) {
let expr = WRAPPER_EXPR_TEMPLATE.replace("@PATH@", &nix_path_literal(path));
(
"nix".into(),
vec![
"profile".into(),
"install".into(),
"--expr".into(),
expr,
EXTRA_FEATURES_FLAG.into(),
EXTRA_FEATURES_VALUE.into(),
],
)
}
fn status_deployed(&self) -> &str {
"nix packages installed"
}
fn status_pending(&self) -> &str {
"nix packages not installed"
}
fn status_ran_different(&self) -> &str {
"nix packages older version"
}
}
fn nix_path_literal(path: &Path) -> String {
let s = path.to_string_lossy();
let escaped = s
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace("${", "\\${");
format!("\"{escaped}\"")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nix_command_identity() {
assert_eq!(NixCommand.handler_name(), HANDLER_NIX);
assert_eq!(NixCommand.phase(), ExecutionPhase::Provision);
assert_eq!(NixCommand.status_deployed(), "nix packages installed");
assert_eq!(NixCommand.status_pending(), "nix packages not installed");
assert_eq!(
NixCommand.status_ran_different(),
"nix packages older version"
);
}
#[test]
fn command_for_emits_profile_install_with_wrapper_expression() {
let (exe, args) = NixCommand.command_for(Path::new("/p/tools/packages.nix"));
assert_eq!(exe, "nix");
assert_eq!(args[0], "profile");
assert_eq!(args[1], "install");
assert_eq!(args[2], "--expr");
let expr = &args[3];
assert!(
expr.contains("import \"/p/tools/packages.nix\""),
"expr should import the manifest path as a Nix string: {expr}"
);
assert!(expr.contains("builtins.isFunction"));
assert!(expr.contains("builtins.isList"));
assert!(expr.contains("builtins.attrValues"));
assert_eq!(args[4], EXTRA_FEATURES_FLAG);
assert_eq!(args[5], EXTRA_FEATURES_VALUE);
}
#[test]
fn command_for_is_shape_agnostic() {
let (e1, a1) = NixCommand.command_for(Path::new("/a/packages.nix"));
let (e2, a2) = NixCommand.command_for(Path::new("/b/packages.nix"));
assert_eq!(e1, e2);
assert_eq!(a1.len(), a2.len());
assert_eq!(a1[0], a2[0]); assert_eq!(a1[1], a2[1]); assert_eq!(a1[2], a2[2]); assert_eq!(a1[4], a2[4]); assert_eq!(a1[5], a2[5]); }
#[test]
fn nix_path_literal_quotes_and_escapes() {
assert_eq!(nix_path_literal(Path::new("/a/b.nix")), "\"/a/b.nix\"");
assert_eq!(
nix_path_literal(Path::new("/weird\"name.nix")),
"\"/weird\\\"name.nix\""
);
assert_eq!(
nix_path_literal(Path::new("/with\\backslash.nix")),
"\"/with\\\\backslash.nix\""
);
assert_eq!(
nix_path_literal(Path::new("/p/${weird}/packages.nix")),
"\"/p/\\${weird}/packages.nix\""
);
}
#[test]
fn validate_is_a_noop_inheriting_the_trait_default() {
use crate::datastore::CommandRunner;
use crate::testing::TempEnvironment;
struct NeverCalledRunner;
impl CommandRunner for NeverCalledRunner {
fn run(
&self,
_e: &str,
_a: &[String],
) -> crate::Result<crate::datastore::CommandOutput> {
panic!("validate must not shell out — it's a no-op");
}
}
let env = TempEnvironment::builder()
.pack("tools")
.file("packages.nix", "anything at all — content is not checked")
.done()
.build();
let abs = env.dotfiles_root.join("tools/packages.nix");
NixCommand
.validate(env.fs.as_ref(), &NeverCalledRunner, &abs)
.expect("validate is a no-op for nix");
}
}