├── rustfmt.toml ├── .envrc ├── synthesize ├── integration-test-cases │ ├── .editorconfig │ ├── 15.09-plain.nix │ ├── 16.03-plain.nix │ ├── 16.09-plain.nix │ ├── 17.03-plain.nix │ ├── 17.09-plain.nix │ ├── 18.03-plain.nix │ ├── 18.09-plain.nix │ ├── 19.03-plain.nix │ ├── 19.09-plain.nix │ ├── 20.03-plain.nix │ ├── 20.09-plain.nix │ ├── 21.05-plain.nix │ ├── 21.11-plain.nix │ ├── verify.sh │ ├── 21.11-specialisations.nix │ └── expected-synthesis │ │ ├── 15.09-plain.json │ │ ├── 16.03-plain.json │ │ ├── 16.09-plain.json │ │ ├── 17.03-plain.json │ │ ├── 17.09-plain.json │ │ ├── 18.03-plain.json │ │ ├── 18.09-plain.json │ │ ├── 19.03-plain.json │ │ ├── 20.03-plain.json │ │ ├── 20.09-plain.json │ │ ├── 19.09-plain.json │ │ ├── 21.05-plain.json │ │ ├── 21.11-plain.json │ │ └── 21.11-specialisations.json ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── Cargo.toml ├── .gitignore ├── shell.nix ├── .github ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── update-flake-lock.yaml │ ├── update.yml │ ├── json-schema.yaml │ └── ci.yml ├── default.nix ├── validate ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── bootspec ├── README.md ├── Cargo.toml ├── src │ ├── error.rs │ ├── deser.rs │ ├── lib.rs │ ├── v1.rs │ └── generation.rs └── rfc0125_spec.json ├── .editorconfig ├── flake.lock ├── README.md ├── LICENSE ├── flake.nix ├── schema.json └── Cargo.lock /rustfmt.toml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.json] 2 | insert_final_newline = false 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "bootspec", 6 | "synthesize", 7 | "validate", 8 | ] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | result* 3 | synthesize/integration-test-cases/builds 4 | synthesize/integration-test-cases/generated-synthesis 5 | 6 | # Docs output 7 | /dist 8 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | (fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; 5 | }) 6 | { src = ./.; }).shellNix 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | (fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; 5 | }) 6 | { src = ./.; }).defaultNix 7 | -------------------------------------------------------------------------------- /validate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "validate" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | bootspec = { path = "../bootspec" } 10 | serde_json = "1.0.99" 11 | -------------------------------------------------------------------------------- /validate/README.md: -------------------------------------------------------------------------------- 1 | # validate 2 | 3 | The `validate` tool is used to validate a [bootspec] document produced 4 | by any tool against the specification. 5 | 6 | [bootspec]: https://github.com/NixOS/rfcs/pull/125 7 | 8 | ## Usage 9 | 10 | ```terminal 11 | $ validate /path/to/bootspec/json/document 12 | ``` 13 | -------------------------------------------------------------------------------- /synthesize/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "synthesize" 3 | version = "0.1.1" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde_json = "1.0.99" 10 | tempfile = "3.23.0" 11 | bootspec = { path = "../bootspec" } 12 | clap = { version = "4.5.48", features = ["derive"] } 13 | 14 | [dev-dependencies] 15 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/15.09-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/15.09.tar.gz"; 4 | sha256 = "sha256:0pn142js99ncn7f53bw7hcp99ldjzb2m7xhjrax00xp72zswzv2n"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/16.03-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/16.03.tar.gz"; 4 | sha256 = "sha256:0m2b5ignccc5i5cyydhgcgbyl8bqip4dz32gw0c6761pd4kgw56v"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/16.09-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/16.09.tar.gz"; 4 | sha256 = "sha256:1cx5cfsp4iiwq8921c15chn1mhjgzydvhdcmrvjmqzinxyz71bzh"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/17.03-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/17.03.tar.gz"; 4 | sha256 = "sha256:1fw9ryrz1qzbaxnjqqf91yxk1pb9hgci0z0pzw53f675almmv9q2"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/17.09-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/17.09.tar.gz"; 4 | sha256 = "sha256:0kpx4h9p1lhjbn1gsil111swa62hmjs9g93xmsavfiki910s73sh"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/18.03-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/18.03.tar.gz"; 4 | sha256 = "sha256:0hk4y2vkgm1qadpsm4b0q1vxq889jhxzjx3ragybrlwwg54mzp4f"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/18.09-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/18.09.tar.gz"; 4 | sha256 = "sha256:1ib96has10v5nr6bzf7v8kw7yzww8zanxgw2qi1ll1sbv6kj6zpd"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/19.03-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/19.03.tar.gz"; 4 | sha256 = "sha256:0q2m2qhyga9yq29yz90ywgjbn9hdahs7i8wwlq7b55rdbyiwa5dy"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/19.09-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/19.09.tar.gz"; 4 | sha256 = "sha256:0mhqhq21y5vrr1f30qd2bvydv4bbbslvyzclhw0kdxmkgg3z4c92"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/20.03-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/20.03.tar.gz"; 4 | sha256 = "sha256:0182ys095dfx02vl2a20j1hz92dx3mfgz2a6fhn31bqlp1wa8hlq"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/20.09-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/20.09.tar.gz"; 4 | sha256 = "sha256:1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/21.05-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/21.05.tar.gz"; 4 | sha256 = "sha256:1ckzhh24mgz6jd1xhfgx0i9mijk6xjqxwsshnvq789xsavrmsc36"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/21.11-plain.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/21.11.tar.gz"; 4 | sha256 = "sha256:162dywda2dvfj1248afxc45kcrg83appjd0nmdb541hl7rnncf02"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ]; 12 | }; 13 | }).config.system.build.toplevel 14 | -------------------------------------------------------------------------------- /bootspec/README.md: -------------------------------------------------------------------------------- 1 | # bootspec 2 | 3 | This crate provides various structures and constants useful for interacting with the NixOS boot specification. 4 | 5 | See: https://github.com/NixOS/rfcs/pull/125. 6 | 7 | The `BootJson` struct implements the `serde::Deserialize` and `serde::Serialize` traits, making it easy to work with existing bootspec documents as well as creating new ones. 8 | 9 | ## Versioning 10 | 11 | * `bootspec` crate versions `1.x` and `2.x` are compatible with [bootspec V1](https://github.com/NixOS/rfcs/pull/125). 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.rs] 16 | indent_size = 4 17 | 18 | [Cargo.lock] 19 | indent_size = unset 20 | 21 | [*.{diff,patch}] 22 | end_of_line = unset 23 | insert_final_newline = unset 24 | trim_trailing_whitespace = unset 25 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/verify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd "$(dirname "$0")" 4 | 5 | rm -rf ./builds 6 | mkdir ./builds 7 | 8 | (for system in ./*.nix; do 9 | printf "%s --out-link ./builds/%s\n" "$system" "$(basename -s ".nix" "$system")" 10 | done) | xargs --max-procs=$(nproc) --max-lines=1 nix-build 11 | 12 | rm -rf generated-synthesis 13 | mkdir generated-synthesis 14 | for out in ./builds/*; do 15 | cargo run --bin synthesize -- --version=1 "$out" "./generated-synthesis/$(basename "$out").json" 16 | done 17 | 18 | diff -r ./expected-synthesis ./generated-synthesis 19 | -------------------------------------------------------------------------------- /synthesize/README.md: -------------------------------------------------------------------------------- 1 | # synthesize 2 | 3 | The `synthesize` tool is used to generate a [bootspec] document that can be 4 | consumed in any tool that conforms to the specification. This tool is mostly 5 | useful for creating a bootspec on generations realised prior to the 6 | implementation of the bootspec in NixOS. 7 | 8 | See: https://github.com/NixOS/rfcs/pull/125 9 | 10 | ## Usage 11 | 12 | ```terminal 13 | $ synthesize /path/to/generation boot.json --version $bootspec_version 14 | ``` 15 | 16 | where `$bootspec_version` is a number referring to the bootspec version you want to synthesize. 17 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/21.11-specialisations.nix: -------------------------------------------------------------------------------- 1 | let 2 | src = builtins.fetchTarball { 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/21.11.tar.gz"; 4 | sha256 = "sha256:162dywda2dvfj1248afxc45kcrg83appjd0nmdb541hl7rnncf02"; 5 | }; 6 | in 7 | (import "${src}/nixos" { 8 | configuration = { 9 | imports = [ 10 | "${src}/nixos/modules/virtualisation/qemu-vm.nix" 11 | ({ pkgs, ... }: { 12 | specialisation.example.configuration = { 13 | environment.systemPackages = [ pkgs.hello ]; 14 | }; 15 | }) 16 | ]; 17 | }; 18 | }).config.system.build.toplevel 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ##### Description 2 | 3 | 7 | 8 | ##### Checklist 9 | 10 | 14 | 15 | - [ ] Built with `cargo build` 16 | - [ ] Formatted with `cargo fmt` 17 | - [ ] Linted with `cargo clippy` 18 | - [ ] Ran tests with `cargo test` 19 | - [ ] Added or updated relevant tests (leave unchecked if not applicable) 20 | - [ ] Added or updated relevant documentation (leave unchecked if not applicable) 21 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/15.09-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 15.09pre-git (Linux 3.18.21)", 4 | "kernel": "/nix/store/vvabsbnxmh8nx6pq0bkk6vpsy5cqf9i5-linux-3.18.21/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/fl1c8cclzzri16zimdfz8wp31w5yc7dp-nixos-15.09pre-git/init", 9 | "initrd": "/nix/store/gqdm8dk6my53kvn23r81win9vadjqv80-initrd/initrd", 10 | "initrdSecrets": null, 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/fl1c8cclzzri16zimdfz8wp31w5yc7dp-nixos-15.09pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1759381078, 6 | "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/16.03-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 16.03pre-git (Linux 4.4.6)", 4 | "kernel": "/nix/store/vmsl93kyx72kh03m0h6r2w5ivywbjxm6-linux-4.4.6/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/k0lfhjxd5znyrqkgrdi3hh7wxr885fcj-nixos-system-nixos-16.03pre-git/init", 9 | "initrd": "/nix/store/q1hg158mvnc9bscc22cv45ys484c4g9x-initrd/initrd", 10 | "initrdSecrets": null, 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/k0lfhjxd5znyrqkgrdi3hh7wxr885fcj-nixos-system-nixos-16.03pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/16.09-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 16.09pre-git (Linux 4.4.23)", 4 | "kernel": "/nix/store/x58d7k8lczvh4qsqaj4jky1hzpc788b4-linux-4.4.23/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/jfwlp9s434kijn4i53fr5pvrryzspwc4-nixos-system-nixos-16.09pre-git/init", 9 | "initrd": "/nix/store/rv6i771w5z01z1xvylxhymp5pw44wc6j-initrd/initrd", 10 | "initrdSecrets": null, 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/jfwlp9s434kijn4i53fr5pvrryzspwc4-nixos-system-nixos-16.09pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/17.03-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 17.03pre-git (Linux 4.9.18)", 4 | "kernel": "/nix/store/syp5ycwxv6yvfv2c9aq3wwqilxpnjvry-linux-4.9.18/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/51yf3by1xpzawi30a1rny34gm1047vg8-nixos-system-nixos-17.03pre-git/init", 9 | "initrd": "/nix/store/spbi6va9dmd41rg15nd9wxjj94097q49-initrd/initrd", 10 | "initrdSecrets": null, 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/51yf3by1xpzawi30a1rny34gm1047vg8-nixos-system-nixos-17.03pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /.github/workflows/update-flake-lock.yaml: -------------------------------------------------------------------------------- 1 | name: update-flake-lock 2 | 3 | on: 4 | workflow_dispatch: # enable manual triggering 5 | schedule: 6 | - cron: '0 0 * * 0' # every Sunday at midnight 7 | 8 | jobs: 9 | lockfile: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v5 13 | - uses: DeterminateSystems/determinate-nix-action@main 14 | - uses: DeterminateSystems/flakehub-cache-action@main 15 | - uses: DeterminateSystems/update-flake-lock@main 16 | with: 17 | pr-title: "Update flake.lock" 18 | pr-labels: | 19 | dependencies 20 | automated 21 | inputs: | 22 | nixpkgs 23 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: update-flake-lock 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 0 * * 0' 6 | 7 | jobs: 8 | lockfile: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v5 13 | - name: Check flake 14 | uses: DeterminateSystems/flake-checker-action@main 15 | with: 16 | fail-mode: true 17 | - name: Install Determinate Nix 18 | uses: DeterminateSystems/determinate-nix-action@main 19 | - name: Enable FlakeHub cache 20 | uses: DeterminateSystems/flakehub-cache-action@main 21 | - name: Update flake.lock 22 | uses: DeterminateSystems/update-flake-lock@main 23 | -------------------------------------------------------------------------------- /bootspec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootspec" 3 | version = "2.0.0" 4 | edition = "2021" 5 | description = "An implementation of NixOS RFC 125's bootspec datatype" 6 | license = "MIT" 7 | repository = "https://github.com/DeterminateSystems/bootspec" 8 | documentation = "https://docs.rs/bootspec/latest/bootspec" 9 | authors = ["Graham Christensen ", "Cole Helbling "] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | serde = { version = "1.0.164", features = ["derive"] } 15 | serde_json = "1.0.99" 16 | thiserror = "1.0.40" 17 | 18 | [dev-dependencies] 19 | tempfile = "3.23.0" 20 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/17.09-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 17.09pre-git (Linux 4.9.52)", 4 | "kernel": "/nix/store/8i1sk0wbwliz0rrpmcq19nm1xlyjn6zg-linux-4.9.52/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/bxahnn6p04ydz5a02002pjcmgqfmq509-nixos-system-nixos-17.09pre-git/init", 9 | "initrd": "/nix/store/nnyp0dqb62dicr9wyw2ykcrc0vk7f5mf-initrd/initrd", 10 | "initrdSecrets": "/nix/store/bxahnn6p04ydz5a02002pjcmgqfmq509-nixos-system-nixos-17.09pre-git/append-initrd-secrets", 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/bxahnn6p04ydz5a02002pjcmgqfmq509-nixos-system-nixos-17.09pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/18.03-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 18.03pre-git (Linux 4.14.32)", 4 | "kernel": "/nix/store/n1fhfw5pyggaybsixvm0haq2h67biah0-linux-4.14.32/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/jr5bql6xdfhhv4bmibiz3b74pjp8sdca-nixos-system-nixos-18.03pre-git/init", 9 | "initrd": "/nix/store/gnjk07h7ynhp3f8x145dv224j0wf0s1h-initrd/initrd", 10 | "initrdSecrets": "/nix/store/jr5bql6xdfhhv4bmibiz3b74pjp8sdca-nixos-system-nixos-18.03pre-git/append-initrd-secrets", 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/jr5bql6xdfhhv4bmibiz3b74pjp8sdca-nixos-system-nixos-18.03pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/18.09-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 18.09pre-git (Linux 4.14.74)", 4 | "kernel": "/nix/store/s1kjv7xs8gaqw2mfhga0yppyl7k78jnf-linux-4.14.74/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/b96sr7vmv90wxfjfk2d7icxh7d77a1na-nixos-system-nixos-18.09pre-git/init", 9 | "initrd": "/nix/store/2ib32agl00qy7sf7ka5sdbrl6kg4b9gi-initrd/initrd", 10 | "initrdSecrets": "/nix/store/b96sr7vmv90wxfjfk2d7icxh7d77a1na-nixos-system-nixos-18.09pre-git/append-initrd-secrets", 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/b96sr7vmv90wxfjfk2d7icxh7d77a1na-nixos-system-nixos-18.09pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/19.03-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 19.03pre-git (Linux 4.19.34)", 4 | "kernel": "/nix/store/3wbab1zxpil3ia7flkmzl8j3qds09dbw-linux-4.19.34/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4" 7 | ], 8 | "init": "/nix/store/wp2gq8zzcixy2d1dvswhbhdl532jxhsf-nixos-system-nixos-19.03pre-git/init", 9 | "initrd": "/nix/store/fv9ag95k2ww4w87grwj6hg0b25is5giy-initrd/initrd", 10 | "initrdSecrets": "/nix/store/wp2gq8zzcixy2d1dvswhbhdl532jxhsf-nixos-system-nixos-19.03pre-git/append-initrd-secrets", 11 | "system": "x86_64-linux", 12 | "toplevel": "/nix/store/wp2gq8zzcixy2d1dvswhbhdl532jxhsf-nixos-system-nixos-19.03pre-git" 13 | }, 14 | "org.nixos.specialisation.v1": {} 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bootspec 2 | 3 | This repository implements datatypes for NixOS RFC-0125 "bootspec" and a synthesis tool to generate bootspec documents for generations which don't have one. 4 | 5 | ## Crates 6 | 7 | ### `bootspec` 8 | 9 | The `bootspec` crate provides various structures and constants useful for interacting with the NixOS boot specification. 10 | 11 | ### `synthesize` 12 | 13 | The `synthesize` crate provides a CLI that, when provided a path to a NixOS generation and an output file, will synthesize a boot specification document from the available information. 14 | 15 | Verify changes to the synthesis tool with `cargo test` and also by running `./synthesize/integration-test-cases/verify.sh` to ensure it generates the same results as before. 16 | 17 | # License 18 | 19 | [MIT](./LICENSE) 20 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/20.03-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 20.03pre-git (Linux 5.4.33)", 4 | "kernel": "/nix/store/li3v6p9mqsspm0zgglzba2szab2zdmiv-linux-5.4.33/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4", 7 | "net.ifnames=0" 8 | ], 9 | "init": "/nix/store/l651p9fhdwkn7p49vawif8a2b4v1nsfl-nixos-system-nixos-20.03pre-git/init", 10 | "initrd": "/nix/store/1j7s94v0axvgwcv4s0ka56xmgvffdlc0-initrd-linux-5.4.33/initrd", 11 | "initrdSecrets": "/nix/store/l651p9fhdwkn7p49vawif8a2b4v1nsfl-nixos-system-nixos-20.03pre-git/append-initrd-secrets", 12 | "system": "x86_64-linux", 13 | "toplevel": "/nix/store/l651p9fhdwkn7p49vawif8a2b4v1nsfl-nixos-system-nixos-20.03pre-git" 14 | }, 15 | "org.nixos.specialisation.v1": {} 16 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/20.09-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 20.09pre-git (Linux 5.4.72)", 4 | "kernel": "/nix/store/ysrwgw1wp9ha0484dh8q1awysn9vfxdy-linux-5.4.72/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4", 7 | "net.ifnames=0" 8 | ], 9 | "init": "/nix/store/6621qzx8dlvwgpjjs1sarw9lxv4x38ps-nixos-system-nixos-20.09pre-git/init", 10 | "initrd": "/nix/store/r7ifs7qvxb5fdz7hhjh5m8a65k34ik25-initrd-linux-5.4.72/initrd", 11 | "initrdSecrets": "/nix/store/6621qzx8dlvwgpjjs1sarw9lxv4x38ps-nixos-system-nixos-20.09pre-git/append-initrd-secrets", 12 | "system": "x86_64-linux", 13 | "toplevel": "/nix/store/6621qzx8dlvwgpjjs1sarw9lxv4x38ps-nixos-system-nixos-20.09pre-git" 14 | }, 15 | "org.nixos.specialisation.v1": {} 16 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/19.09-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 19.09pre-git (Linux 4.19.78)", 4 | "kernel": "/nix/store/s7zp6i6r73a0sri2fihmpnwbqrpsk8fs-linux-4.19.78/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4", 7 | "net.ifnames=0" 8 | ], 9 | "init": "/nix/store/wqr3p7hd4x59g14zx7qkz8sqvgl9ngvl-nixos-system-nixos-19.09pre-git/init", 10 | "initrd": "/nix/store/d6xpsfsby2y1d2hvfhdgss45swrazipf-initrd-linux-4.19.78/initrd", 11 | "initrdSecrets": "/nix/store/wqr3p7hd4x59g14zx7qkz8sqvgl9ngvl-nixos-system-nixos-19.09pre-git/append-initrd-secrets", 12 | "system": "x86_64-linux", 13 | "toplevel": "/nix/store/wqr3p7hd4x59g14zx7qkz8sqvgl9ngvl-nixos-system-nixos-19.09pre-git" 14 | }, 15 | "org.nixos.specialisation.v1": {} 16 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/21.05-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 21.05pre-git (Linux 5.10.37)", 4 | "kernel": "/nix/store/rrmsyrg9ksd9x2cqb01pmbw0yd0anf7v-linux-5.10.37/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4", 7 | "net.ifnames=0" 8 | ], 9 | "init": "/nix/store/sz7d2wmq5jx4rrdmx2ksb9wmc5raqvwn-nixos-system-nixos-21.05pre-git/init", 10 | "initrd": "/nix/store/5ci6pag0hclx1642nm5rs6zjl60drns4-initrd-linux-5.10.37/initrd", 11 | "initrdSecrets": "/nix/store/sz7d2wmq5jx4rrdmx2ksb9wmc5raqvwn-nixos-system-nixos-21.05pre-git/append-initrd-secrets", 12 | "system": "x86_64-linux", 13 | "toplevel": "/nix/store/sz7d2wmq5jx4rrdmx2ksb9wmc5raqvwn-nixos-system-nixos-21.05pre-git" 14 | }, 15 | "org.nixos.specialisation.v1": {} 16 | } -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/21.11-plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 21.11pre-git (Linux 5.10.81)", 4 | "kernel": "/nix/store/hprwry55jwyd71ng7v7c2rhk3a3z1im8-linux-5.10.81/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4", 7 | "net.ifnames=0" 8 | ], 9 | "init": "/nix/store/7xvzw9yxw91vklhyzf0isg9256xh1l5l-nixos-system-nixos-21.11pre-git/init", 10 | "initrd": "/nix/store/69bhfdfv77y0vclnlxqrd8pxjzbkz47w-initrd-linux-5.10.81/initrd", 11 | "initrdSecrets": "/nix/store/7xvzw9yxw91vklhyzf0isg9256xh1l5l-nixos-system-nixos-21.11pre-git/append-initrd-secrets", 12 | "system": "x86_64-linux", 13 | "toplevel": "/nix/store/7xvzw9yxw91vklhyzf0isg9256xh1l5l-nixos-system-nixos-21.11pre-git" 14 | }, 15 | "org.nixos.specialisation.v1": {} 16 | } -------------------------------------------------------------------------------- /bootspec/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[derive(Debug, thiserror::Error)] 4 | pub enum BootspecError { 5 | #[error("failed to synthesize: {0}")] 6 | Synthesize(#[from] SynthesizeError), 7 | #[error(transparent)] 8 | Io(#[from] std::io::Error), 9 | #[error("{0} had an invalid file name")] 10 | InvalidFileName(PathBuf), 11 | #[error("{0} contained invalid UTF8")] 12 | InvalidUtf8(PathBuf), 13 | } 14 | 15 | #[derive(Debug, thiserror::Error)] 16 | pub enum SynthesizeError { 17 | #[error("unsupported schema version {0}")] 18 | UnsupportedVersion(u64), 19 | #[error("failed to canonicalize {path}: {err}")] 20 | Canonicalize { 21 | path: PathBuf, 22 | #[source] 23 | err: std::io::Error, 24 | }, 25 | #[error("failed to read {path}: {err}")] 26 | ReadPath { 27 | path: PathBuf, 28 | #[source] 29 | err: std::io::Error, 30 | }, 31 | #[error("could not find kernel version dir in {0}")] 32 | MissingKernelVersionDir(PathBuf), 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/json-schema.yaml: -------------------------------------------------------------------------------- 1 | name: Bootspec JSON Schema 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | BuildAndPublishJsonSchemaDocs: 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: github-pages 12 | url: ${{ steps.publish.outputs.page_url }} 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | steps: 18 | - uses: actions/checkout@v5 19 | with: 20 | fetch-depth: 0 21 | - uses: DeterminateSystems/determinate-nix-action@main 22 | - uses: DeterminateSystems/flakehub-cache-action@main 23 | - name: Set up GitHub Pages 24 | uses: actions/configure-pages@v5 25 | - name: Generate JSON Schema docs 26 | id: generate 27 | run: | 28 | mkdir -p dist 29 | nix develop --command generate-schema-doc --config expand_buttons=true schema.json dist/index.html 30 | - name: Upload docs 31 | uses: actions/upload-pages-artifact@v4 32 | with: 33 | path: ./dist 34 | - name: Publish docs to GitHub Pages 35 | id: publish 36 | uses: actions/deploy-pages@v4 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Determinate Systems, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /synthesize/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{self, Write}; 3 | use std::path::PathBuf; 4 | 5 | use bootspec::BootJson; 6 | 7 | #[derive(clap::Parser)] 8 | struct Cli { 9 | generation_dir: PathBuf, 10 | out_path: PathBuf, 11 | #[clap(long)] 12 | version: u64, 13 | } 14 | 15 | fn main() -> Result<(), Box> { 16 | if let Err(e) = self::cli() { 17 | writeln!(io::stderr(), "{}", e)?; 18 | 19 | std::process::exit(1); 20 | } 21 | 22 | Ok(()) 23 | } 24 | 25 | fn cli() -> Result<(), Box> { 26 | let args: Cli = clap::Parser::parse(); 27 | let generation_dir = args.generation_dir; 28 | let out_path = args.out_path; 29 | let version = args.version; 30 | 31 | let versioned_spec = BootJson::synthesize_version(&generation_dir, version)?; 32 | 33 | let pretty = serde_json::to_string_pretty(&versioned_spec) 34 | .map_err(|e| format!("Failed to make pretty JSON from bootspec:\n{}", e))?; 35 | 36 | fs::write(&out_path, pretty) 37 | .map_err(|e| format!("Failed to write JSON to '{}':\n{}", out_path.display(), e))?; 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "bootloader-experimentation"; 3 | 4 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 | 6 | outputs = inputs: 7 | let 8 | nameValuePair = name: value: { inherit name value; }; 9 | genAttrs = names: f: builtins.listToAttrs (map (n: nameValuePair n (f n)) names); 10 | allSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 11 | 12 | forAllSystems = f: genAttrs allSystems (system: f { 13 | inherit system; 14 | pkgs = import inputs.nixpkgs { inherit system; }; 15 | }); 16 | in 17 | { 18 | devShell = forAllSystems ({ system, pkgs, ... }: 19 | pkgs.mkShell { 20 | name = "bootspec"; 21 | 22 | buildInputs = with pkgs; [ 23 | cargo 24 | rustc 25 | clippy 26 | codespell 27 | nixpkgs-fmt 28 | rustfmt 29 | jsonschema # provides the jv tool 30 | json-schema-for-humans # provides the generate-schema-doc tool 31 | ]; 32 | }); 33 | 34 | packages = forAllSystems 35 | ({ system, pkgs, ... }: 36 | { 37 | package = pkgs.rustPlatform.buildRustPackage rec { 38 | pname = "bootspec"; 39 | version = "unreleased"; 40 | 41 | src = inputs.self; 42 | 43 | cargoLock.lockFile = ./Cargo.lock; 44 | }; 45 | }); 46 | 47 | defaultPackage = forAllSystems ({ system, ... }: inputs.self.packages.${system}.package); 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /synthesize/integration-test-cases/expected-synthesis/21.11-specialisations.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "label": "NixOS 21.11pre-git (Linux 5.10.81)", 4 | "kernel": "/nix/store/hprwry55jwyd71ng7v7c2rhk3a3z1im8-linux-5.10.81/bzImage", 5 | "kernelParams": [ 6 | "loglevel=4", 7 | "net.ifnames=0" 8 | ], 9 | "init": "/nix/store/kgkjwhscv52r2y1ha1y19lb9h4j3lfrc-nixos-system-nixos-21.11pre-git/init", 10 | "initrd": "/nix/store/69bhfdfv77y0vclnlxqrd8pxjzbkz47w-initrd-linux-5.10.81/initrd", 11 | "initrdSecrets": "/nix/store/kgkjwhscv52r2y1ha1y19lb9h4j3lfrc-nixos-system-nixos-21.11pre-git/append-initrd-secrets", 12 | "system": "x86_64-linux", 13 | "toplevel": "/nix/store/kgkjwhscv52r2y1ha1y19lb9h4j3lfrc-nixos-system-nixos-21.11pre-git" 14 | }, 15 | "org.nixos.specialisation.v1": { 16 | "example": { 17 | "org.nixos.bootspec.v1": { 18 | "label": "NixOS 21.11pre-git (Linux 5.10.81)", 19 | "kernel": "/nix/store/hprwry55jwyd71ng7v7c2rhk3a3z1im8-linux-5.10.81/bzImage", 20 | "kernelParams": [ 21 | "loglevel=4", 22 | "net.ifnames=0" 23 | ], 24 | "init": "/nix/store/3w5kr91xq46638fd310q2sa9mjm6r6hn-nixos-system-nixos-21.11pre-git/init", 25 | "initrd": "/nix/store/69bhfdfv77y0vclnlxqrd8pxjzbkz47w-initrd-linux-5.10.81/initrd", 26 | "initrdSecrets": "/nix/store/3w5kr91xq46638fd310q2sa9mjm6r6hn-nixos-system-nixos-21.11pre-git/append-initrd-secrets", 27 | "system": "x86_64-linux", 28 | "toplevel": "/nix/store/3w5kr91xq46638fd310q2sa9mjm6r6hn-nixos-system-nixos-21.11pre-git" 29 | }, 30 | "org.nixos.specialisation.v1": {} 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /bootspec/rfc0125_spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.nixos.bootspec.v1": { 3 | "system": "x86_64-linux", 4 | "init": "/nix/store/xxx-nixos-system-xxx/init", 5 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 6 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 7 | "kernel": "/nix/store/xxx-linux/bzImage", 8 | "kernelParams": [ 9 | "amd_iommu=on", 10 | "amd_iommu=pt", 11 | "iommu=pt", 12 | "kvm.ignore_msrs=1", 13 | "kvm.report_ignored_msrs=0", 14 | "udev.log_priority=3", 15 | "systemd.unified_cgroup_hierarchy=1", 16 | "loglevel=4" 17 | ], 18 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 19 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 20 | }, 21 | "org.nixos.specialisation.v1": { 22 | "": { 23 | "org.nix-community.test": { 24 | "foo": "bar" 25 | }, 26 | "org.nixos.bootspec.v1": { 27 | "system": "x86_64-linux", 28 | "init": "/nix/store/xxx-nixos-system-xxx/init", 29 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 30 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 31 | "kernel": "/nix/store/xxx-linux/bzImage", 32 | "kernelParams": [ 33 | "amd_iommu=on", 34 | "amd_iommu=pt", 35 | "iommu=pt", 36 | "kvm.ignore_msrs=1", 37 | "kvm.report_ignored_msrs=0", 38 | "udev.log_priority=3", 39 | "systemd.unified_cgroup_hierarchy=1", 40 | "loglevel=4" 41 | ], 42 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 43 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /validate/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{self, Write}; 3 | use std::path::PathBuf; 4 | 5 | use bootspec::BootJson; 6 | 7 | fn main() -> Result<(), Box> { 8 | if let Err(e) = self::cli() { 9 | writeln!(io::stderr(), "{}", e)?; 10 | 11 | std::process::exit(1); 12 | } 13 | 14 | Ok(()) 15 | } 16 | 17 | fn cli() -> Result<(), Box> { 18 | let Args { bootspec_path } = parse_args()?; 19 | 20 | let contents = fs::read_to_string(&bootspec_path)?; 21 | 22 | match serde_json::from_str::(&contents) { 23 | Ok(boot_json) => { 24 | let generation = boot_json.generation; 25 | writeln!( 26 | io::stdout(), 27 | "Bootspec document at '{}' DOES CONTAIN a valid v{} document.", 28 | bootspec_path.display(), 29 | generation.version() 30 | )?; 31 | } 32 | Err(err) => { 33 | return Err(format!( 34 | "Bootspec document at '{}' DOES NOT CONTAIN a valid document:\n{}", 35 | bootspec_path.display(), 36 | err 37 | ) 38 | .into()) 39 | } 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | pub struct Args { 46 | pub bootspec_path: PathBuf, 47 | } 48 | 49 | fn parse_args() -> Result> { 50 | let mut args = std::env::args().skip(1); 51 | 52 | if args.len() != 1 { 53 | writeln!(io::stderr(), "Usage: validate ")?; 54 | std::process::exit(1); 55 | } 56 | 57 | let bootspec_path = args 58 | .next() 59 | .ok_or("Expected path to bootspec document, got none.")? 60 | .parse::()?; 61 | 62 | Ok(Args { bootspec_path }) 63 | } 64 | -------------------------------------------------------------------------------- /bootspec/src/deser.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt; 3 | 4 | use serde::de::{Deserializer, MapAccess, Visitor}; 5 | 6 | use crate::Extensions; 7 | 8 | struct BootSpecExtensionsVisitor; 9 | 10 | impl<'de> Visitor<'de> for BootSpecExtensionsVisitor { 11 | type Value = Extensions; 12 | 13 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 14 | formatter.write_str("a map of bootspec extensions") 15 | } 16 | 17 | fn visit_map(self, mut access: M) -> Result 18 | where 19 | M: MapAccess<'de>, 20 | { 21 | let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0)); 22 | 23 | while let Some((key, value)) = access.next_entry::()? { 24 | // This is very hacky, but necessary because serde does not consume fields in flattened 25 | // enums (which `Generation` is). Without this, the bootspec and specialisation objects 26 | // would be duplicated under the `extensions` field. 27 | // See: https://github.com/serde-rs/serde/issues/2200 28 | if ["org.nixos.bootspec.", "org.nixos.specialisation."] 29 | .iter() 30 | .any(|field| key.starts_with(field)) 31 | { 32 | continue; 33 | } 34 | 35 | map.insert(key, value); 36 | } 37 | 38 | for (k, v) in map.iter() { 39 | if v.is_null() { 40 | return Err(serde::de::Error::custom(format!( 41 | "{k} was null, but null extensions are not allowed" 42 | ))); 43 | } 44 | } 45 | 46 | Ok(map) 47 | } 48 | } 49 | 50 | pub fn skip_generation_fields<'de, D>(deserializer: D) -> Result 51 | where 52 | D: Deserializer<'de>, 53 | { 54 | deserializer.deserialize_map(BootSpecExtensionsVisitor) 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | jobs: 8 | format: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | - uses: DeterminateSystems/determinate-nix-action@main 13 | - uses: DeterminateSystems/flakehub-cache-action@main 14 | - name: Check rustfmt 15 | run: nix develop --command cargo fmt -- --check 16 | 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v5 21 | - uses: DeterminateSystems/determinate-nix-action@main 22 | - uses: DeterminateSystems/flakehub-cache-action@main 23 | - name: Build 24 | run: nix build -L 25 | 26 | NixFlakeCheck: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v5 30 | - uses: DeterminateSystems/flake-checker-action@main 31 | with: 32 | fail-mode: false 33 | 34 | NixFormatting: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v5 38 | with: 39 | fetch-depth: 0 40 | - uses: DeterminateSystems/determinate-nix-action@main 41 | - uses: DeterminateSystems/flakehub-cache-action@main 42 | - name: Check nixpkgs-fmt formatting 43 | run: nix develop --command sh -c "git ls-files '*.nix' | xargs nixpkgs-fmt --check" 44 | 45 | EditorConfig: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v5 49 | with: 50 | fetch-depth: 0 51 | - uses: greut/eclint-action@v0 52 | 53 | ValidateJsonSchema: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v5 57 | with: 58 | fetch-depth: 0 59 | - uses: DeterminateSystems/determinate-nix-action@main 60 | - uses: DeterminateSystems/flakehub-cache-action@main 61 | - name: Validate JSON Schema 62 | run: nix develop --command jv ./schema.json 63 | - name: Validate JSON Schema against example 64 | run: nix develop --command jv ./schema.json ./bootspec/rfc0125_spec.json 65 | 66 | SynthesizeIntegration: 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v5 70 | with: 71 | fetch-depth: 0 72 | - uses: DeterminateSystems/determinate-nix-action@main 73 | - uses: DeterminateSystems/flakehub-cache-action@main 74 | - name: Verify synthesize integration test still passes 75 | run: nix develop -c ./synthesize/integration-test-cases/verify.sh 76 | -------------------------------------------------------------------------------- /bootspec/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod deser; 2 | pub mod error; 3 | pub mod generation; 4 | pub mod v1; 5 | 6 | use std::collections::HashMap; 7 | use std::fmt; 8 | use std::path::{Path, PathBuf}; 9 | 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::error::{BootspecError, SynthesizeError}; 13 | use crate::generation::Generation; 14 | 15 | #[doc(hidden)] 16 | pub(crate) type Result = core::result::Result; 17 | 18 | /// A wrapper type describing the name of a NixOS specialisation. 19 | #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] 20 | pub struct SpecialisationName(pub String); 21 | 22 | impl fmt::Display for SpecialisationName { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | write!(f, "{}", self.0) 25 | } 26 | } 27 | 28 | /// A wrapper type describing the root directory of a NixOS system configuration. 29 | #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] 30 | pub struct SystemConfigurationRoot(pub PathBuf); 31 | 32 | /// The bootspec schema filename. 33 | pub const JSON_FILENAME: &str = "boot.json"; 34 | /// The type for a collection of generic extensions. 35 | pub type Extensions = HashMap; 36 | 37 | // !!! IMPORTANT: KEEP `BootSpec`, `Specialisations`, `Specialisation`, and `SCHEMA_VERSION` IN SYNC !!! 38 | /// The current bootspec generation type. 39 | pub type BootSpec = v1::GenerationV1; 40 | /// The current specialisations type. 41 | pub type Specialisations = v1::SpecialisationsV1; 42 | /// The current specialisations type. 43 | pub type Specialisation = v1::SpecialisationV1; 44 | /// The current bootspec schema version. 45 | pub const SCHEMA_VERSION: u64 = v1::SCHEMA_VERSION; 46 | 47 | /// The current bootspec schema. 48 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 49 | pub struct BootJson { 50 | #[serde(flatten)] 51 | pub generation: Generation, 52 | #[serde( 53 | default = "HashMap::new", 54 | skip_serializing_if = "HashMap::is_empty", 55 | deserialize_with = "deser::skip_generation_fields", 56 | flatten 57 | )] 58 | pub extensions: Extensions, 59 | } 60 | 61 | impl BootJson { 62 | /// Synthesize a [`BootJson`] struct from the path to a generation using the latest 63 | /// specification version defined in this crate ([`SCHEMA_VERSION`]). 64 | /// 65 | /// See also [`BootJson::synthesize_version`]. 66 | /// 67 | /// ## Warnings 68 | /// 69 | /// Extensions will not be synthesized and will be an empty [`HashMap`]. 70 | pub fn synthesize_latest(generation_path: &Path) -> Result { 71 | Self::synthesize_version(generation_path, SCHEMA_VERSION) 72 | } 73 | 74 | /// Synthesize a [`BootJson`] struct from the path to a generation and a specific version. 75 | /// 76 | /// This is useful when used on generations that do not have a bootspec attached to it. 77 | /// This will not synthesize arbitrary extensions. 78 | /// 79 | /// ## Warnings 80 | /// 81 | /// Extensions will not be synthesized and will be an empty [`HashMap`]. 82 | pub fn synthesize_version(generation_path: &Path, version: u64) -> Result { 83 | let generation = match version { 84 | v1::SCHEMA_VERSION => { 85 | let generation = v1::GenerationV1::synthesize(generation_path)?; 86 | Generation::V1(generation) 87 | } 88 | v => { 89 | return Err(BootspecError::Synthesize( 90 | SynthesizeError::UnsupportedVersion(v), 91 | )) 92 | } 93 | }; 94 | 95 | Ok(BootJson { 96 | generation, 97 | extensions: HashMap::new(), 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://raw.githubusercontent.com/DeterminateSystems/bootspec/main/schema.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "title": "NixOS bootspec v1 schema", 5 | "description": "Bootspec is a set of memoized facts about a system's closure. The top-level object may contain arbitrary further keys (\"extensions\") whose semantics may be defined by third parties. The use of reverse-domain-name namespacing is recommended in order to avoid name collisions.", 6 | "type": "object", 7 | "required": ["org.nixos.bootspec.v1"], 8 | "properties": { 9 | "org.nixos.bootspec.v1": { "$ref": "#/$defs/BootspecV1" }, 10 | "org.nixos.specialisation.v1": { 11 | "type": "object", 12 | "patternProperties": { 13 | "^.*$": { 14 | "type": "object", 15 | "properties": { 16 | "org.nixos.bootspec.v1": { "$ref": "#/$defs/BootspecV1" } 17 | }, 18 | "required": ["org.nixos.bootspec.v1"], 19 | "additionalProperties": true 20 | } 21 | } 22 | } 23 | }, 24 | "patternProperties": { 25 | "^.*$": { 26 | "description": "Additional top-level specialisations" 27 | } 28 | }, 29 | "$defs": { 30 | "BootspecV1": { 31 | "type": "object", 32 | "required": ["init", "kernel", "kernelParams", "label", "system", "toplevel"], 33 | "properties": { 34 | "init": { 35 | "allOf": [ 36 | { "$ref": "#/$defs/NixStorePath" } 37 | ], 38 | "description": "Nix store path to the stage-2 init, executed by initrd (if present)." 39 | }, 40 | "kernel": { 41 | "allOf": [ 42 | { "$ref": "#/$defs/NixStorePath" } 43 | ], 44 | "description": "Nix store path to the kernel image." 45 | }, 46 | "kernelParams": { 47 | "type": "array", 48 | "items": { "$ref": "#/$defs/KernelParameter" }, 49 | "description": "List of kernel parameters", 50 | "examples": [ 51 | [ 52 | "amd_iommu=on", 53 | "amd_iommu=pt", 54 | "iommu=pt", 55 | "kvm.ignore_msrs=1", 56 | "kvm.report_ignored_msrs=0", 57 | "udev.log_priority=3", 58 | "systemd.unified_cgroup_hierarchy=1", 59 | "loglevel=4" 60 | ] 61 | ] 62 | }, 63 | "label": { 64 | "type": "string", 65 | "description": "A human-readable label for the system. It should contain the operating system, kernel version,and other user-relevant information to identify the system. This corresponds loosely to `config.system.nixos.label`.", 66 | "examples": ["NixOS 21.11.20210810.dirty (Linux 5.15.30)"] 67 | }, 68 | "system": { 69 | "type": "string", 70 | "description": "Nix system type the bootspec is intended for.", 71 | "examples": ["x86_64-linux", "aarch64-linux"] 72 | }, 73 | "toplevel": { 74 | "allOf": [ 75 | { "$ref": "#/$defs/NixStorePath" } 76 | ], 77 | "description": "Top-level Nix store path of the system closure." 78 | }, 79 | "initrd": { 80 | "allOf": [ 81 | { "$ref": "#/$defs/NixStorePath" } 82 | ], 83 | "description": "Nix store path to the initrd." 84 | }, 85 | "initrdSecrets": { 86 | "allOf": [ 87 | { "$ref": "#/$defs/NixStorePath" } 88 | ], 89 | "description": "Nix store path to a tool that dynamically adds secrets to initrd. Consumers of a bootspec document should copy the file referenced by the `initrd` key to a writable location, ensure that the file is writable, invoke this tool with the path to the initrd as its only argument, and use the initrd as modified by the tool for booting. This may be used to add files from outside the Nix store to the initrd. This tool is expected to run on the system whose boot specification is being set up, and may thus fail if used on a system where the expected stateful files are not in place or whose CPU does not support the instruction set of the system to be booted. If this field is present and the tool fails, no boot configuration should be generated for the system." 90 | } 91 | } 92 | }, 93 | "KernelParameter": { 94 | "type": "string", 95 | "pattern": "^[a-zA-Z0-9._-]+(=[^\\s=]+)?$", 96 | "description": "A kernel parameter in the form key[=value], e.g., loglevel=4 or quiet" 97 | }, 98 | "NixStorePath": { 99 | "type": "string", 100 | "description": "A valid Nix store path" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.15" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 40 | dependencies = [ 41 | "windows-sys 0.48.0", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys 0.52.0", 52 | ] 53 | 54 | [[package]] 55 | name = "bitflags" 56 | version = "2.4.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 59 | 60 | [[package]] 61 | name = "bootspec" 62 | version = "2.0.0" 63 | dependencies = [ 64 | "serde", 65 | "serde_json", 66 | "tempfile", 67 | "thiserror", 68 | ] 69 | 70 | [[package]] 71 | name = "cfg-if" 72 | version = "1.0.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 75 | 76 | [[package]] 77 | name = "clap" 78 | version = "4.5.48" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" 81 | dependencies = [ 82 | "clap_builder", 83 | "clap_derive", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_builder" 88 | version = "4.5.48" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" 91 | dependencies = [ 92 | "anstream", 93 | "anstyle", 94 | "clap_lex", 95 | "strsim", 96 | ] 97 | 98 | [[package]] 99 | name = "clap_derive" 100 | version = "4.5.47" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 103 | dependencies = [ 104 | "heck", 105 | "proc-macro2", 106 | "quote", 107 | "syn", 108 | ] 109 | 110 | [[package]] 111 | name = "clap_lex" 112 | version = "0.7.4" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 115 | 116 | [[package]] 117 | name = "colorchoice" 118 | version = "1.0.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 121 | 122 | [[package]] 123 | name = "errno" 124 | version = "0.3.10" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 127 | dependencies = [ 128 | "libc", 129 | "windows-sys 0.52.0", 130 | ] 131 | 132 | [[package]] 133 | name = "fastrand" 134 | version = "2.3.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 137 | 138 | [[package]] 139 | name = "getrandom" 140 | version = "0.3.2" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 143 | dependencies = [ 144 | "cfg-if", 145 | "libc", 146 | "r-efi", 147 | "wasi", 148 | ] 149 | 150 | [[package]] 151 | name = "heck" 152 | version = "0.5.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 155 | 156 | [[package]] 157 | name = "is_terminal_polyfill" 158 | version = "1.70.1" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 161 | 162 | [[package]] 163 | name = "itoa" 164 | version = "1.0.6" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 167 | 168 | [[package]] 169 | name = "libc" 170 | version = "0.2.171" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 173 | 174 | [[package]] 175 | name = "linux-raw-sys" 176 | version = "0.9.3" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 179 | 180 | [[package]] 181 | name = "once_cell" 182 | version = "1.21.1" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" 185 | 186 | [[package]] 187 | name = "proc-macro2" 188 | version = "1.0.94" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 191 | dependencies = [ 192 | "unicode-ident", 193 | ] 194 | 195 | [[package]] 196 | name = "quote" 197 | version = "1.0.27" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" 200 | dependencies = [ 201 | "proc-macro2", 202 | ] 203 | 204 | [[package]] 205 | name = "r-efi" 206 | version = "5.2.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 209 | 210 | [[package]] 211 | name = "rustix" 212 | version = "1.0.3" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" 215 | dependencies = [ 216 | "bitflags", 217 | "errno", 218 | "libc", 219 | "linux-raw-sys", 220 | "windows-sys 0.52.0", 221 | ] 222 | 223 | [[package]] 224 | name = "ryu" 225 | version = "1.0.13" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 228 | 229 | [[package]] 230 | name = "serde" 231 | version = "1.0.164" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" 234 | dependencies = [ 235 | "serde_derive", 236 | ] 237 | 238 | [[package]] 239 | name = "serde_derive" 240 | version = "1.0.164" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" 243 | dependencies = [ 244 | "proc-macro2", 245 | "quote", 246 | "syn", 247 | ] 248 | 249 | [[package]] 250 | name = "serde_json" 251 | version = "1.0.99" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" 254 | dependencies = [ 255 | "itoa", 256 | "ryu", 257 | "serde", 258 | ] 259 | 260 | [[package]] 261 | name = "strsim" 262 | version = "0.11.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 265 | 266 | [[package]] 267 | name = "syn" 268 | version = "2.0.16" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" 271 | dependencies = [ 272 | "proc-macro2", 273 | "quote", 274 | "unicode-ident", 275 | ] 276 | 277 | [[package]] 278 | name = "synthesize" 279 | version = "0.1.1" 280 | dependencies = [ 281 | "bootspec", 282 | "clap", 283 | "serde_json", 284 | "tempfile", 285 | ] 286 | 287 | [[package]] 288 | name = "tempfile" 289 | version = "3.23.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 292 | dependencies = [ 293 | "fastrand", 294 | "getrandom", 295 | "once_cell", 296 | "rustix", 297 | "windows-sys 0.52.0", 298 | ] 299 | 300 | [[package]] 301 | name = "thiserror" 302 | version = "1.0.40" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 305 | dependencies = [ 306 | "thiserror-impl", 307 | ] 308 | 309 | [[package]] 310 | name = "thiserror-impl" 311 | version = "1.0.40" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 314 | dependencies = [ 315 | "proc-macro2", 316 | "quote", 317 | "syn", 318 | ] 319 | 320 | [[package]] 321 | name = "unicode-ident" 322 | version = "1.0.8" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 325 | 326 | [[package]] 327 | name = "utf8parse" 328 | version = "0.2.1" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 331 | 332 | [[package]] 333 | name = "validate" 334 | version = "0.1.1" 335 | dependencies = [ 336 | "bootspec", 337 | "serde_json", 338 | ] 339 | 340 | [[package]] 341 | name = "wasi" 342 | version = "0.14.2+wasi-0.2.4" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 345 | dependencies = [ 346 | "wit-bindgen-rt", 347 | ] 348 | 349 | [[package]] 350 | name = "windows-sys" 351 | version = "0.48.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 354 | dependencies = [ 355 | "windows-targets 0.48.0", 356 | ] 357 | 358 | [[package]] 359 | name = "windows-sys" 360 | version = "0.52.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 363 | dependencies = [ 364 | "windows-targets 0.52.0", 365 | ] 366 | 367 | [[package]] 368 | name = "windows-targets" 369 | version = "0.48.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 372 | dependencies = [ 373 | "windows_aarch64_gnullvm 0.48.0", 374 | "windows_aarch64_msvc 0.48.0", 375 | "windows_i686_gnu 0.48.0", 376 | "windows_i686_msvc 0.48.0", 377 | "windows_x86_64_gnu 0.48.0", 378 | "windows_x86_64_gnullvm 0.48.0", 379 | "windows_x86_64_msvc 0.48.0", 380 | ] 381 | 382 | [[package]] 383 | name = "windows-targets" 384 | version = "0.52.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 387 | dependencies = [ 388 | "windows_aarch64_gnullvm 0.52.0", 389 | "windows_aarch64_msvc 0.52.0", 390 | "windows_i686_gnu 0.52.0", 391 | "windows_i686_msvc 0.52.0", 392 | "windows_x86_64_gnu 0.52.0", 393 | "windows_x86_64_gnullvm 0.52.0", 394 | "windows_x86_64_msvc 0.52.0", 395 | ] 396 | 397 | [[package]] 398 | name = "windows_aarch64_gnullvm" 399 | version = "0.48.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 402 | 403 | [[package]] 404 | name = "windows_aarch64_gnullvm" 405 | version = "0.52.0" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 408 | 409 | [[package]] 410 | name = "windows_aarch64_msvc" 411 | version = "0.48.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 414 | 415 | [[package]] 416 | name = "windows_aarch64_msvc" 417 | version = "0.52.0" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 420 | 421 | [[package]] 422 | name = "windows_i686_gnu" 423 | version = "0.48.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 426 | 427 | [[package]] 428 | name = "windows_i686_gnu" 429 | version = "0.52.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 432 | 433 | [[package]] 434 | name = "windows_i686_msvc" 435 | version = "0.48.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 438 | 439 | [[package]] 440 | name = "windows_i686_msvc" 441 | version = "0.52.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 444 | 445 | [[package]] 446 | name = "windows_x86_64_gnu" 447 | version = "0.48.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 450 | 451 | [[package]] 452 | name = "windows_x86_64_gnu" 453 | version = "0.52.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 456 | 457 | [[package]] 458 | name = "windows_x86_64_gnullvm" 459 | version = "0.48.0" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 462 | 463 | [[package]] 464 | name = "windows_x86_64_gnullvm" 465 | version = "0.52.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 468 | 469 | [[package]] 470 | name = "windows_x86_64_msvc" 471 | version = "0.48.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 474 | 475 | [[package]] 476 | name = "windows_x86_64_msvc" 477 | version = "0.52.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 480 | 481 | [[package]] 482 | name = "wit-bindgen-rt" 483 | version = "0.39.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 486 | dependencies = [ 487 | "bitflags", 488 | ] 489 | -------------------------------------------------------------------------------- /bootspec/src/v1.rs: -------------------------------------------------------------------------------- 1 | //! The V1 bootspec format. 2 | use std::collections::HashMap; 3 | use std::fs; 4 | use std::path::{Path, PathBuf}; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::deser; 9 | use crate::error::{BootspecError, SynthesizeError}; 10 | use crate::{Extensions, Result, SpecialisationName, SystemConfigurationRoot}; 11 | 12 | /// The V1 bootspec schema version. 13 | pub 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)] 25 | pub 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 | 32 | impl 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 { 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. 67 | pub type SpecialisationsV1 = HashMap; 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)] 73 | pub 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 | 85 | impl 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 { 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")] 103 | pub 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, 110 | /// Path to the init script 111 | pub init: PathBuf, 112 | /// Path to initrd -- $toplevel/initrd 113 | pub initrd: Option, 114 | /// Path to "append-initrd-secrets" script -- $toplevel/append-initrd-secrets 115 | pub initrd_secrets: Option, 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 | 122 | impl BootSpecV1 { 123 | pub(crate) fn synthesize(generation: &Path) -> Result { 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 = 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)] 214 | mod 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>, 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::>(); 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::>(); 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::>(); 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::>(); 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 | } 430 | -------------------------------------------------------------------------------- /bootspec/src/generation.rs: -------------------------------------------------------------------------------- 1 | //! Provides a helper enum for deserializing from all available bootspec versions. 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::v1; 5 | 6 | /// An enum of all available bootspec versions. 7 | /// 8 | /// This enum is nonexhaustive, because there may be future versions added at any point, and tools 9 | /// should explicitly handle them (e.g. by noting they're currently unsupported). 10 | /// 11 | /// ## Warnings 12 | /// 13 | /// If you attempt to deserialize using this struct, you will not get any information about 14 | /// user-provided extensions. For that, you must deserialize with [`crate::BootJson`]. 15 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 16 | #[non_exhaustive] 17 | #[serde(untagged)] 18 | pub enum Generation { 19 | // WARNING: Add new versions to the _top_ of this list. Untagged enums in `serde` always 20 | // deserialize to the first variant that succeeds, and new versions should succeed before old 21 | // versions. 22 | V1(v1::GenerationV1), 23 | } 24 | 25 | impl Generation { 26 | /// The version of the bootspec document. 27 | pub fn version(&self) -> u64 { 28 | use Generation::*; 29 | 30 | match self { 31 | V1(_) => v1::SCHEMA_VERSION, 32 | } 33 | } 34 | } 35 | 36 | impl TryFrom for v1::GenerationV1 { 37 | type Error = crate::BootspecError; 38 | 39 | fn try_from(value: Generation) -> Result { 40 | #[allow(clippy::infallible_destructuring_match)] 41 | let ret = match value { 42 | Generation::V1(v1) => v1, 43 | }; 44 | 45 | Ok(ret) 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use std::collections::HashMap; 52 | use std::path::PathBuf; 53 | 54 | use serde::de::IntoDeserializer; 55 | use serde::{Deserialize, Serialize}; 56 | 57 | use super::Generation; 58 | use crate::{ 59 | v1::{BootSpecV1, GenerationV1}, 60 | BootJson, SpecialisationName, SystemConfigurationRoot, SCHEMA_VERSION, 61 | }; 62 | 63 | #[derive(Debug, Deserialize, Serialize, PartialEq, Default)] 64 | struct TestExtension { 65 | #[serde(rename = "key")] 66 | test: String, 67 | } 68 | 69 | #[derive(Debug, Deserialize, Serialize, PartialEq, Default)] 70 | struct TestOptionalExtension { 71 | #[serde(rename = "key")] 72 | test: Option, 73 | } 74 | 75 | #[test] 76 | fn valid_v1_rfc0125_json() { 77 | // Adapted from the official JSON5 document from the RFC (converted to JSON and modified to 78 | // have a valid `org.nixos.specialisation.v1`). 79 | // https://github.com/NixOS/rfcs/blob/02458c2ecc9f915b143b1923213b40be8ac02a96/rfcs/0125-bootspec.md#bootspec-format-v1 80 | let rfc_json = include_str!("../rfc0125_spec.json"); 81 | let from_json = serde_json::from_str::(rfc_json).unwrap(); 82 | assert_eq!(from_json.version(), 1); 83 | 84 | let Generation::V1(from_json) = from_json; 85 | let keys = from_json 86 | .specialisations 87 | .keys() 88 | .map(ToOwned::to_owned) 89 | .collect::>(); 90 | assert!(keys.contains(&SpecialisationName(String::from("")))); 91 | 92 | assert_eq!( 93 | from_json 94 | .specialisations 95 | .get(&SpecialisationName("".into())) 96 | .unwrap() 97 | .extensions 98 | .get("org.nix-community.test") 99 | .unwrap() 100 | .as_object() 101 | .unwrap() 102 | .get("foo") 103 | .unwrap() 104 | .as_str(), 105 | Some("bar") 106 | ) 107 | } 108 | 109 | #[test] 110 | fn valid_v1_json_basic() { 111 | let json = r#"{ 112 | "org.nixos.bootspec.v1": { 113 | "init": "/nix/store/xxx-nixos-system-xxx/init", 114 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 115 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 116 | "kernel": "/nix/store/xxx-linux/bzImage", 117 | "kernelParams": [ 118 | "amd_iommu=on", 119 | "amd_iommu=pt", 120 | "iommu=pt", 121 | "kvm.ignore_msrs=1", 122 | "kvm.report_ignored_msrs=0", 123 | "udev.log_priority=3", 124 | "systemd.unified_cgroup_hierarchy=1", 125 | "loglevel=4" 126 | ], 127 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 128 | "system": "x86_64-linux", 129 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 130 | }, 131 | "org.nixos.specialisation.v1": {} 132 | }"#; 133 | 134 | let from_json: Generation = serde_json::from_str(json).unwrap(); 135 | let Generation::V1(from_json) = from_json; 136 | 137 | let bootspec = BootSpecV1 { 138 | system: String::from("x86_64-linux"), 139 | label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"), 140 | kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"), 141 | kernel_params: [ 142 | "amd_iommu=on", 143 | "amd_iommu=pt", 144 | "iommu=pt", 145 | "kvm.ignore_msrs=1", 146 | "kvm.report_ignored_msrs=0", 147 | "udev.log_priority=3", 148 | "systemd.unified_cgroup_hierarchy=1", 149 | "loglevel=4", 150 | ] 151 | .iter() 152 | .map(ToString::to_string) 153 | .collect(), 154 | init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"), 155 | initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")), 156 | initrd_secrets: Some(PathBuf::from( 157 | "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 158 | )), 159 | toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")), 160 | }; 161 | let expected = GenerationV1 { 162 | bootspec, 163 | specialisations: HashMap::new(), 164 | }; 165 | 166 | assert_eq!(from_json, expected); 167 | } 168 | 169 | #[test] 170 | fn valid_v1_json_with_typed_extension() { 171 | let json = r#"{ 172 | "org.nixos.bootspec.v1": { 173 | "init": "/nix/store/xxx-nixos-system-xxx/init", 174 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 175 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 176 | "kernel": "/nix/store/xxx-linux/bzImage", 177 | "kernelParams": [ 178 | "amd_iommu=on", 179 | "amd_iommu=pt", 180 | "iommu=pt", 181 | "kvm.ignore_msrs=1", 182 | "kvm.report_ignored_msrs=0", 183 | "udev.log_priority=3", 184 | "systemd.unified_cgroup_hierarchy=1", 185 | "loglevel=4" 186 | ], 187 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 188 | "system": "x86_64-linux", 189 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 190 | }, 191 | "org.nixos.specialisation.v1": {}, 192 | "org.test": { "key": "hello" } 193 | }"#; 194 | 195 | let from_json: BootJson = serde_json::from_str(json).unwrap(); 196 | 197 | let bootspec = BootSpecV1 { 198 | system: String::from("x86_64-linux"), 199 | label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"), 200 | kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"), 201 | kernel_params: [ 202 | "amd_iommu=on", 203 | "amd_iommu=pt", 204 | "iommu=pt", 205 | "kvm.ignore_msrs=1", 206 | "kvm.report_ignored_msrs=0", 207 | "udev.log_priority=3", 208 | "systemd.unified_cgroup_hierarchy=1", 209 | "loglevel=4", 210 | ] 211 | .iter() 212 | .map(ToString::to_string) 213 | .collect(), 214 | init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"), 215 | initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")), 216 | initrd_secrets: Some(PathBuf::from( 217 | "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 218 | )), 219 | toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")), 220 | }; 221 | let generation = GenerationV1 { 222 | bootspec, 223 | specialisations: HashMap::new(), 224 | }; 225 | let expected = BootJson { 226 | generation: Generation::V1(generation), 227 | extensions: HashMap::from([("org.test".into(), serde_json::json!({ "key": "hello" }))]), 228 | }; 229 | 230 | let from_extension: TestExtension = Deserialize::deserialize( 231 | from_json 232 | .extensions 233 | .get("org.test") 234 | .unwrap() 235 | .to_owned() 236 | .into_deserializer(), 237 | ) 238 | .unwrap(); 239 | let expected_extension = TestExtension { 240 | test: "hello".into(), 241 | }; 242 | 243 | assert_eq!(from_json, expected); 244 | assert_eq!(from_extension, expected_extension); 245 | } 246 | 247 | #[test] 248 | fn valid_v1_json_with_typed_optional_extension_fields_and_empty_object() { 249 | let json = r#"{ 250 | "org.nixos.bootspec.v1": { 251 | "init": "/nix/store/xxx-nixos-system-xxx/init", 252 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 253 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 254 | "kernel": "/nix/store/xxx-linux/bzImage", 255 | "kernelParams": [ 256 | "amd_iommu=on", 257 | "amd_iommu=pt", 258 | "iommu=pt", 259 | "kvm.ignore_msrs=1", 260 | "kvm.report_ignored_msrs=0", 261 | "udev.log_priority=3", 262 | "systemd.unified_cgroup_hierarchy=1", 263 | "loglevel=4" 264 | ], 265 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 266 | "system": "x86_64-linux", 267 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 268 | }, 269 | "org.nixos.specialisation.v1": {} 270 | }"#; 271 | 272 | let from_json: BootJson = serde_json::from_str(json).unwrap(); 273 | 274 | let bootspec = BootSpecV1 { 275 | system: String::from("x86_64-linux"), 276 | label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"), 277 | kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"), 278 | kernel_params: [ 279 | "amd_iommu=on", 280 | "amd_iommu=pt", 281 | "iommu=pt", 282 | "kvm.ignore_msrs=1", 283 | "kvm.report_ignored_msrs=0", 284 | "udev.log_priority=3", 285 | "systemd.unified_cgroup_hierarchy=1", 286 | "loglevel=4", 287 | ] 288 | .iter() 289 | .map(ToString::to_string) 290 | .collect(), 291 | init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"), 292 | initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")), 293 | initrd_secrets: Some(PathBuf::from( 294 | "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 295 | )), 296 | toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")), 297 | }; 298 | let generation = GenerationV1 { 299 | bootspec, 300 | specialisations: HashMap::new(), 301 | }; 302 | let expected = BootJson { 303 | generation: Generation::V1(generation), 304 | extensions: HashMap::new(), 305 | }; 306 | 307 | assert_eq!(from_json, expected); 308 | } 309 | 310 | #[test] 311 | fn invalid_v1_json_with_null_extension() { 312 | let json = r#"{ 313 | "org.nixos.bootspec.v1": { 314 | "init": "/nix/store/xxx-nixos-system-xxx/init", 315 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 316 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 317 | "kernel": "/nix/store/xxx-linux/bzImage", 318 | "kernelParams": [ 319 | "amd_iommu=on", 320 | "amd_iommu=pt", 321 | "iommu=pt", 322 | "kvm.ignore_msrs=1", 323 | "kvm.report_ignored_msrs=0", 324 | "udev.log_priority=3", 325 | "systemd.unified_cgroup_hierarchy=1", 326 | "loglevel=4" 327 | ], 328 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 329 | "system": "x86_64-linux", 330 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 331 | }, 332 | "org.nixos.specialisation.v1": {}, 333 | "org.test2": { "hi": null }, 334 | "org.test": null 335 | }"#; 336 | let json_err = serde_json::from_str::(json).unwrap_err(); 337 | assert!(json_err 338 | .to_string() 339 | .contains("org.test was null, but null extensions are not allowed")); 340 | } 341 | 342 | #[test] 343 | fn valid_v1_json_without_extension() { 344 | let json = r#"{ 345 | "org.nixos.bootspec.v1": { 346 | "init": "/nix/store/xxx-nixos-system-xxx/init", 347 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 348 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 349 | "kernel": "/nix/store/xxx-linux/bzImage", 350 | "kernelParams": [ 351 | "amd_iommu=on", 352 | "amd_iommu=pt", 353 | "iommu=pt", 354 | "kvm.ignore_msrs=1", 355 | "kvm.report_ignored_msrs=0", 356 | "udev.log_priority=3", 357 | "systemd.unified_cgroup_hierarchy=1", 358 | "loglevel=4" 359 | ], 360 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 361 | "system": "x86_64-linux", 362 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 363 | }, 364 | "org.nixos.specialisation.v1": {} 365 | }"#; 366 | 367 | let from_json: BootJson = serde_json::from_str(json).unwrap(); 368 | 369 | let bootspec = BootSpecV1 { 370 | system: String::from("x86_64-linux"), 371 | label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"), 372 | kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"), 373 | kernel_params: [ 374 | "amd_iommu=on", 375 | "amd_iommu=pt", 376 | "iommu=pt", 377 | "kvm.ignore_msrs=1", 378 | "kvm.report_ignored_msrs=0", 379 | "udev.log_priority=3", 380 | "systemd.unified_cgroup_hierarchy=1", 381 | "loglevel=4", 382 | ] 383 | .iter() 384 | .map(ToString::to_string) 385 | .collect(), 386 | init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"), 387 | initrd: Some(PathBuf::from("/nix/store/xxx-initrd-linux/initrd")), 388 | initrd_secrets: Some(PathBuf::from( 389 | "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 390 | )), 391 | toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")), 392 | }; 393 | let generation = GenerationV1 { 394 | bootspec, 395 | specialisations: HashMap::new(), 396 | }; 397 | let expected = BootJson { 398 | generation: Generation::V1(generation), 399 | extensions: HashMap::new(), 400 | }; 401 | 402 | assert_eq!(from_json, expected); 403 | } 404 | 405 | #[test] 406 | fn valid_v1_json_without_initrd_and_specialisation() { 407 | let json = r#"{ 408 | "org.nixos.bootspec.v1": { 409 | "init": "/nix/store/xxx-nixos-system-xxx/init", 410 | "kernel": "/nix/store/xxx-linux/bzImage", 411 | "kernelParams": [ 412 | "amd_iommu=on", 413 | "amd_iommu=pt", 414 | "iommu=pt", 415 | "kvm.ignore_msrs=1", 416 | "kvm.report_ignored_msrs=0", 417 | "udev.log_priority=3", 418 | "systemd.unified_cgroup_hierarchy=1", 419 | "loglevel=4" 420 | ], 421 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 422 | "system": "x86_64-linux", 423 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 424 | } 425 | }"#; 426 | 427 | let from_json: BootJson = serde_json::from_str(json).unwrap(); 428 | 429 | let bootspec = BootSpecV1 { 430 | system: String::from("x86_64-linux"), 431 | label: String::from("NixOS 21.11.20210810.dirty (Linux 5.15.30)"), 432 | kernel: PathBuf::from("/nix/store/xxx-linux/bzImage"), 433 | kernel_params: [ 434 | "amd_iommu=on", 435 | "amd_iommu=pt", 436 | "iommu=pt", 437 | "kvm.ignore_msrs=1", 438 | "kvm.report_ignored_msrs=0", 439 | "udev.log_priority=3", 440 | "systemd.unified_cgroup_hierarchy=1", 441 | "loglevel=4", 442 | ] 443 | .iter() 444 | .map(ToString::to_string) 445 | .collect(), 446 | init: PathBuf::from("/nix/store/xxx-nixos-system-xxx/init"), 447 | initrd: None, 448 | initrd_secrets: None, 449 | toplevel: SystemConfigurationRoot(PathBuf::from("/nix/store/xxx-nixos-system-xxx")), 450 | }; 451 | let generation = GenerationV1 { 452 | bootspec, 453 | specialisations: HashMap::new(), 454 | }; 455 | let expected = BootJson { 456 | generation: Generation::V1(generation), 457 | extensions: HashMap::new(), 458 | }; 459 | 460 | assert_eq!(from_json, expected); 461 | } 462 | 463 | #[test] 464 | fn invalid_v1_json_with_null_specialisation() { 465 | let json = r#"{ 466 | "org.nixos.bootspec.v1": { 467 | "init": "/nix/store/xxx-nixos-system-xxx/init", 468 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 469 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 470 | "kernel": "/nix/store/xxx-linux/bzImage", 471 | "kernelParams": [ 472 | "amd_iommu=on", 473 | "amd_iommu=pt", 474 | "iommu=pt", 475 | "kvm.ignore_msrs=1", 476 | "kvm.report_ignored_msrs=0", 477 | "udev.log_priority=3", 478 | "systemd.unified_cgroup_hierarchy=1", 479 | "loglevel=4" 480 | ], 481 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 482 | "system": "x86_64-linux", 483 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 484 | }, 485 | "org.nixos.specialisation.v1": null 486 | }"#; 487 | 488 | let json_err = serde_json::from_str::(json).unwrap_err(); 489 | assert!(json_err.to_string().contains("expected a map")); 490 | } 491 | 492 | #[test] 493 | fn invalid_json_invalid_version() { 494 | let json = format!( 495 | r#"{{ 496 | "org.nixos.bootspec.v{}": {{ 497 | "init": "/nix/store/xxx-nixos-system-xxx/init", 498 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 499 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 500 | "kernel": "/nix/store/xxx-linux/bzImage", 501 | "kernelParams": [ 502 | "amd_iommu=on", 503 | "amd_iommu=pt", 504 | "iommu=pt", 505 | "kvm.ignore_msrs=1", 506 | "kvm.report_ignored_msrs=0", 507 | "udev.log_priority=3", 508 | "systemd.unified_cgroup_hierarchy=1", 509 | "loglevel=4" 510 | ], 511 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 512 | "system": "x86_64-linux", 513 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 514 | }}, 515 | "org.nixos.specialisation.v{}": {{}} 516 | }}"#, 517 | SCHEMA_VERSION + 1, 518 | SCHEMA_VERSION + 1 519 | ); 520 | 521 | let json_err = serde_json::from_str::(&json).unwrap_err(); 522 | assert!(json_err.to_string().contains("did not match any variant")); 523 | } 524 | 525 | #[test] 526 | fn valid_v1_json_to_generation_via_try_into() { 527 | let json = r#"{ 528 | "org.nixos.bootspec.v1": { 529 | "init": "/nix/store/xxx-nixos-system-xxx/init", 530 | "initrd": "/nix/store/xxx-initrd-linux/initrd", 531 | "initrdSecrets": "/nix/store/xxx-append-secrets/bin/append-initrd-secrets", 532 | "kernel": "/nix/store/xxx-linux/bzImage", 533 | "kernelParams": [ 534 | "amd_iommu=on", 535 | "amd_iommu=pt", 536 | "iommu=pt", 537 | "kvm.ignore_msrs=1", 538 | "kvm.report_ignored_msrs=0", 539 | "udev.log_priority=3", 540 | "systemd.unified_cgroup_hierarchy=1", 541 | "loglevel=4" 542 | ], 543 | "label": "NixOS 21.11.20210810.dirty (Linux 5.15.30)", 544 | "system": "x86_64-linux", 545 | "toplevel": "/nix/store/xxx-nixos-system-xxx" 546 | }, 547 | "org.nixos.specialisation.v1": {} 548 | }"#; 549 | 550 | let from_json: BootJson = serde_json::from_str(json).unwrap(); 551 | let _generation: GenerationV1 = from_json.generation.try_into().unwrap(); 552 | } 553 | } 554 | --------------------------------------------------------------------------------