From d0c03e4c5abf82a3218e04b3eb90b566ec2ae5f1 Mon Sep 17 00:00:00 2001 From: nikstur Date: Tue, 12 Aug 2025 22:15:15 +0200 Subject: [PATCH 1/4] nixos-init: init at 0.1.0 --- nixos/modules/module-list.nix | 1 + .../modules/system/activation/nixos-init.nix | 31 ++ nixos/modules/system/activation/top-level.nix | 7 +- nixos/modules/system/boot/systemd/initrd.nix | 58 +++- nixos/tests/activation/nixos-init.nix | 54 +++ nixos/tests/all-tests.nix | 1 + pkgs/by-name/ni/nixos-init/.gitignore | 6 + pkgs/by-name/ni/nixos-init/Cargo.lock | 312 ++++++++++++++++++ pkgs/by-name/ni/nixos-init/Cargo.toml | 27 ++ pkgs/by-name/ni/nixos-init/README.md | 67 ++++ pkgs/by-name/ni/nixos-init/package.nix | 64 ++++ pkgs/by-name/ni/nixos-init/src/activate.rs | 101 ++++++ .../ni/nixos-init/src/chroot_realpath.rs | 52 +++ pkgs/by-name/ni/nixos-init/src/config.rs | 43 +++ pkgs/by-name/ni/nixos-init/src/find_etc.rs | 31 ++ pkgs/by-name/ni/nixos-init/src/fs.rs | 50 +++ pkgs/by-name/ni/nixos-init/src/init.rs | 106 ++++++ pkgs/by-name/ni/nixos-init/src/initrd_init.rs | 26 ++ pkgs/by-name/ni/nixos-init/src/lib.rs | 128 +++++++ pkgs/by-name/ni/nixos-init/src/main.rs | 54 +++ pkgs/by-name/ni/nixos-init/src/proc_mounts.rs | 103 ++++++ pkgs/by-name/ni/nixos-init/src/switch_root.rs | 35 ++ 22 files changed, 1342 insertions(+), 15 deletions(-) create mode 100644 nixos/modules/system/activation/nixos-init.nix create mode 100644 nixos/tests/activation/nixos-init.nix create mode 100644 pkgs/by-name/ni/nixos-init/.gitignore create mode 100644 pkgs/by-name/ni/nixos-init/Cargo.lock create mode 100644 pkgs/by-name/ni/nixos-init/Cargo.toml create mode 100644 pkgs/by-name/ni/nixos-init/README.md create mode 100644 pkgs/by-name/ni/nixos-init/package.nix create mode 100644 pkgs/by-name/ni/nixos-init/src/activate.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/config.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/find_etc.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/fs.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/init.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/initrd_init.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/lib.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/main.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/proc_mounts.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/switch_root.rs diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7f1eb380cd40..189683ba18a8 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1801,6 +1801,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 diff --git a/nixos/modules/system/activation/nixos-init.nix b/nixos/modules/system/activation/nixos-init.nix new file mode 100644 index 000000000000..8b54724d7372 --- /dev/null +++ b/nixos/modules/system/activation/nixos-init.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"; + } + ]; + }; +} diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix index 875e2d5410b3..b287329fd40f 100644 --- a/nixos/modules/system/activation/top-level.nix +++ b/nixos/modules/system/activation/top-level.nix @@ -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 '' diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index 3493f5f9a1a5..2bf68adc0f92 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -564,6 +564,9 @@ in # Resolving sysroot symlinks without code exec "${pkgs.chroot-realpath}/bin/chroot-realpath" ] + ++ 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 +597,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 = { @@ -670,7 +673,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 +700,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" ]; diff --git a/nixos/tests/activation/nixos-init.nix b/nixos/tests/activation/nixos-init.nix new file mode 100644 index 000000000000..091b4727e76a --- /dev/null +++ b/nixos/tests/activation/nixos-init.nix @@ -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") + ''; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 93243f29082a..ac1cd2a05421 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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; diff --git a/pkgs/by-name/ni/nixos-init/.gitignore b/pkgs/by-name/ni/nixos-init/.gitignore new file mode 100644 index 000000000000..ebf7b5ba8869 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/.gitignore @@ -0,0 +1,6 @@ +.direnv/ +result* + +# Rust +**/*.rs.bk # These are backup files generated by rustfmt +target/ diff --git a/pkgs/by-name/ni/nixos-init/Cargo.lock b/pkgs/by-name/ni/nixos-init/Cargo.lock new file mode 100644 index 000000000000..1be95a5be152 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/Cargo.lock @@ -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", +] diff --git a/pkgs/by-name/ni/nixos-init/Cargo.toml b/pkgs/by-name/ni/nixos-init/Cargo.toml new file mode 100644 index 000000000000..0672931045c6 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/Cargo.toml @@ -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 } diff --git a/pkgs/by-name/ni/nixos-init/README.md b/pkgs/by-name/ni/nixos-init/README.md new file mode 100644 index 000000000000..c187f0d07b39 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/README.md @@ -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. diff --git a/pkgs/by-name/ni/nixos-init/package.nix b/pkgs/by-name/ni/nixos-init/package.nix new file mode 100644 index 000000000000..b34f6d570899 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/package.nix @@ -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; + }; +}) diff --git a/pkgs/by-name/ni/nixos-init/src/activate.rs b/pkgs/by-name/ni/nixos-init/src/activate.rs new file mode 100644 index 000000000000..c8fe22447899 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/activate.rs @@ -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, 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 +fn setup_modprobe(modprobe_binary: impl AsRef) -> 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 +fn setup_firmware_search_path(firmware: impl AsRef) -> 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) -> 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) -> Result<()> { + const BINSH_PATH: &str = "/bin/sh"; + atomic_symlink(&sh_binary, format!("{prefix}{BINSH_PATH}")) +} diff --git a/pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs b/pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs new file mode 100644 index 000000000000..3c4fd293ccb8 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs @@ -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 { + 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 = env::args().collect(); + + if args.len() != 3 { + bail!("Usage: {} ", 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(()) +} diff --git a/pkgs/by-name/ni/nixos-init/src/config.rs b/pkgs/by-name/ni/nixos-init/src/config.rs new file mode 100644 index 000000000000..b974943cb56a --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/config.rs @@ -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, + pub env_binary: Option, + pub sh_binary: Option, +} + +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 { + 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 { + env::var(key).with_context(|| format!("Failed to read {key} from environment")) +} + +/// Read an optional environment variable +fn optional(key: &str) -> Option { + env::var(key).ok() +} diff --git a/pkgs/by-name/ni/nixos-init/src/find_etc.rs b/pkgs/by-name/ni/nixos-init/src/find_etc.rs new file mode 100644 index 000000000000..fb9faabe21b2 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/find_etc.rs @@ -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(()) +} diff --git a/pkgs/by-name/ni/nixos-init/src/fs.rs b/pkgs/by-name/ni/nixos-init/src/fs.rs new file mode 100644 index 000000000000..249cce1e2191 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/fs.rs @@ -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, link: impl AsRef) -> 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(()) +} diff --git a/pkgs/by-name/ni/nixos-init/src/init.rs b/pkgs/by-name/ni/nixos-init/src/init.rs new file mode 100644 index 000000000000..aec7db39c951 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/init.rs @@ -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, 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(()) +} diff --git a/pkgs/by-name/ni/nixos-init/src/initrd_init.rs b/pkgs/by-name/ni/nixos-init/src/initrd_init.rs new file mode 100644 index 000000000000..c391f9b9c645 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/initrd_init.rs @@ -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(()) +} diff --git a/pkgs/by-name/ni/nixos-init/src/lib.rs b/pkgs/by-name/ni/nixos-init/src/lib.rs new file mode 100644 index 000000000000..aa123c5e30c2 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/lib.rs @@ -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 { + 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) -> Result { + 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 { + 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 { + 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(()) + } +} diff --git a/pkgs/by-name/ni/nixos-init/src/main.rs b/pkgs/by-name/ni/nixos-init/src/main.rs new file mode 100644 index 000000000000..d2ca7a5f3011 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/main.rs @@ -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(); +} diff --git a/pkgs/by-name/ni/nixos-init/src/proc_mounts.rs b/pkgs/by-name/ni/nixos-init/src/proc_mounts.rs new file mode 100644 index 000000000000..3ab2d505144d --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/proc_mounts.rs @@ -0,0 +1,103 @@ +use std::fs; + +use anyhow::{Context, Result}; + +pub struct Mounts { + inner: Vec, +} + +#[derive(Debug)] +pub struct Mount { + _spec: String, + file: String, + _vfstype: String, + pub mntopts: MntOpts, +} + +#[derive(Debug)] +pub struct MntOpts { + inner: Vec, +} + +impl Mounts { + pub fn parse_from_proc_mounts() -> Result { + let proc_mounts = + fs::read_to_string("/proc/mounts").context("Failed to read /proc/mounts")?; + Self::parse(&proc_mounts) + } + + fn parse(s: &str) -> Result { + 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(()) + } +} diff --git a/pkgs/by-name/ni/nixos-init/src/switch_root.rs b/pkgs/by-name/ni/nixos-init/src/switch_root.rs new file mode 100644 index 000000000000..be1bd1a7e953 --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/switch_root.rs @@ -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) -> 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(()) +} From d9e6299acc46d114fc7969b2d86a1106de9175f5 Mon Sep 17 00:00:00 2001 From: nikstur Date: Sun, 27 Jul 2025 21:18:55 +0200 Subject: [PATCH 2/4] nixos/etc: use nixos-init to find etc --- nixos/modules/system/boot/systemd/initrd.nix | 2 ++ nixos/modules/system/etc/etc-activation.nix | 21 ++------------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index 2bf68adc0f92..b1efa96e8672 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -563,6 +563,8 @@ in # Resolving sysroot symlinks without code exec "${pkgs.chroot-realpath}/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" diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix index 40cb28d0434f..25c3ea7274a6 100644 --- a/nixos/modules/system/etc/etc-activation.nix +++ b/nixos/modules/system/etc/etc-activation.nix @@ -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 - ''; }; } ]; From 3c24952c6dbabdc26ad1dc6c788a6126a9c8871b Mon Sep 17 00:00:00 2001 From: nikstur Date: Tue, 12 Aug 2025 15:45:19 +0200 Subject: [PATCH 3/4] chroot-realpath: drop in favour of nixos-init --- nixos/modules/system/boot/systemd/initrd.nix | 9 +++++-- nixos/tests/installer.nix | 2 +- pkgs/by-name/ch/chroot-realpath/package.nix | 21 --------------- .../by-name/ch/chroot-realpath/src/Cargo.lock | 16 ------------ .../by-name/ch/chroot-realpath/src/Cargo.toml | 10 ------- .../ch/chroot-realpath/src/src/main.rs | 26 ------------------- 6 files changed, 8 insertions(+), 76 deletions(-) delete mode 100644 pkgs/by-name/ch/chroot-realpath/package.nix delete mode 100644 pkgs/by-name/ch/chroot-realpath/src/Cargo.lock delete mode 100644 pkgs/by-name/ch/chroot-realpath/src/Cargo.toml delete mode 100644 pkgs/by-name/ch/chroot-realpath/src/src/main.rs diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index b1efa96e8672..e378dcf830bb 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -562,7 +562,7 @@ 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" ] @@ -620,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= diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix index efc63412d5f7..7a29d8e0ebae 100644 --- a/nixos/tests/installer.nix +++ b/nixos/tests/installer.nix @@ -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 [ ]; diff --git a/pkgs/by-name/ch/chroot-realpath/package.nix b/pkgs/by-name/ch/chroot-realpath/package.nix deleted file mode 100644 index c75262cd3344..000000000000 --- a/pkgs/by-name/ch/chroot-realpath/package.nix +++ /dev/null @@ -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 ]; - }; -} diff --git a/pkgs/by-name/ch/chroot-realpath/src/Cargo.lock b/pkgs/by-name/ch/chroot-realpath/src/Cargo.lock deleted file mode 100644 index 81f594ba3082..000000000000 --- a/pkgs/by-name/ch/chroot-realpath/src/Cargo.lock +++ /dev/null @@ -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", -] diff --git a/pkgs/by-name/ch/chroot-realpath/src/Cargo.toml b/pkgs/by-name/ch/chroot-realpath/src/Cargo.toml deleted file mode 100644 index aeefcfa4b449..000000000000 --- a/pkgs/by-name/ch/chroot-realpath/src/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "chroot-realpath" -version = "0.1.0" -edition = "2021" - -[dependencies] -anyhow = "1.0.98" - -[profile.release] -opt-level = "z" diff --git a/pkgs/by-name/ch/chroot-realpath/src/src/main.rs b/pkgs/by-name/ch/chroot-realpath/src/src/main.rs deleted file mode 100644 index 140ad4aaf689..000000000000 --- a/pkgs/by-name/ch/chroot-realpath/src/src/main.rs +++ /dev/null @@ -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 = env::args().collect(); - - if args.len() != 3 { - bail!("Usage: {} ", 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(()) -} From 904a96c6540c0de7e83d65ad7a69a84a8ca3910e Mon Sep 17 00:00:00 2001 From: nikstur Date: Tue, 23 Sep 2025 00:33:25 +0200 Subject: [PATCH 4/4] doc/rl-2511: add section on nixos-init --- doc/release-notes/rl-2511.section.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release-notes/rl-2511.section.md b/doc/release-notes/rl-2511.section.md index 2ae1b246f392..9d29dfde982c 100644 --- a/doc/release-notes/rl-2511.section.md +++ b/doc/release-notes/rl-2511.section.md @@ -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}