From 46a462b5f34d8ff0a5f810480bdb452af8a494ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 12:52:19 +0200 Subject: [PATCH 01/15] feat: add debian-13 (trixie) container test support Extend buildRootfs with a cloudImgFormat parameter to support extracting from qcow2 images using libguestfs-with-appliance. Add a debian-13 distro entry fetching the genericcloud qcow2 from cloud.debian.org. Unfortunately we have to restrict to x86_64-linux because libguestfs-appliance is not available on aarch64 in nixpkgs. --- lib/container-test-driver/distros.nix | 17 +++++++++++++ lib/container-test-driver/make-rootfs.nix | 30 +++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/container-test-driver/distros.nix b/lib/container-test-driver/distros.nix index 64ee831e..da5da6af 100644 --- a/lib/container-test-driver/distros.nix +++ b/lib/container-test-driver/distros.nix @@ -73,4 +73,21 @@ in }; maskableService = "unattended-upgrades.service"; }; + + debian-13 = { + # x86_64-linux only: the qcow2 extraction path uses libguestfs-with-appliance, + # whose appliance subpackage is not available on aarch64 in nixpkgs. + systems = [ "x86_64-linux" ]; + rootfs = makeRootfs.buildRootfs { + name = "debian-13"; + cloudImgFormat = "qcow2"; + cloudImg = builtins.fetchurl { + url = "https://cloud.debian.org/images/cloud/trixie/20260413-2447/debian-13-genericcloud-amd64-20260413-2447.qcow2"; + sha256 = "0iclqh20da7mm1ijhks66iqy2dz1shpipia1zwpgxpya1c4gg182"; + }; + excludePatterns = ubuntuExcludePatterns; + extraDirs = [ "var/lib/apt/lists/partial" ]; + }; + maskableService = "unattended-upgrades.service"; + }; } diff --git a/lib/container-test-driver/make-rootfs.nix b/lib/container-test-driver/make-rootfs.nix index 8f682705..666afd15 100644 --- a/lib/container-test-driver/make-rootfs.nix +++ b/lib/container-test-driver/make-rootfs.nix @@ -10,6 +10,7 @@ in { name, cloudImg, + cloudImgFormat ? "tar", excludePatterns ? [ ], extraDirs ? [ ], extraSetup ? "", @@ -21,19 +22,38 @@ in map (p: "--exclude='${p}'") excludePatterns ); mkdirCommands = builtins.concatStringsSep "\n " (map (d: "mkdir -p $out/${d}") extraDirs); + extractCommand = + if cloudImgFormat == "tar" then + '' + tar --exclude='dev/*' \ + ${excludeArgs} \ + ${tarExtraFlags} \ + ${tarCompression}xf ${cloudImg} -C $out + '' + else if cloudImgFormat == "qcow2" then + '' + LIBGUESTFS_BACKEND=direct \ + guestfish --ro -a ${cloudImg} -i tar-out / - \ + | tar --exclude='dev/*' \ + ${excludeArgs} \ + -C $out -x + '' + else + throw "buildRootfs: unsupported cloudImgFormat '${cloudImgFormat}' (expected 'tar' or 'qcow2')"; + nativeBuildInputs = [ + pkgs.xz + ] + ++ pkgs.lib.optionals (cloudImgFormat == "qcow2") [ pkgs.libguestfs-with-appliance ]; in pkgs.runCommand "rootfs-${name}" { - nativeBuildInputs = [ pkgs.xz ]; + inherit nativeBuildInputs; } '' mkdir -p $out # Extract cloud image, excluding container-incompatible services - tar --exclude='dev/*' \ - ${excludeArgs} \ - ${tarExtraFlags} \ - ${tarCompression}xf ${cloudImg} -C $out + ${extractCommand} # Ensure build user can modify all extracted files chmod -R u+rwX $out From 192e022072b0b2b1dea9d2349868e5651366853b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 12:52:24 +0200 Subject: [PATCH 02/15] feat(modules): add debian to osVersion preActivationAssertion supported list --- nix/modules/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/modules/default.nix b/nix/modules/default.nix index b81db20d..3a52462a 100644 --- a/nix/modules/default.nix +++ b/nix/modules/default.nix @@ -204,6 +204,7 @@ supportedIds = [ "nixos" "ubuntu" + "debian" ]; in { From 5445e6f5f124abcc73f0812a72a50fca5558ee42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 12:57:45 +0200 Subject: [PATCH 03/15] feat(vm-tests): test on Debian 13 (trixie) in addition to Ubuntu Generalize forEachUbuntuImage to forEachImage and iterates over both ubuntu and debian distros from nix-vm-test. --- testFlake/flake.lock | 6 +- testFlake/vm-tests/default.nix | 100 +++++++++++++---------- testFlake/vm-tests/example.nix | 4 +- testFlake/vm-tests/prepopulate.nix | 4 +- testFlake/vm-tests/security-wrappers.nix | 4 +- testFlake/vm-tests/sudo-module.nix | 4 +- testFlake/vm-tests/sudo.nix | 4 +- testFlake/vm-tests/target-host.nix | 4 +- 8 files changed, 74 insertions(+), 56 deletions(-) diff --git a/testFlake/flake.lock b/testFlake/flake.lock index c8ab1019..f369a18f 100644 --- a/testFlake/flake.lock +++ b/testFlake/flake.lock @@ -129,11 +129,11 @@ ] }, "locked": { - "lastModified": 1775586700, - "narHash": "sha256-D8iv3UHNS0DBt+Ry1lnjHig9XWL+T1j9VIE/p7mF8Bc=", + "lastModified": 1776165142, + "narHash": "sha256-bmPLrDLMb2QjGhWzDHvahyq4nuoSoYEMsgmo+qHiq1k=", "owner": "numtide", "repo": "nix-vm-test", - "rev": "c0325bd08f8897ca6d4e4cab7ac30e466e43cce0", + "rev": "f88465397ca010133084f07cb6f73875223c449d", "type": "github" }, "original": { diff --git a/testFlake/vm-tests/default.nix b/testFlake/vm-tests/default.nix index aa1bde5b..e8458f19 100644 --- a/testFlake/vm-tests/default.nix +++ b/testFlake/vm-tests/default.nix @@ -7,7 +7,18 @@ }: let - forEachUbuntuImage = + distros = { + ubuntu = { + # Ubuntu 20.04 reaches end of life April 2025; drop support. + filter = v: v != "20_04"; + }; + debian = { + # Only Debian 13 (trixie) + filter = v: v == "13"; + }; + }; + + forEachImage = name: { modules, @@ -16,47 +27,54 @@ let projectTest ? test: test.sandboxed, }: let - ubuntu = nix-vm-test.ubuntu; - in - lib.listToAttrs ( - # Ubuntu 20.04 reaches end of life April 2025; drop support. - lib.flip map (lib.filter (v: v != "20_04") (lib.attrNames ubuntu.images)) ( - imageVersion: - let - toplevel = ( - system-manager.lib.makeSystemConfig { - modules = modules ++ [ - ( - { lib, pkgs, ... }: - { - options.hostPkgs = lib.mkOption { - type = lib.types.raw; - readOnly = true; - }; - config = { - nixpkgs.hostPlatform = system; - hostPkgs = pkgs; - }; - } - ) - ]; - } - ); - inherit (toplevel.config) hostPkgs; - in - lib.nameValuePair "vm-ubuntu-${imageVersion}-${name}" ( - projectTest ( - ubuntu.${imageVersion} { - testScript = testScriptFunction { inherit toplevel hostPkgs; }; - extraPathsToRegister = extraPathsToRegister ++ [ - toplevel - ]; - sharedDirs = { }; + mkToplevel = system-manager.lib.makeSystemConfig { + modules = modules ++ [ + ( + { lib, pkgs, ... }: + { + options.hostPkgs = lib.mkOption { + type = lib.types.raw; + readOnly = true; + }; + config = { + nixpkgs.hostPlatform = system; + hostPkgs = pkgs; + }; } ) - ) - ) - ); + ]; + }; + mkTestForDistro = + distroName: distroConfig: + let + distro = nix-vm-test.${distroName}; + versions = lib.filter distroConfig.filter (lib.attrNames distro.images); + in + lib.listToAttrs ( + map ( + imageVersion: + let + toplevel = mkToplevel; + inherit (toplevel.config) hostPkgs; + in + lib.nameValuePair "vm-${distroName}-${imageVersion}-${name}" ( + projectTest ( + distro.${imageVersion} { + testScript = testScriptFunction { inherit toplevel hostPkgs; }; + extraPathsToRegister = extraPathsToRegister ++ [ + toplevel + ]; + sharedDirs = { }; + } + ) + ) + ) versions + ); + in + lib.foldlAttrs ( + acc: distroName: distroConfig: + acc // mkTestForDistro distroName distroConfig + ) { } distros; newConfig = system-manager.lib.makeSystemConfig { modules = [ @@ -142,7 +160,7 @@ let file: import file { inherit - forEachUbuntuImage + forEachImage newConfig system-manager system diff --git a/testFlake/vm-tests/example.nix b/testFlake/vm-tests/example.nix index 1a6b95f4..f7d18e94 100644 --- a/testFlake/vm-tests/example.nix +++ b/testFlake/vm-tests/example.nix @@ -1,11 +1,11 @@ { - forEachUbuntuImage, + forEachImage, newConfig, system-manager, ... }: -forEachUbuntuImage "example" { +forEachImage "example" { modules = [ ../../examples/example.nix ]; diff --git a/testFlake/vm-tests/prepopulate.nix b/testFlake/vm-tests/prepopulate.nix index 266ebcda..9f201d51 100644 --- a/testFlake/vm-tests/prepopulate.nix +++ b/testFlake/vm-tests/prepopulate.nix @@ -1,11 +1,11 @@ { - forEachUbuntuImage, + forEachImage, newConfig, system-manager, ... }: -forEachUbuntuImage "prepopulate" { +forEachImage "prepopulate" { modules = [ ../../examples/example.nix ]; diff --git a/testFlake/vm-tests/security-wrappers.nix b/testFlake/vm-tests/security-wrappers.nix index 223c5a14..fa3be2d3 100644 --- a/testFlake/vm-tests/security-wrappers.nix +++ b/testFlake/vm-tests/security-wrappers.nix @@ -2,12 +2,12 @@ # This must run in a VM because setting SUID bits and file capabilities # requires privileges that the nix build sandbox seccomp filter blocks. { - forEachUbuntuImage, + forEachImage, system-manager, ... }: -forEachUbuntuImage "security-wrappers" { +forEachImage "security-wrappers" { modules = [ ( { pkgs, ... }: diff --git a/testFlake/vm-tests/sudo-module.nix b/testFlake/vm-tests/sudo-module.nix index 0c9e7512..d40c2a7c 100644 --- a/testFlake/vm-tests/sudo-module.nix +++ b/testFlake/vm-tests/sudo-module.nix @@ -1,12 +1,12 @@ # Test sudo module: sudoers generation, no Nix-built sudo in PATH/wrappers, # and host sudo works with the generated config. { - forEachUbuntuImage, + forEachImage, system-manager, ... }: -forEachUbuntuImage "sudo-module" { +forEachImage "sudo-module" { modules = [ ( { ... }: diff --git a/testFlake/vm-tests/sudo.nix b/testFlake/vm-tests/sudo.nix index 187de41c..f058bed3 100644 --- a/testFlake/vm-tests/sudo.nix +++ b/testFlake/vm-tests/sudo.nix @@ -3,13 +3,13 @@ # 1. Running system-manager as non-root without --sudo fails # 2. Running system-manager with --sudo succeeds { - forEachUbuntuImage, + forEachImage, system-manager, system, ... }: -forEachUbuntuImage "sudo" { +forEachImage "sudo" { modules = [ ../../examples/example.nix ]; diff --git a/testFlake/vm-tests/target-host.nix b/testFlake/vm-tests/target-host.nix index 4cada210..3d678887 100644 --- a/testFlake/vm-tests/target-host.nix +++ b/testFlake/vm-tests/target-host.nix @@ -2,13 +2,13 @@ # This test runs the engine directly via SSH from the host (test driver) to the VM # It tests that the engine can be invoked remotely, which is the core of --target-host { - forEachUbuntuImage, + forEachImage, system-manager, system, ... }: -forEachUbuntuImage "target-host" { +forEachImage "target-host" { modules = [ ../../examples/example.nix ]; From e0729fa49a5104598dd93b8882c6b8e26afd93c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 13:24:28 +0200 Subject: [PATCH 04/15] docs: mention debian 13 alongside ubuntu in supported-distro statements --- CONTRIBUTING.md | 57 +++++++++++++++++----- README.md | 4 +- docs/site/how-to/install.md | 2 +- docs/site/how-to/test-configuration.md | 4 +- docs/site/reference/supported-platforms.md | 6 +-- docs/site/tutorials/getting-started.md | 2 +- 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 251aa898..160406b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,21 +46,56 @@ Before creating a new issue, please [search existing issues](https://github.com/ ## Adding New Distributions -System Manager officially supports Ubuntu and NixOS. To add support for another distribution: +System Manager officially supports Ubuntu, Debian, and NixOS. +Promoting a new distribution to officially-supported status means it is exercised by CI on every PR and a regression in it blocks the build. -1. Initialize a new flake with distribution checks disabled: - ```sh - nix run 'github:numtide/system-manager' -- init --flake --allow-any-distro - ``` +### Trying a distribution informally -2. Switch to the new configuration: - ```sh - nix run 'github:numtide/system-manager' -- switch --flake '.' - ``` +If you just want to run System Manager on an untested distribution without contributing it back, initialize a flake and disable the OS check by setting `system-manager.allowAnyDistro = true` in your configuration module: + +```nix +{ + config.system-manager.allowAnyDistro = true; +} +``` + +Then iterate with `nix run 'github:numtide/system-manager' -- switch --flake '.'` and debug any errors using the FAQ, GitHub issues, or a discussion. +Once the distribution is stable for your use case, consider upstreaming it via the steps below. + +### Adding official support + +Adding a distribution touches four areas: the OS allow-list, the container test driver, the VM test driver, and the documentation. + +**1. Add the distribution ID to the OS allow-list.** +Edit `nix/modules/default.nix` and append the `/etc/os-release` `ID` value to the `supportedIds` list inside the `osVersion` pre-activation assertion. +The check is bypassed when users set `system-manager.allowAnyDistro = true`, but the allow-list is what controls the default. + +**2. Add a container test entry.** +Container tests live under `testFlake/container-tests/` and are parameterized over every distribution declared in `lib/container-test-driver/distros.nix`. +Adding a new entry there causes all existing tests to automatically generate a `container--*` variant via `forEachDistro`. + +The entry must supply `systems`, a `rootfs` derivation built by `lib.container-test-driver.make-rootfs.buildRootfs`, and a `maskableService` (a systemd unit that test scripts may mask, typically `unattended-upgrades.service` or equivalent). + +`buildRootfs` accepts two upstream image formats via `cloudImgFormat`. +The default `"tar"` consumes a flat root tarball such as Ubuntu's `*-server-cloudimg-amd64-root.tar.xz` and is the simplest path. +The `"qcow2"` branch extracts the rootfs from a cloud disk image using `guestfish tar-out`; this pulls in `libguestfs-with-appliance`, which restricts the entry to `x86_64-linux` because the appliance subpackage is not built for aarch64 in nixpkgs. +Pin a specific dated build directory upstream rather than `latest/` and obtain the SHA256 with `nix-prefetch-url`. + +Reuse the existing `excludePatterns` (which strip container-incompatible systemd units) and `extraDirs` (per-package-manager directories like `var/lib/apt/lists/partial`) as a starting point and trim or extend them based on the first build. + +**3. Add a VM test entry.** +VM tests live under `testFlake/vm-tests/` and iterate over distributions exposed by `nix-vm-test`. +Edit the `distros` attrset in `testFlake/vm-tests/default.nix` to add a key matching the `nix-vm-test` distribution name (`ubuntu`, `debian`, `fedora`, `rocky`). +Each entry takes a `filter` predicate that selects which versions to exercise — use it to skip versions you do not want in the matrix. +If `nix-vm-test` does not yet support the distribution, support must be added there first. -3. Debug any errors that occur. Refer to the FAQ, GitHub issues, or open a discussion for help. +**4. Run the test matrix and triage failures.** +Build the new check attributes via `nix build .#checks.x86_64-linux.container--*` and `vm--*-*` and triage any failures. +Prefer fixing tests to be distribution-agnostic over skipping them. -4. Once stable, the distribution can be added to the `supportedIds` list in the [system-manager module](./nix/modules/default.nix). +**5. Update documentation.** +The user-facing platform statement lives in `docs/site/reference/supported-platforms.md`, with secondary mentions in `README.md`, `docs/site/how-to/install.md`, `docs/site/tutorials/getting-started.md`, and `docs/site/how-to/test-configuration.md`. +Mention the new distribution alongside the existing supported ones. ## Creating an Ad-Hoc Release diff --git a/README.md b/README.md index 23f61bb2..b7194278 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ You can find the [full documentation here](https://system-manager.net/main/). ## Quick Example to Get Started -We will assume you're using a non-NixOS distribution (such as Ubuntu) and you have Nix already installed, with flakes enabled. +We will assume you're using a non-NixOS distribution (such as Ubuntu or Debian) and you have Nix already installed, with flakes enabled. System Manager has an "init" subcommand that can build a set of starting files for you. By default, it places the files in `~/.config/system-manager`. You can run this init subcommand by typing: @@ -214,7 +214,7 @@ Then re-run System Manager and your changes will take effect; now you should hav ## Supported Systems -System Manager is currently only supported on NixOS and Ubuntu. However, it can be used on other distributions by enabling the following: +System Manager is currently supported on NixOS, Ubuntu, and Debian. However, it can be used on other distributions by enabling the following: ```nix { diff --git a/docs/site/how-to/install.md b/docs/site/how-to/install.md index 5e780cb8..12e70750 100644 --- a/docs/site/how-to/install.md +++ b/docs/site/how-to/install.md @@ -4,7 +4,7 @@ To use System Manager, you need: -* **A Linux machine.** We've tested System Manager with Ubuntu both as standalone and under Windows Subsystem for Linux (WSL). +* **A Linux machine.** We've tested System Manager with Ubuntu (both as standalone and under Windows Subsystem for Linux) and Debian 13 (trixie). * **At least 12GB Disk Space.** However, we recommend at least 16GB, as you will be very tight for space with under 16GB. (This is primarily due to Nix; if you're using System Manager to configure, for example, small servers on the Cloud, 8GB simply won't be enough.) * **Nix installed system-wide** with flakes enabled. (System Manager doesn't work with a per-user installation of Nix) diff --git a/docs/site/how-to/test-configuration.md b/docs/site/how-to/test-configuration.md index 67b6dc3b..a267adca 100644 --- a/docs/site/how-to/test-configuration.md +++ b/docs/site/how-to/test-configuration.md @@ -166,7 +166,7 @@ Test "Verify application files" (0.3s) ```python start_all() -# Wait for Ubuntu systemd to be ready +# Wait for the distro's systemd to be ready machine.wait_for_unit("multi-user.target") # Activate system-manager configuration (displays full activation output) @@ -196,7 +196,7 @@ with subtest("Verify service is responding"): The test framework: -1. Builds an Ubuntu 24.04 container image with the nix-installer binary included +1. Builds a container image (Ubuntu 22.04, Ubuntu 24.04, or Debian 13) with the nix-installer binary included 2. Starts the container using systemd-nspawn within the Nix build sandbox 3. Installs Nix via nix-installer at container startup (multi-user mode with daemon) 4. Copies your system-manager profile closure into the container via `nix copy` diff --git a/docs/site/reference/supported-platforms.md b/docs/site/reference/supported-platforms.md index 90bbbc56..aa069a11 100644 --- a/docs/site/reference/supported-platforms.md +++ b/docs/site/reference/supported-platforms.md @@ -9,7 +9,7 @@ System Manager runs on Linux systems that use systemd for service management. | Ubuntu 22.04+ | Tested | Primary development platform | | Ubuntu on WSL2 | Tested | Windows Subsystem for Linux | | NixOS | Tested | Works alongside existing NixOS configuration | -| Debian | Community | Should work; similar to Ubuntu | +| Debian 13 (trixie) | Tested | Container and VM tests | | Fedora | Community | Should work; uses systemd | | Arch Linux | Community | Should work; uses systemd | @@ -29,7 +29,7 @@ System Manager runs on Linux systems that use systemd for service management. ## Platform detection System Manager checks the platform at activation time using a pre-activation assertion that reads `/etc/os-release`. -By default, it only allows activation on Ubuntu and NixOS. +By default, it only allows activation on Ubuntu, Debian, and NixOS. ### Enabling other distributions @@ -44,7 +44,7 @@ To allow System Manager to run on untested distributions, set the `system-manage ``` This disables the OS check entirely. -There is no option to selectively allow specific distributions; the check is either on (default, allowing only Ubuntu and NixOS) or off. +There is no option to selectively allow specific distributions; the check is either on (default, allowing only Ubuntu, Debian, and NixOS) or off. ## Limitations diff --git a/docs/site/tutorials/getting-started.md b/docs/site/tutorials/getting-started.md index 58ea9412..5e466df7 100644 --- a/docs/site/tutorials/getting-started.md +++ b/docs/site/tutorials/getting-started.md @@ -2,7 +2,7 @@ If you've heard of NixOS, you've probably heard that it lets you define your entire system in configuration files and then reproduce that system anywhere with a single command. System Manager brings that same declarative model to other Linux distributions*, with no reinstalling, no switching operating systems, and no special prerequisites beyond having Nix installed. -*Presently, System Manager is only tested on Ubuntu, and is limited to only Linux distributions based on systemd. +*Presently, System Manager is tested on Ubuntu and Debian, and is limited to only Linux distributions based on systemd. # Initializing Your System From 1a897ecf3e9a6350a962ab835c74daff0023de1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 14:08:19 +0200 Subject: [PATCH 05/15] refactor(container-tests): move cloud image pins to images.json Extract the URL + sha256 pairs from lib/container-test-driver/distros.nix into lib/container-test-driver/images.json. This should make it easier to update cloud image versions in the future. --- lib/container-test-driver/distros.nix | 52 ++++++++------------------- lib/container-test-driver/images.json | 32 +++++++++++++++++ 2 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 lib/container-test-driver/images.json diff --git a/lib/container-test-driver/distros.nix b/lib/container-test-driver/distros.nix index da5da6af..5383b694 100644 --- a/lib/container-test-driver/distros.nix +++ b/lib/container-test-driver/distros.nix @@ -4,6 +4,16 @@ }: let makeRootfs = import ./make-rootfs.nix { inherit pkgs system; }; + images = builtins.fromJSON (builtins.readFile ./images.json); + + fetchCloudImg = + distroName: + let + entry = images.${distroName}.${system} or (throw "Unsupported system for ${distroName}: ${system}"); + in + builtins.fetchurl { + inherit (entry) url sha256; + }; ubuntuExcludePatterns = [ "etc/systemd/system/network-online.target.wants/*" @@ -23,25 +33,10 @@ let in { ubuntu-22_04 = { - systems = [ - "x86_64-linux" - "aarch64-linux" - ]; + systems = builtins.attrNames images.ubuntu-22_04; rootfs = makeRootfs.buildRootfs { name = "ubuntu-22_04"; - cloudImg = - if system == "x86_64-linux" then - builtins.fetchurl { - url = "https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-amd64-root.tar.xz"; - sha256 = "05gw1sspv9d4m5yazc8105yc2vr3y9xkwnwilnzn774w9nivwib3"; - } - else if system == "aarch64-linux" then - builtins.fetchurl { - url = "https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-arm64-root.tar.xz"; - sha256 = "1aya4ainn5289bhczbx97dxv7ck8ng3kmz8yiicz8ynvyfg6mvrq"; - } - else - throw "Unsupported system: ${system}"; + cloudImg = fetchCloudImg "ubuntu-22_04"; excludePatterns = ubuntuExcludePatterns; extraDirs = [ "var/lib/apt/lists/partial" ]; }; @@ -49,25 +44,10 @@ in }; ubuntu-24_04 = { - systems = [ - "x86_64-linux" - "aarch64-linux" - ]; + systems = builtins.attrNames images.ubuntu-24_04; rootfs = makeRootfs.buildRootfs { name = "ubuntu-24_04"; - cloudImg = - if system == "x86_64-linux" then - builtins.fetchurl { - url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz"; - sha256 = "0y3d55f5qy7bxm3mfmnxzpmwp88d7iiszc57z5b9npc6xgwi28np"; - } - else if system == "aarch64-linux" then - builtins.fetchurl { - url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz"; - sha256 = "1l4l0llfffspzgnmwhax0fcnjn8ih8n4azhfaghng2hh1xvr4a17"; - } - else - throw "Unsupported system: ${system}"; + cloudImg = fetchCloudImg "ubuntu-24_04"; excludePatterns = ubuntuExcludePatterns; extraDirs = [ "var/lib/apt/lists/partial" ]; }; @@ -75,9 +55,7 @@ in }; debian-13 = { - # x86_64-linux only: the qcow2 extraction path uses libguestfs-with-appliance, - # whose appliance subpackage is not available on aarch64 in nixpkgs. - systems = [ "x86_64-linux" ]; + systems = builtins.attrNames images.debian-13; rootfs = makeRootfs.buildRootfs { name = "debian-13"; cloudImgFormat = "qcow2"; diff --git a/lib/container-test-driver/images.json b/lib/container-test-driver/images.json new file mode 100644 index 00000000..0c45f411 --- /dev/null +++ b/lib/container-test-driver/images.json @@ -0,0 +1,32 @@ +{ + "debian-13": { + "aarch64-linux": { + "sha256": "02jhbgc97kapbdnk8ny6ql12g9xvi34jpirp5kxzgcz1b5wabwg4", + "url": "https://cloud.debian.org/images/cloud/trixie/20260413-2447/debian-13-genericcloud-arm64-20260413-2447.tar.xz" + }, + "x86_64-linux": { + "sha256": "049a06pb4bqpmy6qp3jgzblrwim9i492153mcglm53jngh48r5p5", + "url": "https://cloud.debian.org/images/cloud/trixie/20260413-2447/debian-13-genericcloud-amd64-20260413-2447.tar.xz" + } + }, + "ubuntu-22_04": { + "aarch64-linux": { + "sha256": "1aya4ainn5289bhczbx97dxv7ck8ng3kmz8yiicz8ynvyfg6mvrq", + "url": "https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-arm64-root.tar.xz" + }, + "x86_64-linux": { + "sha256": "05gw1sspv9d4m5yazc8105yc2vr3y9xkwnwilnzn774w9nivwib3", + "url": "https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-amd64-root.tar.xz" + } + }, + "ubuntu-24_04": { + "aarch64-linux": { + "sha256": "1l4l0llfffspzgnmwhax0fcnjn8ih8n4azhfaghng2hh1xvr4a17", + "url": "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz" + }, + "x86_64-linux": { + "sha256": "0y3d55f5qy7bxm3mfmnxzpmwp88d7iiszc57z5b9npc6xgwi28np", + "url": "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz" + } + } +} From d9d408cf13609cb559a3dfc65f02452a5f991525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 14:08:19 +0200 Subject: [PATCH 06/15] feat(container-tests): add disk-tarball cloud image support for Debian Add a new cloudImgFormat = "disk-tarball" path to buildRootfs that unpacks Debian's *-genericcloud-*.tar.xz, extracts the largest GPT partition with sfdisk + dd, and dumps its ext4 contents. All required tools are cross-architecture in nixpkgs, unlike the previous qcow2 path that depended on libguestfs-with-appliance (which is x86_64 only). Switch debian-13 over to the new format and add aarch64-linux to its images.json entry. The qcow2 branch stays in place for future distros that only publish qcow2. --- CONTRIBUTING.md | 11 ++++--- lib/container-test-driver/distros.nix | 7 ++-- lib/container-test-driver/make-rootfs.nix | 40 +++++++++++++++++++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 160406b8..b82921b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,10 +76,13 @@ Adding a new entry there causes all existing tests to automatically generate a ` The entry must supply `systems`, a `rootfs` derivation built by `lib.container-test-driver.make-rootfs.buildRootfs`, and a `maskableService` (a systemd unit that test scripts may mask, typically `unattended-upgrades.service` or equivalent). -`buildRootfs` accepts two upstream image formats via `cloudImgFormat`. -The default `"tar"` consumes a flat root tarball such as Ubuntu's `*-server-cloudimg-amd64-root.tar.xz` and is the simplest path. -The `"qcow2"` branch extracts the rootfs from a cloud disk image using `guestfish tar-out`; this pulls in `libguestfs-with-appliance`, which restricts the entry to `x86_64-linux` because the appliance subpackage is not built for aarch64 in nixpkgs. -Pin a specific dated build directory upstream rather than `latest/` and obtain the SHA256 with `nix-prefetch-url`. +`buildRootfs` accepts three upstream image formats via `cloudImgFormat`, and the right choice depends on what the distribution publishes: + +- `"tar"` (default) consumes a flat rootfs tarball such as Ubuntu's `*-server-cloudimg-amd64-root.tar.xz`. This is the simplest path, has no architecture restrictions, and should be preferred whenever the distribution ships a rootfs tarball. +- `"disk-tarball"` consumes a `.tar.xz` that wraps a raw disk image, such as Debian's `*-genericcloud-*.tar.xz`. It unpacks the outer tarball, locates the root partition with `sfdisk -J` + `jq`, extracts it with `dd`, and dumps the ext4 filesystem into `$out` via `debugfs -R "rdump / $out"`. All required tools (`util-linux`, `e2fsprogs`, `jq`) are cross-architecture in nixpkgs, so this works on both `x86_64-linux` and `aarch64-linux`. `excludePatterns` are applied as a post-extraction prune pass rather than as tar `--exclude` flags. Note: this currently assumes the root filesystem is ext4; a btrfs-backed rootfs (such as Fedora Workstation) would need a `btrfs restore`-based variant added alongside. +- `"qcow2"` extracts the rootfs from a qcow2 cloud disk image using `guestfish tar-out`. It pulls in `libguestfs-with-appliance`, whose `libguestfs-appliance` subpackage is marked `meta.platforms = [ "i686-linux" "x86_64-linux" ]` in nixpkgs, so entries using this format must restrict `systems` to `x86_64-linux`. Use only as a last resort, when the distribution publishes neither a rootfs tarball nor a disk-in-tarball variant. + +Pin a specific dated build directory upstream rather than `latest/` and obtain the SHA256 with `nix-prefetch-url`. URL and hash go in `lib/container-test-driver/images.json`; `distros.nix` reads them automatically. Reuse the existing `excludePatterns` (which strip container-incompatible systemd units) and `extraDirs` (per-package-manager directories like `var/lib/apt/lists/partial`) as a starting point and trim or extend them based on the first build. diff --git a/lib/container-test-driver/distros.nix b/lib/container-test-driver/distros.nix index 5383b694..221bad60 100644 --- a/lib/container-test-driver/distros.nix +++ b/lib/container-test-driver/distros.nix @@ -58,11 +58,8 @@ in systems = builtins.attrNames images.debian-13; rootfs = makeRootfs.buildRootfs { name = "debian-13"; - cloudImgFormat = "qcow2"; - cloudImg = builtins.fetchurl { - url = "https://cloud.debian.org/images/cloud/trixie/20260413-2447/debian-13-genericcloud-amd64-20260413-2447.qcow2"; - sha256 = "0iclqh20da7mm1ijhks66iqy2dz1shpipia1zwpgxpya1c4gg182"; - }; + cloudImgFormat = "disk-tarball"; + cloudImg = fetchCloudImg "debian-13"; excludePatterns = ubuntuExcludePatterns; extraDirs = [ "var/lib/apt/lists/partial" ]; }; diff --git a/lib/container-test-driver/make-rootfs.nix b/lib/container-test-driver/make-rootfs.nix index 666afd15..6e2a89ed 100644 --- a/lib/container-test-driver/make-rootfs.nix +++ b/lib/container-test-driver/make-rootfs.nix @@ -22,6 +22,9 @@ in map (p: "--exclude='${p}'") excludePatterns ); mkdirCommands = builtins.concatStringsSep "\n " (map (d: "mkdir -p $out/${d}") extraDirs); + excludePruneCommands = builtins.concatStringsSep "\n " ( + map (p: "rm -rf $out/${p}") excludePatterns + ); extractCommand = if cloudImgFormat == "tar" then '' @@ -38,12 +41,45 @@ in ${excludeArgs} \ -C $out -x '' + else if cloudImgFormat == "disk-tarball" then + '' + set -euo pipefail + + workdir=$(mktemp -d) + tar -C "$workdir" -xf ${cloudImg} + rawimg=$(ls "$workdir"/*.raw | head -n1) + if [ -z "$rawimg" ]; then + echo "disk-tarball: no *.raw file inside ${cloudImg}" >&2 + exit 1 + fi + + # Pick the largest partition + read -r start size <<<"$(sfdisk -J "$rawimg" \ + | jq -r '.partitiontable.partitions | max_by(.size) | "\(.start) \(.size)"')" + dd if="$rawimg" of="$workdir/root.ext4" \ + bs=512 skip="$start" count="$size" status=none + + debugfs -R "rdump / $out" "$workdir/root.ext4" >/dev/null 2>&1 + + # debugfs rdump has no --exclude, so apply excludePatterns via a + # post-extraction prune pass. Also strip /dev/* to match the tar + # path (which uses tar --exclude='dev/*'). + rm -rf $out/dev/* + ${excludePruneCommands} + + rm -rf "$workdir" + '' else - throw "buildRootfs: unsupported cloudImgFormat '${cloudImgFormat}' (expected 'tar' or 'qcow2')"; + throw "buildRootfs: unsupported cloudImgFormat '${cloudImgFormat}' (expected 'tar', 'qcow2', or 'disk-tarball')"; nativeBuildInputs = [ pkgs.xz ] - ++ pkgs.lib.optionals (cloudImgFormat == "qcow2") [ pkgs.libguestfs-with-appliance ]; + ++ pkgs.lib.optionals (cloudImgFormat == "qcow2") [ pkgs.libguestfs-with-appliance ] + ++ pkgs.lib.optionals (cloudImgFormat == "disk-tarball") [ + pkgs.util-linux + pkgs.e2fsprogs + pkgs.jq + ]; in pkgs.runCommand "rootfs-${name}" { From 5dad65df79f104dd291f6fd0c87d3da956fbc92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 14:08:19 +0200 Subject: [PATCH 07/15] feat(container-tests): add script to refresh cloud image URLs and hashes The update script is modeled on numtide/nix-vm-test's scripts/update-images.py but adapted to our needs: Ubuntu *-root.tar.xz rootfs tarballs instead of qcow2 disk images, Debian genericcloud instead of generic. --- tools/update-images.py | 140 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 tools/update-images.py diff --git a/tools/update-images.py b/tools/update-images.py new file mode 100755 index 00000000..63f71e7f --- /dev/null +++ b/tools/update-images.py @@ -0,0 +1,140 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i python3 -p python3 python3Packages.beautifulsoup4 python3Packages.requests nix-prefetch + +import json +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +import requests +from bs4 import BeautifulSoup + +IMAGES_JSON = ( + Path(__file__).resolve().parent.parent + / "lib" + / "container-test-driver" + / "images.json" +) + +# distro -> upstream index URL (must end with "/") +UBUNTU_RELEASES = { + "ubuntu-22_04": "https://cloud-images.ubuntu.com/releases/jammy/", + "ubuntu-24_04": "https://cloud-images.ubuntu.com/releases/noble/", +} + +DEBIAN_RELEASES = { + "debian-13": "https://cloud.debian.org/images/cloud/trixie/", +} + + +def nix_hash(url: str) -> str: + print(f"[+] nix-prefetch-url {url}", file=sys.stderr) + result = subprocess.run( + ["nix-prefetch-url", "--type", "sha256", url], + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + + +def latest_dated_subdir(index_url: str, pattern: re.Pattern) -> str: + """Return the lexically newest dated subdirectory under index_url that matches pattern.""" + page = requests.get(index_url, timeout=30) + page.raise_for_status() + soup = BeautifulSoup(page.content, "html.parser") + candidates = [] + for link in soup.find_all("a"): + href = link.get("href", "") + m = pattern.match(href) + if m: + candidates.append((m.group("date"), href)) + if not candidates: + raise RuntimeError(f"no dated subdirectories matched at {index_url}") + candidates.sort(key=lambda kv: datetime.strptime(kv[0][:8], "%Y%m%d")) + return candidates[-1][1] + + +def ubuntu_rootfs(release: str, base_url: str) -> dict: + """Return { system: { url, sha256 } } for the latest release-* build under base_url.""" + pattern = re.compile(r"^release-(?P\d{8})/$") + latest = latest_dated_subdir(base_url, pattern) + build_url = f"{base_url}{latest}" + print(f"[+] {release}: {build_url}", file=sys.stderr) + + page = requests.get(build_url, timeout=30) + page.raise_for_status() + soup = BeautifulSoup(page.content, "html.parser") + + # Filenames look like ubuntu-22.04-server-cloudimg-amd64-root.tar.xz + rootfs_re = re.compile( + r"^.*-server-cloudimg-(?Pamd64|arm64)-root\.tar\.xz$" + ) + arch_to_system = {"amd64": "x86_64-linux", "arm64": "aarch64-linux"} + + out = {} + for link in soup.find_all("a"): + href = link.get("href", "") + m = rootfs_re.match(href) + if not m: + continue + system = arch_to_system[m.group("arch")] + url = f"{build_url}{href}" + out[system] = {"url": url, "sha256": nix_hash(url)} + if not out: + raise RuntimeError(f"no rootfs tarballs found under {build_url}") + return out + + +def debian_genericcloud(release: str, base_url: str) -> dict: + """Return { system: { url, sha256 } } for the latest dated debian build.""" + pattern = re.compile(r"^(?P\d{8}-\d{4})/$") + latest = latest_dated_subdir(base_url, pattern) + build_url = f"{base_url}{latest}" + print(f"[+] {release}: {build_url}", file=sys.stderr) + + page = requests.get(build_url, timeout=30) + page.raise_for_status() + soup = BeautifulSoup(page.content, "html.parser") + + # Filenames look like debian-13-genericcloud-amd64-20260413-2447.tar.xz + tarball_re = re.compile( + r"^.*-genericcloud-(?Pamd64|arm64)-\d{8}-\d{4}\.tar\.xz$" + ) + arch_to_system = {"amd64": "x86_64-linux", "arm64": "aarch64-linux"} + + out: dict[str, dict] = {} + seen_urls: set[str] = set() + for link in soup.find_all("a"): + href = link.get("href", "") + m = tarball_re.match(href) + if not m: + continue + url = f"{build_url}{href}" + if url in seen_urls: + continue + seen_urls.add(url) + system = arch_to_system[m.group("arch")] + out[system] = {"url": url, "sha256": nix_hash(url)} + if not out: + raise RuntimeError(f"no genericcloud tarballs found under {build_url}") + return out + + +def main() -> None: + images: dict[str, dict] = {} + + for release, url in UBUNTU_RELEASES.items(): + images[release] = ubuntu_rootfs(release, url) + + for release, url in DEBIAN_RELEASES.items(): + images[release] = debian_genericcloud(release, url) + + IMAGES_JSON.write_text(json.dumps(images, indent=2, sort_keys=True) + "\n") + print(f"[+] wrote {IMAGES_JSON}", file=sys.stderr) + + +if __name__ == "__main__": + main() From 3d5a7f12a32beb3e482ace5d9edbacf47f7f251f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Wed, 15 Apr 2026 22:00:39 +0200 Subject: [PATCH 08/15] feat(container-tests): update container image --- lib/container-test-driver/images.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/container-test-driver/images.json b/lib/container-test-driver/images.json index 0c45f411..b98b7837 100644 --- a/lib/container-test-driver/images.json +++ b/lib/container-test-driver/images.json @@ -11,22 +11,22 @@ }, "ubuntu-22_04": { "aarch64-linux": { - "sha256": "1aya4ainn5289bhczbx97dxv7ck8ng3kmz8yiicz8ynvyfg6mvrq", - "url": "https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-arm64-root.tar.xz" + "sha256": "0fg2jv8wi5cqlw0sl07xi3jmyyk1056pzybfqgpxs3nx7crhajlf", + "url": "https://cloud-images.ubuntu.com/releases/jammy/release-20260320/ubuntu-22.04-server-cloudimg-arm64-root.tar.xz" }, "x86_64-linux": { - "sha256": "05gw1sspv9d4m5yazc8105yc2vr3y9xkwnwilnzn774w9nivwib3", - "url": "https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-amd64-root.tar.xz" + "sha256": "1dafwd805bh4c243g7p16jixb52amh09p23ibx1lyj4k50d5yrad", + "url": "https://cloud-images.ubuntu.com/releases/jammy/release-20260320/ubuntu-22.04-server-cloudimg-amd64-root.tar.xz" } }, "ubuntu-24_04": { "aarch64-linux": { - "sha256": "1l4l0llfffspzgnmwhax0fcnjn8ih8n4azhfaghng2hh1xvr4a17", - "url": "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz" + "sha256": "07vjiwx0smm6hyahi8w9y54zyxbpczfaqh75ihvi4msmmhbwb0m6", + "url": "https://cloud-images.ubuntu.com/releases/noble/release-20260321/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz" }, "x86_64-linux": { - "sha256": "0y3d55f5qy7bxm3mfmnxzpmwp88d7iiszc57z5b9npc6xgwi28np", - "url": "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz" + "sha256": "0xyp9pin8m6ys0gxk6spq83dgdfl3malbvps7v78hrp7sjg3rp0k", + "url": "https://cloud-images.ubuntu.com/releases/noble/release-20260321/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz" } } } From fe3d0a6f8433e46532b85860af43b2db475b2269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Wed, 15 Apr 2026 22:56:48 +0200 Subject: [PATCH 09/15] ci: weekly workflow to refresh container test cloud image pins Run tools/update-images.py on a Monday-morning cron and open a PR with any URL/hash diffs, mirroring update-nix-version.yml. --- .github/workflows/update-images.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/update-images.yml diff --git a/.github/workflows/update-images.yml b/.github/workflows/update-images.yml new file mode 100644 index 00000000..1057c49c --- /dev/null +++ b/.github/workflows/update-images.yml @@ -0,0 +1,28 @@ +name: update-images +on: + workflow_dispatch: + schedule: + - cron: "0 5 * * 1" # runs weekly every Monday at 05:00 UTC + +jobs: + bump-images: + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repo" + uses: "actions/checkout@v6" + - name: "Install the Nix package manager" + uses: "cachix/install-nix-action@master" + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - name: "Refresh container test cloud image pins" + run: tools/update-images.py + - name: "Create Pull Request" + uses: "peter-evans/create-pull-request@v8" + with: + branch: "auto_update_images" + title: "Bump container test cloud image pins" + body: | + Automatically refreshed the cloud image URLs and sha256 hashes in + `lib/container-test-driver/images.json` by running + `tools/update-images.py`. + commit-message: "chore(container-tests): bump cloud image pins" From 6d5f2adac8443983e594eb5f0ec5880f14768b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 14:23:24 +0200 Subject: [PATCH 10/15] feat: test on Fedora 43 in addition to Ubuntu and Debian --- lib/container-test-driver/distros.nix | 34 +++++++++++++++++++++++++++ nix/modules/default.nix | 1 + testFlake/vm-tests/default.nix | 4 ++++ 3 files changed, 39 insertions(+) diff --git a/lib/container-test-driver/distros.nix b/lib/container-test-driver/distros.nix index 221bad60..3719dd4d 100644 --- a/lib/container-test-driver/distros.nix +++ b/lib/container-test-driver/distros.nix @@ -54,6 +54,40 @@ in maskableService = "unattended-upgrades.service"; }; + fedora-43 = { + # x86_64-linux only: the qcow2 extraction path uses libguestfs-with-appliance, + # whose appliance subpackage is not available on aarch64 in nixpkgs. + systems = [ "x86_64-linux" ]; + rootfs = makeRootfs.buildRootfs { + name = "fedora-43"; + cloudImgFormat = "qcow2"; + cloudImg = builtins.fetchurl { + url = "https://dl.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2"; + sha256 = "0bxbr2kf6ija4rg36mnspg5qbk0767bn44133zfdilkwm7478rc4"; + }; + excludePatterns = ubuntuExcludePatterns ++ [ + # systemd-firstboot blocks boot waiting for interactive configuration + "usr/lib/systemd/system/systemd-firstboot.service" + # auditd/audit-rules fail inside nspawn (no audit netlink) + "usr/lib/systemd/system/auditd.service" + "usr/lib/systemd/system/audit-rules.service" + # NetworkManager-wait-online blocks multi-user.target inside nspawn + "usr/lib/systemd/system/NetworkManager-wait-online.service" + ]; + extraDirs = [ + "var/cache/dnf" + "var/lib/dnf" + ]; + # Pre-seed firstboot markers so systemd considers the container already configured. + extraSetup = '' + : > $out/etc/machine-id + echo 'LANG=C.UTF-8' > $out/etc/locale.conf + echo 'KEYMAP=us' > $out/etc/vconsole.conf + ''; + }; + maskableService = "dnf-automatic.timer"; + }; + debian-13 = { systems = builtins.attrNames images.debian-13; rootfs = makeRootfs.buildRootfs { diff --git a/nix/modules/default.nix b/nix/modules/default.nix index 3a52462a..740fc190 100644 --- a/nix/modules/default.nix +++ b/nix/modules/default.nix @@ -205,6 +205,7 @@ "nixos" "ubuntu" "debian" + "fedora" ]; in { diff --git a/testFlake/vm-tests/default.nix b/testFlake/vm-tests/default.nix index e8458f19..a4f2dddb 100644 --- a/testFlake/vm-tests/default.nix +++ b/testFlake/vm-tests/default.nix @@ -16,6 +16,10 @@ let # Only Debian 13 (trixie) filter = v: v == "13"; }; + fedora = { + # Only Fedora 41 + filter = v: v == "41"; + }; }; forEachImage = From 124edfb31170a71ec0889326945e827d83178899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 14:23:46 +0200 Subject: [PATCH 11/15] docs: mention Fedora 43 as supported-distro --- README.md | 4 ++-- docs/site/how-to/install.md | 2 +- docs/site/how-to/test-configuration.md | 2 +- docs/site/reference/supported-platforms.md | 6 +++--- docs/site/tutorials/getting-started.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b7194278..3a315797 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ You can find the [full documentation here](https://system-manager.net/main/). ## Quick Example to Get Started -We will assume you're using a non-NixOS distribution (such as Ubuntu or Debian) and you have Nix already installed, with flakes enabled. +We will assume you're using a non-NixOS distribution (such as Ubuntu, Debian, or Fedora) and you have Nix already installed, with flakes enabled. System Manager has an "init" subcommand that can build a set of starting files for you. By default, it places the files in `~/.config/system-manager`. You can run this init subcommand by typing: @@ -214,7 +214,7 @@ Then re-run System Manager and your changes will take effect; now you should hav ## Supported Systems -System Manager is currently supported on NixOS, Ubuntu, and Debian. However, it can be used on other distributions by enabling the following: +System Manager is currently supported on NixOS, Ubuntu, Debian, and Fedora. However, it can be used on other distributions by enabling the following: ```nix { diff --git a/docs/site/how-to/install.md b/docs/site/how-to/install.md index 12e70750..4cc72ea1 100644 --- a/docs/site/how-to/install.md +++ b/docs/site/how-to/install.md @@ -4,7 +4,7 @@ To use System Manager, you need: -* **A Linux machine.** We've tested System Manager with Ubuntu (both as standalone and under Windows Subsystem for Linux) and Debian 13 (trixie). +* **A Linux machine.** We've tested System Manager with Ubuntu (both as standalone and under Windows Subsystem for Linux) Debian 13 (trixie), and Fedora 43. * **At least 12GB Disk Space.** However, we recommend at least 16GB, as you will be very tight for space with under 16GB. (This is primarily due to Nix; if you're using System Manager to configure, for example, small servers on the Cloud, 8GB simply won't be enough.) * **Nix installed system-wide** with flakes enabled. (System Manager doesn't work with a per-user installation of Nix) diff --git a/docs/site/how-to/test-configuration.md b/docs/site/how-to/test-configuration.md index a267adca..643e63e8 100644 --- a/docs/site/how-to/test-configuration.md +++ b/docs/site/how-to/test-configuration.md @@ -196,7 +196,7 @@ with subtest("Verify service is responding"): The test framework: -1. Builds a container image (Ubuntu 22.04, Ubuntu 24.04, or Debian 13) with the nix-installer binary included +1. Builds a container image (Ubuntu 22.04, Ubuntu 24.04, Debian 13, or Fedora 43) with the nix-installer binary included 2. Starts the container using systemd-nspawn within the Nix build sandbox 3. Installs Nix via nix-installer at container startup (multi-user mode with daemon) 4. Copies your system-manager profile closure into the container via `nix copy` diff --git a/docs/site/reference/supported-platforms.md b/docs/site/reference/supported-platforms.md index aa069a11..c68dba91 100644 --- a/docs/site/reference/supported-platforms.md +++ b/docs/site/reference/supported-platforms.md @@ -10,7 +10,7 @@ System Manager runs on Linux systems that use systemd for service management. | Ubuntu on WSL2 | Tested | Windows Subsystem for Linux | | NixOS | Tested | Works alongside existing NixOS configuration | | Debian 13 (trixie) | Tested | Container and VM tests | -| Fedora | Community | Should work; uses systemd | +| Fedora 43 | Tested | Container and VM tests without SELinux support | | Arch Linux | Community | Should work; uses systemd | ## Requirements @@ -29,7 +29,7 @@ System Manager runs on Linux systems that use systemd for service management. ## Platform detection System Manager checks the platform at activation time using a pre-activation assertion that reads `/etc/os-release`. -By default, it only allows activation on Ubuntu, Debian, and NixOS. +By default, it only allows activation on Ubuntu, Debian, Fedora, and NixOS. ### Enabling other distributions @@ -44,7 +44,7 @@ To allow System Manager to run on untested distributions, set the `system-manage ``` This disables the OS check entirely. -There is no option to selectively allow specific distributions; the check is either on (default, allowing only Ubuntu, Debian, and NixOS) or off. +There is no option to selectively allow specific distributions; the check is either on (default, allowing only Ubuntu, Debian, Fedora, and NixOS) or off. ## Limitations diff --git a/docs/site/tutorials/getting-started.md b/docs/site/tutorials/getting-started.md index 5e466df7..97021e5c 100644 --- a/docs/site/tutorials/getting-started.md +++ b/docs/site/tutorials/getting-started.md @@ -2,7 +2,7 @@ If you've heard of NixOS, you've probably heard that it lets you define your entire system in configuration files and then reproduce that system anywhere with a single command. System Manager brings that same declarative model to other Linux distributions*, with no reinstalling, no switching operating systems, and no special prerequisites beyond having Nix installed. -*Presently, System Manager is tested on Ubuntu and Debian, and is limited to only Linux distributions based on systemd. +*Presently, System Manager is tested on Ubuntu, Debian, and Fedora, and is limited to only Linux distributions based on systemd. # Initializing Your System From 00a41ab653316cb5e14d52d78a4cd2a75093a8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 14:50:56 +0200 Subject: [PATCH 12/15] feat(vm-tests): exercise fedora 43 via nix-vm-test fedora-43 branch --- testFlake/flake.lock | 7 ++++--- testFlake/flake.nix | 2 +- testFlake/vm-tests/default.nix | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/testFlake/flake.lock b/testFlake/flake.lock index f369a18f..56db65d5 100644 --- a/testFlake/flake.lock +++ b/testFlake/flake.lock @@ -129,15 +129,16 @@ ] }, "locked": { - "lastModified": 1776165142, - "narHash": "sha256-bmPLrDLMb2QjGhWzDHvahyq4nuoSoYEMsgmo+qHiq1k=", + "lastModified": 1776289539, + "narHash": "sha256-Opze91tWTXeE+5hzChVZ9K5NMPt39uO+jfAOHn3DRJs=", "owner": "numtide", "repo": "nix-vm-test", - "rev": "f88465397ca010133084f07cb6f73875223c449d", + "rev": "dfa6ba3d23d79add9dcca93f6b7f7e8199214897", "type": "github" }, "original": { "owner": "numtide", + "ref": "feat/fedora-43", "repo": "nix-vm-test", "type": "github" } diff --git a/testFlake/flake.nix b/testFlake/flake.nix index d86837d8..39573a8a 100644 --- a/testFlake/flake.nix +++ b/testFlake/flake.nix @@ -11,7 +11,7 @@ system-manager-v1-1-0.url = "github:numtide/system-manager/v1.1.0"; nixpkgs.follows = "system-manager/nixpkgs"; nix-vm-test = { - url = "github:numtide/nix-vm-test"; + url = "github:numtide/nix-vm-test/feat/fedora-43"; inputs.nixpkgs.follows = "nixpkgs"; }; sops-nix.url = "github:Mic92/sops-nix"; diff --git a/testFlake/vm-tests/default.nix b/testFlake/vm-tests/default.nix index a4f2dddb..c01dd979 100644 --- a/testFlake/vm-tests/default.nix +++ b/testFlake/vm-tests/default.nix @@ -17,8 +17,8 @@ let filter = v: v == "13"; }; fedora = { - # Only Fedora 41 - filter = v: v == "41"; + # Only Fedora 43 + filter = v: v == "43"; }; }; From 1826a6d2d0f20a88f05dd297435557f864940f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 16:54:26 +0200 Subject: [PATCH 13/15] feat(users-groups): declare keys group so sops-install-secrets finds it on fedora --- nix/modules/upstream/nixpkgs/users-groups.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/modules/upstream/nixpkgs/users-groups.nix b/nix/modules/upstream/nixpkgs/users-groups.nix index dcc16d91..fee924f1 100644 --- a/nix/modules/upstream/nixpkgs/users-groups.nix +++ b/nix/modules/upstream/nixpkgs/users-groups.nix @@ -887,6 +887,7 @@ in video.gid = lib.mkDefault 44; dialout.gid = lib.mkDefault 20; nogroup.gid = lib.mkDefault 65534; + keys.gid = lib.mkDefault 96; users.gid = lib.mkDefault 100; nixbld.gid = lib.mkDefault ids.gids.nixbld; utmp.gid = lib.mkDefault 43; From d6ecf1c1007d1a47fc2be89c08bb77a1629830c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 16:54:26 +0200 Subject: [PATCH 14/15] feat(users-groups): make default group gids distro-aware via new targetDistro option --- nix/modules/default.nix | 19 ++ nix/modules/upstream/nixpkgs/users-groups.nix | 84 ++++---- testFlake/vm-tests/default.nix | 180 +++++++++--------- testFlake/vm-tests/example.nix | 16 +- testFlake/vm-tests/prepopulate.nix | 9 +- testFlake/vm-tests/security-wrappers.nix | 2 +- testFlake/vm-tests/sudo-module.nix | 2 +- testFlake/vm-tests/sudo.nix | 2 +- testFlake/vm-tests/target-host.nix | 2 +- 9 files changed, 186 insertions(+), 130 deletions(-) diff --git a/nix/modules/default.nix b/nix/modules/default.nix index 740fc190..fe4cd443 100644 --- a/nix/modules/default.nix +++ b/nix/modules/default.nix @@ -131,6 +131,25 @@ system-manager = { allowAnyDistro = lib.mkEnableOption "the usage of system-manager on untested distributions"; + targetDistro = lib.mkOption { + type = + with lib.types; + nullOr (enum [ + "ubuntu" + "debian" + "fedora" + "nixos" + ]); + default = null; + description = '' + The Linux distribution system-manager is being activated on. + When set, default users/groups are adjusted to match the + distribution's expectations (e.g. fedora ships `nobody` at + GID 65534 instead of `nogroup`, and `wheel` at GID 10 instead + of `uucp`). Leave `null` to apply the Debian/Ubuntu defaults. + ''; + }; + preActivationAssertions = lib.mkOption { type = with lib.types; diff --git a/nix/modules/upstream/nixpkgs/users-groups.nix b/nix/modules/upstream/nixpkgs/users-groups.nix index fee924f1..afc01246 100644 --- a/nix/modules/upstream/nixpkgs/users-groups.nix +++ b/nix/modules/upstream/nixpkgs/users-groups.nix @@ -862,42 +862,62 @@ in uid = ids.uids.nobody; isSystemUser = true; description = "Unprivileged account (don't use!)"; - group = "nogroup"; + # Fedora ships gid 65534 as `nobody`, Debian/Ubuntu ship it as + # `nogroup`. Both names must work on both distros, but we set + # the primary group to match what the distro already has so + # userborn does not try to rename an existing group. + group = if config.system-manager.targetDistro == "fedora" then "nobody" else "nogroup"; }; }; - # GIDs are set to match Debian/Ubuntu defaults to avoid conflicts. - # NixOS uses different GIDs which conflict with existing system groups. + # GIDs are set to match the target distribution's defaults so + # userborn's mutable-users mode doesn't try to create groups that + # already exist at those GIDs under a different name. # - # To add a user to a group via extraGroups, the group must be declared here. - # For pre-existing system groups, declare them with the matching GID. - users.groups = { - root.gid = lib.mkDefault 0; - wheel.gid = lib.mkDefault 900; - sudo.gid = lib.mkDefault 27; - disk.gid = lib.mkDefault 6; - kmem.gid = lib.mkDefault 15; - tty.gid = lib.mkDefault 5; - floppy.gid = lib.mkDefault 25; - uucp.gid = lib.mkDefault 10; - lp.gid = lib.mkDefault 7; - cdrom.gid = lib.mkDefault 24; - tape.gid = lib.mkDefault 26; - audio.gid = lib.mkDefault 29; - video.gid = lib.mkDefault 44; - dialout.gid = lib.mkDefault 20; - nogroup.gid = lib.mkDefault 65534; - keys.gid = lib.mkDefault 96; - users.gid = lib.mkDefault 100; - nixbld.gid = lib.mkDefault ids.gids.nixbld; - utmp.gid = lib.mkDefault 43; - adm.gid = lib.mkDefault 4; - input.gid = lib.mkDefault 996; - kvm.gid = lib.mkDefault 994; - render.gid = lib.mkDefault 993; - sgx.gid = lib.mkDefault 995; - shadow.gid = lib.mkDefault 42; - }; + # To add a user to a group via extraGroups, the group must be + # declared here. For pre-existing system groups, declare them + # with the matching GID. + users.groups = + let + onFedora = config.system-manager.targetDistro == "fedora"; + in + { + root.gid = lib.mkDefault 0; + sudo.gid = lib.mkDefault 27; + disk.gid = lib.mkDefault 6; + tty.gid = lib.mkDefault 5; + lp.gid = lib.mkDefault 7; + keys.gid = lib.mkDefault 96; + users.gid = lib.mkDefault 100; + nixbld.gid = lib.mkDefault ids.gids.nixbld; + adm.gid = lib.mkDefault 4; + shadow.gid = lib.mkDefault 42; + + # Groups whose GID differs between Debian/Ubuntu and Fedora. + # Prefer the target distribution's numbering so mutable-mode + # userborn finds the group by name without GID collisions. + wheel.gid = lib.mkDefault (if onFedora then 10 else 900); + kmem.gid = lib.mkDefault (if onFedora then 9 else 15); + floppy.gid = lib.mkDefault (if onFedora then 19 else 25); + cdrom.gid = lib.mkDefault (if onFedora then 11 else 24); + tape.gid = lib.mkDefault (if onFedora then 33 else 26); + audio.gid = lib.mkDefault (if onFedora then 63 else 29); + video.gid = lib.mkDefault (if onFedora then 39 else 44); + dialout.gid = lib.mkDefault (if onFedora then 18 else 20); + utmp.gid = lib.mkDefault (if onFedora then 22 else 43); + } + // lib.optionalAttrs (!onFedora) { + # Debian/Ubuntu ship `nogroup` at 65534 and `uucp` at 10. + # Fedora ships `nobody` at 65534 and `wheel` at 10; declaring + # these here would race userborn against the existing groups. + nogroup.gid = lib.mkDefault 65534; + uucp.gid = lib.mkDefault 10; + } + // lib.optionalAttrs onFedora { + # Fedora's `nobody` group occupies 65534; declare it so the + # `nobody` user above can reference it by name. + nobody.gid = lib.mkDefault 65534; + }; systemd.services.linger-users = lib.mkIf ((length lingeringUsers) > 0) { wantedBy = [ "multi-user.target" ]; diff --git a/testFlake/vm-tests/default.nix b/testFlake/vm-tests/default.nix index c01dd979..3089ebf5 100644 --- a/testFlake/vm-tests/default.nix +++ b/testFlake/vm-tests/default.nix @@ -27,27 +27,30 @@ let { modules, testScriptFunction, - extraPathsToRegister ? [ ], + extraPathsToRegister ? (_distroName: [ ]), projectTest ? test: test.sandboxed, }: let - mkToplevel = system-manager.lib.makeSystemConfig { - modules = modules ++ [ - ( - { lib, pkgs, ... }: - { - options.hostPkgs = lib.mkOption { - type = lib.types.raw; - readOnly = true; - }; - config = { - nixpkgs.hostPlatform = system; - hostPkgs = pkgs; - }; - } - ) - ]; - }; + mkToplevel = + distroName: + system-manager.lib.makeSystemConfig { + modules = modules ++ [ + ( + { lib, pkgs, ... }: + { + options.hostPkgs = lib.mkOption { + type = lib.types.raw; + readOnly = true; + }; + config = { + nixpkgs.hostPlatform = system; + hostPkgs = pkgs; + system-manager.targetDistro = distroName; + }; + } + ) + ]; + }; mkTestForDistro = distroName: distroConfig: let @@ -58,14 +61,14 @@ let map ( imageVersion: let - toplevel = mkToplevel; + toplevel = mkToplevel distroName; inherit (toplevel.config) hostPkgs; in lib.nameValuePair "vm-${distroName}-${imageVersion}-${name}" ( projectTest ( distro.${imageVersion} { - testScript = testScriptFunction { inherit toplevel hostPkgs; }; - extraPathsToRegister = extraPathsToRegister ++ [ + testScript = testScriptFunction { inherit toplevel hostPkgs distroName; }; + extraPathsToRegister = extraPathsToRegister distroName ++ [ toplevel ]; sharedDirs = { }; @@ -80,92 +83,95 @@ let acc // mkTestForDistro distroName distroConfig ) { } distros; - newConfig = system-manager.lib.makeSystemConfig { - modules = [ - ( - { lib, pkgs, ... }: - { - imports = [ sops-nix.nixosModules.sops ]; - config = { - nixpkgs.hostPlatform = system; + mkNewConfig = + distroName: + system-manager.lib.makeSystemConfig { + modules = [ + ( + { lib, pkgs, ... }: + { + imports = [ sops-nix.nixosModules.sops ]; + config = { + nixpkgs.hostPlatform = system; + system-manager.targetDistro = distroName; - services.nginx.enable = false; + services.nginx.enable = false; - environment = { - etc = { - foo_new = { - text = '' - This is just a test! - ''; + environment = { + etc = { + foo_new = { + text = '' + This is just a test! + ''; + }; }; + + systemPackages = [ + pkgs.fish + ]; }; - systemPackages = [ - pkgs.fish - ]; - }; + systemd.services = { + new-service = { + enable = true; + description = "new-service"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecReload = "${lib.getBin pkgs.coreutils}/bin/true"; + }; + wantedBy = [ + "system-manager.target" + "default.target" + ]; + script = '' + sleep 2 + ''; + }; + }; - systemd.services = { - new-service = { + nix = { enable = true; - description = "new-service"; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecReload = "${lib.getBin pkgs.coreutils}/bin/true"; + settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + trusted-users = [ "zimbatm" ]; }; - wantedBy = [ - "system-manager.target" - "default.target" - ]; - script = '' - sleep 2 - ''; }; - }; - nix = { - enable = true; - settings = { - experimental-features = [ - "nix-command" - "flakes" + users.users.zimbatm = { + isNormalUser = true; + extraGroups = [ + "wheel" + "sudo" ]; - trusted-users = [ "zimbatm" ]; + initialPassword = "test123"; }; - }; - - users.users.zimbatm = { - isNormalUser = true; - extraGroups = [ - "wheel" - "sudo" - ]; - initialPassword = "test123"; - }; - sops = { - age.generateKey = false; - age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ../sops/secrets.yaml; - secrets.test = { }; - }; - systemd.services.sops-install-secrets = { - before = [ "sysinit-reactivation.target" ]; - requiredBy = [ "sysinit-reactivation.target" ]; + sops = { + age.generateKey = false; + age.keyFile = "/run/age-keys.txt"; + defaultSopsFile = ../sops/secrets.yaml; + secrets.test = { }; + }; + systemd.services.sops-install-secrets = { + before = [ "sysinit-reactivation.target" ]; + requiredBy = [ "sysinit-reactivation.target" ]; + }; }; - }; - } - ) - ]; - }; + } + ) + ]; + }; callTest = file: import file { inherit forEachImage - newConfig + mkNewConfig system-manager system lib diff --git a/testFlake/vm-tests/example.nix b/testFlake/vm-tests/example.nix index f7d18e94..08d028a7 100644 --- a/testFlake/vm-tests/example.nix +++ b/testFlake/vm-tests/example.nix @@ -1,6 +1,6 @@ { forEachImage, - newConfig, + mkNewConfig, system-manager, ... }: @@ -9,12 +9,20 @@ forEachImage "example" { modules = [ ../../examples/example.nix ]; - extraPathsToRegister = [ - newConfig + extraPathsToRegister = distroName: [ + (mkNewConfig distroName) ../sops/age-keys.txt ]; testScriptFunction = - { toplevel, hostPkgs, ... }: + { + toplevel, + hostPkgs, + distroName, + ... + }: + let + newConfig = mkNewConfig distroName; + in #python '' # Start all machines in parallel diff --git a/testFlake/vm-tests/prepopulate.nix b/testFlake/vm-tests/prepopulate.nix index 9f201d51..0df0494d 100644 --- a/testFlake/vm-tests/prepopulate.nix +++ b/testFlake/vm-tests/prepopulate.nix @@ -1,6 +1,6 @@ { forEachImage, - newConfig, + mkNewConfig, system-manager, ... }: @@ -9,9 +9,12 @@ forEachImage "prepopulate" { modules = [ ../../examples/example.nix ]; - extraPathsToRegister = [ newConfig ]; + extraPathsToRegister = distroName: [ (mkNewConfig distroName) ]; testScriptFunction = - { toplevel, ... }: + { toplevel, distroName, ... }: + let + newConfig = mkNewConfig distroName; + in '' # Start all machines in parallel start_all() diff --git a/testFlake/vm-tests/security-wrappers.nix b/testFlake/vm-tests/security-wrappers.nix index fa3be2d3..3450829f 100644 --- a/testFlake/vm-tests/security-wrappers.nix +++ b/testFlake/vm-tests/security-wrappers.nix @@ -21,7 +21,7 @@ forEachImage "security-wrappers" { } ) ]; - extraPathsToRegister = [ ]; + extraPathsToRegister = _distroName: [ ]; testScriptFunction = { toplevel, hostPkgs, ... }: '' diff --git a/testFlake/vm-tests/sudo-module.nix b/testFlake/vm-tests/sudo-module.nix index d40c2a7c..1afd4eba 100644 --- a/testFlake/vm-tests/sudo-module.nix +++ b/testFlake/vm-tests/sudo-module.nix @@ -40,7 +40,7 @@ forEachImage "sudo-module" { } ) ]; - extraPathsToRegister = [ ]; + extraPathsToRegister = _distroName: [ ]; testScriptFunction = { toplevel, hostPkgs, ... }: '' diff --git a/testFlake/vm-tests/sudo.nix b/testFlake/vm-tests/sudo.nix index f058bed3..f9fe0ae4 100644 --- a/testFlake/vm-tests/sudo.nix +++ b/testFlake/vm-tests/sudo.nix @@ -13,7 +13,7 @@ forEachImage "sudo" { modules = [ ../../examples/example.nix ]; - extraPathsToRegister = [ + extraPathsToRegister = _distroName: [ system-manager.packages.x86_64-linux.default ]; testScriptFunction = diff --git a/testFlake/vm-tests/target-host.nix b/testFlake/vm-tests/target-host.nix index 3d678887..2a0a3f9f 100644 --- a/testFlake/vm-tests/target-host.nix +++ b/testFlake/vm-tests/target-host.nix @@ -12,7 +12,7 @@ forEachImage "target-host" { modules = [ ../../examples/example.nix ]; - extraPathsToRegister = [ + extraPathsToRegister = _distroName: [ system-manager.packages.x86_64-linux.default ]; # Use driver instead of sandboxed since we need network access from the test script From 3faf21be7ff610a462ec56e8dce0a80a3b574853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 14 Apr 2026 16:54:26 +0200 Subject: [PATCH 15/15] fix(users): prefer /usr/sbin/nologin over merged-usr /bin/nologin on shell restore --- .../src/activate/users.rs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/system-manager-engine/src/activate/users.rs b/crates/system-manager-engine/src/activate/users.rs index b1107915..ca0512ab 100644 --- a/crates/system-manager-engine/src/activate/users.rs +++ b/crates/system-manager-engine/src/activate/users.rs @@ -60,14 +60,22 @@ pub fn lock_managed_users() -> Result<()> { } /// Resolves a base shell path (after prefix stripping) to an existing FHS location. +/// +/// For nologin, always prefer `/usr/sbin/nologin` regardless of whether the +/// original base path also happens to resolve: on merged-usr distributions +/// (e.g. fedora) `/bin` is a symlink to `/usr/bin`, so a base of +/// `/bin/nologin` would `exists()`-check true via `/usr/bin/nologin` and we +/// would restore the wrong path, leaving `/etc/passwd` with `/bin/nologin` +/// instead of the `/usr/sbin/nologin` every distro ships. fn resolve_shell(base: &str) -> Result<&str> { - if Path::new(base).exists() { - return Ok(base); - } - match base { - "/bin/nologin" | "/sbin/nologin" => { - for fallback in ["/usr/sbin/nologin", "/usr/bin/nologin", "/bin/nologin"] { + "/bin/nologin" | "/sbin/nologin" | "/usr/sbin/nologin" | "/usr/bin/nologin" => { + for fallback in [ + "/usr/sbin/nologin", + "/usr/bin/nologin", + "/sbin/nologin", + "/bin/nologin", + ] { if Path::new(fallback).exists() { return Ok(fallback); } @@ -75,6 +83,9 @@ fn resolve_shell(base: &str) -> Result<&str> { anyhow::bail!("No valid nologin shell found for base path '{}'", base); } _ => { + if Path::new(base).exists() { + return Ok(base); + } for fallback in ["/bin/sh", "/usr/bin/sh"] { if Path::new(fallback).exists() { return Ok(fallback);