1use std::collections::HashMap;
4use std::io::Write as IoWrite;
5use std::process::Command;
6
7use crate::config::DeploymentPlan;
8use crate::error::{NixError, Result};
9use crate::health::HealthStatus;
10
11pub struct BuildResults {
13 pub paths: HashMap<String, String>,
15 pub errors: HashMap<String, NixError>,
17}
18
19pub fn build_closures(
24 plan: &DeploymentPlan,
25 flake_uri: &str,
26 healthy_builders: Option<&[HealthStatus]>,
27) -> Result<BuildResults> {
28 let mut results = BuildResults {
29 paths: HashMap::new(),
30 errors: HashMap::new(),
31 };
32
33 let machines_file = healthy_builders
35 .map(|builders| generate_machines_file_from_healthy(builders))
36 .transpose()?;
37
38 for target in &plan.targets {
40 if !target.needs_build {
41 results
42 .paths
43 .insert(target.node.name.clone(), target.toplevel_path.clone());
44 continue;
45 }
46
47 match build_host(flake_uri, &target.node.name, machines_file.as_deref()) {
48 Ok(path) => {
49 results.paths.insert(target.node.name.clone(), path);
50 }
51 Err(e) => {
52 results.errors.insert(target.node.name.clone(), e);
53 }
54 }
55 }
56
57 Ok(results)
58}
59
60pub fn build_flake_attr(flake_attr: &str, machines_file: Option<&str>) -> Result<String> {
65 let mut cmd = Command::new("nix");
66 cmd.args(["build", flake_attr, "--no-link", "--print-out-paths"]);
67
68 if let Some(path) = machines_file {
69 cmd.arg("--builders").arg(format!("@{}", path));
70 }
71
72 let output = cmd.output().map_err(|e| NixError::BuildFailed {
73 host: flake_attr.to_string(),
74 message: format!("failed to run nix build: {}", e),
75 })?;
76
77 if !output.status.success() {
78 let stderr = String::from_utf8_lossy(&output.stderr);
79 return Err(NixError::BuildFailed {
80 host: flake_attr.to_string(),
81 message: stderr.to_string(),
82 });
83 }
84
85 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
86 if path.is_empty() {
87 return Err(NixError::BuildFailed {
88 host: flake_attr.to_string(),
89 message: "nix build returned empty path".to_string(),
90 });
91 }
92
93 Ok(path)
94}
95
96pub fn build_host(flake_uri: &str, hostname: &str, machines_file: Option<&str>) -> Result<String> {
98 let attr = format!(
99 "{}#nixosConfigurations.{}.config.system.build.toplevel",
100 flake_uri, hostname
101 );
102 build_flake_attr(&attr, machines_file)
103}
104
105pub fn generate_machines_file_from_healthy(builders: &[HealthStatus]) -> Result<String> {
108 let content: String = builders
109 .iter()
110 .filter(|b| b.healthy)
111 .map(|b| {
112 let key = b.builder.ssh_key.as_deref().unwrap_or("-");
113 let features = b.builder.features.join(",");
114 let systems = b.builder.systems.join(",");
115 format!(
116 "{}://{}@{} {} {} {} {} {}",
117 b.builder.protocol,
118 b.builder.user,
119 b.builder.host,
120 systems,
121 key,
122 b.builder.max_jobs,
123 b.builder.speed_factor,
124 features
125 )
126 })
127 .collect::<Vec<_>>()
128 .join("\n");
129
130 let dir = std::env::temp_dir().join("consortium-nix");
131 std::fs::create_dir_all(&dir).map_err(|e| NixError::MachinesFile {
132 path: dir.clone(),
133 source: e,
134 })?;
135
136 let path = dir.join("machines");
137 let mut file = std::fs::File::create(&path).map_err(|e| NixError::MachinesFile {
138 path: path.clone(),
139 source: e,
140 })?;
141 file.write_all(content.as_bytes())
142 .map_err(|e| NixError::MachinesFile {
143 path: path.clone(),
144 source: e,
145 })?;
146
147 Ok(path.to_string_lossy().to_string())
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::config::Builder;
154
155 #[test]
156 fn test_generate_machines_file() {
157 let builders = vec![HealthStatus {
158 builder: Builder {
159 host: "192.168.1.121".to_string(),
160 user: "root".to_string(),
161 max_jobs: 16,
162 speed_factor: 2,
163 systems: vec!["x86_64-linux".to_string()],
164 features: vec!["big-parallel".to_string(), "kvm".to_string()],
165 ssh_key: None,
166 protocol: "ssh-ng".to_string(),
167 },
168 healthy: true,
169 latency_ms: Some(5),
170 error: None,
171 }];
172
173 let path = generate_machines_file_from_healthy(&builders).unwrap();
174 let content = std::fs::read_to_string(&path).unwrap();
175 assert!(content.contains("ssh-ng://root@192.168.1.121"));
176 assert!(content.contains("x86_64-linux"));
177 assert!(content.contains("big-parallel,kvm"));
178 std::fs::remove_file(path).ok();
179 }
180}