bootspec/
v1.rs

1//! The V1 bootspec format.
2use 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
12/// The V1 bootspec schema version.
13pub const SCHEMA_VERSION: u64 = 1;
14
15/// A V1 bootspec generation.
16///
17/// This structure represents an entire V1 generation (i.e. it includes the `org.nixos.bootspec.v1`
18/// and `org.nixos.specialisation.v1` structures).
19///
20/// ## Warnings
21///
22/// If you attempt to deserialize using this struct, you will not get any information about
23/// user-provided extensions. For that, you must deserialize with [`crate::BootJson`].
24#[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    /// Synthesize a [`GenerationV1`] struct from the path to a NixOS generation.
34    ///
35    /// This is useful when used on generations that do not have a bootspec attached to it.
36    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
64/// A mapping of V1 bootspec specialisations.
65///
66/// This structure represents the contents of the `org.nixos.specialisation.v1` key.
67pub type SpecialisationsV1 = HashMap<SpecialisationName, SpecialisationV1>;
68
69/// A V1 bootspec specialisation.
70///
71/// This structure represents a single specialisation contained in the `org.nixos.specialisation.v1` key.
72#[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    /// Synthesize a [`SpecialisationV1`] struct from the path to a NixOS generation.
87    ///
88    /// This is useful when used on generations that do not have a bootspec attached to it.
89    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/// A V1 bootspec toplevel.
99///
100/// This structure represents the contents of the `org.nixos.bootspec.v1` key.
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102#[serde(rename_all = "camelCase")]
103pub struct BootSpecV1 {
104    /// Label for the system closure
105    pub label: String,
106    /// Path to kernel (bzImage) -- $toplevel/kernel
107    pub kernel: PathBuf,
108    /// list of kernel parameters
109    pub kernel_params: Vec<String>,
110    /// Path to the init script
111    pub init: PathBuf,
112    /// Path to initrd -- $toplevel/initrd
113    pub initrd: Option<PathBuf>,
114    /// Path to "append-initrd-secrets" script -- $toplevel/append-initrd-secrets
115    pub initrd_secrets: Option<PathBuf>,
116    /// System double, e.g. x86_64-linux, for the system closure
117    pub system: String,
118    /// config.system.build.toplevel path
119    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}