From bea6b628899e89d58a23541b20f57ca9ebac5a72 Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Fri, 6 Mar 2026 21:26:59 +0800 Subject: [PATCH 01/12] feat: add boot.kernel.sysctl and boot.kernelModules --- nix/modules/upstream/nixpkgs/default.nix | 53 ++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 316a8383..3654c855 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -2,8 +2,23 @@ nixosModulesPath, lib, pkgs, + config, ... }: +let + modulesTypeDesc = '' + This can either be a list of modules, or an attrset. In an + attrset, names that are set to `true` represent modules that will + be included. Note that setting these names to `false` does not + prevent the module from being loaded. + ''; + kernelModulesConf = pkgs.writeText "nixos.conf" '' + ${lib.concatStringsSep "\n" config.boot.kernelModules} + ''; + attrNamesToTrue = lib.types.coercedTo (lib.types.listOf lib.types.str) ( + enabledList: lib.genAttrs enabledList (_attrName: true) + ) (lib.types.attrsOf lib.types.bool); +in { imports = [ ./dhparams.nix @@ -29,6 +44,7 @@ "/security/sudo.nix" "/security/wrappers/" "/services/web-servers/nginx/" + "/config/sysctl.nix" # nix settings "/config/nix.nix" "/services/system/userborn.nix" @@ -38,11 +54,27 @@ options = # We need to ignore a bunch of options that are used in NixOS modules but # that don't apply to system-manager configs. - # TODO: can we print an informational message for things like kernel modules - # to inform users that they need to be enabled in the host system? { - boot = lib.mkOption { - type = lib.types.raw; + boot = { + kernelModules = lib.mkOption { + type = attrNamesToTrue; + default = { }; + description = '' + The set of kernel modules to be loaded in the second stage of + the boot process. + + ${modulesTypeDesc} + ''; + apply = mods: lib.attrNames (lib.filterAttrs (_: v: v) mods); + }; + + kernelPackages = lib.mkOption { + type = lib.types.raw; + default = { + kernel.version = "stub"; + }; + description = "Stub kernel packages for compatibility; not actively used in system-manager."; + }; }; # nixos/modules/services/system/userborn.nix still depends on activation scripts @@ -70,4 +102,17 @@ defaultText = lib.literalExpression "pkgs.glibcLocales"; }; }; + + config = { + # Create /etc/modules-load.d/system-manager.conf, which is read by + # systemd-modules-load.service to load required kernel modules. + environment.etc = lib.mkIf (config.boot.kernelModules != { }) { + "modules-load.d/system-manager.conf".source = kernelModulesConf; + }; + + # config/sysctl.nix assumes it can freely configure systemd-sysctl.service. + # However, in our case, the service is managed by the host system, + # so we default to enable = false; to avoid unintended interference. + systemd.services.systemd-sysctl.enable = lib.mkDefault false; + }; } From 81061be1b79dfce5e380bea72fb5b602d491b6cd Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Fri, 6 Mar 2026 23:41:03 +0800 Subject: [PATCH 02/12] feat: add systemd overrideStrategy support --- nix/modules/systemd.nix | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/nix/modules/systemd.nix b/nix/modules/systemd.nix index d7441b08..fc5bedf0 100644 --- a/nix/modules/systemd.nix +++ b/nix/modules/systemd.nix @@ -259,7 +259,15 @@ in done done - for i in ${toString (lib.mapAttrsToList (n: v: v.unit) enabledUnits)}; do + for i in ${ + toString ( + lib.mapAttrsToList (n: v: v.unit) ( + lib.filterAttrs ( + n: v: (lib.attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists" + ) enabledUnits + ) + ) + }; do fn=$(basename $i/*) if [ -e $out/$fn ]; then if [ "$(readlink -f $i/$fn)" = /dev/null ]; then @@ -273,6 +281,18 @@ in fi done + for i in ${ + toString ( + lib.mapAttrsToList (n: v: v.unit) ( + lib.filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") enabledUnits + ) + ) + }; do + fn=$(basename $i/*) + mkdir -p $out/$fn.d + ln -s $i/$fn $out/$fn.d/overrides.conf + done + ${lib.concatStrings ( lib.mapAttrsToList ( name: unit: From 52cdbb1de7c4326a1d812ca349bfa8edbf5d37ae Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Fri, 6 Mar 2026 23:41:09 +0800 Subject: [PATCH 03/12] feat: restart related systemd services automatically --- nix/modules/upstream/nixpkgs/default.nix | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 3654c855..281e976b 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -110,9 +110,25 @@ in "modules-load.d/system-manager.conf".source = kernelModulesConf; }; - # config/sysctl.nix assumes it can freely configure systemd-sysctl.service. - # However, in our case, the service is managed by the host system, - # so we default to enable = false; to avoid unintended interference. - systemd.services.systemd-sysctl.enable = lib.mkDefault false; + systemd.services.systemd-modules-load.overrideStrategy = "asDropin"; + systemd.services.systemd-modules-load = { + wantedBy = [ + "system-manager.target" + "multi-user.target" + ]; + restartTriggers = [ kernelModulesConf ]; + serviceConfig = { + SuccessExitStatus = "0 1"; + }; + }; + + systemd.services.systemd-sysctl.overrideStrategy = "asDropin"; + systemd.services.systemd-sysctl = { + wantedBy = [ + "system-manager.target" + "multi-user.target" + ]; + restartTriggers = [ config.environment.etc."sysctl.d/60-nixos.conf".source ]; + }; }; } From cdf90b981cc48b9f794abc78e848d04ddb5c0639 Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Sun, 8 Mar 2026 13:35:55 +0800 Subject: [PATCH 04/12] fix: missing dir to copy systemd services from --- nix/modules/systemd.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/modules/systemd.nix b/nix/modules/systemd.nix index fc5bedf0..b28926d5 100644 --- a/nix/modules/systemd.nix +++ b/nix/modules/systemd.nix @@ -253,7 +253,7 @@ in for package in $packages do - for hook in $package/lib/systemd/system/* + for hook in $package/etc/systemd/system/* $package/lib/systemd/system/* do ln -s $hook $out/ done From 083267b9bbfcec02e0c6e61a10c834abaa3dd44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 10 Mar 2026 13:38:00 +0100 Subject: [PATCH 05/12] fix: guard systemd-modules-load service behind kernelModules condition Wrap the systemd-modules-load drop-in in mkIf so it is only created when boot.kernelModules is non-empty. --- nix/modules/upstream/nixpkgs/default.nix | 36 +++++++++++++----------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 281e976b..6fad3c53 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -110,25 +110,27 @@ in "modules-load.d/system-manager.conf".source = kernelModulesConf; }; - systemd.services.systemd-modules-load.overrideStrategy = "asDropin"; - systemd.services.systemd-modules-load = { - wantedBy = [ - "system-manager.target" - "multi-user.target" - ]; - restartTriggers = [ kernelModulesConf ]; - serviceConfig = { - SuccessExitStatus = "0 1"; + systemd.services = { + systemd-modules-load = lib.mkIf (config.boot.kernelModules != [ ]) { + overrideStrategy = "asDropin"; + wantedBy = [ + "system-manager.target" + "multi-user.target" + ]; + restartTriggers = [ kernelModulesConf ]; + serviceConfig = { + SuccessExitStatus = "0 1"; + }; }; - }; - systemd.services.systemd-sysctl.overrideStrategy = "asDropin"; - systemd.services.systemd-sysctl = { - wantedBy = [ - "system-manager.target" - "multi-user.target" - ]; - restartTriggers = [ config.environment.etc."sysctl.d/60-nixos.conf".source ]; + systemd-sysctl.overrideStrategy = "asDropin"; + systemd-sysctl = { + wantedBy = [ + "system-manager.target" + "multi-user.target" + ]; + restartTriggers = [ config.environment.etc."sysctl.d/60-nixos.conf".source ]; + }; }; }; } From b959c5d18dc58abda0faf10d028328577475d3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 10 Mar 2026 13:38:34 +0100 Subject: [PATCH 06/12] fix: guard systemd-sysctl service behind sysctl config existence Wrap the systemd-sysctl drop-in in mkIf to avoid referencing sysctl.d/60-nixos.conf when it does not exist. --- nix/modules/upstream/nixpkgs/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 6fad3c53..3d7b0b0e 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -123,8 +123,8 @@ in }; }; - systemd-sysctl.overrideStrategy = "asDropin"; - systemd-sysctl = { + systemd-sysctl = lib.mkIf (config.environment.etc ? "sysctl.d/60-nixos.conf") { + overrideStrategy = "asDropin"; wantedBy = [ "system-manager.target" "multi-user.target" From b75f5f1274273fd3f52f1ad78894bbdd6ac85c1b Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Sat, 2 May 2026 13:20:48 +0800 Subject: [PATCH 07/12] test: port tests --- .../systemd-override-strategy.nix | 87 +++++++++++++++++++ testFlake/vm-tests/boot-config.nix | 74 ++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 testFlake/container-tests/systemd-override-strategy.nix create mode 100644 testFlake/vm-tests/boot-config.nix diff --git a/testFlake/container-tests/systemd-override-strategy.nix b/testFlake/container-tests/systemd-override-strategy.nix new file mode 100644 index 00000000..f1628ac7 --- /dev/null +++ b/testFlake/container-tests/systemd-override-strategy.nix @@ -0,0 +1,87 @@ +{ forEachDistro, ... }: + +forEachDistro "systemd-override-strategy" { + modules = [ + ( + { pkgs, ... }: + let + etcOnlyUnit = pkgs.writeTextDir "etc/systemd/system/etc-only.service" '' + [Unit] + Description=Unit shipped from etc/systemd/system + + [Service] + Type=oneshot + ExecStart=/bin/sh -c 'touch /run/etc-only-service-started' + ''; + + asDropinBaseUnit = pkgs.writeTextDir "etc/systemd/system/as-dropin.service" '' + [Unit] + Description=Base unit from package (asDropin) + + [Service] + Type=oneshot + ExecStart=/bin/true + ''; + + asDropinIfExistsBaseUnit = pkgs.writeTextDir "lib/systemd/system/as-if-exists.service" '' + [Unit] + Description=Base unit from package (asDropinIfExists) + + [Service] + Type=oneshot + ExecStart=/bin/true + ''; + in + { + systemd.packages = [ + etcOnlyUnit + asDropinBaseUnit + asDropinIfExistsBaseUnit + ]; + + systemd.services = { + as-dropin = { + enable = true; + overrideStrategy = "asDropin"; + description = "Override generated as explicit drop-in"; + script = '' + echo "as-dropin override" + ''; + }; + + as-if-exists = { + enable = true; + description = "Override generated as drop-in only if base unit exists"; + script = '' + echo "as-if-exists override" + ''; + }; + }; + } + ) + ]; + testScriptFunction = + { toplevel, hostPkgs, ... }: + '' + start_all() + machine.wait_for_unit("multi-user.target") + + machine.activate() + machine.wait_for_unit("system-manager.target") + + with subtest("Unit file from package etc/systemd/system is copied"): + unit = machine.file("/etc/systemd/system/etc-only.service") + assert unit.exists, "etc-only.service should exist" + assert unit.is_symlink or unit.is_file, "etc-only.service should be a file or symlink" + machine.succeed("systemctl start etc-only.service") + machine.succeed("test -f /run/etc-only-service-started") + + with subtest("overrideStrategy=asDropin produces a drop-in"): + machine.succeed("test -L /etc/systemd/system/as-dropin.service") + machine.succeed("test -L /etc/systemd/system/as-dropin.service.d/overrides.conf") + + with subtest("default overrideStrategy behaves as asDropinIfExists"): + machine.succeed("test -L /etc/systemd/system/as-if-exists.service") + machine.succeed("test -L /etc/systemd/system/as-if-exists.service.d/overrides.conf") + ''; +} diff --git a/testFlake/vm-tests/boot-config.nix b/testFlake/vm-tests/boot-config.nix new file mode 100644 index 00000000..9c7d9711 --- /dev/null +++ b/testFlake/vm-tests/boot-config.nix @@ -0,0 +1,74 @@ +{ + forEachImage, + system-manager, + system, + ... +}: + +let + emptyConfig = system-manager.lib.makeSystemConfig { + modules = [ + { + nixpkgs.hostPlatform = system; + } + ]; + }; +in +forEachImage "boot-config" { + modules = [ + ( + { ... }: + { + boot.kernel.sysctl = { + "net.ipv4.ip_forward" = 1; + "vm.swappiness" = 10; + }; + boot.kernelModules = [ "veth" ]; + } + ) + ]; + extraPathsToRegister = [ emptyConfig ]; + testScriptFunction = + { toplevel, hostPkgs, ... }: + '' + start_all() + vm.wait_for_unit("default.target") + + # Activate empty config: modules-load.d config should not be created + ${system-manager.lib.activateProfileSnippet { + node = "vm"; + profile = emptyConfig; + }} + vm.wait_for_unit("system-manager.target") + + vm.fail("test -f /etc/modules-load.d/system-manager.conf") + vm.fail("test -d /etc/systemd/system/systemd-modules-load.service.d") + # sysctl drop-in exists even without explicit config (upstream defaults) + vm.succeed("test -e /etc/systemd/system/systemd-sysctl.service.d/overrides.conf") + + # Activate with kernel modules: config should exist + ${system-manager.lib.activateProfileSnippet { + node = "vm"; + profile = toplevel; + }} + vm.wait_for_unit("system-manager.target") + + vm.succeed("test -f /etc/modules-load.d/system-manager.conf") + vm.succeed("grep -q veth /etc/modules-load.d/system-manager.conf") + vm.succeed("test -e /etc/systemd/system/systemd-modules-load.service.d/overrides.conf") + + # Verify sysctl config file + vm.succeed("test -f /etc/sysctl.d/60-nixos.conf") + vm.succeed("grep -q net.ipv4.ip_forward /etc/sysctl.d/60-nixos.conf") + vm.succeed("grep -q vm.swappiness /etc/sysctl.d/60-nixos.conf") + vm.succeed("test -e /etc/systemd/system/systemd-sysctl.service.d/overrides.conf") + + ip_forward = vm.succeed("sysctl -n net.ipv4.ip_forward").strip() + assert ip_forward == "1", f"Expected net.ipv4.ip_forward=1, got {ip_forward}" + + swappiness = vm.succeed("sysctl -n vm.swappiness").strip() + assert swappiness == "10", f"Expected vm.swappiness=10, got {swappiness}" + + vm.succeed("lsmod | grep -q veth") + ''; +} From 40135801058702242ae9de1ec8f5b30862795603 Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Sat, 2 May 2026 13:38:42 +0800 Subject: [PATCH 08/12] fix: simplify sysctl config --- nix/modules/upstream/nixpkgs/default.nix | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 3d7b0b0e..d6e8f7bf 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -12,7 +12,7 @@ let be included. Note that setting these names to `false` does not prevent the module from being loaded. ''; - kernelModulesConf = pkgs.writeText "nixos.conf" '' + kernelModulesConf = pkgs.writeText "system-manager.conf" '' ${lib.concatStringsSep "\n" config.boot.kernelModules} ''; attrNamesToTrue = lib.types.coercedTo (lib.types.listOf lib.types.str) ( @@ -106,7 +106,7 @@ in config = { # Create /etc/modules-load.d/system-manager.conf, which is read by # systemd-modules-load.service to load required kernel modules. - environment.etc = lib.mkIf (config.boot.kernelModules != { }) { + environment.etc = lib.mkIf (config.boot.kernelModules != [ ]) { "modules-load.d/system-manager.conf".source = kernelModulesConf; }; @@ -123,13 +123,9 @@ in }; }; - systemd-sysctl = lib.mkIf (config.environment.etc ? "sysctl.d/60-nixos.conf") { + systemd-sysctl = { overrideStrategy = "asDropin"; - wantedBy = [ - "system-manager.target" - "multi-user.target" - ]; - restartTriggers = [ config.environment.etc."sysctl.d/60-nixos.conf".source ]; + wantedBy = [ "system-manager.target" ]; }; }; }; From 7f089f1d76f54b6ebe26d22fc1419ec549606bd0 Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Sat, 2 May 2026 14:06:12 +0800 Subject: [PATCH 09/12] fix(etc-files): handle systemd dependency symlinks Canonicalizing relative symlinks in systemd dependency directories (.wants, .requires) could resolve to incorrect paths if the target unit comes from the host system. Read the symlink target directly to preserve the intended relative path. --- .../src/activate/etc_files.rs | 83 ++++++++++++++----- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/crates/system-manager-engine/src/activate/etc_files.rs b/crates/system-manager-engine/src/activate/etc_files.rs index 3904d87b..604f4764 100644 --- a/crates/system-manager-engine/src/activate/etc_files.rs +++ b/crates/system-manager-engine/src/activate/etc_files.rs @@ -228,34 +228,30 @@ fn list_static_entries(config_entries: &EtcFilesConfig) -> anyhow::Result {}", file_path.display(), link_target.display()); let replace_existing = config_entries .entries .iter() - .find(|e| e.1.target == target) + .find(|e| e.1.target == target_path) .map(|e| e.1.replace_existing) .unwrap_or(false); let etc_file = EtcFile { source: StorePath { - store_path: canon_path, + store_path: link_target, }, - target: PathBuf::from("/etc").join(target), + target: PathBuf::from("/etc").join(target_path), uid: 0, gid: 0, group: "".to_string(), @@ -264,12 +260,53 @@ fn list_static_entries(config_entries: &EtcFilesConfig) -> anyhow::Result Date: Sat, 2 May 2026 14:07:58 +0800 Subject: [PATCH 10/12] style: nix fmt --- crates/system-manager-engine/src/activate/etc_files.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/system-manager-engine/src/activate/etc_files.rs b/crates/system-manager-engine/src/activate/etc_files.rs index 604f4764..33dfd6fc 100644 --- a/crates/system-manager-engine/src/activate/etc_files.rs +++ b/crates/system-manager-engine/src/activate/etc_files.rs @@ -234,13 +234,18 @@ fn list_static_entries(config_entries: &EtcFilesConfig) -> anyhow::Result {}", file_path.display(), link_target.display()); + log::debug!( + "{} is a dependency symlink -> {}", + file_path.display(), + link_target.display() + ); let replace_existing = config_entries .entries .iter() From 87899572e750f771787cbfb88068cc0067b244a6 Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Mon, 4 May 2026 10:48:50 +0800 Subject: [PATCH 11/12] feat(sysctl): add sysctl module and option Introduce a dedicated module for managing kernel sysctl parameters. This module provides the `boot.kernel.sysctl` option to configure runtime kernel parameters. It also moves the `systemd-sysctl` service definition into this module, making it conditional on the `sysctl` option being used. Special merging logic is included for `net.core.rmem_max` and `net.core.wmem_max` to ensure the highest value is applied. --- nix/modules/upstream/nixpkgs/default.nix | 7 +- nix/modules/upstream/nixpkgs/sysctl.nix | 82 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 nix/modules/upstream/nixpkgs/sysctl.nix diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index d6e8f7bf..9ee47e71 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -28,6 +28,7 @@ in ./programs/ssh.nix ./security-wrappers.nix ./security/sudo.nix + ./sysctl.nix ./userborn.nix ./users-groups.nix ../sops-nix.nix @@ -44,7 +45,6 @@ in "/security/sudo.nix" "/security/wrappers/" "/services/web-servers/nginx/" - "/config/sysctl.nix" # nix settings "/config/nix.nix" "/services/system/userborn.nix" @@ -122,11 +122,6 @@ in SuccessExitStatus = "0 1"; }; }; - - systemd-sysctl = { - overrideStrategy = "asDropin"; - wantedBy = [ "system-manager.target" ]; - }; }; }; } diff --git a/nix/modules/upstream/nixpkgs/sysctl.nix b/nix/modules/upstream/nixpkgs/sysctl.nix new file mode 100644 index 00000000..437d1648 --- /dev/null +++ b/nix/modules/upstream/nixpkgs/sysctl.nix @@ -0,0 +1,82 @@ +{ + config, + lib, + ... +}: +let + + sysctlOption = lib.mkOptionType { + name = "sysctl option value"; + check = + val: + let + checkType = x: lib.isBool x || lib.isString x || lib.isInt x || x == null; + in + checkType val || (val._type or "" == "override" && checkType val.content); + merge = loc: defs: lib.mergeOneOption loc defs; + }; + +in +{ + + options = { + + boot.kernel.sysctl = lib.mkOption { + type = + let + highestValueType = lib.types.ints.unsigned // { + merge = loc: defs: lib.foldl (a: b: if b.value == null then null else lib.max a b.value) 0 defs; + }; + in + lib.types.submodule { + freeformType = lib.types.attrsOf sysctlOption; + options = { + "net.core.rmem_max" = lib.mkOption { + type = lib.types.nullOr highestValueType; + default = null; + description = "The maximum receive socket buffer size in bytes. In case of conflicting values, the highest will be used."; + }; + + "net.core.wmem_max" = lib.mkOption { + type = lib.types.nullOr highestValueType; + default = null; + description = "The maximum send socket buffer size in bytes. In case of conflicting values, the highest will be used."; + }; + }; + }; + default = { }; + example = lib.literalExpression '' + { "net.ipv4.tcp_syncookies" = false; "vm.swappiness" = 60; } + ''; + description = '' + Runtime parameters of the Linux kernel, as set by + {manpage}`sysctl(8)`. Note that sysctl + parameters names must be enclosed in quotes + (e.g. `"vm.swappiness"` instead of + `vm.swappiness`). The value of each + parameter may be a string, integer, boolean, or null + (signifying the option will not appear at all). + ''; + + }; + + }; + + config = { + + environment.etc = lib.mkIf (config.boot.kernel.sysctl != { }) { + "sysctl.d/60-system-manager.conf".text = lib.concatStrings ( + lib.mapAttrsToList ( + n: v: lib.optionalString (v != null) "${n}=${if v == false then "0" else toString v}\n" + ) config.boot.kernel.sysctl + ); + }; + + systemd.services.systemd-sysctl = lib.mkIf (config.boot.kernel.sysctl != { }) { + overrideStrategy = "asDropin"; + wantedBy = [ "system-manager.target" ]; + restartTriggers = [ config.environment.etc."sysctl.d/60-system-manager.conf".source ]; + }; + + }; +} From c03d5c0713447f41e44779e13b5c722692412bee Mon Sep 17 00:00:00 2001 From: yuxqiu Date: Mon, 4 May 2026 12:03:15 +0800 Subject: [PATCH 12/12] feat(environment): append to existing session variables Session variables now append to existing values using shell expansion (`${VAR:+:$VAR}`) instead of overwriting them. This ensures that pre-existing values (e.g., from a host environment) are preserved, aligning with common expectations for variables like `PATH` or `XDG_DATA_DIRS`. New tests verify this append behavior and confirm that regular `environment.variables` still overwrite. --- nix/modules/environment.nix | 18 ++++++++++++++++- .../container-tests/environment-variables.nix | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/nix/modules/environment.nix b/nix/modules/environment.nix index 363dcae5..bfc0e78f 100644 --- a/nix/modules/environment.nix +++ b/nix/modules/environment.nix @@ -103,6 +103,13 @@ Setting a variable to `null` does nothing. You can override a variable set by another module to `null` to unset it. + Unlike [](#opt-environment.variables), session variables will + append the existing value of the variable using the + `''${parameter:+word}` shell expansion. For example, setting + `XDG_DATA_DIRS` to `"/nix/share"` will produce + `export XDG_DATA_DIRS="/nix/share''${XDG_DATA_DIRS:+:''$XDG_DATA_DIRS}"`, + which preserves any pre-existing value. + Note: unlike NixOS, system-manager does not manage PAM on the host, so these variables are not injected by pam_env into non-shell sessions (e.g. graphical logins). @@ -125,6 +132,15 @@ config = let pathDir = "/run/system-manager/sw"; + + sessionVarNames = builtins.attrNames config.environment.sessionVariables; + + exportLine = + k: v: + if builtins.elem k sessionVarNames then + "export ${k}=\"" + v + "$\{" + k + ":+:$" + k + "}\"" + else + "export ${k}=\"" + v + "\""; in { environment = { @@ -138,7 +154,7 @@ etc = { "profile.d/system-manager-path.sh".source = pkgs.writeText "system-manager-path.sh" '' - ${lib.concatLines (lib.mapAttrsToList (k: v: ''export ${k}="${v}"'') config.environment.variables)} + ${lib.concatLines (lib.mapAttrsToList exportLine config.environment.variables)} export PATH=${pathDir}/bin:''${PATH} if [ -d "/etc/profiles/per-user/$USER/bin" ]; then export PATH="/etc/profiles/per-user/$USER/bin:$PATH" diff --git a/testFlake/container-tests/environment-variables.nix b/testFlake/container-tests/environment-variables.nix index c36237ea..3e806015 100644 --- a/testFlake/container-tests/environment-variables.nix +++ b/testFlake/container-tests/environment-variables.nix @@ -17,6 +17,10 @@ forEachDistro "environment-variables" { environment.sessionVariables = { SESSION_VAR = "from-session"; + XDG_DATA_DIRS = [ + "/nix/share1" + "/nix/share2" + ]; }; environment.systemPackages = [ pkgs.hello ]; @@ -55,6 +59,22 @@ forEachDistro "environment-variables" { value = machine.succeed("bash --login -c 'echo $SESSION_VAR'").strip() assert value == "from-session", f"Expected 'from-session', got: '{value}'" + with subtest("sessionVariables append existing values"): + value = machine.succeed("bash --login -c 'SESSION_VAR=existing; export SESSION_VAR; source /etc/profile.d/system-manager-path.sh; echo $SESSION_VAR'").strip() + assert value == "from-session:existing", f"Expected 'from-session:existing', got: '{value}'" + + with subtest("sessionVariables with list value append existing values"): + value = machine.succeed("bash --login -c 'XDG_DATA_DIRS=/host/share; export XDG_DATA_DIRS; source /etc/profile.d/system-manager-path.sh; echo $XDG_DATA_DIRS'").strip() + assert value == "/nix/share1:/nix/share2:/host/share", f"Expected '/nix/share1:/nix/share2:/host/share', got: '{value}'" + + with subtest("sessionVariables with list value works without pre-existing value"): + value = machine.succeed("bash --login -c 'unset XDG_DATA_DIRS; source /etc/profile.d/system-manager-path.sh; echo $XDG_DATA_DIRS'").strip() + assert value == "/nix/share1:/nix/share2", f"Expected '/nix/share1:/nix/share2', got: '{value}'" + + with subtest("environment.variables overwrite existing values"): + value = machine.succeed("bash --login -c 'FOO=existing; export FOO; source /etc/profile.d/system-manager-path.sh; echo $FOO'").strip() + assert value == "bar", f"Expected 'bar', got: '{value}'" + with subtest("extraSetup removes binary from system PATH"): machine.fail("test -e /run/system-manager/sw/bin/hello") '';