use eyre::{Result, bail};
use crate::config::{Config, Settings};
use crate::system;
use crate::system::packages::nix::{Installable, nix_string};
#[derive(Debug, usage_rs::Args)]
#[usage(
verbatim_doc_comment,
example(r###"mise bootstrap packages export --format nix > packages.nix"###)
)]
pub(crate) struct SystemExport {
#[usage(long, choices("nix"))]
format: String,
}
impl SystemExport {
pub(crate) async fn run(self) -> Result<()> {
debug_assert_eq!(self.format, "nix");
let config = Config::get().await?;
let enabled = Settings::get()
.system_packages
.managers
.as_ref()
.is_none_or(|managers| managers.iter().any(|m| m == "nix"));
let packages = if enabled {
system::nix_packages_for_export(&config)
} else {
vec![]
};
let module = render(
packages
.iter()
.map(|(spec, package)| (spec.as_str(), package.version())),
)?;
miseprint!("{module}")?;
Ok(())
}
}
fn render<'a>(packages: impl IntoIterator<Item = (&'a str, &'a str)>) -> Result<String> {
let mut attributes = std::collections::BTreeSet::new();
for (spec, version) in packages {
let name = spec
.strip_prefix("nix:")
.ok_or_else(|| eyre::eyre!("expected a nix: package: {spec}"))?;
let request = Installable::parse(name)?;
if request.explicit {
bail!(
"cannot export '{spec}': use a shorthand attribute such as nix:ripgrep; the importing NixOS configuration owns the package source"
);
}
if version != "latest" {
bail!(
"cannot export version pin '{spec}' = '{version}': pin nixpkgs in the importing NixOS configuration instead"
);
}
attributes.insert(
request
.attribute
.split('.')
.map(nix_string)
.collect::<Vec<_>>()
.join("."),
);
}
let mut output = String::from(
"# Generated by mise bootstrap packages export --format nix\n{ pkgs, ... }:\n{\n environment.systemPackages = [\n",
);
for attribute in attributes {
output.push_str(&format!(" pkgs.{attribute}\n"));
}
output.push_str(" ];\n}\n");
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn export_is_sorted_and_quotes_attribute_segments() {
let module = render([
("nix:python3Packages.pip", "latest"),
("nix:hello-world", "latest"),
("nix:hello-world", "latest"),
])
.unwrap();
assert!(
module.contains(" pkgs.\"hello-world\"\n pkgs.\"python3Packages\".\"pip\"\n")
);
assert_eq!(module.matches("hello-world").count(), 1);
assert!(render([]).unwrap().contains("systemPackages = [\n ];"));
}
#[test]
fn export_rejects_sources_pins_and_expressions() {
for (name, version) in [
("nix:github:owner/repo#hello", "latest"),
("nix:hello", "1.2"),
("nix:${builtins.abort}", "latest"),
("nix:hello\"; throw", "latest"),
] {
assert!(render([(name, version)]).is_err());
}
}
}