nixos-init: read the required paths from a file in the toplevel instead of from the environment

This is a first step towards using nixos-init also during re-activation.
This commit is contained in:
r-vdp
2026-01-26 11:54:07 +01:00
parent e47a09cc35
commit b5bb5d22da
9 changed files with 219 additions and 80 deletions
+45 -24
View File
@@ -20,28 +20,49 @@ in
package = lib.mkPackageOption pkgs "nixos-init" { };
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = "nixos-init can only be used with boot.initrd.systemd.enable";
}
{
assertion = config.system.etc.overlay.enable;
message = "nixos-init can only be used with system.etc.overlay.enable";
}
{
assertion = config.services.userborn.enable || config.systemd.sysusers.enable;
message = "nixos-init can only be used with services.userborn.enable or systemd.sysusers.enable";
}
{
assertion = config.boot.postBootCommands == "";
message = "nixos-init cannot be used with boot.postBootCommands";
}
{
assertion = config.powerManagement.powerUpCommands == "";
message = "nixos-init cannot be used with powerManagement.powerUpCommands";
}
];
};
config = lib.mkMerge [
{
boot.bootspec.extensions = {
"org.nixos.nixos-init.v1" = {
firmware = "${config.hardware.firmware}/lib/firmware";
modprobe_binary = "${pkgs.kmod}/bin/modprobe";
nix_store_mount_opts = config.boot.nixStoreMountOpts;
}
// lib.optionalAttrs (config.environment.binsh != null) {
sh_binary = config.environment.binsh;
}
// lib.optionalAttrs (config.environment.usrbinenv != null) {
env_binary = config.environment.usrbinenv;
}
// lib.optionalAttrs config.system.etc.overlay.enable {
etc_metadata_image = config.system.build.etcMetadataImage;
etc_basedir = config.system.build.etcBasedir;
};
};
}
(lib.mkIf cfg.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = "nixos-init can only be used with boot.initrd.systemd.enable";
}
{
assertion = config.system.etc.overlay.enable;
message = "nixos-init can only be used with system.etc.overlay.enable";
}
{
assertion = config.services.userborn.enable || config.systemd.sysusers.enable;
message = "nixos-init can only be used with services.userborn.enable or systemd.sysusers.enable";
}
{
assertion = config.boot.postBootCommands == "";
message = "nixos-init cannot be used with boot.postBootCommands";
}
{
assertion = config.powerManagement.powerUpCommands == "";
message = "nixos-init cannot be used with powerManagement.powerUpCommands";
}
];
})
];
}
@@ -33,11 +33,6 @@ let
ln -s ${config.system.build.etc}/etc $out/etc
${lib.optionalString config.system.etc.overlay.enable ''
ln -s ${config.system.build.etcMetadataImage} $out/etc-metadata-image
ln -s ${config.system.build.etcBasedir} $out/etc-basedir
''}
ln -s ${config.system.path} $out/sw
ln -s "$systemd" $out/systemd
@@ -733,17 +733,6 @@ in
cfg.package.util-linux
config.system.nixos-init.package
];
environment = {
FIRMWARE = "${config.hardware.firmware}/lib/firmware";
MODPROBE_BINARY = "${pkgs.kmod}/bin/modprobe";
NIX_STORE_MOUNT_OPTS = lib.concatStringsSep "," config.boot.nixStoreMountOpts;
}
// lib.optionalAttrs (config.environment.usrbinenv != null) {
ENV_BINARY = config.environment.usrbinenv;
}
// lib.optionalAttrs (config.environment.binsh != null) {
SH_BINARY = config.environment.binsh;
};
serviceConfig = {
ExecStart = [
""
+130
View File
@@ -14,6 +14,17 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bootspec"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75726e2aa2b4c5a9d5c4cf3cb7f24658b6ec861616088f3ef3fb72edc0599286"
dependencies = [
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
@@ -73,6 +84,12 @@ version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "itoa"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
[[package]]
name = "libc"
version = "0.2.174"
@@ -91,14 +108,23 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "nixos-init"
version = "0.1.0"
dependencies = [
"anyhow",
"bootspec",
"env_logger",
"indoc",
"log",
"serde",
"serde_json",
"tempfile",
]
@@ -108,6 +134,24 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
@@ -127,6 +171,66 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "ryu"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.20.0"
@@ -140,6 +244,32 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
+3
View File
@@ -7,6 +7,9 @@ edition = "2024"
anyhow = "1.0.98"
log = "0.4.27"
env_logger = { version = "0.11.8", default-features = false }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
bootspec = "2.0.0"
[dev-dependencies]
tempfile = "3.20.0"
+24 -28
View File
@@ -1,43 +1,39 @@
use std::env;
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use bootspec::BootJson;
#[derive(Deserialize)]
pub struct Config {
pub firmware: String,
pub modprobe_binary: String,
pub nix_store_mount_opts: Vec<String>,
pub env_binary: Option<String>,
pub sh_binary: Option<String>,
pub etc_basedir: Option<String>,
pub etc_metadata_image: Option<String>,
}
impl Config {
/// Read the config from the environment.
///
/// These options are provided by wrapping the binary when assembling the toplevel.
pub fn from_env() -> Result<Self> {
let nix_store_mount_opts = required("NIX_STORE_MOUNT_OPTS")?
.split(',')
.map(std::borrow::ToOwned::to_owned)
.collect();
/// Read the config from the metadata file in the toplevel directory.
pub fn from_toplevel(toplevel: impl AsRef<Path>, prefix: &str) -> Result<Self> {
let bootspec_path =
PathBuf::from(prefix).join(toplevel.as_ref().join("boot.json").strip_prefix("/")?);
Ok(Self {
firmware: required("FIRMWARE")?,
modprobe_binary: required("MODPROBE_BINARY")?,
nix_store_mount_opts,
env_binary: optional("ENV_BINARY"),
sh_binary: optional("SH_BINARY"),
})
let boot_json: BootJson = fs::read(bootspec_path)
.context("Failed to read bootspec file")
.and_then(|raw| serde_json::from_slice(&raw).context("Failed to read bootspec JSON"))?;
let config = boot_json
.extensions
.get("org.nixos.nixos-init.v1")
.context("Failed to extract nixos-init bootspec extension")
.and_then(|v| {
serde_json::from_value(v.clone()).context("Failed to deserialise config")
})?;
Ok(config)
}
}
/// Read a required environment variable
///
/// Fail with useful context if the variable is not set in the environment.
fn required(key: &str) -> Result<String> {
env::var(key).with_context(|| format!("Failed to read {key} from environment"))
}
/// Read an optional environment variable
fn optional(key: &str) -> Option<String> {
env::var(key).ok()
}
+13 -9
View File
@@ -2,6 +2,7 @@ use std::{os::unix, path::Path};
use anyhow::{Context, Result};
use crate::config::Config;
use crate::{SYSROOT_PATH, canonicalize_in_chroot, find_toplevel_in_prefix};
/// Entrypoint for the `find-etc` binary.
@@ -12,20 +13,23 @@ use crate::{SYSROOT_PATH, canonicalize_in_chroot, find_toplevel_in_prefix};
/// need to re-build it.
pub fn find_etc() -> Result<()> {
let toplevel = find_toplevel_in_prefix(SYSROOT_PATH)?;
let config = Config::from_toplevel(&toplevel, SYSROOT_PATH)?;
let etc_metadata_image = Path::new(SYSROOT_PATH).join(
canonicalize_in_chroot(SYSROOT_PATH, &toplevel.join("etc-metadata-image"))?
.strip_prefix("/")?,
);
let basedir = config
.etc_basedir
.context("Failed to read etc_basedir from bootspec")?;
let etc_basedir = Path::new(SYSROOT_PATH)
.join(canonicalize_in_chroot(SYSROOT_PATH, Path::new(&basedir))?.strip_prefix("/")?);
let etc_basedir = Path::new(SYSROOT_PATH).join(
canonicalize_in_chroot(SYSROOT_PATH, &toplevel.join("etc-basedir"))?.strip_prefix("/")?,
);
let metadata_image = config
.etc_metadata_image
.context("Failed to read etc_metadata_image from bootspec")?;
let etc_metadata_image = Path::new(SYSROOT_PATH)
.join(canonicalize_in_chroot(SYSROOT_PATH, Path::new(&metadata_image))?.strip_prefix("/")?);
unix::fs::symlink(etc_basedir, "/etc-basedir").context("Failed to link /etc-basedir")?;
unix::fs::symlink(etc_metadata_image, "/etc-metadata-image")
.context("Failed to link /etc-metadata-image")?;
unix::fs::symlink(etc_basedir, "/etc-basedir").context("Failed to link /etc-basedir")?;
Ok(())
}
+1 -1
View File
@@ -60,7 +60,7 @@ fn remount_nix_store(prefix: &str, nix_store_mount_opts: &[String]) -> Result<()
if let Some(last_nix_store_mount) = mounts.find_mountpoint(&nix_store_path) {
for opt in nix_store_mount_opts {
if !last_nix_store_mount.mntopts.contains(opt) {
missing_opts.push(opt.to_string());
missing_opts.push(opt.clone());
}
}
if !missing_opts.is_empty() {
@@ -8,13 +8,14 @@ use crate::{
///
/// Initialize `NixOS` from a systemd initrd.
pub fn initrd_init() -> Result<()> {
let config = Config::from_env().context("Failed to get configuration")?;
let init_in_sysroot =
find_init_in_prefix(SYSROOT_PATH).context("Failed to find init in sysroot")?;
let init_path = if let Ok(toplevel) = verify_init_is_nixos(SYSROOT_PATH, &init_in_sysroot) {
log::info!("Initializing NixOS...");
init(SYSROOT_PATH, toplevel, &config)?;
let config = Config::from_toplevel(&toplevel, SYSROOT_PATH)
.context("Failed to get configuration")?;
init(SYSROOT_PATH, &toplevel, &config)?;
None
} else {
log::info!("Not initializing NixOS. Switching to new root immediately...");