nixos-init: init at 0.1.0 (#433154)

This commit is contained in:
misuzu
2025-09-23 12:48:56 +00:00
committed by GitHub
29 changed files with 1355 additions and 110 deletions
+1
View File
@@ -9,6 +9,7 @@
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
View File
@@ -1803,6 +1803,7 @@
./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
@@ -0,0 +1,31 @@
{
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";
}
];
};
}
@@ -14,12 +14,15 @@ let
${
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
View File
@@ -562,7 +562,12 @@ in
"${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);
@@ -594,7 +599,7 @@ in
) cfg.automounts
);
services.initrd-find-nixos-closure = {
services.initrd-find-nixos-closure = lib.mkIf (!config.system.nixos-init.enable) {
description = "Find NixOS closure";
unitConfig = {
@@ -615,7 +620,12 @@ in
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=
@@ -670,7 +680,7 @@ in
}
];
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" ];
@@ -697,17 +707,46 @@ in
'';
};
# 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
View File
@@ -144,15 +144,10 @@
{
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";
@@ -160,20 +155,8 @@
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
View File
@@ -0,0 +1,54 @@
{ 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
View File
@@ -190,6 +190,7 @@ in
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
View File
@@ -774,7 +774,7 @@ let
config.boot.bootspec.package
]
++ optionals clevisTest [ pkgs.klibc ]
++ optional systemdStage1 pkgs.chroot-realpath;
++ optional systemdStage1 config.system.nixos-init.package;
nix.settings = {
substituters = mkForce [ ];
@@ -1,21 +0,0 @@
{
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
View File
@@ -1,16 +0,0 @@
# 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",
]
@@ -1,10 +0,0 @@
[package]
name = "chroot-realpath"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.98"
[profile.release]
opt-level = "z"
@@ -1,26 +0,0 @@
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
View File
@@ -0,0 +1,6 @@
.direnv/
result*
# Rust
**/*.rs.bk # These are backup files generated by rustfmt
target/
+312
View File
@@ -0,0 +1,312 @@
# 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
View File
@@ -0,0 +1,27 @@
[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
View File
@@ -0,0 +1,67 @@
# `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
View File
@@ -0,0 +1,64 @@
{
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
View File
@@ -0,0 +1,101 @@
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}"))
}
@@ -0,0 +1,52 @@
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
View File
@@ -0,0 +1,43 @@
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()
}
@@ -0,0 +1,31 @@
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
View File
@@ -0,0 +1,50 @@
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
View File
@@ -0,0 +1,106 @@
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(())
}
@@ -0,0 +1,26 @@
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
View File
@@ -0,0 +1,128 @@
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
View File
@@ -0,0 +1,54 @@
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();
}
@@ -0,0 +1,103 @@
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(())
}
}
@@ -0,0 +1,35 @@
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(())
}