nixos-init: init at 0.1.0 (#433154)

misuzu e7fa9ff1 958326b9

+1
doc/release-notes/rl-2511.section.md
···
and newer series. However, embedded chips without LSX (Loongson SIMD eXtension), such as 2K0300 SoC, are not
supported. `pkgsCross.loongarch64-linux-embedded` can be used to build software and systems for these platforms.
- The official Nix formatter `nixfmt` is now stable and available as `pkgs.nixfmt`, deprecating the temporary `pkgs.nixfmt-rfc-style` attribute. The classic `nixfmt` will stay available for some more time as `pkgs.nixfmt-classic`.
+
- Added `nixos-init`, a Rust-based bashless initialization system for systemd initrd. This allows to build NixOS systems without any interpreter. Enable via `system.nixos-init.enable = true;`.
## Backward Incompatibilities {#sec-nixpkgs-release-25.11-incompatibilities}
+1
nixos/modules/module-list.nix
···
./system/activation/activatable-system.nix
./system/activation/activation-script.nix
./system/activation/bootspec.nix
+
./system/activation/nixos-init.nix
./system/activation/pre-switch-check.nix
./system/activation/specialisation.nix
./system/activation/switchable-system.nix
+31
nixos/modules/system/activation/nixos-init.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
+
let
+
cfg = config.system.nixos-init;
+
in
+
{
+
options.system.nixos-init = {
+
enable = lib.mkEnableOption ''
+
nixos-init, a system for bashless initialization.
+
+
This doesn't use any `activationScripts`. Anything set in these options is
+
a no-op here.
+
'';
+
+
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 systemd initrd";
+
}
+
];
+
};
+
}
+5 -2
nixos/modules/system/activation/top-level.nix
···
${
if config.boot.initrd.enable && config.boot.initrd.systemd.enable then
''
-
cp ${config.system.build.bootStage2} $out/prepare-root
-
substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
# This must not be a symlink or the abs_path of the grub builder for the tests
# will resolve the symlink and we end up with a path that doesn't point to a
# system closure.
cp "$systemd/lib/systemd/systemd" $out/init
+
+
${lib.optionalString (!config.system.nixos-init.enable) ''
+
cp ${config.system.build.bootStage2} $out/prepare-root
+
substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
+
''}
''
else
''
+54 -15
nixos/modules/system/boot/systemd/initrd.nix
···
"${pkgs.glibc}/lib/libnss_files.so.2"
# Resolving sysroot symlinks without code exec
-
"${pkgs.chroot-realpath}/bin/chroot-realpath"
+
"${config.system.nixos-init.package}/bin/chroot-realpath"
+
# Find the etc paths
+
"${config.system.nixos-init.package}/bin/find-etc"
+
]
+
++ lib.optionals config.system.nixos-init.enable [
+
"${config.system.nixos-init.package}/bin/initrd-init"
]
++ jobScripts
++ map (c: builtins.removeAttrs c [ "text" ]) (builtins.attrValues cfg.contents);
···
) cfg.automounts
);
-
services.initrd-find-nixos-closure = {
+
services.initrd-find-nixos-closure = lib.mkIf (!config.system.nixos-init.enable) {
description = "Find NixOS closure";
unitConfig = {
···
script = # bash
''
set -uo pipefail
-
export PATH="/bin:${cfg.package.util-linux}/bin:${pkgs.chroot-realpath}/bin"
+
export PATH="/bin:${
+
lib.makeBinPath [
+
cfg.package.util-linux
+
config.system.nixos-init.package
+
]
+
}"
# Figure out what closure to boot
closure=
···
}
];
-
services.initrd-nixos-activation = {
+
services.initrd-nixos-activation = lib.mkIf (!config.system.nixos-init.enable) {
after = [ "initrd-switch-root.target" ];
requiredBy = [ "initrd-switch-root.service" ];
before = [ "initrd-switch-root.service" ];
···
'';
};
-
# This will either call systemctl with the new init as the last parameter (which
-
# is the case when not booting a NixOS system) or with an empty string, causing
-
# systemd to bypass its verification code that checks whether the next file is a systemd
-
# and using its compiled-in value
-
services.initrd-switch-root.serviceConfig = {
-
EnvironmentFile = "-/etc/switch-root.conf";
-
ExecStart = [
-
""
-
''systemctl --no-block switch-root /sysroot "''${NEW_INIT}"''
-
];
-
};
+
services.initrd-switch-root =
+
if config.system.nixos-init.enable then
+
{
+
path = [
+
cfg.package
+
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 = [
+
""
+
"${config.system.nixos-init.package}/bin/initrd-init"
+
];
+
};
+
}
+
else
+
# This will either call systemctl with the new init as the last parameter (which
+
# is the case when not booting a NixOS system) or with an empty string, causing
+
# systemd to bypass its verification code that checks whether the next file is a systemd
+
# and using its compiled-in value
+
{
+
serviceConfig = {
+
EnvironmentFile = "-/etc/switch-root.conf";
+
ExecStart = [
+
""
+
''systemctl --no-block switch-root /sysroot "''${NEW_INIT}"''
+
];
+
};
+
};
services.panic-on-fail = {
wantedBy = [ "emergency.target" ];
+2 -19
nixos/modules/system/etc/etc-activation.nix
···
{
initrd-find-etc = {
description = "Find the path to the etc metadata image and based dir";
-
requires = [
-
config.boot.initrd.systemd.services.initrd-find-nixos-closure.name
-
];
-
after = [
-
config.boot.initrd.systemd.services.initrd-find-nixos-closure.name
-
];
before = [ "shutdown.target" ];
conflicts = [ "shutdown.target" ];
requiredBy = [ "initrd.target" ];
+
path = [ config.system.nixos-init.package ];
unitConfig = {
DefaultDependencies = false;
RequiresMountsFor = "/sysroot/nix/store";
···
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
+
ExecStart = "${config.system.nixos-init.package}/bin/find-etc";
};
-
-
script = # bash
-
''
-
set -uo pipefail
-
-
closure="$(realpath /nixos-closure)"
-
-
metadata_image="$(${pkgs.chroot-realpath}/bin/chroot-realpath /sysroot "$closure/etc-metadata-image")"
-
ln -s "/sysroot$metadata_image" /etc-metadata-image
-
-
basedir="$(${pkgs.chroot-realpath}/bin/chroot-realpath /sysroot "$closure/etc-basedir")"
-
ln -s "/sysroot$basedir" /etc-basedir
-
'';
};
}
];
+54
nixos/tests/activation/nixos-init.nix
···
+
{ lib, pkgs, ... }:
+
+
{
+
name = "nixos-init";
+
+
meta.maintainers = with lib.maintainers; [ nikstur ];
+
+
nodes.machine =
+
{ modulesPath, ... }:
+
{
+
imports = [
+
"${modulesPath}/profiles/perlless.nix"
+
];
+
virtualisation.mountHostNixStore = false;
+
virtualisation.useNixStoreImage = true;
+
+
system.nixos-init.enable = true;
+
# Forcibly set this to only these specific values.
+
boot.nixStoreMountOpts = lib.mkForce [
+
"nodev"
+
"nosuid"
+
];
+
};
+
+
testScript =
+
{ nodes, ... }: # python
+
''
+
with subtest("init"):
+
with subtest("/nix/store is mounted with the correct options"):
+
findmnt_output = machine.succeed("findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store").strip()
+
print(findmnt_output)
+
t.assertIn("nodev", findmnt_output)
+
t.assertIn("nosuid", findmnt_output)
+
+
t.assertEqual("${nodes.machine.system.build.toplevel}", machine.succeed("readlink /run/booted-system").strip())
+
+
with subtest("activation"):
+
t.assertEqual("${nodes.machine.system.build.toplevel}", machine.succeed("readlink /run/current-system").strip())
+
t.assertEqual("${nodes.machine.hardware.firmware}/lib/firmware", machine.succeed("cat /sys/module/firmware_class/parameters/path").strip())
+
t.assertEqual("${pkgs.kmod}/bin/modprobe", machine.succeed("cat /proc/sys/kernel/modprobe").strip())
+
t.assertEqual("${nodes.machine.environment.usrbinenv}", machine.succeed("readlink /usr/bin/env").strip())
+
t.assertEqual("${nodes.machine.environment.binsh}", machine.succeed("readlink /bin/sh").strip())
+
+
machine.wait_for_unit("multi-user.target")
+
with subtest("systemd state passing"):
+
systemd_analyze_output = machine.succeed("systemd-analyze")
+
print(systemd_analyze_output)
+
t.assertIn("(initrd)", systemd_analyze_output, "systemd-analyze has no information about the initrd")
+
+
ps_output = machine.succeed("ps ax -o command | grep systemd | head -n 1")
+
print(ps_output)
+
t.assertIn("--deserialize", ps_output, "--deserialize flag wasn't passed to systemd")
+
'';
+
}
+1
nixos/tests/all-tests.nix
···
activation-etc-overlay-mutable = runTest ./activation/etc-overlay-mutable.nix;
activation-lib = pkgs.callPackage ../modules/system/activation/lib/test.nix { };
activation-nix-channel = runTest ./activation/nix-channel.nix;
+
activation-nixos-init = runTest ./activation/nixos-init.nix;
activation-perlless = runTest ./activation/perlless.nix;
activation-var = runTest ./activation/var.nix;
actual = runTest ./actual.nix;
+1 -1
nixos/tests/installer.nix
···
config.boot.bootspec.package
]
++ optionals clevisTest [ pkgs.klibc ]
-
++ optional systemdStage1 pkgs.chroot-realpath;
+
++ optional systemdStage1 config.system.nixos-init.package;
nix.settings = {
substituters = mkForce [ ];
-21
pkgs/by-name/ch/chroot-realpath/package.nix
···
-
{
-
lib,
-
rustPlatform,
-
}:
-
-
let
-
cargo = lib.importTOML ./src/Cargo.toml;
-
in
-
rustPlatform.buildRustPackage {
-
pname = cargo.package.name;
-
version = cargo.package.version;
-
-
src = ./src;
-
-
cargoLock.lockFile = ./src/Cargo.lock;
-
-
meta = {
-
description = "Output a path's realpath within a chroot";
-
maintainers = [ lib.maintainers.elvishjerricco ];
-
};
-
}
-16
pkgs/by-name/ch/chroot-realpath/src/Cargo.lock
···
-
# This file is automatically @generated by Cargo.
-
# It is not intended for manual editing.
-
version = 4
-
-
[[package]]
-
name = "anyhow"
-
version = "1.0.98"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
-
-
[[package]]
-
name = "chroot-realpath"
-
version = "0.1.0"
-
dependencies = [
-
"anyhow",
-
]
-10
pkgs/by-name/ch/chroot-realpath/src/Cargo.toml
···
-
[package]
-
name = "chroot-realpath"
-
version = "0.1.0"
-
edition = "2021"
-
-
[dependencies]
-
anyhow = "1.0.98"
-
-
[profile.release]
-
opt-level = "z"
-26
pkgs/by-name/ch/chroot-realpath/src/src/main.rs
···
-
use std::env;
-
use std::io::{stdout, Write};
-
use std::os::unix::ffi::OsStrExt;
-
use std::os::unix::fs;
-
-
use anyhow::{bail, Context, Result};
-
-
fn main() -> Result<()> {
-
let args: Vec<String> = env::args().collect();
-
-
if args.len() != 3 {
-
bail!("Usage: {} <chroot> <path>", args[0]);
-
}
-
-
fs::chroot(&args[1]).context("Failed to chroot")?;
-
std::env::set_current_dir("/").context("Failed to change directory")?;
-
-
let path = std::fs::canonicalize(&args[2])
-
.with_context(|| format!("Failed to canonicalize {}", args[2]))?;
-
-
stdout()
-
.write_all(path.into_os_string().as_bytes())
-
.context("Failed to write output")?;
-
-
Ok(())
-
}
+6
pkgs/by-name/ni/nixos-init/.gitignore
···
+
.direnv/
+
result*
+
+
# Rust
+
**/*.rs.bk # These are backup files generated by rustfmt
+
target/
+312
pkgs/by-name/ni/nixos-init/Cargo.lock
···
+
# This file is automatically @generated by Cargo.
+
# It is not intended for manual editing.
+
version = 4
+
+
[[package]]
+
name = "anyhow"
+
version = "1.0.98"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+
[[package]]
+
name = "bitflags"
+
version = "2.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+
[[package]]
+
name = "cfg-if"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+
[[package]]
+
name = "env_filter"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+
dependencies = [
+
"log",
+
]
+
+
[[package]]
+
name = "env_logger"
+
version = "0.11.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+
dependencies = [
+
"env_filter",
+
"log",
+
]
+
+
[[package]]
+
name = "errno"
+
version = "0.3.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+
dependencies = [
+
"libc",
+
"windows-sys 0.60.2",
+
]
+
+
[[package]]
+
name = "fastrand"
+
version = "2.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+
[[package]]
+
name = "getrandom"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"r-efi",
+
"wasi",
+
]
+
+
[[package]]
+
name = "indoc"
+
version = "2.0.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
+
+
[[package]]
+
name = "libc"
+
version = "0.2.174"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+
[[package]]
+
name = "linux-raw-sys"
+
version = "0.9.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+
[[package]]
+
name = "log"
+
version = "0.4.27"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+
[[package]]
+
name = "nixos-init"
+
version = "0.1.0"
+
dependencies = [
+
"anyhow",
+
"env_logger",
+
"indoc",
+
"log",
+
"tempfile",
+
]
+
+
[[package]]
+
name = "once_cell"
+
version = "1.21.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+
[[package]]
+
name = "r-efi"
+
version = "5.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+
[[package]]
+
name = "rustix"
+
version = "1.0.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
+
dependencies = [
+
"bitflags",
+
"errno",
+
"libc",
+
"linux-raw-sys",
+
"windows-sys 0.60.2",
+
]
+
+
[[package]]
+
name = "tempfile"
+
version = "3.20.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+
dependencies = [
+
"fastrand",
+
"getrandom",
+
"once_cell",
+
"rustix",
+
"windows-sys 0.59.0",
+
]
+
+
[[package]]
+
name = "wasi"
+
version = "0.14.2+wasi-0.2.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+
dependencies = [
+
"wit-bindgen-rt",
+
]
+
+
[[package]]
+
name = "windows-link"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.59.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+
dependencies = [
+
"windows-targets 0.52.6",
+
]
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.60.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+
dependencies = [
+
"windows-targets 0.53.3",
+
]
+
+
[[package]]
+
name = "windows-targets"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+
dependencies = [
+
"windows_aarch64_gnullvm 0.52.6",
+
"windows_aarch64_msvc 0.52.6",
+
"windows_i686_gnu 0.52.6",
+
"windows_i686_gnullvm 0.52.6",
+
"windows_i686_msvc 0.52.6",
+
"windows_x86_64_gnu 0.52.6",
+
"windows_x86_64_gnullvm 0.52.6",
+
"windows_x86_64_msvc 0.52.6",
+
]
+
+
[[package]]
+
name = "windows-targets"
+
version = "0.53.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
+
dependencies = [
+
"windows-link",
+
"windows_aarch64_gnullvm 0.53.0",
+
"windows_aarch64_msvc 0.53.0",
+
"windows_i686_gnu 0.53.0",
+
"windows_i686_gnullvm 0.53.0",
+
"windows_i686_msvc 0.53.0",
+
"windows_x86_64_gnu 0.53.0",
+
"windows_x86_64_gnullvm 0.53.0",
+
"windows_x86_64_msvc 0.53.0",
+
]
+
+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+
[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+
[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.53.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+
[[package]]
+
name = "wit-bindgen-rt"
+
version = "0.39.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+
dependencies = [
+
"bitflags",
+
]
+27
pkgs/by-name/ni/nixos-init/Cargo.toml
···
+
[package]
+
name = "nixos-init"
+
version = "0.1.0"
+
edition = "2024"
+
+
[dependencies]
+
anyhow = "1.0.98"
+
log = "0.4.27"
+
env_logger = { version = "0.11.8", default-features = false }
+
+
[dev-dependencies]
+
tempfile = "3.20.0"
+
indoc = "2.0.6"
+
+
[profile.release]
+
opt-level = "z"
+
panic = "abort"
+
lto = true
+
strip = true
+
+
[lints.rust]
+
unsafe_code = "forbid"
+
+
[lints.clippy]
+
all = { level = "deny" }
+
pedantic = { level = "deny" }
+
missing_errors_doc = { level = "allow", priority = 1 }
+67
pkgs/by-name/ni/nixos-init/README.md
···
+
# `nixos-init`
+
+
A system for the initialization of NixOS.
+
+
The most important task of `nixos-init` is to work around the constraints of the
+
Filesystem Hierarchy Standard (FHS) that are imposed by other tools (most
+
importantly systemd itself).
+
+
The primary design principle is to do the minimal work required to start
+
systemd. Everything that can be done later SHOULD be done later (i.e. after
+
systemd has already started). This isn't controversial either, this is the
+
basic principle behind initrds in the first place.
+
+
Adding functionality to this init should be done with care and only when
+
strictly necessary. It should always be a last resort. It is explicitly not
+
designed to be extended dynamically by downstream users of NixOS.
+
+
The goal of `nixos-init` is to eventually entirely replace
+
`system.activationScripts` for booting, enable bashless activation and
+
ultimately make NixOS overall more
+
robust.
+
+
## Reasoning
+
+
- We already have a native API that can be used to easily extend the system
+
(`systemd.services`). This is in all ways superior to the stringified and
+
sequential nature of `system.activationScripts`.
+
- For the remaining functionality, a fully fledged programming language makes
+
writing correct software easier and should improve the quality of the NixOS
+
boot code.
+
- Most things can be started much later than one might assume. Because systemd
+
services are parallelized, this should improve start up time.
+
+
## Invariants
+
+
For now, this does not try to replace all uses of `system.activationScripts`.
+
For the first iteration it only tries to offer an alternative to the
+
prepare-root in the systemd initrd.
+
+
It never intends to improve or replace scripted initrd. Scripted initrd is
+
being phased out, supporting it is out of scope.
+
+
## Components
+
+
`nixos-init` consists of a few components split into separate entrypoints.
+
However, these are not separate binaries but a single multicall binary. This
+
allows us to re-use the libc of the main binary and thus reduce the size of the
+
closure. Currently nixos-init comes in at ~500 KiB.
+
+
- `initrd-init`: Initializes the system on boot, setting up the tree for
+
systemd to start.
+
- `find-etc`: Finds the `/etc` paths in `/sysroot` so that the initrd doesn't
+
directly depend on the toplevel reducing the need to rebuild the initrd on
+
every generation.
+
- `chroot-realpath`: Figures out the canonical path inside a chroot.
+
+
## Future
+
+
Current usages of `activationScripts`:
+
+
1. Initialization of the system
+
1.1. In initrd
+
1.2. As PID 1 if there is no initrd (e.g. for containers or cloud VMs).
+
2. Re-activation of the system via switch-to-configuration.
+
3. Installation of a system with `nixos-enter` (chroot).
+
+
Currently, `nixos-init` only addresses 1.1. At least 1.2 is also in scope.
+64
pkgs/by-name/ni/nixos-init/package.nix
···
+
{
+
lib,
+
rustPlatform,
+
clippy,
+
rustfmt,
+
nixosTests,
+
}:
+
+
let
+
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
+
in
+
rustPlatform.buildRustPackage (finalAttrs: {
+
pname = cargoToml.package.name;
+
inherit (cargoToml.package) version;
+
+
__structuredAttrs = true;
+
+
src = lib.sourceFilesBySuffices ./. [
+
".rs"
+
".toml"
+
".lock"
+
];
+
+
cargoLock = {
+
lockFile = ./Cargo.lock;
+
};
+
+
stripAllList = [ "bin" ];
+
+
passthru.tests = {
+
lint-format = finalAttrs.finalPackage.overrideAttrs (
+
_: previousAttrs: {
+
pname = previousAttrs.pname + "-lint-format";
+
nativeCheckInputs = (previousAttrs.nativeCheckInputs or [ ]) ++ [
+
clippy
+
rustfmt
+
];
+
checkPhase = ''
+
cargo clippy
+
cargo fmt --check
+
'';
+
}
+
);
+
inherit (nixosTests) activation-nixos-init;
+
};
+
+
binaries = [
+
"initrd-init"
+
"find-etc"
+
"chroot-realpath"
+
];
+
+
postInstall = ''
+
for binary in "''${binaries[@]}"; do
+
ln -s $out/bin/nixos-init $out/bin/$binary
+
done
+
'';
+
+
meta = {
+
license = lib.licenses.mit;
+
maintainers = with lib.maintainers; [ nikstur ];
+
platforms = lib.platforms.linux;
+
};
+
})
+101
pkgs/by-name/ni/nixos-init/src/activate.rs
···
+
use std::{fs, path::Path};
+
+
use anyhow::{Context, Result};
+
+
use crate::{config::Config, fs::atomic_symlink};
+
+
/// Activate the system.
+
///
+
/// This runs both during boot and during re-activation initiated by switch-to-configuration.
+
pub fn activate(prefix: &str, toplevel: impl AsRef<Path>, config: &Config) -> Result<()> {
+
log::info!("Setting up /run/current-system...");
+
atomic_symlink(&toplevel, format!("{prefix}/run/current-system"))?;
+
+
log::info!("Setting up modprobe...");
+
setup_modprobe(&config.modprobe_binary)?;
+
+
log::info!("Setting up firmware search paths...");
+
setup_firmware_search_path(&config.firmware)?;
+
+
if let Some(env_path) = &config.env_binary {
+
log::info!("Setting up /usr/bin/env...");
+
setup_usrbinenv(prefix, env_path)?;
+
} else {
+
log::info!("No env binary provided. Not setting up /usr/bin/env.");
+
}
+
+
if let Some(sh_path) = &config.sh_binary {
+
log::info!("Setting up /bin/sh...");
+
setup_binsh(prefix, sh_path)?;
+
} else {
+
log::info!("No sh binary provided. Not setting up /bin/sh.");
+
}
+
+
Ok(())
+
}
+
+
/// Setup modprobe so that the kernel can find the wrapped binary.
+
///
+
/// See <https://docs.kernel.org/admin-guide/sysctl/kernel.html#modprobe>
+
fn setup_modprobe(modprobe_binary: impl AsRef<Path>) -> Result<()> {
+
// This uses the procfs setup in the initrd, which is fine because it points to the same kernel
+
// a procfs in a chroot would.
+
const MODPROBE_PATH: &str = "/proc/sys/kernel/modprobe";
+
+
fs::write(
+
MODPROBE_PATH,
+
modprobe_binary.as_ref().as_os_str().as_encoded_bytes(),
+
)
+
.with_context(|| {
+
format!(
+
"Failed to populate modprobe path with {}",
+
modprobe_binary.as_ref().display()
+
)
+
})?;
+
Ok(())
+
}
+
+
/// Setup the firmware search path so that the kernel can find the firmware.
+
///
+
/// See <https://www.kernel.org/doc/html/latest/driver-api/firmware/fw_search_path.html>
+
fn setup_firmware_search_path(firmware: impl AsRef<Path>) -> Result<()> {
+
// This uses the sysfs setup in the initrd, which is fine because it points to the same kernel
+
// a procfs in a chroot would.
+
const FIRMWARE_SERCH_PATH: &str = "/sys/module/firmware_class/parameters/path";
+
+
if Path::new(FIRMWARE_SERCH_PATH).exists() {
+
fs::write(
+
FIRMWARE_SERCH_PATH,
+
firmware.as_ref().as_os_str().as_encoded_bytes(),
+
)
+
.with_context(|| {
+
format!(
+
"Failed to populate firmware search path with {}",
+
firmware.as_ref().display()
+
)
+
})?;
+
}
+
+
Ok(())
+
}
+
+
/// Setup `/usr/bin/env`.
+
///
+
/// We have to setup `/usr` for `NixOS` to work.
+
///
+
/// We do this here accidentally. `/usr/bin/env` is currently load-bearing for `NixOS`.
+
fn setup_usrbinenv(prefix: &str, env_binary: impl AsRef<Path>) -> Result<()> {
+
const USRBINENV_PATH: &str = "/usr/bin/env";
+
+
fs::create_dir_all(format!("{prefix}/usr/bin")).context("Failed to create /usr/bin")?;
+
atomic_symlink(&env_binary, format!("{prefix}{USRBINENV_PATH}"))
+
}
+
+
/// Setup /bin/sh.
+
///
+
/// `/bin/sh` is an essential part of a Linux system as this path is hardcoded in the `system()` call
+
/// from libc. See `man systemd(3)`.
+
fn setup_binsh(prefix: &str, sh_binary: impl AsRef<Path>) -> Result<()> {
+
const BINSH_PATH: &str = "/bin/sh";
+
atomic_symlink(&sh_binary, format!("{prefix}{BINSH_PATH}"))
+
}
+52
pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs
···
+
use std::{
+
env,
+
io::{Write, stdout},
+
os::unix::ffi::OsStrExt,
+
os::unix::fs,
+
path::{Path, PathBuf},
+
process::Command,
+
};
+
+
use anyhow::{Context, Result, bail};
+
+
/// Canonicalize `path` in a chroot at the specified `root`.
+
pub fn canonicalize_in_chroot(root: &str, path: &Path) -> Result<PathBuf> {
+
let output = Command::new("chroot-realpath")
+
.arg(root)
+
.arg(path.as_os_str())
+
.output()
+
.context("Failed to run chroot-realpath. Most likely, the binary is not on PATH")?;
+
+
if !output.status.success() {
+
bail!(
+
"chroot-realpath exited unsuccessfully: {}",
+
String::from_utf8_lossy(&output.stderr)
+
);
+
}
+
+
let output =
+
String::from_utf8(output.stdout).context("Failed to decode stdout of chroot-realpath")?;
+
+
Ok(PathBuf::from(&output))
+
}
+
+
/// Entrypoint for the `chroot-realpath` binary.
+
pub fn chroot_realpath() -> Result<()> {
+
let args: Vec<String> = env::args().collect();
+
+
if args.len() != 3 {
+
bail!("Usage: {} <chroot> <path>", args[0]);
+
}
+
+
fs::chroot(&args[1]).context("Failed to chroot")?;
+
std::env::set_current_dir("/").context("Failed to change directory")?;
+
+
let path = std::fs::canonicalize(&args[2])
+
.with_context(|| format!("Failed to canonicalize {}", args[2]))?;
+
+
stdout()
+
.write_all(path.into_os_string().as_bytes())
+
.context("Failed to write output")?;
+
+
Ok(())
+
}
+43
pkgs/by-name/ni/nixos-init/src/config.rs
···
+
use std::env;
+
+
use anyhow::{Context, Result};
+
+
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>,
+
}
+
+
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();
+
+
Ok(Self {
+
firmware: required("FIRMWARE")?,
+
modprobe_binary: required("MODPROBE_BINARY")?,
+
nix_store_mount_opts,
+
env_binary: optional("ENV_BINARY"),
+
sh_binary: optional("SH_BINARY"),
+
})
+
}
+
}
+
+
/// 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()
+
}
+31
pkgs/by-name/ni/nixos-init/src/find_etc.rs
···
+
use std::{os::unix, path::Path};
+
+
use anyhow::{Context, Result};
+
+
use crate::{SYSROOT_PATH, canonicalize_in_chroot, find_toplevel_in_prefix};
+
+
/// Entrypoint for the `find-etc` binary.
+
///
+
/// Find the etc related paths in /sysroot.
+
///
+
/// This avoids needing a reference to the toplevel embedded in the initrd and thus reduces the
+
/// need to re-build it.
+
pub fn find_etc() -> Result<()> {
+
let toplevel = find_toplevel_in_prefix(SYSROOT_PATH)?;
+
+
let etc_metadata_image = Path::new(SYSROOT_PATH).join(
+
canonicalize_in_chroot(SYSROOT_PATH, &toplevel.join("etc-metadata-image"))?
+
.strip_prefix("/")?,
+
);
+
+
let etc_basedir = Path::new(SYSROOT_PATH).join(
+
canonicalize_in_chroot(SYSROOT_PATH, &toplevel.join("etc-basedir"))?.strip_prefix("/")?,
+
);
+
+
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(())
+
}
+50
pkgs/by-name/ni/nixos-init/src/fs.rs
···
+
use std::{fs, path::Path};
+
+
use anyhow::{Context, Result, anyhow};
+
+
/// Atomically symlink a file.
+
///
+
/// This will first symlink the original to a temporary path with a `.tmp` suffix and then move the
+
/// symlink to its actual path.
+
/// The temporary and actual paths are located in the same directory, which is created if it does
+
/// not exist.
+
pub fn atomic_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
+
let mut i = 0;
+
+
let tmp_path = loop {
+
let parent = link
+
.as_ref()
+
.parent()
+
.ok_or(anyhow!("Failed to determine parent of {:?}", link.as_ref()))?;
+
if !parent.exists() {
+
std::fs::create_dir(parent)?;
+
}
+
+
let mut tmp_path = link.as_ref().as_os_str().to_os_string();
+
tmp_path.push(format!(".tmp{i}"));
+
+
let res = std::os::unix::fs::symlink(&original, &tmp_path);
+
match res {
+
Ok(()) => break tmp_path,
+
Err(err) => {
+
if err.kind() != std::io::ErrorKind::AlreadyExists {
+
return Err(err).context(format!(
+
"Failed to symlink to temporary file {}",
+
tmp_path.display()
+
));
+
}
+
}
+
}
+
i += 1;
+
};
+
+
fs::rename(&tmp_path, &link).with_context(|| {
+
format!(
+
"Failed to rename {} to {}",
+
tmp_path.display(),
+
link.as_ref().display()
+
)
+
})?;
+
+
Ok(())
+
}
+106
pkgs/by-name/ni/nixos-init/src/init.rs
···
+
use std::{fs, os::unix::fs::PermissionsExt, path::Path, process::Command};
+
+
use anyhow::{Context, Result};
+
+
use crate::{activate::activate, config::Config, fs::atomic_symlink, proc_mounts::Mounts};
+
+
const NIX_STORE_PATH: &str = "/nix/store";
+
+
fn prefixed_store_path(prefix: &str) -> String {
+
format!("{prefix}{NIX_STORE_PATH}")
+
}
+
+
/// Initialize the system in a prefix.
+
///
+
/// This is done only once during the boot of the system.
+
///
+
/// It is not designed to be re-executed during the lifetime of a system boot cycle.
+
pub fn init(prefix: &str, toplevel: impl AsRef<Path>, config: &Config) -> Result<()> {
+
log::info!("Setting up /nix/store permissions...");
+
setup_nix_store_permissions(prefix);
+
+
log::info!("Remounting /nix/store with the correct options...");
+
remount_nix_store(prefix, &config.nix_store_mount_opts)?;
+
+
log::info!("Setting up /run/booted-system...");
+
atomic_symlink(&toplevel, format!("{prefix}/run/booted-system"))?;
+
+
log::info!("Activating the system...");
+
activate(prefix, toplevel, config)?;
+
+
Ok(())
+
}
+
+
/// Set up the correct permissions for the Nix Store.
+
///
+
/// Gracefully fail if they cannot be changed to accommodate read-only filesystems.
+
fn setup_nix_store_permissions(prefix: &str) {
+
const ROOT_UID: u32 = 0;
+
const NIXBUILD_GID: u32 = 0;
+
const NIX_STORE_MODE: u32 = 0o1775;
+
+
let nix_store_path = prefixed_store_path(prefix);
+
+
std::os::unix::fs::chown(&nix_store_path, Some(ROOT_UID), Some(NIXBUILD_GID)).ok();
+
fs::metadata(&nix_store_path)
+
.map(|metadata| {
+
let mut permissions = metadata.permissions();
+
permissions.set_mode(NIX_STORE_MODE);
+
})
+
.ok();
+
}
+
+
/// Remount the Nix Store in a prefix with the provided options.
+
fn remount_nix_store(prefix: &str, nix_store_mount_opts: &[String]) -> Result<()> {
+
let nix_store_path = prefixed_store_path(prefix);
+
+
let mut missing_opts = Vec::new();
+
let mounts = Mounts::parse_from_proc_mounts()?;
+
+
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());
+
}
+
}
+
if !missing_opts.is_empty() {
+
log::info!(
+
"/nix/store is missing mount options: {}.",
+
missing_opts.join(",")
+
);
+
}
+
} else {
+
log::info!("/nix/store is not a mountpoint.");
+
missing_opts.extend_from_slice(nix_store_mount_opts);
+
}
+
+
if !missing_opts.is_empty() {
+
log::info!("Remounting /nix/store with {}...", missing_opts.join(","));
+
+
mount(&["--bind", &nix_store_path, &nix_store_path])?;
+
mount(&[
+
"-o",
+
&format!("remount,bind,{}", missing_opts.join(",")),
+
&nix_store_path,
+
])?;
+
}
+
+
Ok(())
+
}
+
+
/// Call `mount` with the provided `args`.
+
fn mount(args: &[&str]) -> Result<()> {
+
let output = Command::new("mount")
+
.args(args)
+
.output()
+
.context("Failed to run mount. Most likely, the binary is not on PATH")?;
+
+
if !output.status.success() {
+
return Err(anyhow::anyhow!(
+
"mount executed unsuccessfully: {}",
+
String::from_utf8_lossy(&output.stdout)
+
));
+
}
+
+
Ok(())
+
}
+26
pkgs/by-name/ni/nixos-init/src/initrd_init.rs
···
+
use anyhow::{Context, Result};
+
+
use crate::{
+
SYSROOT_PATH, config::Config, find_init_in_prefix, init, switch_root, verify_init_is_nixos,
+
};
+
+
/// Entrypoint for the `initrd-bin` binary.
+
///
+
/// 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)?;
+
None
+
} else {
+
log::info!("Not initializing NixOS. Switching to new root immediately...");
+
Some(init_in_sysroot)
+
};
+
+
switch_root(init_path)?;
+
Ok(())
+
}
+128
pkgs/by-name/ni/nixos-init/src/lib.rs
···
+
mod activate;
+
mod chroot_realpath;
+
mod config;
+
mod find_etc;
+
mod fs;
+
mod init;
+
mod initrd_init;
+
mod proc_mounts;
+
mod switch_root;
+
+
use std::path::{Path, PathBuf};
+
+
use anyhow::{Context, Result, bail};
+
+
pub use crate::{
+
activate::activate,
+
chroot_realpath::{canonicalize_in_chroot, chroot_realpath},
+
find_etc::find_etc,
+
init::init,
+
initrd_init::initrd_init,
+
switch_root::switch_root,
+
};
+
+
pub const SYSROOT_PATH: &str = "/sysroot";
+
+
/// Find the path to the toplevel closure of the system in a prefix.
+
///
+
/// Uses the `init=` parameter on the kernel command-line.
+
///
+
/// Returns the relative path of the init to the prefix, e.g. without the `/sysroot` prefix.
+
pub fn find_toplevel_in_prefix(prefix: &str) -> Result<PathBuf> {
+
let init_in_sysroot = find_init_in_prefix(prefix)?;
+
verify_init_is_nixos(prefix, init_in_sysroot)
+
}
+
+
/// Verify that an init path is inside a `NixOS` toplevel directory.
+
///
+
/// If the path is verified, returns the path to the toplevel.
+
///
+
/// Check for the file `nixos-version` inside the toplevel to verify.
+
///
+
/// # Errors
+
///
+
/// If the init is not inside a `NixOS` toplevel return an error.
+
pub fn verify_init_is_nixos(prefix: &str, path: impl AsRef<Path>) -> Result<PathBuf> {
+
let toplevel = path
+
.as_ref()
+
.parent()
+
.map(Path::to_path_buf)
+
.context("Provided init= is not in a directory")?;
+
+
let stripped_toplevel = toplevel
+
.strip_prefix("/")
+
.with_context(|| format!("Failed to strip / from {}", toplevel.display()))?;
+
+
let nixos_version_in_prefix = Path::new(prefix)
+
.join(stripped_toplevel)
+
.join("nixos-version");
+
+
if !nixos_version_in_prefix
+
.try_exists()
+
.context("Failed to check whether nixos-version exists in toplevel")?
+
{
+
bail!(
+
"Failed to verify init {} is inside a NixOS toplevel",
+
path.as_ref().display()
+
)
+
}
+
Ok(toplevel)
+
}
+
+
/// Find the canonical path of the init in a prefix.
+
///
+
/// Uses the `init=` parameter on the kernel command-line.
+
///
+
/// Returns the relative path of the init to the prefix, e.g. without the `/sysroot` prefix.
+
pub fn find_init_in_prefix(prefix: &str) -> Result<PathBuf> {
+
let cmdline = std::fs::read_to_string("/proc/cmdline")?;
+
let init = extract_init(&cmdline)?;
+
let canonicalized_init = canonicalize_in_chroot(prefix, &init)?;
+
log::info!("Found init: {}.", canonicalized_init.display());
+
Ok(canonicalized_init)
+
}
+
+
/// Extract the value of the `init` parameter from the given kernel `cmdline`.
+
fn extract_init(cmdline: &str) -> Result<PathBuf> {
+
let init_params: Vec<&str> = cmdline
+
.split_ascii_whitespace()
+
.filter(|p| p.starts_with("init="))
+
.collect();
+
+
if init_params.len() != 1 {
+
bail!("Expected exactly one init param on kernel cmdline: {cmdline}")
+
}
+
+
let init = init_params
+
.first()
+
.and_then(|s| s.split('=').next_back())
+
.context("Failed to extract init path from kernel cmdline: {cmdline}")?;
+
+
Ok(PathBuf::from(init))
+
}
+
+
#[cfg(test)]
+
mod tests {
+
use super::*;
+
+
use std::fs;
+
+
use tempfile::tempdir;
+
+
#[test]
+
fn test_verify_init_is_nixos() -> Result<()> {
+
let prefix = tempdir()?;
+
let toplevel = prefix.path().join("toplevel");
+
fs::create_dir(&toplevel)?;
+
+
let init = &toplevel.join("init");
+
fs::write(init, "init")?;
+
+
let nixos_version = toplevel.join("nixos-version");
+
fs::write(&nixos_version, "25.11")?;
+
+
verify_init_is_nixos(prefix.path().to_str().unwrap(), "/toplevel/init")?;
+
+
Ok(())
+
}
+
}
+54
pkgs/by-name/ni/nixos-init/src/main.rs
···
+
use std::{env, io::Write, process::ExitCode};
+
+
use log::Level;
+
+
use nixos_init::{chroot_realpath, find_etc, initrd_init};
+
+
fn main() -> ExitCode {
+
let arg0 = env::args()
+
.next()
+
.and_then(|c| c.split('/').next_back().map(std::borrow::ToOwned::to_owned))
+
.expect("Failed to retrieve name of binary");
+
+
setup_logger();
+
let entrypoint = match arg0.as_str() {
+
"find-etc" => find_etc,
+
"chroot-realpath" => chroot_realpath,
+
"initrd-init" => initrd_init,
+
_ => {
+
log::error!("Command {arg0} unknown");
+
return ExitCode::FAILURE;
+
}
+
};
+
+
match entrypoint() {
+
Ok(()) => ExitCode::SUCCESS,
+
Err(err) => {
+
log::error!("{err:#}.");
+
ExitCode::FAILURE
+
}
+
}
+
}
+
+
// Setup the logger to use the kernel's `printk()` scheme.
+
//
+
// This way, systemd can interpret the levels correctly.
+
fn setup_logger() {
+
let env = env_logger::Env::default().filter_or("LOG_LEVEL", "info");
+
+
env_logger::Builder::from_env(env)
+
.format(|buf, record| {
+
writeln!(
+
buf,
+
"<{}>{}",
+
match record.level() {
+
Level::Error => 3,
+
Level::Warn => 4,
+
Level::Info => 6,
+
Level::Debug | Level::Trace => 7,
+
},
+
record.args()
+
)
+
})
+
.init();
+
}
+103
pkgs/by-name/ni/nixos-init/src/proc_mounts.rs
···
+
use std::fs;
+
+
use anyhow::{Context, Result};
+
+
pub struct Mounts {
+
inner: Vec<Mount>,
+
}
+
+
#[derive(Debug)]
+
pub struct Mount {
+
_spec: String,
+
file: String,
+
_vfstype: String,
+
pub mntopts: MntOpts,
+
}
+
+
#[derive(Debug)]
+
pub struct MntOpts {
+
inner: Vec<String>,
+
}
+
+
impl Mounts {
+
pub fn parse_from_proc_mounts() -> Result<Self> {
+
let proc_mounts =
+
fs::read_to_string("/proc/mounts").context("Failed to read /proc/mounts")?;
+
Self::parse(&proc_mounts)
+
}
+
+
fn parse(s: &str) -> Result<Self> {
+
let mut inner = Vec::new();
+
for line in s.lines() {
+
let mut split = line.split_whitespace();
+
let mount = Mount {
+
_spec: split.next().context("Failed to parse spec")?.to_string(),
+
file: split.next().context("Failed to parse file")?.to_string(),
+
_vfstype: split.next().context("Failed to parse vfstype")?.to_string(),
+
mntopts: MntOpts::parse(split.next().context("Failed to parse mntopts")?),
+
};
+
inner.push(mount);
+
}
+
Ok(Self { inner })
+
}
+
+
pub fn find_mountpoint(&self, mountpoint: &str) -> Option<&Mount> {
+
self.inner.iter().rev().find(|m| m.file == mountpoint)
+
}
+
}
+
+
impl MntOpts {
+
fn parse(s: &str) -> Self {
+
let mut vec = Vec::new();
+
for sp in s.split(',') {
+
vec.push(sp.to_string());
+
}
+
+
Self { inner: vec }
+
}
+
+
pub fn contains(&self, s: &str) -> bool {
+
self.inner.contains(&s.to_string())
+
}
+
}
+
+
#[cfg(test)]
+
mod tests {
+
use super::*;
+
+
use indoc::indoc;
+
+
#[test]
+
fn test_proc_mounts_parsing() -> Result<()> {
+
let s = indoc! {r"
+
/dev/mapper/root / btrfs rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=5,subvol=/ 0 0
+
tmpfs /run tmpfs rw,nosuid,nodev,size=15350916k,nr_inodes=819200,mode=755 0 0
+
devtmpfs /dev devtmpfs rw,nosuid,size=3070184k,nr_inodes=7671201,mode=755 0 0
+
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=3,mode=620,ptmxmode=666 0 0
+
tmpfs /dev/shm tmpfs rw,nosuid,nodev 0 0
+
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
+
ramfs /run/keys ramfs rw,nosuid,nodev,relatime,mode=750 0 0
+
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
+
/dev/mapper/root /nix/store btrfs ro,nosuid,nodev,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=5,subvol=/ 0 0
+
securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
+
cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot 0 0
+
none /sys/fs/pstore pstore rw,nosuid,nodev,noexec,relatime 0 0
+
efivarfs /sys/firmware/efi/efivars efivarfs rw,nosuid,nodev,noexec,relatime 0 0
+
bpf /sys/fs/bpf bpf rw,nosuid,nodev,noexec,relatime,mode=700 0 0
+
hugetlbfs /dev/hugepages hugetlbfs rw,nosuid,nodev,relatime,pagesize=2M 0 0
+
mqueue /dev/mqueue mqueue rw,nosuid,nodev,noexec,relatime 0 0
+
debugfs /sys/kernel/debug debugfs rw,nosuid,nodev,noexec,relatime 0 0
+
tracefs /sys/kernel/tracing tracefs rw,nosuid,nodev,noexec,relatime 0 0
+
"};
+
+
let mounts = Mounts::parse(s)?;
+
if let Some(nix_store_mount) = mounts.find_mountpoint("/nix/store") {
+
println!("{nix_store_mount:?}");
+
println!("{:?}", nix_store_mount.mntopts);
+
assert!(nix_store_mount.mntopts.contains("ro"));
+
assert!(!nix_store_mount.mntopts.contains("no"));
+
}
+
+
Ok(())
+
}
+
}
+35
pkgs/by-name/ni/nixos-init/src/switch_root.rs
···
+
use std::{io::Write, path::PathBuf, process::Command};
+
+
use anyhow::{Context, Result, bail};
+
+
use crate::SYSROOT_PATH;
+
+
/// Switch root from initrd.
+
///
+
/// If the provided init is `None`, systemd is used as the next init.
+
pub fn switch_root(init: Option<PathBuf>) -> Result<()> {
+
log::info!("Switching root to {SYSROOT_PATH}...");
+
+
let mut cmd = Command::new("systemctl");
+
cmd.arg("--no-block").arg("switch-root").arg(SYSROOT_PATH);
+
+
if let Some(init) = init {
+
log::info!("Using init {}.", init.display());
+
cmd.arg(init);
+
} else {
+
log::info!("Using built-in systemd as init.");
+
cmd.arg("");
+
}
+
+
let output = cmd
+
.output()
+
.context("Failed to run systemctl switch-root. Most likely the binary is not on PATH")?;
+
+
let _ = std::io::stderr().write_all(&output.stderr);
+
+
if !output.status.success() {
+
bail!("systemctl switch-root exited unsuccessfully");
+
}
+
+
Ok(())
+
}