1use thiserror::Error;
8
9use tatara_nix::{Artifact, MultiSynthesizer};
10
11use crate::set::{PackageSet, PackageSetError};
12
13#[derive(Debug, Error)]
14pub enum NixpkgsMirrorError {
15 #[error("package set: {0}")]
16 Set(#[from] PackageSetError),
17
18 #[error("package missing while enumerating: {0}")]
19 EnumerationGap(String),
20}
21
22pub struct NixpkgsMirror {
28 pub out_prefix: String,
32 pub header: String,
34}
35
36impl Default for NixpkgsMirror {
37 fn default() -> Self {
38 Self {
39 out_prefix: "pkgs".into(),
40 header: "; AUTO-GENERATED by tatara-pkgs — do not hand-edit.\n\
41 ; Source of truth: the upstream PackageSet pin in tend config.\n\n"
42 .into(),
43 }
44 }
45}
46
47impl NixpkgsMirror {
48 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
53 self.out_prefix = prefix.into();
54 self
55 }
56
57 pub fn render_one(&self, name: &str, pkg_set_expr: Option<&str>) -> Artifact {
59 let path = format!("{}/{}.tl", self.out_prefix, name_to_file(name));
60 let pkg_set_line = match pkg_set_expr {
61 Some(expr) => format!(
62 " :bridge (:attr-path \"{}\" :pkg-set {})\n",
63 name,
64 nix_string_literal(expr)
65 ),
66 None => format!(" :bridge (:attr-path \"{}\")\n", name),
67 };
68 let content = format!(
69 "{}(defderivation\n :name \"{}\"\n{})\n",
70 self.header, name, pkg_set_line
71 );
72 Artifact::new(path, content)
73 }
74}
75
76fn nix_string_literal(s: &str) -> String {
77 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
81}
82
83fn name_to_file(name: &str) -> String {
84 name.replace('.', "/")
87}
88
89impl MultiSynthesizer for NixpkgsMirror {
90 type Input = dyn PackageSet;
91
92 fn generate_all(&self, input: &Self::Input) -> Vec<Artifact> {
93 let mut out = Vec::new();
94 for name in input.names() {
95 let d = match input.get(&name) {
96 Ok(Some(d)) => d,
97 _ => continue, };
99 let pkg_set_expr = d.bridge.as_ref().and_then(|b| b.pkg_set.clone());
100 out.push(self.render_one(&name, pkg_set_expr.as_deref()));
101 }
102 out
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::NixpkgsBridge;
110
111 #[test]
112 fn mirror_emits_one_artifact_per_package() {
113 let src = NixpkgsBridge::new()
114 .with_names(vec!["hello".into(), "bash".into()])
115 .with_label("nixpkgs");
116 let mirror = NixpkgsMirror::new().with_prefix("pkgs");
117 let arts = mirror.generate_all(&src);
118 assert_eq!(arts.len(), 2);
119 let paths: Vec<_> = arts.iter().map(|a| a.path.as_str()).collect();
120 assert!(paths.contains(&"pkgs/hello.tl"));
121 assert!(paths.contains(&"pkgs/bash.tl"));
122 }
123
124 #[test]
125 fn emitted_tl_reads_back_via_tatara_lisp() {
126 let mirror = NixpkgsMirror::new();
128 let art = mirror.render_one("hello", None);
129 let forms = tatara_lisp::read(&art.content).unwrap();
130 let last = forms.last().unwrap();
133 let list = last.as_list().unwrap();
134 assert_eq!(list[0].as_symbol(), Some("defderivation"));
135 }
136
137 #[test]
138 fn dotted_names_map_to_nested_directory_paths() {
139 let mirror = NixpkgsMirror::new().with_prefix("pkgs");
140 let art = mirror.render_one("python3Packages.requests", None);
141 assert_eq!(art.path, "pkgs/python3Packages/requests.tl");
142 }
143
144 #[test]
145 fn custom_pkg_set_is_quoted_into_emitted_form() {
146 let mirror = NixpkgsMirror::new();
147 let art = mirror.render_one("mypkg", Some("import ./release.nix {}"));
148 assert!(art.content.contains(":pkg-set \"import ./release.nix {}\""));
149 }
150}