1use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::deser;
9use crate::error::{BootspecError, SynthesizeError};
10use crate::{Extensions, Result, SpecialisationName, SystemConfigurationRoot};
11
12pub const SCHEMA_VERSION: u64 = 1;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct GenerationV1 {
26 #[serde(rename = "org.nixos.bootspec.v1")]
27 pub bootspec: BootSpecV1,
28 #[serde(rename = "org.nixos.specialisation.v1", default = "HashMap::new")]
29 pub specialisations: SpecialisationsV1,
30}
31
32impl GenerationV1 {
33 pub fn synthesize(generation_path: &Path) -> Result<Self> {
37 let bootspec = BootSpecV1::synthesize(generation_path)?;
38
39 let mut specialisations = HashMap::new();
40 if let Ok(specialisations_dirs) = fs::read_dir(generation_path.join("specialisation")) {
41 for specialisation in specialisations_dirs.map(|res| res.map(|e| e.path())) {
42 let specialisation = specialisation?;
43 let name = specialisation
44 .file_name()
45 .ok_or(BootspecError::InvalidFileName(specialisation.clone()))?
46 .to_str()
47 .ok_or(BootspecError::InvalidUtf8(specialisation.clone()))?;
48 let toplevel = fs::canonicalize(generation_path.join("specialisation").join(name))?;
49
50 specialisations.insert(
51 SpecialisationName(name.to_string()),
52 SpecialisationV1::synthesize(&toplevel)?,
53 );
54 }
55 }
56
57 Ok(Self {
58 bootspec,
59 specialisations,
60 })
61 }
62}
63
64pub type SpecialisationsV1 = HashMap<SpecialisationName, SpecialisationV1>;
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73pub struct SpecialisationV1 {
74 #[serde(flatten)]
75 pub generation: GenerationV1,
76 #[serde(
77 default = "HashMap::new",
78 skip_serializing_if = "HashMap::is_empty",
79 deserialize_with = "deser::skip_generation_fields",
80 flatten
81 )]
82 pub extensions: Extensions,
83}
84
85impl SpecialisationV1 {
86 pub fn synthesize(generation_path: &Path) -> Result<Self> {
90 let generation = GenerationV1::synthesize(generation_path)?;
91 Ok(Self {
92 generation,
93 extensions: HashMap::new(),
94 })
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102#[serde(rename_all = "camelCase")]
103pub struct BootSpecV1 {
104 pub label: String,
106 pub kernel: PathBuf,
108 pub kernel_params: Vec<String>,
110 pub init: PathBuf,
112 pub initrd: Option<PathBuf>,
114 pub initrd_secrets: Option<PathBuf>,
116 pub system: String,
118 pub toplevel: SystemConfigurationRoot,
120}
121
122impl BootSpecV1 {
123 pub(crate) fn synthesize(generation: &Path) -> Result<Self> {
124 let generation = generation
125 .canonicalize()
126 .map_err(|e| SynthesizeError::Canonicalize {
127 path: generation.to_path_buf(),
128 err: e,
129 })?;
130
131 let version_file = generation.join("nixos-version");
132 let system_version =
133 fs::read_to_string(version_file.clone()).map_err(|e| SynthesizeError::ReadPath {
134 path: version_file,
135 err: e,
136 })?;
137
138 let system_file = generation.join("system");
139 let system =
140 fs::read_to_string(system_file.clone()).map_err(|e| SynthesizeError::ReadPath {
141 path: system_file,
142 err: e,
143 })?;
144
145 let kernel_file = generation.join("kernel");
146 let kernel =
147 fs::canonicalize(kernel_file.clone()).map_err(|e| SynthesizeError::Canonicalize {
148 path: kernel_file,
149 err: e,
150 })?;
151
152 let kernel_modules_path = generation.join("kernel-modules/lib/modules");
153 let kernel_modules = fs::canonicalize(kernel_modules_path.clone()).map_err(|e| {
154 SynthesizeError::Canonicalize {
155 path: kernel_modules_path,
156 err: e,
157 }
158 })?;
159 let versioned_kernel_modules = fs::read_dir(kernel_modules.clone())
160 .map_err(|e| SynthesizeError::ReadPath {
161 path: kernel_modules.clone(),
162 err: e,
163 })?
164 .map(|res| res.map(|e| e.path()))
165 .next()
166 .ok_or(SynthesizeError::MissingKernelVersionDir(kernel_modules))??;
167 let kernel_version = versioned_kernel_modules
168 .file_name()
169 .ok_or(BootspecError::InvalidFileName(
170 versioned_kernel_modules.clone(),
171 ))?
172 .to_str()
173 .ok_or(BootspecError::InvalidUtf8(versioned_kernel_modules.clone()))?;
174
175 let kernel_params: Vec<String> = fs::read_to_string(generation.join("kernel-params"))?
176 .split(' ')
177 .map(str::to_string)
178 .collect();
179
180 let init = generation.join("init");
181
182 let initrd_path = generation.join("initrd");
183 let initrd = if initrd_path.exists() {
184 Some(fs::canonicalize(initrd_path.clone()).map_err(|e| {
185 SynthesizeError::Canonicalize {
186 path: initrd_path,
187 err: e,
188 }
189 })?)
190 } else {
191 None
192 };
193
194 let initrd_secrets = if generation.join("append-initrd-secrets").exists() {
195 Some(generation.join("append-initrd-secrets"))
196 } else {
197 None
198 };
199
200 Ok(Self {
201 label: format!("NixOS {} (Linux {})", system_version, kernel_version),
202 kernel,
203 kernel_params,
204 init,
205 initrd,
206 initrd_secrets,
207 system,
208 toplevel: SystemConfigurationRoot(generation),
209 })
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use std::fs;
216 use std::path::{Path, PathBuf};
217
218 use super::{BootSpecV1, SystemConfigurationRoot};
219 use crate::JSON_FILENAME;
220 use tempfile::TempDir;
221
222 fn create_generation_files_and_dirs(
223 generation: &Path,
224 kernel_version: &str,
225 system: &str,
226 system_version: &str,
227 kernel_params: &[String],
228 ) {
229 fs::create_dir_all(
230 generation.join(format!("kernel-modules/lib/modules/{}", kernel_version)),
231 )
232 .expect("Failed to write to test generation");
233 fs::create_dir_all(generation.join("specialisation"))
234 .expect("Failed to write to test generation");
235 fs::create_dir_all(generation.join("bootspec"))
236 .expect("Failed to create the bootspec directory during test scaffolding");
237
238 fs::write(generation.join("nixos-version"), system_version)
239 .expect("Failed to write to test generation");
240 fs::write(generation.join("system"), system).expect("Failed to write system double");
241 fs::write(generation.join("kernel"), "").expect("Failed to write to test generation");
242 fs::write(generation.join("kernel-params"), kernel_params.join(" "))
243 .expect("Failed to write to test generation");
244 fs::write(generation.join("init"), "").expect("Failed to write to test generation");
245 fs::write(generation.join("initrd"), "").expect("Failed to write to test generation");
246 fs::write(generation.join("append-initrd-secrets"), "")
247 .expect("Failed to write to test generation");
248 }
249
250 fn scaffold(
251 system: &str,
252 system_version: &str,
253 kernel_version: &str,
254 kernel_params: &[String],
255 specialisations: Option<Vec<&str>>,
256 specialisations_have_boot_spec: bool,
257 ) -> PathBuf {
258 let temp_dir = TempDir::new().expect("Failed to create tempdir for test generation");
259 let generation = temp_dir.keep();
260
261 create_generation_files_and_dirs(
262 &generation,
263 kernel_version,
264 system,
265 system_version,
266 kernel_params,
267 );
268
269 if let Some(specialisations) = specialisations {
270 for spec_name in specialisations {
271 let spec_path = generation.join("specialisation").join(spec_name);
272 fs::create_dir_all(&spec_path).expect("Failed to write to test generation");
273
274 create_generation_files_and_dirs(
275 &spec_path,
276 kernel_version,
277 system_version,
278 system,
279 kernel_params,
280 );
281
282 if specialisations_have_boot_spec {
283 fs::write(spec_path.join(JSON_FILENAME), "")
284 .expect("Failed to write to test generation");
285 }
286 }
287 }
288
289 generation
290 }
291
292 #[test]
293 fn no_bootspec_no_specialisation() {
294 let system = String::from("x86_64-linux");
295 let system_version = String::from("test-version-1");
296 let kernel_version = String::from("1.1.1-test1");
297 let kernel_params = [
298 "udev.log_priority=3",
299 "systemd.unified_cgroup_hierarchy=1",
300 "loglevel=4",
301 ]
302 .iter()
303 .map(ToString::to_string)
304 .collect::<Vec<_>>();
305
306 let generation = scaffold(
307 &system,
308 &system_version,
309 &kernel_version,
310 &kernel_params,
311 None,
312 false,
313 );
314 let spec = BootSpecV1::synthesize(&generation).unwrap();
315
316 assert_eq!(
317 spec,
318 BootSpecV1 {
319 system,
320 label: "NixOS test-version-1 (Linux 1.1.1-test1)".into(),
321 kernel: generation.join("kernel"),
322 kernel_params,
323 init: generation.join("init"),
324 initrd: Some(generation.join("initrd")),
325 initrd_secrets: Some(generation.join("append-initrd-secrets")),
326 toplevel: SystemConfigurationRoot(generation),
327 }
328 );
329 }
330
331 #[test]
332 fn no_bootspec_with_specialisation_no_bootspec() {
333 let system = String::from("x86_64-linux");
334 let system_version = String::from("test-version-2");
335 let kernel_version = String::from("1.1.1-test2");
336 let kernel_params = [
337 "udev.log_priority=3",
338 "systemd.unified_cgroup_hierarchy=1",
339 "loglevel=4",
340 ]
341 .iter()
342 .map(ToString::to_string)
343 .collect::<Vec<_>>();
344 let specialisations = vec!["spec1", "spec2"];
345
346 let generation = scaffold(
347 &system,
348 &system_version,
349 &kernel_version,
350 &kernel_params,
351 Some(specialisations),
352 false,
353 );
354
355 BootSpecV1::synthesize(&generation).unwrap();
356 }
357
358 #[test]
359 fn with_bootspec_no_specialisation() {
360 let system = String::from("x86_64-linux");
361 let system_version = String::from("test-version-3");
362 let kernel_version = String::from("1.1.1-test3");
363 let kernel_params = [
364 "udev.log_priority=3",
365 "systemd.unified_cgroup_hierarchy=1",
366 "loglevel=4",
367 ]
368 .iter()
369 .map(ToString::to_string)
370 .collect::<Vec<_>>();
371
372 let generation = scaffold(
373 &system,
374 &system_version,
375 &kernel_version,
376 &kernel_params,
377 None,
378 false,
379 );
380
381 fs::write(generation.join(JSON_FILENAME), "").expect("Failed to write to test generation");
382
383 let spec = BootSpecV1::synthesize(&generation).unwrap();
384
385 assert_eq!(
386 spec,
387 BootSpecV1 {
388 system,
389 label: "NixOS test-version-3 (Linux 1.1.1-test3)".into(),
390 kernel: generation.join("kernel"),
391 kernel_params,
392 init: generation.join("init"),
393 initrd: Some(generation.join("initrd")),
394 initrd_secrets: Some(generation.join("append-initrd-secrets")),
395 toplevel: SystemConfigurationRoot(generation)
396 }
397 );
398 }
399
400 #[test]
401 fn with_bootspec_with_specialisations_with_bootspec() {
402 let system = String::from("x86_64-linux");
403 let system_version = String::from("test-version-4");
404 let kernel_version = String::from("1.1.1-test4");
405 let kernel_params = [
406 "udev.log_priority=3",
407 "systemd.unified_cgroup_hierarchy=1",
408 "loglevel=4",
409 ]
410 .iter()
411 .map(ToString::to_string)
412 .collect::<Vec<_>>();
413 let specialisations = vec!["spec1", "spec2"];
414
415 let generation = scaffold(
416 &system,
417 &system_version,
418 &kernel_version,
419 &kernel_params,
420 Some(specialisations),
421 true,
422 );
423
424 fs::write(generation.join("bootspec").join(JSON_FILENAME), "")
425 .expect("Failed to write to test generation");
426
427 BootSpecV1::synthesize(&generation).unwrap();
428 }
429}