nixos-init: init at 0.1.0 (#433154)
This commit is contained in:
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
''
|
||||
|
||||
@@ -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" ];
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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")
|
||||
'';
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.direnv/
|
||||
result*
|
||||
|
||||
# Rust
|
||||
**/*.rs.bk # These are backup files generated by rustfmt
|
||||
target/
|
||||
Generated
+312
@@ -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",
|
||||
]
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
};
|
||||
})
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user