push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
{
options,
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkOption
types
;
systemBuilderArgs = {
activationScript = config.system.activationScripts.script;
dryActivationScript = config.system.dryActivationScript;
};
in
{
options = {
system.activatable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to add the activation script to the system profile.
The default, to have the script available all the time, is what we normally
do, but for image based systems, this may not be needed or not be desirable.
'';
};
system.activatableSystemBuilderCommands = options.system.systemBuilderCommands // {
description = ''
Like `system.systemBuilderCommands`, but only for the commands that are
needed *both* when the system is activatable and when it isn't.
Disclaimer: This option might go away in the future. It might be
superseded by separating switch-to-configuration into a separate script
which will make this option superfluous. See
https://github.com/NixOS/nixpkgs/pull/263462#discussion_r1373104845 for
a discussion.
'';
};
system.build.separateActivationScript = mkOption {
type = types.package;
description = ''
A separate activation script package that's not part of the system profile.
This is useful for configurations where `system.activatable` is `false`.
Otherwise, you can just use `system.build.toplevel`.
'';
};
};
config = {
system.activatableSystemBuilderCommands = ''
echo "$activationScript" > $out/activate
echo "$dryActivationScript" > $out/dry-activate
substituteInPlace $out/activate --subst-var-by out ''${!toplevelVar}
substituteInPlace $out/dry-activate --subst-var-by out ''${!toplevelVar}
chmod u+x $out/activate $out/dry-activate
unset activationScript dryActivationScript
'';
system.systemBuilderCommands = lib.mkIf config.system.activatable config.system.activatableSystemBuilderCommands;
system.systemBuilderArgs = lib.mkIf config.system.activatable (
systemBuilderArgs
// {
toplevelVar = "out";
}
);
system.build.separateActivationScript =
pkgs.runCommand "separate-activation-script"
(
systemBuilderArgs
// {
toplevelVar = "toplevel";
toplevel = config.system.build.toplevel;
}
)
''
mkdir $out
${config.system.activatableSystemBuilderCommands}
'';
};
}

View File

@@ -0,0 +1,331 @@
# generate the script used to activate the configuration.
{
config,
lib,
pkgs,
...
}:
with lib;
let
addAttributeName = mapAttrs (
a: v:
v
// {
text = ''
#### Activation script snippet ${a}:
_localstatus=0
${v.text}
if (( _localstatus > 0 )); then
printf "Activation script snippet '%s' failed (%s)\n" "${a}" "$_localstatus"
fi
'';
}
);
systemActivationScript =
set: onlyDry:
let
set' = mapAttrs (
_: v: if isString v then (noDepEntry v) // { supportsDryActivation = false; } else v
) set;
withHeadlines = addAttributeName set';
# When building a dry activation script, this replaces all activation scripts
# that do not support dry mode with a comment that does nothing. Filtering these
# activation scripts out so they don't get generated into the dry activation script
# does not work because when an activation script that supports dry mode depends on
# an activation script that does not, the dependency cannot be resolved and the eval
# fails.
withDrySnippets = mapAttrs (
a: v:
if onlyDry && !v.supportsDryActivation then
v
// {
text = "#### Activation script snippet ${a} does not support dry activation.";
}
else
v
) withHeadlines;
in
''
#!${pkgs.runtimeShell}
source ${./lib/lib.sh}
systemConfig='@out@'
export PATH=/empty
for i in ${toString path}; do
PATH=$PATH:$i/bin:$i/sbin
done
_status=0
trap "_status=1 _localstatus=\$?" ERR
# Ensure a consistent umask.
umask 0022
${textClosureMap id withDrySnippets (attrNames withDrySnippets)}
''
+ optionalString (!onlyDry) ''
# Make this configuration the current configuration.
# The readlink is there to ensure that when $systemConfig = /system
# (which is a symlink to the store), /run/current-system is still
# used as a garbage collection root.
ln -sfn "$(readlink -f "$systemConfig")" /run/current-system
exit $_status
'';
path =
with pkgs;
map getBin [
coreutils
gnugrep
findutils
getent
stdenv.cc.libc # nscd in update-users-groups.pl
shadow
util-linux # needed for mount and mountpoint
];
scriptType =
withDry:
with types;
let
scriptOptions = {
deps = mkOption {
type = types.listOf types.str;
default = [ ];
description = "List of dependencies. The script will run after these.";
};
text = mkOption {
type = types.lines;
description = "The content of the script.";
};
}
// optionalAttrs withDry {
supportsDryActivation = mkOption {
type = types.bool;
default = false;
description = ''
Whether this activation script supports being dry-activated.
These activation scripts will also be executed on dry-activate
activations with the environment variable
`NIXOS_ACTION` being set to `dry-activate`.
it's important that these activation scripts don't
modify anything about the system when the variable is set.
'';
};
};
in
either str (submodule {
options = scriptOptions;
});
in
{
###### interface
options = {
system.activationScripts = mkOption {
default = { };
example = literalExpression ''
{
stdio = {
# Run after /dev has been mounted
deps = [ "specialfs" ];
text =
'''
# Needed by some programs.
ln -sfn /proc/self/fd /dev/fd
ln -sfn /proc/self/fd/0 /dev/stdin
ln -sfn /proc/self/fd/1 /dev/stdout
ln -sfn /proc/self/fd/2 /dev/stderr
''';
};
}
'';
description = ''
A set of shell script fragments that are executed when a NixOS
system configuration is activated. Examples are updating
/etc, creating accounts, and so on. Since these are executed
every time you boot the system or run
{command}`nixos-rebuild`, it's important that they are
idempotent and fast.
'';
type = types.attrsOf (scriptType true);
apply =
set:
set
// {
script = systemActivationScript set false;
};
};
system.dryActivationScript = mkOption {
description = "The shell script that is to be run when dry-activating a system.";
readOnly = true;
internal = true;
default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
defaultText = literalMD "generated activation script";
};
system.userActivationScripts = mkOption {
default = { };
example = literalExpression ''
{ plasmaSetup = {
text = '''
''${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5"
''';
deps = [];
};
}
'';
description = ''
A set of shell script fragments that are executed by a systemd user
service when a NixOS system configuration is activated. Examples are
rebuilding the .desktop file cache for showing applications in the menu.
Since these are executed every time you run
{command}`nixos-rebuild`, it's important that they are
idempotent and fast.
'';
type = with types; attrsOf (scriptType false);
apply = set: {
script = ''
export PATH=
for i in ${toString path}; do
PATH=$PATH:$i/bin:$i/sbin
done
_status=0
trap "_status=1 _localstatus=\$?" ERR
${
let
set' = mapAttrs (n: v: if isString v then noDepEntry v else v) set;
withHeadlines = addAttributeName set';
in
textClosureMap id withHeadlines (attrNames withHeadlines)
}
exit $_status
'';
};
};
environment.usrbinenv = mkOption {
default = "${pkgs.coreutils}/bin/env";
defaultText = literalExpression ''"''${pkgs.coreutils}/bin/env"'';
example = literalExpression ''"''${pkgs.busybox}/bin/env"'';
type = types.nullOr types.path;
visible = false;
description = ''
The {manpage}`env(1)` executable that is linked system-wide to
`/usr/bin/env`.
'';
};
system.build.installBootLoader = mkOption {
internal = true;
default = pkgs.writeShellScript "no-bootloader" ''
echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2
'';
defaultText = lib.literalExpression ''
pkgs.writeShellScript "no-bootloader" '''
echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2
'''
'';
description = ''
A program that writes a bootloader installation script to the path passed in the first command line argument.
See `pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs`.
'';
type = types.unique {
message = ''
Only one bootloader can be enabled at a time. This requirement has not
been checked until NixOS 22.05. Earlier versions defaulted to the last
definition. Change your configuration to enable only one bootloader.
'';
} (types.either types.str types.package);
};
};
###### implementation
config = {
system.activationScripts.stdio = ""; # obsolete
system.activationScripts.var = ""; # obsolete
systemd.tmpfiles.rules = [
"D /var/empty 0555 root root -"
"h /var/empty - - - - +i"
]
++ lib.optionals config.nix.enable [
# Prevent the current configuration from being garbage-collected.
"d /nix/var/nix/gcroots -"
"L+ /nix/var/nix/gcroots/current-system - - - - /run/current-system"
];
system.activationScripts.usrbinenv =
if config.environment.usrbinenv != null then
''
mkdir -p /usr/bin
chmod 0755 /usr/bin
ln -sfn ${config.environment.usrbinenv} /usr/bin/.env.tmp
mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env
''
else
''
rm -f /usr/bin/env
if test -d /usr/bin; then rmdir --ignore-fail-on-non-empty /usr/bin; fi
if test -d /usr; then rmdir --ignore-fail-on-non-empty /usr; fi
'';
system.activationScripts.specialfs = ''
specialMount() {
local device="$1"
local mountPoint="$2"
local options="$3"
local fsType="$4"
if mountpoint -q "$mountPoint"; then
local options="remount,$options"
else
mkdir -p "$mountPoint"
chmod 0755 "$mountPoint"
fi
mount -t "$fsType" -o "$options" "$device" "$mountPoint"
}
source ${config.system.build.earlyMountScript}
'';
systemd.user = lib.mkIf config.system.activatable {
services.nixos-activation = {
description = "Run user-specific NixOS activation";
script = config.system.userActivationScripts.script;
unitConfig.ConditionUser = "!@system";
serviceConfig.Type = "oneshot";
wantedBy = [ "default.target" ];
};
};
};
}

View File

@@ -0,0 +1,31 @@
import "struct"
#BootspecV1: {
system: string
init: string
initrd?: string
initrdSecrets?: string
kernel: string
kernelParams: [...string]
label: string
toplevel: string
}
// A restricted document does not allow any official specialisation
// information in it to avoid "recursive specialisations".
#RestrictedDocument: struct.MinFields(1) & {
"org.nixos.bootspec.v1": #BootspecV1
[=~"^"]: #BootspecExtension
}
// Specialisations are a hashmap of strings
#BootspecSpecialisationV1: [string]: #RestrictedDocument
// Bootspec extensions are defined by the extension author.
#BootspecExtension: {...}
// A "full" document allows official specialisation information
// in the top-level with a reserved namespaced key.
Document: #RestrictedDocument & {
"org.nixos.specialisation.v1"?: #BootspecSpecialisationV1
}

View File

@@ -0,0 +1,147 @@
# Note that these schemas are defined by RFC-0125.
# This document is considered a stable API, and is depended upon by external tooling.
# Changes to the structure of the document, or the semantics of the values should go through an RFC.
#
# See: https://github.com/NixOS/rfcs/pull/125
{
config,
pkgs,
lib,
...
}:
let
cfg = config.boot.bootspec;
children = lib.mapAttrs (
childName: childConfig: childConfig.configuration.system.build.toplevel
) config.specialisation;
hasAtLeastOneInitrdSecret = lib.length (lib.attrNames config.boot.initrd.secrets) > 0;
schemas = {
v1 = rec {
filename = "boot.json";
json = pkgs.writeText filename (
builtins.toJSON
# Merge extensions first to not let them shadow NixOS bootspec data.
(
cfg.extensions
// {
"org.nixos.bootspec.v1" = {
system = config.boot.kernelPackages.stdenv.hostPlatform.system;
kernel = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
kernelParams = config.boot.kernelParams;
label = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
}
// lib.optionalAttrs config.boot.initrd.enable {
initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
}
// lib.optionalAttrs hasAtLeastOneInitrdSecret {
initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
};
}
)
);
generator =
let
# NOTE: Be careful to not introduce excess newlines at the end of the
# injectors, as that may affect the pipes and redirects.
# Inject toplevel and init into the bootspec.
# This can only be done here because we *cannot* depend on $out
# referring to the toplevel, except by living in the toplevel itself.
toplevelInjector =
lib.escapeShellArgs [
"${pkgs.buildPackages.jq}/bin/jq"
''
."org.nixos.bootspec.v1".toplevel = $toplevel |
."org.nixos.bootspec.v1".init = $init
''
"--sort-keys"
"--arg"
"toplevel"
"${placeholder "out"}"
"--arg"
"init"
"${placeholder "out"}/init"
]
+ " < ${json}";
# We slurp all specialisations and inject them as values, such that
# `.specialisations.${name}` embeds the specialisation's bootspec
# document.
specialisationInjector =
let
specialisationLoader = (
lib.mapAttrsToList (
childName: childToplevel:
lib.escapeShellArgs [
"--slurpfile"
childName
"${childToplevel}/${filename}"
]
) children
);
in
lib.escapeShellArgs [
"${pkgs.buildPackages.jq}/bin/jq"
"--sort-keys"
''."org.nixos.specialisation.v1" = ($ARGS.named | map_values(. | first))''
]
+ " ${lib.concatStringsSep " " specialisationLoader}";
in
"${toplevelInjector} | ${specialisationInjector} > $out/${filename}";
validator = pkgs.writeCueValidator ./bootspec.cue {
document = "Document"; # Universal validator for any version as long the schema is correctly set.
};
};
};
in
{
options.boot.bootspec = {
enable =
lib.mkEnableOption "the generation of RFC-0125 bootspec in $system/boot.json, e.g. /run/current-system/boot.json"
// {
default = true;
internal = true;
};
enableValidation = lib.mkEnableOption ''
the validation of bootspec documents for each build.
This will introduce Go in the build-time closure as we are relying on [Cuelang](https://cuelang.org/) for schema validation.
Enable this option if you want to ascertain that your documents are correct
'';
package = lib.mkPackageOption pkgs "bootspec" { };
extensions = lib.mkOption {
# NOTE(RaitoBezarius): this is not enough to validate: extensions."osRelease" = drv; those are picked up by cue validation.
type = lib.types.attrsOf lib.types.anything; # <namespace>: { ...namespace-specific fields }
default = { };
description = ''
User-defined data that extends the bootspec document.
To reduce incompatibility and prevent names from clashing
between applications, it is **highly recommended** to use a
unique namespace for your extensions.
'';
};
# This will be run as a part of the `systemBuilder` in ./top-level.nix. This
# means `$out` points to the output of `config.system.build.toplevel` and can
# be used for a variety of things (though, for now, it's only used to report
# the path of the `toplevel` itself and the `init` executable).
writer = lib.mkOption {
internal = true;
default = schemas.v1.generator;
};
validator = lib.mkOption {
internal = true;
default = schemas.v1.validator;
};
filename = lib.mkOption {
internal = true;
default = schemas.v1.filename;
};
};
}

View File

@@ -0,0 +1,5 @@
# shellcheck shell=bash
warn() {
printf "\033[1;35mwarning:\033[0m %s\n" "$*" >&2
}

View File

@@ -0,0 +1,41 @@
# Run:
# nix-build -A nixosTests.activation-lib
{
lib,
stdenv,
testers,
}:
let
inherit (lib) fileset;
runTests = stdenv.mkDerivation {
name = "tests-activation-lib";
src = fileset.toSource {
root = ./.;
fileset = fileset.unions [
./lib.sh
./test.sh
];
};
buildPhase = ":";
doCheck = true;
postUnpack = ''
patchShebangs --build .
'';
checkPhase = ''
./test.sh
'';
installPhase = ''
touch $out
'';
};
runShellcheck = testers.shellcheck {
name = "activation-lib";
src = runTests.src;
};
in
lib.recurseIntoAttrs {
inherit runTests runShellcheck;
}

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Run:
# ./test.sh
# or:
# nix-build -A nixosTests.activation-lib
cd "$(dirname "${BASH_SOURCE[0]}")"
set -euo pipefail
# report failure
onerr() {
set +e
# find failed statement
echo "call trace:"
local i=0
while t="$(caller $i)"; do
line="${t%% *}"
file="${t##* }"
echo " $file:$line" >&2
((i++))
done
# red
printf "\033[1;31mtest failed\033[0m\n" >&2
exit 1
}
trap onerr ERR
# shellcheck source-path=SCRIPTDIR
source ./lib.sh
(warn hi, this works >/dev/null) 2>&1 | grep -E $'.*warning:.* hi, this works' >/dev/null
# green
printf "\033[1;32mok\033[0m\n"

View File

@@ -0,0 +1,31 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.system.nixos-init;
in
{
options.system.nixos-init = {
enable = lib.mkEnableOption ''
nixos-init, a system for bashless initialization.
This doesn't use any `activationScripts`. Anything set in these options is
a no-op here.
'';
package = lib.mkPackageOption pkgs "nixos-init" { };
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = "nixos-init can only be used with systemd initrd";
}
];
};
}

View File

@@ -0,0 +1,9 @@
{ lib, ... }:
with lib;
{
boot.loader.grub.device = mkOverride 0 "nodev";
specialisation = mkOverride 0 { };
isSpecialisation = mkOverride 0 true;
}

View File

@@ -0,0 +1,54 @@
{
lib,
config,
pkgs,
...
}:
let
preSwitchCheckScript = lib.concatLines (
lib.mapAttrsToList (name: text: ''
# pre-switch check ${name}
if ! (
${text}
) >&2 ; then
echo "Pre-switch check '${name}' failed" >&2
exit 1
fi
'') config.system.preSwitchChecks
);
in
{
options.system.preSwitchChecksScript = lib.mkOption {
type = lib.types.pathInStore;
internal = true;
readOnly = true;
default = lib.getExe (
pkgs.writeShellApplication {
name = "pre-switch-checks";
text = preSwitchCheckScript;
}
);
};
options.system.preSwitchChecks = lib.mkOption {
default = { };
example = lib.literalExpression ''
{ failsEveryTime =
'''
false
''';
}
'';
description = ''
A set of shell script fragments that are executed before the switch to a
new NixOS system configuration. A failure in any of these fragments will
cause the switch to fail and exit early.
The scripts receive the new configuration path and the action verb passed
to switch-to-configuration, as the first and second positional arguments
(meaning that you can access them using `$1` and `$2`, respectively).
'';
type = lib.types.attrsOf lib.types.str;
};
}

View File

@@ -0,0 +1,107 @@
{
config,
lib,
pkgs,
extendModules,
noUserModules,
...
}:
let
inherit (lib)
concatStringsSep
escapeShellArg
hasInfix
mapAttrs
mapAttrsToList
mkOption
types
;
# This attribute is responsible for creating boot entries for
# child configuration. They are only (directly) accessible
# when the parent configuration is boot default. For example,
# you can provide an easy way to boot the same configuration
# as you use, but with another kernel
# !!! fix this
children = mapAttrs (
childName: childConfig: childConfig.configuration.system.build.toplevel
) config.specialisation;
in
{
options = {
isSpecialisation = mkOption {
type = lib.types.bool;
internal = true;
default = false;
description = "Whether this system is a specialisation of another.";
};
specialisation = mkOption {
default = { };
example = lib.literalExpression "{ fewJobsManyCores.configuration = { nix.settings = { core = 0; max-jobs = 1; }; }; }";
description = ''
Additional configurations to build. If
`inheritParentConfig` is true, the system
will be based on the overall system configuration.
To switch to a specialised configuration
(e.g. `fewJobsManyCores`) at runtime, run:
```
sudo /run/current-system/specialisation/fewJobsManyCores/bin/switch-to-configuration test
```
'';
type = types.attrsOf (
types.submodule (
local@{ ... }:
let
extend = if local.config.inheritParentConfig then extendModules else noUserModules.extendModules;
in
{
options.inheritParentConfig = mkOption {
type = types.bool;
default = true;
description = "Include the entire system's configuration. Set to false to make a completely differently configured system.";
};
options.configuration = mkOption {
default = { };
description = ''
Arbitrary NixOS configuration.
Anything you can add to a normal NixOS configuration, you can add
here, including imports and config values, although nested
specialisations will be ignored.
'';
visible = "shallow";
inherit (extend { modules = [ ./no-clone.nix ]; }) type;
};
}
)
);
};
};
config = {
assertions = mapAttrsToList (name: _: {
assertion = !hasInfix "/" name;
message = ''
Specialisation names must not contain forward slashes.
Invalid specialisation name: ${name}
'';
}) config.specialisation;
system.systemBuilderCommands = ''
mkdir $out/specialisation
${concatStringsSep "\n" (
mapAttrsToList (name: path: "ln -s ${path} $out/specialisation/${escapeShellArg name}") children
)}
'';
};
# uses extendModules to generate a type
meta.buildDocsInSandbox = false;
}

View File

@@ -0,0 +1,53 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [
(lib.mkRemovedOptionModule [ "system" "switch" "enableNg" ] ''
This option controlled the usage of the new switch-to-configuration-ng,
which is now the only switch-to-configuration implementation. This option
can be removed from configuration. If there are outstanding issues
preventing you from using the new implementation, please open an issue on
GitHub.
'')
];
options.system.switch.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to include the capability to switch configurations.
Disabling this makes the system unable to be reconfigured via `nixos-rebuild`.
This is good for image based appliances where updates are handled
outside the image. Reducing features makes the image lighter and
slightly more secure.
'';
};
config = lib.mkIf config.system.switch.enable {
# Use a subshell so we can source makeWrapper's setup hook without
# affecting the rest of activatableSystemBuilderCommands.
system.activatableSystemBuilderCommands = ''
(
source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook
mkdir $out/bin
ln -sf ${lib.getExe pkgs.switch-to-configuration-ng} $out/bin/switch-to-configuration
wrapProgram $out/bin/switch-to-configuration \
--set OUT $out \
--set TOPLEVEL ''${!toplevelVar} \
--set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \
--set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \
--set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecksScript} \
--set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \
--set SYSTEMD ${config.systemd.package}
)
'';
};
}

View File

@@ -0,0 +1,35 @@
{
lib,
nixos,
expect,
testers,
}:
let
node-forbiddenDependencies-fail = nixos (
{ ... }:
{
system.forbiddenDependenciesRegexes = [ "-dev$" ];
environment.etc."dev-dependency" = {
text = "${expect.dev}";
};
documentation.enable = false;
fileSystems."/".device = "ignore-root-device";
boot.loader.grub.enable = false;
}
);
node-forbiddenDependencies-succeed = nixos (
{ ... }:
{
system.forbiddenDependenciesRegexes = [ "-dev$" ];
system.extraDependencies = [ expect.dev ];
documentation.enable = false;
fileSystems."/".device = "ignore-root-device";
boot.loader.grub.enable = false;
}
);
in
lib.recurseIntoAttrs {
test-forbiddenDependencies-fail = testers.testBuildFailure node-forbiddenDependencies-fail.config.system.build.toplevel;
test-forbiddenDependencies-succeed =
node-forbiddenDependencies-succeed.config.system.build.toplevel;
}

View File

@@ -0,0 +1,407 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
systemBuilder = ''
mkdir $out
${
if config.boot.initrd.enable && config.boot.initrd.systemd.enable then
''
# This must not be a symlink or the abs_path of the grub builder for the tests
# will resolve the symlink and we end up with a path that doesn't point to a
# system closure.
cp "$systemd/lib/systemd/systemd" $out/init
${lib.optionalString (!config.system.nixos-init.enable) ''
cp ${config.system.build.bootStage2} $out/prepare-root
substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
''}
''
else
''
cp ${config.system.build.bootStage2} $out/init
substituteInPlace $out/init --subst-var-by systemConfig $out
''
}
ln -s ${config.system.build.etc}/etc $out/etc
${lib.optionalString config.system.etc.overlay.enable ''
ln -s ${config.system.build.etcMetadataImage} $out/etc-metadata-image
ln -s ${config.system.build.etcBasedir} $out/etc-basedir
''}
ln -s ${config.system.path} $out/sw
ln -s "$systemd" $out/systemd
echo -n "systemd ${toString config.systemd.package.interfaceVersion}" > $out/init-interface-version
echo -n "$nixosLabel" > $out/nixos-version
echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system
${config.system.systemBuilderCommands}
cp "$extraDependenciesPath" "$out/extra-dependencies"
${optionalString (!config.boot.isContainer && config.boot.bootspec.enable) ''
${config.boot.bootspec.writer}
${optionalString config.boot.bootspec.enableValidation ''${config.boot.bootspec.validator} "$out/${config.boot.bootspec.filename}"''}
''}
${config.system.extraSystemBuilderCmds}
'';
# Putting it all together. This builds a store path containing
# symlinks to the various parts of the built configuration (the
# kernel, systemd units, init scripts, etc.) as well as a script
# `switch-to-configuration' that activates the configuration and
# makes it bootable. See `activatable-system.nix`.
baseSystem = pkgs.stdenvNoCC.mkDerivation (
{
name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
preferLocalBuild = true;
allowSubstitutes = false;
passAsFile = [ "extraDependencies" ];
buildCommand = systemBuilder;
systemd = config.systemd.package;
nixosLabel = config.system.nixos.label;
inherit (config.system) extraDependencies;
}
// config.system.systemBuilderArgs
);
# Handle assertions and warnings
baseSystemAssertWarn = lib.asserts.checkAssertWarn config.assertions config.warnings baseSystem;
# Replace runtime dependencies
system =
let
inherit (config.system.replaceDependencies) replacements cutoffPackages;
in
if replacements == [ ] then
# Avoid IFD if possible, by sidestepping replaceDependencies if no replacements are specified.
baseSystemAssertWarn
else
(pkgs.replaceDependencies.override {
replaceDirectDependencies = pkgs.replaceDirectDependencies.override {
nix = config.nix.package;
};
})
{
drv = baseSystemAssertWarn;
inherit replacements cutoffPackages;
};
systemWithBuildDeps = system.overrideAttrs (o: {
systemBuildClosure = pkgs.closureInfo { rootPaths = [ system.drvPath ]; };
buildCommand = o.buildCommand + ''
ln -sn $systemBuildClosure $out/build-closure
'';
});
in
{
imports = [
../build.nix
(mkRemovedOptionModule [
"nesting"
"clone"
] "Use `specialisation.«name» = { inheritParentConfig = true; configuration = { ... }; }` instead.")
(mkRemovedOptionModule [
"nesting"
"children"
] "Use `specialisation.«name».configuration = { ... }` instead.")
(mkRenamedOptionModule
[ "system" "forbiddenDependenciesRegex" ]
[ "system" "forbiddenDependenciesRegexes" ]
)
(mkRenamedOptionModule
[ "system" "replaceRuntimeDependencies" ]
[ "system" "replaceDependencies" "replacements" ]
)
];
options = {
system.boot.loader.id = mkOption {
internal = true;
default = "";
description = ''
Id string of the used bootloader.
'';
};
system.boot.loader.kernelFile = mkOption {
internal = true;
default = pkgs.stdenv.hostPlatform.linux-kernel.target;
defaultText = literalExpression "pkgs.stdenv.hostPlatform.linux-kernel.target";
type = types.str;
description = ''
Name of the kernel file to be passed to the bootloader.
'';
};
system.boot.loader.initrdFile = mkOption {
internal = true;
default = "initrd";
type = types.str;
description = ''
Name of the initrd file to be passed to the bootloader.
'';
};
system.build = {
toplevel = mkOption {
type = types.package;
readOnly = true;
description = ''
This option contains the store path that typically represents a NixOS system.
You can read this path in a custom deployment tool for example.
'';
};
};
system.copySystemConfiguration = mkOption {
type = types.bool;
default = false;
description = ''
If enabled, copies the NixOS configuration file
(usually {file}`/etc/nixos/configuration.nix`)
and symlinks it from the resulting system
(getting to {file}`/run/current-system/configuration.nix`).
Note that only this single file is copied, even if it imports others.
Warning: This feature cannot be used when the system is configured by a flake
'';
};
system.systemBuilderCommands = mkOption {
type = types.lines;
internal = true;
default = "";
description = ''
This code will be added to the builder creating the system store path.
'';
};
system.systemBuilderArgs = mkOption {
type = types.attrsOf types.unspecified;
internal = true;
default = { };
description = ''
`lib.mkDerivation` attributes that will be passed to the top level system builder.
'';
};
system.forbiddenDependenciesRegexes = mkOption {
default = [ ];
example = [ "-dev$" ];
type = types.listOf types.str;
description = ''
POSIX Extended Regular Expressions that match store paths that
should not appear in the system closure, with the exception of {option}`system.extraDependencies`, which is not checked.
'';
};
system.extraSystemBuilderCmds = mkOption {
type = types.lines;
internal = true;
default = "";
description = ''
This code will be added to the builder creating the system store path.
'';
};
system.extraDependencies = mkOption {
type = types.listOf types.pathInStore;
default = [ ];
description = ''
A list of paths that should be included in the system
closure but generally not visible to users.
This option has also been used for build-time checks, but the
`system.checks` option is more appropriate for that purpose as checks
should not leave a trace in the built system configuration.
'';
};
system.checks = mkOption {
type = types.listOf types.package;
default = [ ];
description = ''
Packages that are added as dependencies of the system's build, usually
for the purpose of validating some part of the configuration.
Unlike `system.extraDependencies`, these store paths do not
become part of the built system configuration.
'';
};
system.replaceDependencies = {
replacements = mkOption {
default = [ ];
example = lib.literalExpression "[ ({ oldDependency = pkgs.openssl; newDependency = pkgs.callPackage /path/to/openssl { }; }) ]";
type = types.listOf (
types.submodule (
{ ... }:
{
imports = [
(mkRenamedOptionModule [ "original" ] [ "oldDependency" ])
(mkRenamedOptionModule [ "replacement" ] [ "newDependency" ])
];
options.oldDependency = mkOption {
type = types.package;
description = "The original package to override.";
};
options.newDependency = mkOption {
type = types.package;
description = "The replacement package.";
};
}
)
);
apply = map (
{ oldDependency, newDependency, ... }:
{
inherit oldDependency newDependency;
}
);
description = ''
List of packages to override without doing a full rebuild.
The original derivation and replacement derivation must have the same
name length, and ideally should have close-to-identical directory layout.
'';
};
cutoffPackages = mkOption {
default = lib.optionals config.boot.initrd.enable [ config.system.build.initialRamdisk ];
defaultText = literalExpression "lib.optionals config.boot.initrd.enable [ config.system.build.initialRamdisk ]";
type = types.listOf types.package;
description = ''
Packages to which no replacements should be applied.
The initrd is matched by default, because its structure renders the replacement process ineffective and prone to breakage.
'';
};
};
system.name = mkOption {
type = types.str;
default = if config.networking.hostName == "" then "unnamed" else config.networking.hostName;
defaultText = literalExpression ''
if config.networking.hostName == ""
then "unnamed"
else config.networking.hostName;
'';
description = ''
The name of the system used in the {option}`system.build.toplevel` derivation.
That derivation has the following name:
`"nixos-system-''${config.system.name}-''${config.system.nixos.label}"`
'';
};
system.includeBuildDependencies = mkOption {
type = types.bool;
default = false;
description = ''
Whether to include the build closure of the whole system in
its runtime closure. This can be useful for making changes
fully offline, as it includes all sources, patches, and
intermediate outputs required to build all the derivations
that the system depends on.
Note that this includes _all_ the derivations, down from the
included applications to their sources, the compilers used to
build them, and even the bootstrap compiler used to compile
the compilers. This increases the size of the system and the
time needed to download its dependencies drastically: a
minimal configuration with no extra services enabled grows
from ~670MiB in size to 13.5GiB, and takes proportionally
longer to download.
'';
};
};
config = {
assertions = [
{
assertion = config.system.copySystemConfiguration -> !lib.inPureEvalMode;
message = "system.copySystemConfiguration is not supported with flakes";
}
];
system.extraSystemBuilderCmds =
optionalString config.system.copySystemConfiguration ''
ln -s '${import ../../../lib/from-env.nix "NIXOS_CONFIG" <nixos-config>}' \
"$out/configuration.nix"
''
+ optionalString (config.system.forbiddenDependenciesRegexes != [ ]) (
lib.concatStringsSep "\n" (
map (regex: ''
if [[ ${regex} != "" && -n $closureInfo ]]; then
if forbiddenPaths="$(grep -E -- "${regex}" $closureInfo/store-paths)"; then
echo -e "System closure $out contains the following disallowed paths:\n$forbiddenPaths"
exit 1
fi
fi
'') config.system.forbiddenDependenciesRegexes
)
);
system.systemBuilderArgs = {
# Legacy environment variables. These were used by the activation script,
# but some other script might still depend on them, although unlikely.
installBootLoader = config.system.build.installBootLoader;
localeArchive = "${config.i18n.glibcLocales}/lib/locale/locale-archive";
distroId = config.system.nixos.distroId;
perl = pkgs.perl.withPackages (
p: with p; [
ConfigIniFiles
FileSlurp
]
);
# End if legacy environment variables
preSwitchCheck = config.system.preSwitchChecksScript;
# Not actually used in the builder. `passedChecks` is just here to create
# the build dependencies. Checks are similar to build dependencies in the
# sense that if they fail, the system build fails. However, checks do not
# produce any output of value, so they are not used by the system builder.
# In fact, using them runs the risk of accidentally adding unneeded paths
# to the system closure, which defeats the purpose of the `system.checks`
# option, as opposed to `system.extraDependencies`.
passedChecks = concatStringsSep " " config.system.checks;
}
// lib.optionalAttrs (config.system.forbiddenDependenciesRegexes != [ ]) {
closureInfo = pkgs.closureInfo {
rootPaths = [
# override to avoid infinite recursion (and to allow using extraDependencies to add forbidden dependencies)
(config.system.build.toplevel.overrideAttrs (_: {
extraDependencies = [ ];
closureInfo = null;
}))
];
};
};
system.build.toplevel =
if config.system.includeBuildDependencies then systemWithBuildDeps else system;
};
}

View File

@@ -0,0 +1,426 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkOption
mkDefault
types
optionalString
;
cfg = config.boot.binfmt;
makeBinfmtLine =
name:
{
recognitionType,
offset,
magicOrExtension,
mask,
preserveArgvZero,
openBinary,
matchCredentials,
fixBinary,
...
}:
let
type = if recognitionType == "magic" then "M" else "E";
offset' = toString offset;
mask' = toString mask;
interpreter = "/run/binfmt/${name}";
flags =
if !(matchCredentials -> openBinary) then
throw "boot.binfmt.registrations.${name}: you can't specify openBinary = false when matchCredentials = true."
else
optionalString preserveArgvZero "P"
+ optionalString (openBinary && !matchCredentials) "O"
+ optionalString matchCredentials "C"
+ optionalString fixBinary "F";
in
":${name}:${type}:${offset'}:${magicOrExtension}:${mask'}:${interpreter}:${flags}";
mkInterpreter =
name:
{ interpreter, wrapInterpreterInShell, ... }:
if wrapInterpreterInShell then
pkgs.writeShellScript "${name}-interpreter" ''
#!${pkgs.bash}/bin/sh
exec -- ${interpreter} "$@"
''
else
interpreter;
# Mapping of systems to “magicOrExtension” and “mask”. Mostly taken from:
# - https://github.com/cleverca22/nixos-configs/blob/master/qemu.nix
# and
# - https://github.com/qemu/qemu/blob/master/scripts/qemu-binfmt-conf.sh
# TODO: maybe put these in a JSON file?
magics = {
armv6l-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\x00\xff\xfe\xff\xff\xff'';
};
armv7l-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\x00\xff\xfe\xff\xff\xff'';
};
aarch64-linux = {
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\x00\xff\xfe\xff\xff\xff'';
};
aarch64_be-linux = {
magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
i386-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03\x00'';
mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
i486-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06\x00'';
mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
i586-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06\x00'';
mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
i686-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x06\x00'';
mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
x86_64-linux = {
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00'';
mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
alpha-linux = {
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x26\x90'';
mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
sparc64-linux = {
magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
sparc-linux = {
magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x12'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
powerpc-linux = {
magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x14'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
powerpc64-linux = {
magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x15'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
powerpc64le-linux = {
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x15\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\x00'';
};
mips-linux = {
magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20'';
};
mipsel-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00'';
};
mips64-linux = {
magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
mips64el-linux = {
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
mips64-linuxabin32 = {
magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20'';
};
mips64el-linuxabin32 = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00'';
};
riscv32-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
riscv64-linux = {
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
loongarch64-linux = {
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02\x01'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\xfc\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
wasm32-wasi = {
magicOrExtension = ''\x00asm'';
mask = ''\xff\xff\xff\xff'';
};
wasm64-wasi = {
magicOrExtension = ''\x00asm'';
mask = ''\xff\xff\xff\xff'';
};
s390x-linux = {
magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x16'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
x86_64-windows.magicOrExtension = "MZ";
i686-windows.magicOrExtension = "MZ";
};
in
{
imports = [
(lib.mkRenamedOptionModule [ "boot" "binfmtMiscRegistrations" ] [ "boot" "binfmt" "registrations" ])
];
options = {
boot.binfmt = {
registrations = mkOption {
default = { };
description = ''
Extra binary formats to register with the kernel.
See <https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html> for more details.
'';
type = types.attrsOf (
types.submodule (
{ config, ... }:
{
options = {
recognitionType = mkOption {
default = "magic";
description = "Whether to recognize executables by magic number or extension.";
type = types.enum [
"magic"
"extension"
];
};
offset = mkOption {
default = null;
description = "The byte offset of the magic number used for recognition.";
type = types.nullOr types.int;
};
magicOrExtension = mkOption {
description = "The magic number or extension to match on.";
type = types.str;
};
mask = mkOption {
default = null;
description = "A mask to be ANDed with the byte sequence of the file before matching";
type = types.nullOr types.str;
};
interpreter = mkOption {
description = ''
The interpreter to invoke to run the program.
Note that the actual registration will point to
/run/binfmt/''${name}, so the kernel interpreter length
limit doesn't apply.
'';
type = types.path;
};
preserveArgvZero = mkOption {
default = false;
description = ''
Whether to pass the original argv[0] to the interpreter.
See the description of the 'P' flag in the kernel docs
for more details;
'';
type = types.bool;
};
openBinary = mkOption {
default = config.matchCredentials;
description = ''
Whether to pass the binary to the interpreter as an open
file descriptor, instead of a path.
'';
type = types.bool;
};
matchCredentials = mkOption {
default = false;
description = ''
Whether to launch with the credentials and security
token of the binary, not the interpreter (e.g. setuid
bit).
See the description of the 'C' flag in the kernel docs
for more details.
Implies/requires openBinary = true.
'';
type = types.bool;
};
fixBinary = mkOption {
default = false;
description = ''
Whether to open the interpreter file as soon as the
registration is loaded, rather than waiting for a
relevant file to be invoked.
See the description of the 'F' flag in the kernel docs
for more details.
'';
type = types.bool;
};
wrapInterpreterInShell = mkOption {
default = true;
description = ''
Whether to wrap the interpreter in a shell script.
This allows a shell command to be set as the interpreter.
'';
type = types.bool;
};
interpreterSandboxPath = mkOption {
internal = true;
default = null;
description = ''
Path of the interpreter to expose in the build sandbox.
'';
type = types.nullOr types.path;
};
};
}
)
);
};
emulatedSystems = mkOption {
default = [ ];
example = [
"wasm32-wasi"
"x86_64-windows"
"aarch64-linux"
];
description = ''
List of systems to emulate. Will also configure Nix to
support your new systems.
Warning: the builder can execute all emulated systems within the same build, which introduces impurities in the case of cross compilation.
'';
type = types.listOf (types.enum (builtins.attrNames magics));
};
addEmulatedSystemsToNixSandbox = mkOption {
type = types.bool;
default = true;
example = false;
description = ''
Whether to add the {option}`boot.binfmt.emulatedSystems` to {option}`nix.settings.extra-platforms`.
Disable this to use remote builders for those platforms, while allowing testing binaries locally.
'';
};
preferStaticEmulators = mkOption {
default = false;
description = ''
Whether to use static emulators when available.
This enables the kernel to preload the emulator binaries when
the binfmt registrations are added, obviating the need to make
the emulator binaries available inside chroots and chroot-like
sandboxes.
'';
type = types.bool;
};
};
};
config = {
assertions = lib.mapAttrsToList (name: reg: {
assertion = reg.fixBinary -> !reg.wrapInterpreterInShell;
message = "boot.binfmt.registrations.\"${name}\" cannot have fixBinary when the interpreter is invoked through a shell.";
}) cfg.registrations;
boot.binfmt.registrations = builtins.listToAttrs (
map (
system:
assert system != pkgs.stdenv.hostPlatform.system;
{
name = system;
value =
{ config, ... }:
let
elaborated = lib.systems.elaborate { inherit system; };
useStaticEmulator = cfg.preferStaticEmulators && elaborated.staticEmulatorAvailable pkgs;
interpreter = elaborated.emulator (if useStaticEmulator then pkgs.pkgsStatic else pkgs);
inherit (elaborated) qemuArch;
isQemu = "qemu-${qemuArch}" == baseNameOf interpreter;
interpreterReg =
let
wrapperName = "qemu-${qemuArch}-binfmt-P";
wrapper = pkgs.wrapQemuBinfmtP wrapperName interpreter;
in
if isQemu && !useStaticEmulator then "${wrapper}/bin/${wrapperName}" else interpreter;
in
(
{
preserveArgvZero = mkDefault isQemu;
interpreter = mkDefault interpreterReg;
fixBinary = mkDefault useStaticEmulator;
wrapInterpreterInShell = mkDefault (!config.preserveArgvZero && !config.fixBinary);
interpreterSandboxPath = mkDefault (dirOf (dirOf config.interpreter));
}
// (magics.${system} or (throw "Cannot create binfmt registration for system ${system}"))
);
}
) cfg.emulatedSystems
);
nix.settings = lib.mkIf (cfg.addEmulatedSystemsToNixSandbox && cfg.emulatedSystems != [ ]) {
extra-platforms =
cfg.emulatedSystems ++ lib.optional pkgs.stdenv.hostPlatform.isx86_64 "i686-linux";
extra-sandbox-paths =
let
ruleFor = system: cfg.registrations.${system};
hasWrappedRule = lib.any (system: (ruleFor system).wrapInterpreterInShell) cfg.emulatedSystems;
in
[ "/run/binfmt" ]
++ lib.optional hasWrappedRule "${pkgs.bash}"
++ (map (system: (ruleFor system).interpreterSandboxPath) cfg.emulatedSystems);
};
environment.etc."binfmt.d/nixos.conf".source = builtins.toFile "binfmt_nixos.conf" (
lib.concatStringsSep "\n" (lib.mapAttrsToList makeBinfmtLine config.boot.binfmt.registrations)
);
systemd = lib.mkMerge [
{
tmpfiles.rules = [
"d /run/binfmt 0755 -"
]
++ lib.mapAttrsToList (name: interpreter: "L+ /run/binfmt/${name} - - - - ${interpreter}") (
lib.mapAttrs mkInterpreter config.boot.binfmt.registrations
);
}
(lib.mkIf (config.boot.binfmt.registrations != { }) {
additionalUpstreamSystemUnits = [
"proc-sys-fs-binfmt_misc.automount"
"proc-sys-fs-binfmt_misc.mount"
"systemd-binfmt.service"
];
services.systemd-binfmt.after = [ "systemd-tmpfiles-setup.service" ];
services.systemd-binfmt.restartTriggers = [ (builtins.toJSON config.boot.binfmt.registrations) ];
})
];
};
}

View File

@@ -0,0 +1,51 @@
# Clevis {#module-boot-clevis}
[Clevis](https://github.com/latchset/clevis)
is a framework for automated decryption of resources.
Clevis allows for secure unattended disk decryption during boot, using decryption policies that must be satisfied for the data to decrypt.
## Create a JWE file containing your secret {#module-boot-clevis-create-secret}
The first step is to embed your secret in a [JWE](https://en.wikipedia.org/wiki/JSON_Web_Encryption) file.
JWE files have to be created through the clevis command line. 3 types of policies are supported:
1) TPM policies
Secrets are pinned against the presence of a TPM2 device, for example:
```
echo -n hi | clevis encrypt tpm2 '{}' > hi.jwe
```
2) Tang policies
Secrets are pinned against the presence of a Tang server, for example:
```
echo -n hi | clevis encrypt tang '{"url": "http://tang.local"}' > hi.jwe
```
3) Shamir Secret Sharing
Using Shamir's Secret Sharing ([sss](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing)), secrets are pinned using a combination of the two preceding policies. For example:
```
echo -n hi | clevis encrypt sss \
'{"t": 2, "pins": {"tpm2": {"pcr_ids": "0"}, "tang": {"url": "http://tang.local"}}}' \
> hi.jwe
```
For more complete documentation on how to generate a secret with clevis, see the [clevis documentation](https://github.com/latchset/clevis).
## Activate unattended decryption of a resource at boot {#module-boot-clevis-activate}
In order to activate unattended decryption of a resource at boot, enable the `clevis` module:
```nix
{ boot.initrd.clevis.enable = true; }
```
Then, specify the device you want to decrypt using a given clevis secret. Clevis will automatically try to decrypt the device at boot and will fallback to interactive unlocking if the decryption policy is not fulfilled.
```nix
{ boot.initrd.clevis.devices."/dev/nvme0n1p1".secretFile = ./nvme0n1p1.jwe; }
```
Only `bcachefs`, `zfs` and `luks` encrypted devices are supported at this time.

View File

@@ -0,0 +1,121 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.initrd.clevis;
systemd = config.boot.initrd.systemd;
supportedFs = [
"zfs"
"bcachefs"
];
in
{
meta.maintainers = with lib.maintainers; [
julienmalka
camillemndn
];
meta.doc = ./clevis.md;
options = {
boot.initrd.clevis.enable = lib.mkEnableOption "Clevis in initrd";
boot.initrd.clevis.package = lib.mkPackageOption pkgs "clevis" { };
boot.initrd.clevis.devices = lib.mkOption {
description = "Encrypted devices that need to be unlocked at boot using Clevis";
default = { };
type = lib.types.attrsOf (
lib.types.submodule {
options.secretFile = lib.mkOption {
description = "Clevis JWE file used to decrypt the device at boot, in concert with the chosen pin (one of TPM2, Tang server, or SSS).";
type = lib.types.path;
};
}
);
};
boot.initrd.clevis.useTang = lib.mkOption {
description = "Whether the Clevis JWE file used to decrypt the devices uses a Tang server as a pin.";
default = false;
type = lib.types.bool;
};
};
config = lib.mkIf cfg.enable {
# Implementation of clevis unlocking for the supported filesystems are located directly in the respective modules.
assertions = (
lib.attrValues (
lib.mapAttrs (device: _: {
assertion =
(lib.any (
fs:
fs.device == device && (lib.elem fs.fsType supportedFs)
|| (fs.fsType == "zfs" && lib.hasPrefix "${device}/" fs.device)
) config.system.build.fileSystems)
|| (lib.hasAttr device config.boot.initrd.luks.devices);
message = ''No filesystem or LUKS device with the name ${device} is declared in your configuration.'';
}) cfg.devices
)
);
warnings =
if
cfg.useTang && !config.boot.initrd.network.enable && !config.boot.initrd.systemd.network.enable
then
[ "In order to use a Tang pinned secret you must configure networking in initrd" ]
else
[ ];
boot.initrd = {
extraUtilsCommands = lib.mkIf (!systemd.enable) ''
copy_bin_and_libs ${pkgs.jose}/bin/jose
copy_bin_and_libs ${pkgs.curl}/bin/curl
copy_bin_and_libs ${pkgs.bashNonInteractive}/bin/bash
copy_bin_and_libs ${pkgs.tpm2-tools}/bin/.tpm2-wrapped
mv $out/bin/{.tpm2-wrapped,tpm2}
cp {${pkgs.tpm2-tss},$out}/lib/libtss2-tcti-device.so.0
copy_bin_and_libs ${cfg.package}/bin/.clevis-wrapped
mv $out/bin/{.clevis-wrapped,clevis}
for BIN in ${cfg.package}/bin/clevis-decrypt*; do
copy_bin_and_libs $BIN
done
for BIN in $out/bin/clevis{,-decrypt{,-null,-tang,-tpm2}}; do
sed -i $BIN -e 's,${pkgs.bashNonInteractive},,' -e 's,${pkgs.coreutils},,'
done
sed -i $out/bin/clevis-decrypt-tpm2 -e 's,tpm2_,tpm2 ,'
'';
secrets = lib.mapAttrs' (
name: value: lib.nameValuePair "/etc/clevis/${name}.jwe" value.secretFile
) cfg.devices;
systemd = {
extraBin = lib.mkIf systemd.enable {
clevis = "${cfg.package}/bin/clevis";
curl = "${pkgs.curl}/bin/curl";
};
storePaths = lib.mkIf systemd.enable [
cfg.package
"${pkgs.jose}/bin/jose"
"${pkgs.curl}/bin/curl"
"${pkgs.tpm2-tools}/bin/tpm2_createprimary"
"${pkgs.tpm2-tools}/bin/tpm2_flushcontext"
"${pkgs.tpm2-tools}/bin/tpm2_load"
"${pkgs.tpm2-tools}/bin/tpm2_unseal"
];
};
};
};
}

View File

@@ -0,0 +1,41 @@
{
config,
lib,
options,
...
}:
{
###### interface
options = {
systemd.enableEmergencyMode = lib.mkOption {
default = true;
type = lib.types.bool;
description = ''
Whether to enable emergency mode, which is an
{command}`sulogin` shell started on the console if
mounting a filesystem fails. Since some machines (like EC2
instances) have no console of any kind, emergency mode doesn't
make sense, and it's better to continue with the boot insofar
as possible.
For initrd emergency access, use ${options.boot.initrd.systemd.emergencyAccess} instead.
'';
};
};
###### implementation
config = {
systemd.additionalUpstreamSystemUnits = lib.optionals config.systemd.enableEmergencyMode [
"emergency.target"
"emergency.service"
];
};
}

View File

@@ -0,0 +1,62 @@
# This module automatically grows the root partition.
# This allows an instance to be created with a bigger root filesystem
# than provided by the machine image.
{
config,
lib,
pkgs,
...
}:
with lib;
{
imports = [
(mkRenamedOptionModule [ "virtualisation" "growPartition" ] [ "boot" "growPartition" ])
];
options = {
boot.growPartition = mkEnableOption "growing the root partition on boot";
};
config = mkIf config.boot.growPartition {
assertions = [
{
assertion = !config.boot.initrd.systemd.repart.enable && !config.systemd.repart.enable;
message = "systemd-repart already grows the root partition and thus you should not use boot.growPartition";
}
];
systemd.services.growpart = {
wantedBy = [ "-.mount" ];
after = [ "-.mount" ];
before = [
"systemd-growfs-root.service"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
TimeoutSec = "infinity";
# growpart returns 1 if the partition is already grown
SuccessExitStatus = "0 1";
};
script = ''
rootDevice="${config.fileSystems."/".device}"
rootDevice="$(readlink -f "$rootDevice")"
parentDevice="$rootDevice"
while [ "''${parentDevice%[0-9]}" != "''${parentDevice}" ]; do
parentDevice="''${parentDevice%[0-9]}";
done
partNum="''${rootDevice#"''${parentDevice}"}"
if [ "''${parentDevice%[0-9]p}" != "''${parentDevice}" ] && [ -b "''${parentDevice%p}" ]; then
parentDevice="''${parentDevice%p}"
fi
"${pkgs.cloud-utils.guest}/bin/growpart" "$parentDevice" "$partNum"
'';
};
};
}

View File

@@ -0,0 +1,174 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.boot.initrd.network;
dhcpInterfaces = lib.attrNames (
lib.filterAttrs (iface: v: v.useDHCP == true) (config.networking.interfaces or { })
);
doDhcp = cfg.udhcpc.enable || dhcpInterfaces != [ ];
dhcpIfShellExpr =
if config.networking.useDHCP || cfg.udhcpc.enable then
"$(ls /sys/class/net/ | grep -v ^lo$)"
else
lib.concatMapStringsSep " " lib.escapeShellArg dhcpInterfaces;
udhcpcScript = pkgs.writeScript "udhcp-script" ''
#! /bin/sh
if [ "$1" = bound ]; then
ip address add "$ip/$mask" dev "$interface"
if [ -n "$mtu" ]; then
ip link set mtu "$mtu" dev "$interface"
fi
if [ -n "$staticroutes" ]; then
echo "$staticroutes" \
| sed -r "s@(\S+) (\S+)@ ip route add \"\1\" via \"\2\" dev \"$interface\" ; @g" \
| sed -r "s@ via \"0\.0\.0\.0\"@@g" \
| /bin/sh
fi
if [ -n "$router" ]; then
ip route add "$router" dev "$interface" # just in case if "$router" is not within "$ip/$mask" (e.g. Hetzner Cloud)
ip route add default via "$router" dev "$interface"
fi
if [ -n "$dns" ]; then
rm -f /etc/resolv.conf
for server in $dns; do
echo "nameserver $server" >> /etc/resolv.conf
done
fi
fi
'';
udhcpcArgs = toString cfg.udhcpc.extraArgs;
in
{
options = {
boot.initrd.network.enable = mkOption {
type = types.bool;
default = false;
description = ''
Add network connectivity support to initrd. The network may be
configured using the `ip` kernel parameter,
as described in [the kernel documentation](https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt).
Otherwise, if
{option}`networking.useDHCP` is enabled, an IP address
is acquired using DHCP.
You should add the module(s) required for your network card to
boot.initrd.availableKernelModules.
`lspci -v | grep -iA8 'network\|ethernet'`
will tell you which.
'';
};
boot.initrd.network.flushBeforeStage2 = mkOption {
type = types.bool;
default = !config.boot.initrd.systemd.enable;
defaultText = "!config.boot.initrd.systemd.enable";
description = ''
Whether to clear the configuration of the interfaces that were set up in
the initrd right before stage 2 takes over. Stage 2 will do the regular network
configuration based on the NixOS networking options.
The default is false when systemd is enabled in initrd,
because the systemd-networkd documentation suggests it.
'';
};
boot.initrd.network.udhcpc.enable = mkOption {
default = config.networking.useDHCP && !config.boot.initrd.systemd.enable;
defaultText = "networking.useDHCP";
type = types.bool;
description = ''
Enables the udhcpc service during stage 1 of the boot process. This
defaults to {option}`networking.useDHCP`. Therefore, this useful if
useDHCP is off but the initramfs should do dhcp.
'';
};
boot.initrd.network.udhcpc.extraArgs = mkOption {
default = [ ];
type = types.listOf types.str;
description = ''
Additional command-line arguments passed verbatim to
udhcpc if {option}`boot.initrd.network.enable` and
{option}`boot.initrd.network.udhcpc.enable` are enabled.
'';
};
boot.initrd.network.postCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands to be executed after stage 1 of the
boot has initialised the network.
'';
};
};
config = mkIf cfg.enable {
boot.initrd.kernelModules = [ "af_packet" ];
boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
copy_bin_and_libs ${pkgs.klibc}/lib/klibc/bin.static/ipconfig
'';
boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) (
mkBefore (
# Search for interface definitions in command line.
''
ifaces=""
for o in $(cat /proc/cmdline); do
case $o in
ip=*)
ipconfig $o && ifaces="$ifaces $(echo $o | cut -d: -f6)"
;;
esac
done
''
# Otherwise, use DHCP.
+ optionalString doDhcp ''
# Bring up all interfaces.
for iface in ${dhcpIfShellExpr}; do
echo "bringing up network interface $iface..."
ip link set dev "$iface" up && ifaces="$ifaces $iface"
done
# Acquire DHCP leases.
for iface in ${dhcpIfShellExpr}; do
echo "acquiring IP address via DHCP on $iface..."
udhcpc --quit --now -i $iface -O staticroutes --script ${udhcpcScript} ${udhcpcArgs}
done
''
+ cfg.postCommands
)
);
boot.initrd.postMountCommands =
mkIf (cfg.flushBeforeStage2 && !config.boot.initrd.systemd.enable)
''
for iface in $ifaces; do
ip address flush dev "$iface"
ip link set dev "$iface" down
done
'';
};
}

View File

@@ -0,0 +1,102 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.boot.initrd.network.openvpn;
in
{
options = {
boot.initrd.network.openvpn.enable = mkOption {
type = types.bool;
default = false;
description = ''
Starts an OpenVPN client during initrd boot. It can be used to e.g.
remotely accessing the SSH service controlled by
{option}`boot.initrd.network.ssh` or other network services
included. Service is killed when stage-1 boot is finished.
'';
};
boot.initrd.network.openvpn.configuration = mkOption {
type = types.path; # Same type as boot.initrd.secrets
description = ''
The configuration file for OpenVPN.
::: {.warning}
Unless your bootloader supports initrd secrets, this configuration
is stored insecurely in the global Nix store.
:::
'';
example = literalExpression "./configuration.ovpn";
};
};
config = mkIf (config.boot.initrd.network.enable && cfg.enable) {
assertions = [
{
assertion = cfg.configuration != null;
message = "You should specify a configuration for initrd OpenVPN";
}
];
# Add kernel modules needed for OpenVPN
boot.initrd.kernelModules = [
"tun"
"tap"
];
# Add openvpn and ip binaries to the initrd
# The shared libraries are required for DNS resolution
boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
copy_bin_and_libs ${pkgs.iproute2}/bin/ip
cp -pv ${pkgs.glibc}/lib/libresolv.so.2 $out/lib
cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib
'';
boot.initrd.systemd.storePaths = [
"${pkgs.openvpn}/bin/openvpn"
"${pkgs.iproute2}/bin/ip"
"${pkgs.glibc}/lib/libresolv.so.2"
"${pkgs.glibc}/lib/libnss_dns.so.2"
];
boot.initrd.secrets = {
"/etc/initrd.ovpn" = cfg.configuration;
};
# openvpn --version would exit with 1 instead of 0
boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
$out/bin/openvpn --show-gateway
'';
boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
openvpn /etc/initrd.ovpn &
'';
boot.initrd.systemd.services.openvpn = {
wantedBy = [ "initrd.target" ];
path = [ pkgs.iproute2 ];
after = [
"network.target"
"initrd-nixos-copy-secrets.service"
];
serviceConfig.ExecStart = "${pkgs.openvpn}/bin/openvpn /etc/initrd.ovpn";
serviceConfig.Type = "notify";
};
};
}

View File

@@ -0,0 +1,364 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.boot.initrd.network.ssh;
shell = if cfg.shell == null then "/bin/ash" else cfg.shell;
inherit (config.programs.ssh) package;
enabled =
let
initrd = config.boot.initrd;
in
(initrd.network.enable || initrd.systemd.network.enable) && cfg.enable;
in
{
options.boot.initrd.network.ssh = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Start SSH service during initrd boot. It can be used to debug failing
boot on a remote server, enter pasphrase for an encrypted partition etc.
Service is killed when stage-1 boot is finished.
The sshd configuration is largely inherited from
{option}`services.openssh`.
'';
};
port = mkOption {
type = types.port;
default = 22;
description = ''
Port on which SSH initrd service should listen.
'';
};
shell = mkOption {
type = types.nullOr types.str;
default = null;
defaultText = ''"/bin/ash"'';
description = ''
Login shell of the remote user. Can be used to limit actions user can do.
'';
};
hostKeys = mkOption {
type = types.listOf (types.either types.str types.path);
default = [ ];
example = [
"/etc/secrets/initrd/ssh_host_rsa_key"
"/etc/secrets/initrd/ssh_host_ed25519_key"
];
description = ''
Specify SSH host keys to import into the initrd.
To generate keys, use
{manpage}`ssh-keygen(1)`
as root:
```
ssh-keygen -t rsa -N "" -f /etc/secrets/initrd/ssh_host_rsa_key
ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key
```
::: {.warning}
Unless your bootloader supports initrd secrets, these keys
are stored insecurely in the global Nix store. Do NOT use
your regular SSH host private keys for this purpose or
you'll expose them to regular users!
Additionally, even if your initrd supports secrets, if
you're using initrd SSH to unlock an encrypted disk then
using your regular host keys exposes the private keys on
your unencrypted boot partition.
:::
'';
};
ignoreEmptyHostKeys = mkOption {
type = types.bool;
default = false;
description = ''
Allow leaving {option}`config.boot.initrd.network.ssh.hostKeys` empty,
to deploy ssh host keys out of band.
'';
};
authorizedKeys = mkOption {
type = types.listOf types.str;
default = config.users.users.root.openssh.authorizedKeys.keys;
defaultText = literalExpression "config.users.users.root.openssh.authorizedKeys.keys";
description = ''
Authorized keys for the root user on initrd.
You can combine the `authorizedKeys` and `authorizedKeyFiles` options.
'';
example = [
"ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host"
"ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar"
];
};
authorizedKeyFiles = mkOption {
type = types.listOf types.path;
default = config.users.users.root.openssh.authorizedKeys.keyFiles;
defaultText = literalExpression "config.users.users.root.openssh.authorizedKeys.keyFiles";
description = ''
Authorized keys taken from files for the root user on initrd.
You can combine the `authorizedKeyFiles` and `authorizedKeys` options.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Verbatim contents of {file}`sshd_config`.";
};
};
imports =
map
(
opt:
mkRemovedOptionModule
(
[
"boot"
"initrd"
"network"
"ssh"
]
++ [ opt ]
)
''
The initrd SSH functionality now uses OpenSSH rather than Dropbear.
If you want to keep your existing initrd SSH host keys, convert them with
$ dropbearconvert dropbear openssh dropbear_host_$type_key ssh_host_$type_key
and then set options.boot.initrd.network.ssh.hostKeys.
''
)
[
"hostRSAKey"
"hostDSSKey"
"hostECDSAKey"
];
config =
let
# Nix complains if you include a store hash in initrd path names, so
# as an awful hack we drop the first character of the hash.
initrdKeyPath =
path:
if isString path then
path
else
let
name = builtins.baseNameOf path;
in
builtins.unsafeDiscardStringContext ("/etc/ssh/" + substring 1 (stringLength name) name);
sshdCfg = config.services.openssh;
sshdConfig = ''
UsePAM no
Port ${toString cfg.port}
PasswordAuthentication no
AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u
ChallengeResponseAuthentication no
${flip concatMapStrings cfg.hostKeys (path: ''
HostKey ${initrdKeyPath path}
'')}
''
+ lib.optionalString (sshdCfg.settings.KexAlgorithms != null) ''
KexAlgorithms ${concatStringsSep "," sshdCfg.settings.KexAlgorithms}
''
+ lib.optionalString (sshdCfg.settings.Ciphers != null) ''
Ciphers ${concatStringsSep "," sshdCfg.settings.Ciphers}
''
+ lib.optionalString (sshdCfg.settings.Macs != null) ''
MACs ${concatStringsSep "," sshdCfg.settings.Macs}
''
+ ''
LogLevel ${sshdCfg.settings.LogLevel}
${
if sshdCfg.settings.UseDns then
''
UseDNS yes
''
else
''
UseDNS no
''
}
${optionalString (!config.boot.initrd.systemd.enable) ''
SshdAuthPath /bin/sshd-auth
SshdSessionPath /bin/sshd-session
''}
${cfg.extraConfig}
'';
in
mkIf enabled {
assertions = [
{
assertion = cfg.authorizedKeys != [ ] || cfg.authorizedKeyFiles != [ ];
message = "You should specify at least one authorized key for initrd SSH";
}
{
assertion = (cfg.hostKeys != [ ]) || cfg.ignoreEmptyHostKeys;
message = ''
You must now pre-generate the host keys for initrd SSH.
See the boot.initrd.network.ssh.hostKeys documentation
for instructions.
'';
}
];
warnings = lib.optional (config.boot.initrd.systemd.enable && cfg.shell != null) ''
Please set 'boot.initrd.systemd.users.root.shell' instead of 'boot.initrd.network.ssh.shell'
'';
boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
copy_bin_and_libs ${package}/bin/sshd
copy_bin_and_libs ${package}/libexec/sshd-auth
copy_bin_and_libs ${package}/libexec/sshd-session
cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
'';
boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
# sshd requires a host key to check config, so we pass in the test's
tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
# keys from Nix store are world-readable, which sshd doesn't like
chmod 600 "$tmpkey"
echo -n ${escapeShellArg sshdConfig} |
$out/bin/sshd -t -f /dev/stdin \
-h "$tmpkey"
rm "$tmpkey"
'';
boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
echo '${shell}' > /etc/shells
echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd
echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
echo 'passwd: files' > /etc/nsswitch.conf
mkdir -p /var/log /var/empty
touch /var/log/lastlog
mkdir -p /etc/ssh
echo -n ${escapeShellArg sshdConfig} > /etc/ssh/sshd_config
echo "export PATH=$PATH" >> /etc/profile
echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> /etc/profile
mkdir -p /root/.ssh
${concatStrings (
map (key: ''
echo ${escapeShellArg key} >> /root/.ssh/authorized_keys
'') cfg.authorizedKeys
)}
${concatStrings (
map (keyFile: ''
cat ${keyFile} >> /root/.ssh/authorized_keys
'') cfg.authorizedKeyFiles
)}
${flip concatMapStrings cfg.hostKeys (path: ''
# keys from Nix store are world-readable, which sshd doesn't like
chmod 0600 "${initrdKeyPath path}"
'')}
/bin/sshd -e
'';
boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
# Stop sshd cleanly before stage 2.
#
# If you want to keep it around to debug post-mount SSH issues,
# run `touch /.keep_sshd` (either from an SSH session or in
# another initrd hook like preDeviceCommands).
if ! [ -e /.keep_sshd ]; then
pkill -x sshd
fi
'';
boot.initrd.secrets = listToAttrs (
map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys
);
# Systemd initrd stuff
boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable {
users.sshd = {
uid = 1;
group = "sshd";
};
groups.sshd = {
gid = 1;
};
users.root.shell = mkIf (
config.boot.initrd.network.ssh.shell != null
) config.boot.initrd.network.ssh.shell;
contents = {
"/etc/ssh/sshd_config".text = sshdConfig;
"/etc/ssh/authorized_keys.d/root".text = concatStringsSep "\n" (
config.boot.initrd.network.ssh.authorizedKeys
++ (map (file: lib.fileContents file) config.boot.initrd.network.ssh.authorizedKeyFiles)
);
};
storePaths = [
"${package}/bin/sshd"
"${package}/libexec/sshd-auth"
"${package}/libexec/sshd-session"
];
services.sshd = {
description = "SSH Daemon";
wantedBy = [ "initrd.target" ];
after = [
"network.target"
"initrd-nixos-copy-secrets.service"
];
before = [ "shutdown.target" ];
conflicts = [ "shutdown.target" ];
# Keys from Nix store are world-readable, which sshd doesn't
# like. If this were a real nix store and not the initrd, we
# neither would nor could do this
preStart = flip concatMapStrings cfg.hostKeys (path: ''
/bin/chmod 0600 "${initrdKeyPath path}"
'');
unitConfig.DefaultDependencies = false;
serviceConfig = {
ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config";
Type = "simple";
KillMode = "process";
Restart = "on-failure";
};
};
};
};
}

View File

@@ -0,0 +1,520 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
inherit (config.boot) kernelPatches;
inherit (config.boot.kernel) features randstructSeed;
inherit (config.boot.kernelPackages) kernel;
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. For that, use
{option}`boot.blacklistedKernelModules`.
'';
kernelModulesConf = pkgs.writeText "nixos.conf" ''
${concatStringsSep "\n" config.boot.kernelModules}
'';
# A list of attrnames is coerced into an attrset of bools by
# setting the values to true.
attrNamesToTrue = types.coercedTo (types.listOf types.str) (
enabledList: lib.genAttrs enabledList (_attrName: true)
) (types.attrsOf types.bool);
in
{
###### interface
options = {
boot.kernel.enable =
mkEnableOption "the Linux kernel. This is useful for systemd-like containers which do not require a kernel"
// {
default = true;
};
boot.kernel.features = mkOption {
default = { };
example = literalExpression "{ debug = true; }";
internal = true;
description = ''
This option allows to enable or disable certain kernel features.
It's not API, because it's about kernel feature sets, that
make sense for specific use cases. Mostly along with programs,
which would have separate nixos options.
`grep features pkgs/os-specific/linux/kernel/common-config.nix`
'';
};
boot.kernelPackages = mkOption {
default = pkgs.linuxPackages;
type = types.raw;
apply =
kernelPackages:
kernelPackages.extend (
self: super: {
kernel = super.kernel.override (originalArgs: {
inherit randstructSeed;
kernelPatches = (originalArgs.kernelPatches or [ ]) ++ kernelPatches;
features = lib.recursiveUpdate super.kernel.features features;
});
}
);
# We don't want to evaluate all of linuxPackages for the manual
# - some of it might not even evaluate correctly.
defaultText = literalExpression "pkgs.linuxPackages";
example = literalExpression "pkgs.linuxKernel.packages.linux_5_10";
description = ''
This option allows you to override the Linux kernel used by
NixOS. Since things like external kernel module packages are
tied to the kernel you're using, it also overrides those.
This option is a function that takes Nixpkgs as an argument
(as a convenience), and returns an attribute set containing at
the very least an attribute {var}`kernel`.
Additional attributes may be needed depending on your
configuration. For instance, if you use the NVIDIA X driver,
then it also needs to contain an attribute
{var}`nvidia_x11`.
Please note that we strictly support kernel versions that are
maintained by the Linux developers only. More information on the
availability of kernel versions is documented
[in the Linux section of the manual](https://nixos.org/manual/nixos/unstable/index.html#sec-kernel-config).
'';
};
boot.kernelPatches = mkOption {
type = types.listOf types.attrs;
default = [ ];
example = literalExpression ''
[
{
name = "foo";
patch = ./foo.patch;
structuredExtraConfig.FOO = lib.kernel.yes;
features.foo = true;
}
{
name = "foo-ml-mbox";
patch = (fetchurl {
url = "https://lore.kernel.org/lkml/19700205182810.58382-1-email@domain/t.mbox.gz";
hash = "sha256-...";
});
}
]
'';
description = ''
A list of additional patches to apply to the kernel.
Every item should be an attribute set with the following attributes:
```nix
{
name = "foo"; # descriptive name, required
patch = ./foo.patch; # path or derivation that contains the patch source
# (required, but can be null if only config changes
# are needed)
structuredExtraConfig = { # attrset of extra configuration parameters without the CONFIG_ prefix
FOO = lib.kernel.yes; # (optional)
}; # values should generally be lib.kernel.yes,
# lib.kernel.no or lib.kernel.module
features = { # attrset of extra "features" the kernel is considered to have
foo = true; # (may be checked by other NixOS modules, optional)
};
extraConfig = "FOO y"; # extra configuration options in string form without the CONFIG_ prefix
# (optional, multiple lines allowed to specify multiple options)
# (deprecated, use structuredExtraConfig instead)
}
```
There's a small set of existing kernel patches in Nixpkgs, available as `pkgs.kernelPatches`,
that follow this format and can be used directly.
'';
};
boot.kernel.randstructSeed = mkOption {
type = types.str;
default = "";
example = "my secret seed";
description = ''
Provides a custom seed for the {var}`RANDSTRUCT` security
option of the Linux kernel. Note that {var}`RANDSTRUCT` is
only enabled in NixOS hardened kernels. Using a custom seed requires
building the kernel and dependent packages locally, since this
customization happens at build time.
'';
};
boot.kernelParams = mkOption {
type = types.listOf (
types.strMatching ''([^"[:space:]]|"[^"]*")+''
// {
name = "kernelParam";
description = "string, with spaces inside double quotes";
}
);
default = [ ];
description = "Parameters added to the kernel command line.";
};
boot.consoleLogLevel = mkOption {
type = types.int;
default = 4;
description = ''
The kernel console `loglevel`. All Kernel Messages with a log level smaller
than this setting will be printed to the console.
'';
};
boot.vesa = mkOption {
type = types.bool;
default = false;
description = ''
(Deprecated) This option, if set, activates the VESA 800x600 video
mode on boot and disables kernel modesetting. It is equivalent to
specifying `[ "vga=0x317" "nomodeset" ]` in the
{option}`boot.kernelParams` option. This option is
deprecated as of 2020: Xorg now works better with modesetting, and
you might want a different VESA vga setting, anyway.
'';
};
boot.extraModulePackages = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression "[ config.boot.kernelPackages.nvidia_x11 ]";
description = "A list of additional packages supplying kernel modules.";
};
boot.kernelModules = mkOption {
type = attrNamesToTrue;
default = { };
description = ''
The set of kernel modules to be loaded in the second stage of
the boot process. Note that modules that are needed to
mount the root file system should be added to
{option}`boot.initrd.availableKernelModules` or
{option}`boot.initrd.kernelModules`.
${modulesTypeDesc}
'';
apply = mods: lib.attrNames (lib.filterAttrs (_: v: v) mods);
};
boot.initrd.availableKernelModules = mkOption {
type = attrNamesToTrue;
default = { };
example = [
"sata_nv"
"ext3"
];
description = ''
The set of kernel modules in the initial ramdisk used during the
boot process. This set must include all modules necessary for
mounting the root device. That is, it should include modules
for the physical device (e.g., SCSI drivers) and for the file
system (e.g., ext3). The set specified here is automatically
closed under the module dependency relation, i.e., all
dependencies of the modules list here are included
automatically. The modules listed here are available in the
initrd, but are only loaded on demand (e.g., the ext3 module is
loaded automatically when an ext3 filesystem is mounted, and
modules for PCI devices are loaded when they match the PCI ID
of a device in your system). To force a module to be loaded,
include it in {option}`boot.initrd.kernelModules`.
${modulesTypeDesc}
'';
apply = mods: lib.attrNames (lib.filterAttrs (_: v: v) mods);
};
boot.initrd.kernelModules = mkOption {
type = attrNamesToTrue;
default = { };
description = ''
Set of modules that are always loaded by the initrd.
${modulesTypeDesc}
'';
apply = mods: lib.attrNames (lib.filterAttrs (_: v: v) mods);
};
boot.initrd.includeDefaultModules = mkOption {
type = types.bool;
default = true;
description = ''
This option, if set, adds a collection of default kernel modules
to {option}`boot.initrd.availableKernelModules` and
{option}`boot.initrd.kernelModules`.
'';
};
boot.initrd.allowMissingModules = mkOption {
type = types.bool;
default = false;
description = ''
Whether the initrd can be built even though modules listed in
{option}`boot.initrd.kernelModules` or
{option}`boot.initrd.availableKernelModules` are missing from
the kernel. This is useful when combining configurations that
include a lot of modules, such as
{option}`hardware.enableAllHardware`, with kernels that don't
provide as many modules as typical NixOS kernels.
Note that enabling this is discouraged. Instead, try disabling
individual modules by setting e.g.
`boot.initrd.availableKernelModules.foo = lib.mkForce false;`
'';
};
system.modulesTree = mkOption {
type = types.listOf types.path;
internal = true;
default = [ ];
description = ''
Tree of kernel modules. This includes the kernel, plus modules
built outside of the kernel. Combine these into a single tree of
symlinks because modprobe only supports one directory.
'';
# Convert the list of path to only one path.
apply =
let
kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
in
modules: (pkgs.aggregateModules modules).override { name = kernel-name + "-modules"; };
};
system.requiredKernelConfig = mkOption {
default = [ ];
example = literalExpression ''
with config.lib.kernelConfig; [
(isYes "MODULES")
(isEnabled "FB_CON_DECOR")
(isEnabled "BLK_DEV_INITRD")
]
'';
internal = true;
type = types.listOf types.attrs;
description = ''
This option allows modules to specify the kernel config options that
must be set (or unset) for the module to work. Please use the
lib.kernelConfig functions to build list elements.
'';
};
};
###### implementation
config = mkMerge [
(mkIf config.boot.initrd.enable {
boot.initrd.availableKernelModules = optionals config.boot.initrd.includeDefaultModules (
[
# Note: most of these (especially the SATA/PATA modules)
# shouldn't be included by default since nixos-generate-config
# detects them, but I'm keeping them for now for backwards
# compatibility.
# Some SATA/PATA stuff.
"ahci"
"sata_nv"
"sata_via"
"sata_sis"
"sata_uli"
"ata_piix"
"pata_marvell"
# NVMe
"nvme"
# Standard SCSI stuff.
"sd_mod"
"sr_mod"
# SD cards and internal eMMC drives.
"mmc_block"
# Support USB keyboards, in case the boot fails and we only have
# a USB keyboard, or for LUKS passphrase prompt.
"uhci_hcd"
"ehci_hcd"
"ehci_pci"
"ohci_hcd"
"ohci_pci"
"xhci_hcd"
"xhci_pci"
"usbhid"
"hid_generic"
"hid_lenovo"
"hid_apple"
"hid_roccat"
"hid_logitech_hidpp"
"hid_logitech_dj"
"hid_microsoft"
"hid_cherry"
"hid_corsair"
]
++ optionals pkgs.stdenv.hostPlatform.isx86 [
# Misc. x86 keyboard stuff.
"pcips2"
"atkbd"
"i8042"
]
);
boot.initrd.kernelModules = optionals config.boot.initrd.includeDefaultModules [
# For LVM.
"dm_mod"
];
})
(mkIf config.boot.kernel.enable {
system.build = { inherit kernel; };
system.modulesTree = [ (lib.getOutput "modules" kernel) ] ++ config.boot.extraModulePackages;
# Not required for, e.g., containers as they don't have their own kernel or initrd.
# They boot directly into stage 2.
system.systemBuilderArgs.kernelParams = config.boot.kernelParams;
system.systemBuilderCommands =
let
kernelPath = "${config.boot.kernelPackages.kernel}/" + "${config.system.boot.loader.kernelFile}";
initrdPath = "${config.system.build.initialRamdisk}/" + "${config.system.boot.loader.initrdFile}";
in
''
if [ ! -f ${kernelPath} ]; then
echo "The bootloader cannot find the proper kernel image."
echo "(Expecting ${kernelPath})"
false
fi
ln -s ${kernelPath} $out/kernel
ln -s ${config.system.modulesTree} $out/kernel-modules
${optionalString (config.hardware.deviceTree.package != null) ''
ln -s ${config.hardware.deviceTree.package} $out/dtbs
''}
echo -n "$kernelParams" > $out/kernel-params
ln -s ${initrdPath} $out/initrd
${optionalString (config.boot.initrd.secrets != { }) ''
ln -s ${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets $out
''}
ln -s ${config.hardware.firmware}/lib/firmware $out/firmware
'';
# Implement consoleLogLevel both in early boot and using sysctl
# (so you don't need to reboot to have changes take effect).
boot.kernelParams = [
"loglevel=${toString config.boot.consoleLogLevel}"
]
++ optionals config.boot.vesa [
"vga=0x317"
"nomodeset"
];
boot.kernel.sysctl."kernel.printk" = mkDefault config.boot.consoleLogLevel;
boot.kernelModules = [
"loop"
"atkbd"
];
# Create /etc/modules-load.d/nixos.conf, which is read by
# systemd-modules-load.service to load required kernel modules.
environment.etc = {
"modules-load.d/nixos.conf".source = kernelModulesConf;
};
systemd.services.systemd-modules-load = {
wantedBy = [ "multi-user.target" ];
restartTriggers = [ kernelModulesConf ];
serviceConfig = {
# Ignore failed module loads. Typically some of the
# modules in boot.kernelModules are "nice to have but
# not required" (e.g. acpi-cpufreq), so we don't want to
# barf on those.
SuccessExitStatus = "0 1";
};
};
lib.kernelConfig = {
isYes = option: {
assertion = config: config.isYes option;
message = "CONFIG_${option} is not yes!";
configLine = "CONFIG_${option}=y";
};
isNo = option: {
assertion = config: config.isNo option;
message = "CONFIG_${option} is not no!";
configLine = "CONFIG_${option}=n";
};
isModule = option: {
assertion = config: config.isModule option;
message = "CONFIG_${option} is not built as a module!";
configLine = "CONFIG_${option}=m";
};
### Usually you will just want to use these two
# True if yes or module
isEnabled = option: {
assertion = config: config.isEnabled option;
message = "CONFIG_${option} is not enabled!";
configLine = "CONFIG_${option}=y";
};
# True if no or omitted
isDisabled = option: {
assertion = config: config.isDisabled option;
message = "CONFIG_${option} is not disabled!";
configLine = "CONFIG_${option}=n";
};
};
# The config options that all modules can depend upon
system.requiredKernelConfig =
with config.lib.kernelConfig;
[
# !!! Should this really be needed?
(isYes "BINFMT_ELF")
]
++ (optional (randstructSeed != "") (isYes "GCC_PLUGIN_RANDSTRUCT"));
# nixpkgs kernels are assumed to have all required features
assertions =
if config.boot.kernelPackages.kernel ? features then
[ ]
else
let
cfg = config.boot.kernelPackages.kernel.config;
in
map (attrs: {
assertion = attrs.assertion cfg;
inherit (attrs) message;
}) config.system.requiredKernelConfig;
})
];
}

View File

@@ -0,0 +1,149 @@
{ lib, config, ... }:
with lib;
let
mergeFalseByDefault =
locs: defs:
if defs == [ ] then
abort "This case should never happen."
else if any (x: x == false) (getValues defs) then
false
else
true;
kernelItem = types.submodule {
options = {
tristate = mkOption {
type = types.enum [
"y"
"m"
"n"
null
];
default = null;
internal = true;
visible = true;
description = ''
Use this field for tristate kernel options expecting a "y" or "m" or "n".
'';
};
freeform = mkOption {
type = types.nullOr types.str // {
merge = mergeEqualOption;
};
default = null;
example = ''MMC_BLOCK_MINORS.freeform = "32";'';
description = ''
Freeform description of a kernel configuration item value.
'';
};
optional = mkOption {
type = types.bool // {
merge = mergeFalseByDefault;
};
default = false;
description = ''
Whether option should generate a failure when unused.
Upon merging values, mandatory wins over optional.
'';
};
};
};
mkValue =
with lib;
val:
let
isNumber =
c:
elem c [
"0"
"1"
"2"
"3"
"4"
"5"
"6"
"7"
"8"
"9"
];
in
if (val == "") then
"\"\""
else if val == "y" || val == "m" || val == "n" then
val
else if all isNumber (stringToCharacters val) then
val
else if substring 0 2 val == "0x" then
val
else
val; # FIXME: fix quoting one day
# generate nix intermediate kernel config file of the form
#
# VIRTIO_MMIO m
# VIRTIO_BLK y
# VIRTIO_CONSOLE n
# NET_9P_VIRTIO? y
#
# Borrowed from copumpkin https://github.com/NixOS/nixpkgs/pull/12158
# returns a string, expr should be an attribute set
# Use mkValuePreprocess to preprocess option values, aka mark 'modules' as 'yes' or vice-versa
# use the identity if you don't want to override the configured values
generateNixKConf =
exprs:
let
mkConfigLine =
key: item:
let
val = if item.freeform != null then item.freeform else item.tristate;
in
optionalString (val != null) (
if (item.optional) then "${key}? ${mkValue val}\n" else "${key} ${mkValue val}\n"
);
mkConf = cfg: concatStrings (mapAttrsToList mkConfigLine cfg);
in
mkConf exprs;
in
{
options = {
intermediateNixConfig = mkOption {
readOnly = true;
type = types.lines;
example = ''
USB? y
DEBUG n
'';
description = ''
The result of converting the structured kernel configuration in settings
to an intermediate string that can be parsed by generate-config.pl to
answer the kernel `make defconfig`.
'';
};
settings = mkOption {
type = types.attrsOf kernelItem;
example = literalExpression ''
with lib.kernel; {
"9P_NET" = yes;
USB = option yes;
MMC_BLOCK_MINORS = freeform "32";
}'';
description = ''
Structured kernel configuration.
'';
};
};
config = {
intermediateNixConfig = generateNixKConf config.settings;
};
}

View File

@@ -0,0 +1,46 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.boot.kexec;
in
{
options.boot.kexec = {
enable = lib.mkEnableOption "kexec" // {
default = lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.kexec-tools;
defaultText = lib.literalExpression ''lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.kexec-tools'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.kexec-tools ];
systemd.services.prepare-kexec = {
description = "Preparation for kexec";
wantedBy = [ "kexec.target" ];
before = [ "systemd-kexec.service" ];
unitConfig.DefaultDependencies = false;
serviceConfig.Type = "oneshot";
path = [ pkgs.kexec-tools ];
script = ''
# Don't load the current system profile if we already have a kernel loaded
if [[ 1 = "$(</sys/kernel/kexec_loaded)" ]] ; then
echo "kexec kernel has already been loaded, prepare-kexec skipped"
exit 0
fi
p=$(readlink -f /nix/var/nix/profiles/system)
if ! [[ -d $p ]]; then
echo "Could not find system profile for prepare-kexec"
exit 1
fi
echo "Loading NixOS system via kexec."
exec kexec --load "$p/kernel" --initrd="$p/initrd" --append="$(cat "$p/kernel-params") init=$p/init"
'';
};
};
}

View File

@@ -0,0 +1,17 @@
{ lib, ... }:
{
options.boot.loader.efi = {
canTouchEfiVariables = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Whether the installation process is allowed to modify EFI boot variables.";
};
efiSysMountPoint = lib.mkOption {
default = "/boot";
type = lib.types.str;
description = "Where the EFI System Partition is mounted.";
};
};
}

View File

@@ -0,0 +1,27 @@
# External Bootloader Backends {#sec-bootloader-external}
NixOS has support for several bootloader backends by default: systemd-boot, grub, uboot, etc.
The built-in bootloader backend support is generic and supports most use cases.
Some users may prefer to create advanced workflows around managing the bootloader and bootable entries.
You can replace the built-in bootloader support with your own tooling using the "external" bootloader option.
Imagine you have created a new package called FooBoot.
FooBoot provides a program at `${pkgs.fooboot}/bin/fooboot-install` which takes the system closure's path as its only argument and configures the system's bootloader.
You can enable FooBoot like this:
```nix
{ pkgs, ... }:
{
boot.loader.external = {
enable = true;
installHook = "${pkgs.fooboot}/bin/fooboot-install";
};
}
```
## Developing Custom Bootloader Backends {#sec-bootloader-external-developing}
Bootloaders should use [RFC-0125](https://github.com/NixOS/rfcs/pull/125)'s Bootspec format and synthesis tools to identify the key properties for bootable system generations.

View File

@@ -0,0 +1,45 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.boot.loader.external;
in
{
meta = {
maintainers = with maintainers; [
cole-h
grahamc
raitobezarius
];
doc = ./external.md;
};
options.boot.loader.external = {
enable = mkEnableOption "using an external tool to install your bootloader";
installHook = mkOption {
type = with types; path;
description = ''
The full path to a program of your choosing which performs the bootloader installation process.
The program will be called with an argument pointing to the output of the system's toplevel.
'';
};
};
config = mkIf cfg.enable {
boot.loader = {
grub.enable = mkDefault false;
systemd-boot.enable = mkDefault false;
supportsInitrdSecrets = mkDefault false;
};
system.build.installBootLoader = cfg.installHook;
};
}

View File

@@ -0,0 +1,105 @@
#! @bash@/bin/sh -e
shopt -s nullglob
export PATH=/empty:@path@
default=$1
if test -z "$1"; then
echo "Syntax: generations-dir-builder.sh <DEFAULT-CONFIG>"
exit 1
fi
echo "updating the boot generations directory..."
mkdir -p /boot
rm -Rf /boot/system* || true
target=/boot/grub/menu.lst
tmp=$target.tmp
# Convert a path to a file in the Nix store such as
# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>.
cleanName() {
local path="$1"
echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g'
}
# Copy a file from the Nix store to /boot/kernels.
declare -A filesCopied
copyToKernelsDir() {
local src="$1"
local dst="/boot/kernels/$(cleanName $src)"
# Don't copy the file if $dst already exists. This means that we
# have to create $dst atomically to prevent partially copied
# kernels or initrd if this script is ever interrupted.
if ! test -e $dst; then
local dstTmp=$dst.tmp.$$
cp $src $dstTmp
mv $dstTmp $dst
fi
filesCopied[$dst]=1
result=$dst
}
# Copy its kernel and initrd to /boot/kernels.
addEntry() {
local path="$1"
local generation="$2"
local outdir=/boot/system-$generation
if ! test -e $path/kernel -a -e $path/initrd; then
return
fi
local kernel=$(readlink -f $path/kernel)
local initrd=$(readlink -f $path/initrd)
if test -n "@copyKernels@"; then
copyToKernelsDir $kernel; kernel=$result
copyToKernelsDir $initrd; initrd=$result
fi
mkdir -p $outdir
ln -sf $(readlink -f $path) $outdir/system
ln -sf $(readlink -f $path/init) $outdir/init
ln -sf $initrd $outdir/initrd
ln -sf $kernel $outdir/kernel
if test $(readlink -f "$path") = "$default"; then
cp "$kernel" /boot/nixos-kernel
cp "$initrd" /boot/nixos-initrd
cp "$(readlink -f "$path/init")" /boot/nixos-init
mkdir -p /boot/default
# ln -sfT: overrides target even if it exists.
ln -sfT $(readlink -f $path) /boot/default/system
ln -sfT $(readlink -f $path/init) /boot/default/init
ln -sfT $initrd /boot/default/initrd
ln -sfT $kernel /boot/default/kernel
fi
}
if test -n "@copyKernels@"; then
mkdir -p /boot/kernels
fi
# Add all generations of the system profile to the menu, in reverse
# (most recent to least recent) order.
for generation in $(
(cd /nix/var/nix/profiles && ls -d system-*-link) \
| sed 's/system-\([0-9]\+\)-link/\1/' \
| sort -n -r); do
link=/nix/var/nix/profiles/system-$generation-link
addEntry $link $generation
done
# Remove obsolete files from /boot/kernels.
for fn in /boot/kernels/*; do
if ! test "${filesCopied[$fn]}" = 1; then
rm -vf -- "$fn"
fi
done

View File

@@ -0,0 +1,72 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
generationsDirBuilder = pkgs.replaceVarsWith {
src = ./generations-dir-builder.sh;
isExecutable = true;
replacements = {
inherit (pkgs) bash;
path = lib.makeBinPath [
pkgs.coreutils
pkgs.gnused
pkgs.gnugrep
];
inherit (config.boot.loader.generationsDir) copyKernels;
};
};
in
{
options = {
boot.loader.generationsDir = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to create symlinks to the system generations under
`/boot`. When enabled,
`/boot/default/kernel`,
`/boot/default/initrd`, etc., are updated to
point to the current generation's kernel image, initial RAM
disk, and other bootstrap files.
This optional is not necessary with boot loaders such as GNU GRUB
for which the menu is updated to point to the latest bootstrap
files. However, it is needed for U-Boot on platforms where the
boot command line is stored in flash memory rather than in a
menu file.
'';
};
copyKernels = mkOption {
default = false;
type = types.bool;
description = ''
Whether to copy the necessary boot files into /boot, so
/nix/store is not needed by the boot loader.
'';
};
};
};
config = mkIf config.boot.loader.generationsDir.enable {
system.build.installBootLoader = generationsDirBuilder;
system.boot.loader.id = "generationsDir";
system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
};
}

View File

@@ -0,0 +1,137 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
blCfg = config.boot.loader;
dtCfg = config.hardware.deviceTree;
cfg = blCfg.generic-extlinux-compatible;
timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout;
# The builder used to write during system activation
builder = import ./extlinux-conf-builder.nix { inherit lib pkgs; };
# The builder exposed in populateCmd, which runs on the build architecture
populateBuilder = import ./extlinux-conf-builder.nix {
inherit lib;
pkgs = pkgs.buildPackages;
};
in
{
options = {
boot.loader.generic-extlinux-compatible = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to generate an extlinux-compatible configuration file
under `/boot/extlinux.conf`. For instance,
U-Boot's generic distro boot support uses this file format.
See [U-boot's documentation](https://u-boot.readthedocs.io/en/latest/develop/distro.html)
for more information.
'';
};
useGenerationDeviceTree = mkOption {
default = true;
type = types.bool;
description = ''
Whether to generate Device Tree-related directives in the
extlinux configuration.
When enabled, the bootloader will attempt to load the device
tree binaries from the generation's kernel.
Note that this affects all generations, regardless of the
setting value used in their configurations.
'';
};
configurationLimit = mkOption {
default = 20;
example = 10;
type = types.int;
description = ''
Maximum number of configurations in the boot menu.
'';
};
mirroredBoots = mkOption {
default = [ { path = "/boot"; } ];
example = [
{ path = "/boot1"; }
{ path = "/boot2"; }
];
description = ''
Mirror the boot configuration to multiple paths.
'';
type =
with types;
listOf (submodule {
options = {
path = mkOption {
example = "/boot1";
type = types.str;
description = ''
The path to the boot directory where the extlinux-compatible
configuration files will be written.
'';
};
};
});
};
populateCmd = mkOption {
type = types.str;
readOnly = true;
description = ''
Contains the builder command used to populate an image,
honoring all options except the `-c <path-to-default-configuration>`
argument.
Useful to have for sdImage.populateRootCommands
'';
};
};
};
config =
let
builderArgs =
"-g ${toString cfg.configurationLimit} -t ${timeoutStr}"
+ lib.optionalString (dtCfg.name != null) " -n ${dtCfg.name}"
+ lib.optionalString (!cfg.useGenerationDeviceTree) " -r";
installBootLoader = pkgs.writeScript "install-extlinux-conf.sh" (
''
#!${pkgs.runtimeShell}
set -e
''
+ flip concatMapStrings cfg.mirroredBoots (args: ''
${builder} ${builderArgs} -d '${args.path}' -c "$@"
'')
);
in
mkIf cfg.enable {
system.build.installBootLoader = installBootLoader;
system.boot.loader.id = "generic-extlinux-compatible";
boot.loader.generic-extlinux-compatible.populateCmd = "${populateBuilder} ${builderArgs}";
assertions = [
{
assertion = cfg.mirroredBoots != [ ];
message = ''
You must not remove all elements from option 'boot.loader.generic-extlinux-compatible.mirroredBoots',
otherwise the system will not be bootable.
'';
}
];
};
}

View File

@@ -0,0 +1,14 @@
{ lib, pkgs }:
pkgs.replaceVarsWith {
src = ./extlinux-conf-builder.sh;
isExecutable = true;
replacements = {
path = lib.makeBinPath [
pkgs.coreutils
pkgs.gnused
pkgs.gnugrep
];
inherit (pkgs) bash;
};
}

View File

@@ -0,0 +1,170 @@
#! @bash@/bin/sh -e
shopt -s nullglob
export PATH=/empty:@path@
usage() {
echo "usage: $0 -t <timeout> -c <path-to-default-configuration> [-d <boot-dir>] [-g <num-generations>] [-n <dtbName>] [-r]" >&2
exit 1
}
timeout= # Timeout in centiseconds
menu=1 # Enable menu by default
default= # Default configuration
target=/boot # Target directory
numGenerations=0 # Number of other generations to include in the menu
while getopts "t:c:d:g:n:r" opt; do
case "$opt" in
t) # U-Boot interprets '0' as infinite
if [ "$OPTARG" -lt 0 ]; then
# When negative (or null coerced to -1), disable timeout which means that we wait forever for input
timeout=0
elif [ "$OPTARG" = 0 ]; then
# When zero, which means disabled in Nix module, disable menu which results in instant boot of the default item
# .. timeout is actually ignored by u-Boot but set here for the rest of the script
timeout=1
menu=0
else
# Positive results in centi-seconds of timeout, which when passed with no input results in boot of the default item
timeout=$((OPTARG * 10))
fi
;;
c) default="$OPTARG" ;;
d) target="$OPTARG" ;;
g) numGenerations="$OPTARG" ;;
n) dtbName="$OPTARG" ;;
r) noDeviceTree=1 ;;
\?) usage ;;
esac
done
[ "$timeout" = "" -o "$default" = "" ] && usage
mkdir -p $target/nixos
mkdir -p $target/extlinux
# Convert a path to a file in the Nix store such as
# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>.
cleanName() {
local path="$1"
echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g'
}
# Copy a file from the Nix store to $target/nixos.
declare -A filesCopied
copyToKernelsDir() {
local src=$(readlink -f "$1")
local dst="$target/nixos/$(cleanName $src)"
# Don't copy the file if $dst already exists. This means that we
# have to create $dst atomically to prevent partially copied
# kernels or initrd if this script is ever interrupted.
if ! test -e $dst; then
local dstTmp=$dst.tmp.$$
cp -r $src $dstTmp
mv $dstTmp $dst
fi
filesCopied[$dst]=1
result=$dst
}
# Copy its kernel, initrd and dtbs to $target/nixos, and echo out an
# extlinux menu entry
addEntry() {
local path=$(readlink -f "$1")
local tag="$2" # Generation number or 'default'
if ! test -e $path/kernel -a -e $path/initrd; then
return
fi
copyToKernelsDir "$path/kernel"; kernel=$result
copyToKernelsDir "$path/initrd"; initrd=$result
dtbDir=$(readlink -m "$path/dtbs")
if [ -e "$dtbDir" ]; then
copyToKernelsDir "$dtbDir"; dtbs=$result
fi
timestampEpoch=$(stat -L -c '%Z' $path)
timestamp=$(date "+%Y-%m-%d %H:%M" -d @$timestampEpoch)
nixosLabel="$(cat $path/nixos-version)"
extraParams="$(cat $path/kernel-params)"
echo
echo "LABEL nixos-$tag"
if [ "$tag" = "default" ]; then
echo " MENU LABEL NixOS - Default"
else
echo " MENU LABEL NixOS - Configuration $tag ($timestamp - $nixosLabel)"
fi
echo " LINUX ../nixos/$(basename $kernel)"
echo " INITRD ../nixos/$(basename $initrd)"
echo " APPEND init=$path/init $extraParams"
if [ -n "$noDeviceTree" ]; then
return
fi
if [ -d "$dtbDir" ]; then
# if a dtbName was specified explicitly, use that, else use FDTDIR
if [ -n "$dtbName" ]; then
echo " FDT ../nixos/$(basename $dtbs)/${dtbName}"
else
echo " FDTDIR ../nixos/$(basename $dtbs)"
fi
else
if [ -n "$dtbName" ]; then
echo "Explicitly requested dtbName $dtbName, but there's no FDTDIR - bailing out." >&2
exit 1
fi
fi
}
tmpFile="$target/extlinux/extlinux.conf.tmp.$$"
cat > $tmpFile <<EOF
# Generated file, all changes will be lost on nixos-rebuild!
# Change this to e.g. nixos-42 to temporarily boot to an older configuration.
DEFAULT nixos-default
TIMEOUT $timeout
EOF
[ "$menu" == "1" ] \
&& echo "MENU TITLE ------------------------------------------------------------" >> $tmpFile
addEntry $default default >> $tmpFile
if [ "$numGenerations" -gt 0 ]; then
# Add up to $numGenerations generations of the system profile to the menu,
# in reverse (most recent to least recent) order.
for generation in $(
(cd /nix/var/nix/profiles && ls -d system-*-link) \
| sed 's/system-\([0-9]\+\)-link/\1/' \
| sort -n -r \
| head -n $numGenerations); do
link=/nix/var/nix/profiles/system-$generation-link
addEntry $link "${generation}-default"
for specialisation in $(
ls /nix/var/nix/profiles/system-$generation-link/specialisation \
| sort -n -r); do
link=/nix/var/nix/profiles/system-$generation-link/specialisation/$specialisation
addEntry $link "${generation}-${specialisation}"
done
done >> $tmpFile
fi
mv -f $tmpFile $target/extlinux/extlinux.conf
# Remove obsolete files from $target/nixos.
for fn in $target/nixos/*; do
if ! test "${filesCopied[$fn]}" = 1; then
echo "Removing no longer needed boot file: $fn"
chmod +w -- "$fn"
rm -rf -- "$fn"
fi
done

View File

@@ -0,0 +1,956 @@
{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
all
concatMap
concatMapStrings
concatStrings
escapeShellArg
flip
foldr
forEach
hasPrefix
mapAttrsToList
literalExpression
makeBinPath
mkDefault
mkIf
mkMerge
mkOption
mkRemovedOptionModule
mkRenamedOptionModule
optional
optionals
optionalString
replaceStrings
types
;
cfg = config.boot.loader.grub;
efi = config.boot.loader.efi;
grubPkgs =
# Package set of targeted architecture
if cfg.forcei686 then pkgs.pkgsi686Linux else pkgs;
realGrub =
if cfg.zfsSupport then
grubPkgs.grub2.override {
zfsSupport = true;
zfs = cfg.zfsPackage;
}
else
grubPkgs.grub2;
grub =
# Don't include GRUB if we're only generating a GRUB menu (e.g.,
# in EC2 instances).
if cfg.devices == [ "nodev" ] then null else realGrub;
grubEfi = if cfg.efiSupport then realGrub.override { efiSupport = cfg.efiSupport; } else null;
f = x: optionalString (x != null) ("" + x);
grubConfig =
args:
let
efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint;
efiSysMountPoint' = replaceStrings [ "/" ] [ "-" ] efiSysMountPoint;
in
pkgs.writeText "grub-config.xml" (
builtins.toXML {
splashImage = f cfg.splashImage;
splashMode = f cfg.splashMode;
backgroundColor = f cfg.backgroundColor;
entryOptions = f cfg.entryOptions;
subEntryOptions = f cfg.subEntryOptions;
# PC platforms (like x86_64-linux) have a non-EFI target (`grubTarget`), but other platforms
# (like aarch64-linux) have an undefined `grubTarget`. Avoid providing the path to a non-EFI
# GRUB on those platforms.
grub = f (if (grub.grubTarget or "") != "" then grub else "");
grubTarget = f (grub.grubTarget or "");
shell = "${pkgs.runtimeShell}";
fullName = lib.getName realGrub;
fullVersion = lib.getVersion realGrub;
grubEfi = f grubEfi;
grubTargetEfi = optionalString cfg.efiSupport (f (grubEfi.grubTarget or ""));
bootPath = args.path;
storePath = config.boot.loader.grub.storePath;
bootloaderId =
if args.efiBootloaderId == null then
"${config.system.nixos.distroName}${efiSysMountPoint'}"
else
args.efiBootloaderId;
timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
theme = f cfg.theme;
inherit efiSysMountPoint;
inherit (args) devices;
inherit (efi) canTouchEfiVariables;
inherit (cfg)
extraConfig
extraPerEntryConfig
extraEntries
forceInstall
useOSProber
extraGrubInstallArgs
extraEntriesBeforeNixOS
extraPrepareConfig
configurationLimit
copyKernels
default
fsIdentifier
efiSupport
efiInstallAsRemovable
gfxmodeEfi
gfxmodeBios
gfxpayloadEfi
gfxpayloadBios
users
timeoutStyle
;
path =
with pkgs;
makeBinPath (
[
coreutils
gnused
gnugrep
findutils
diffutils
btrfs-progs
util-linux
mdadm
]
++ optional cfg.efiSupport efibootmgr
++ optionals cfg.useOSProber [
busybox
os-prober
]
);
font = lib.optionalString (cfg.font != null) (
if lib.last (lib.splitString "." cfg.font) == "pf2" then cfg.font else "${convertedFont}"
);
}
);
bootDeviceCounters = foldr (device: attr: attr // { ${device} = (attr.${device} or 0) + 1; }) { } (
concatMap (args: args.devices) cfg.mirroredBoots
);
convertedFont = (
pkgs.runCommand "grub-font-converted.pf2" { } (
builtins.concatStringsSep " " (
[
"${realGrub}/bin/grub-mkfont"
cfg.font
"--output"
"$out"
]
++ (optional (cfg.fontSize != null) "--size ${toString cfg.fontSize}")
)
)
);
defaultSplash = pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath;
in
{
###### interface
options = {
boot.loader.grub = {
enable = mkOption {
default = !config.boot.isContainer;
defaultText = literalExpression "!config.boot.isContainer";
type = types.bool;
description = ''
Whether to enable the GNU GRUB boot loader.
'';
};
version = mkOption {
visible = false;
type = types.int;
};
device = mkOption {
default = "";
example = "/dev/disk/by-id/wwn-0x500001234567890a";
type = types.str;
description = ''
The device on which the GRUB boot loader will be installed.
The special value `nodev` means that a GRUB
boot menu will be generated, but GRUB itself will not
actually be installed. To install GRUB on multiple devices,
use `boot.loader.grub.devices`.
'';
};
devices = mkOption {
default = [ ];
example = [ "/dev/disk/by-id/wwn-0x500001234567890a" ];
type = types.listOf types.str;
description = ''
The devices on which the boot loader, GRUB, will be
installed. Can be used instead of `device` to
install GRUB onto multiple devices.
'';
};
users = mkOption {
default = { };
example = {
root = {
hashedPasswordFile = "/path/to/file";
};
};
description = ''
User accounts for GRUB. When specified, the GRUB command line and
all boot options except the default are password-protected.
All passwords and hashes provided will be stored in /boot/grub/grub.cfg,
and will be visible to any local user who can read this file. Additionally,
any passwords and hashes provided directly in a Nix configuration
(as opposed to external files) will be copied into the Nix store, and
will be visible to all local users.
'';
type = types.attrsOf (
types.submodule {
options = {
hashedPasswordFile = mkOption {
example = "/path/to/file";
default = null;
type = with types; uniq (nullOr str);
description = ''
Specifies the path to a file containing the password hash
for the account, generated with grub-mkpasswd-pbkdf2.
This hash will be stored in /boot/grub/grub.cfg, and will
be visible to any local user who can read this file.
'';
};
hashedPassword = mkOption {
example = "grub.pbkdf2.sha512.10000.674DFFDEF76E13EA...2CC972B102CF4355";
default = null;
type = with types; uniq (nullOr str);
description = ''
Specifies the password hash for the account,
generated with grub-mkpasswd-pbkdf2.
This hash will be copied to the Nix store, and will be visible to all local users.
'';
};
passwordFile = mkOption {
example = "/path/to/file";
default = null;
type = with types; uniq (nullOr str);
description = ''
Specifies the path to a file containing the
clear text password for the account.
This password will be stored in /boot/grub/grub.cfg, and will
be visible to any local user who can read this file.
'';
};
password = mkOption {
example = "Pa$$w0rd!";
default = null;
type = with types; uniq (nullOr str);
description = ''
Specifies the clear text password for the account.
This password will be copied to the Nix store, and will be visible to all local users.
'';
};
};
}
);
};
mirroredBoots = mkOption {
default = [ ];
example = [
{
path = "/boot1";
devices = [ "/dev/disk/by-id/wwn-0x500001234567890a" ];
}
{
path = "/boot2";
devices = [ "/dev/disk/by-id/wwn-0x500009876543210a" ];
}
];
description = ''
Mirror the boot configuration to multiple partitions and install grub
to the respective devices corresponding to those partitions.
'';
type =
with types;
listOf (submodule {
options = {
path = mkOption {
example = "/boot1";
type = types.str;
description = ''
The path to the boot directory where GRUB will be written. Generally
this boot path should double as an EFI path.
'';
};
efiSysMountPoint = mkOption {
default = null;
example = "/boot1/efi";
type = types.nullOr types.str;
description = ''
The path to the efi system mount point. Usually this is the same
partition as the above path and can be left as null.
'';
};
efiBootloaderId = mkOption {
default = null;
example = "NixOS-fsid";
type = types.nullOr types.str;
description = ''
The id of the bootloader to store in efi nvram.
The default is to name it NixOS and append the path or efiSysMountPoint.
This is only used if `boot.loader.efi.canTouchEfiVariables` is true.
'';
};
devices = mkOption {
default = [ ];
example = [
"/dev/disk/by-id/wwn-0x500001234567890a"
"/dev/disk/by-id/wwn-0x500009876543210a"
];
type = types.listOf types.str;
description = ''
The path to the devices which will have the GRUB MBR written.
Note these are typically device paths and not paths to partitions.
'';
};
};
});
};
configurationName = mkOption {
default = "";
example = "Stable 2.6.21";
type = types.str;
description = ''
GRUB entry name instead of default.
'';
};
storePath = mkOption {
default = "/nix/store";
type = types.str;
description = ''
Path to the Nix store when looking for kernels at boot.
Only makes sense when copyKernels is false.
'';
};
extraPrepareConfig = mkOption {
default = "";
type = types.lines;
description = ''
Additional bash commands to be run at the script that
prepares the GRUB menu entries.
'';
};
extraConfig = mkOption {
default = "";
example = ''
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal_input --append serial
terminal_output --append serial
'';
type = types.lines;
description = ''
Additional GRUB commands inserted in the configuration file
just before the menu entries.
'';
};
extraGrubInstallArgs = mkOption {
default = [ ];
example = [ "--modules=nativedisk ahci pata part_gpt part_msdos diskfilter mdraid1x lvm ext2" ];
type = types.listOf types.str;
description = ''
Additional arguments passed to `grub-install`.
A use case for this is to build specific GRUB2 modules
directly into the GRUB2 kernel image, so that they are available
and activated even in the `grub rescue` shell.
They are also necessary when the BIOS/UEFI is bugged and cannot
correctly read large disks (e.g. above 2 TB), so GRUB2's own
`nativedisk` and related modules can be used
to use its own disk drivers. The example shows one such case.
This is also useful for booting from USB.
See the
[
GRUB source code
](https://git.savannah.gnu.org/cgit/grub.git/tree/grub-core/commands/nativedisk.c?h=grub-2.04#n326)
for which disk modules are available.
The list elements are passed directly as `argv`
arguments to the `grub-install` program, in order.
'';
};
extraInstallCommands = mkOption {
default = "";
example = ''
# the example below generates detached signatures that GRUB can verify
# https://www.gnu.org/software/grub/manual/grub/grub.html#Using-digital-signatures
''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -name '*.sig' -delete
old_gpg_home=$GNUPGHOME
export GNUPGHOME="$(mktemp -d)"
''${pkgs.gnupg}/bin/gpg --import ''${priv_key} > /dev/null 2>&1
''${pkgs.findutils}/bin/find /boot -not -path "/boot/efi/*" -type f -exec ''${pkgs.gnupg}/bin/gpg --detach-sign "{}" \; > /dev/null 2>&1
rm -rf $GNUPGHOME
export GNUPGHOME=$old_gpg_home
'';
type = types.lines;
description = ''
Additional shell commands inserted in the bootloader installer
script after generating menu entries.
'';
};
extraPerEntryConfig = mkOption {
default = "";
example = "root (hd0)";
type = types.lines;
description = ''
Additional GRUB commands inserted in the configuration file
at the start of each NixOS menu entry.
'';
};
extraEntries = mkOption {
default = "";
type = types.lines;
example = ''
# GRUB 2 example
menuentry "Windows 7" {
chainloader (hd0,4)+1
}
# GRUB 2 with UEFI example, chainloading another distro
menuentry "Fedora" {
set root=(hd1,1)
chainloader /efi/fedora/grubx64.efi
}
'';
description = ''
Any additional entries you want added to the GRUB boot menu.
'';
};
extraEntriesBeforeNixOS = mkOption {
default = false;
type = types.bool;
description = ''
Whether extraEntries are included before the default option.
'';
};
extraFiles = mkOption {
type = types.attrsOf types.path;
default = { };
example = literalExpression ''
{ "memtest.bin" = "''${pkgs.memtest86plus}/memtest.bin"; }
'';
description = ''
A set of files to be copied to {file}`/boot`.
Each attribute name denotes the destination file name in
{file}`/boot`, while the corresponding
attribute value specifies the source file.
'';
};
useOSProber = mkOption {
default = false;
type = types.bool;
description = ''
If set to true, append entries for other OSs detected by os-prober.
'';
};
splashImage = mkOption {
type = types.nullOr types.path;
example = literalExpression "./my-background.png";
description = ''
Background image used for GRUB.
Set to `null` to run GRUB in text mode.
::: {.note}
File must be one of .png, .tga, .jpg, or .jpeg. JPEG images must
not be progressive.
The image will be scaled if necessary to fit the screen.
:::
'';
};
backgroundColor = mkOption {
type = types.nullOr types.str;
example = "#7EBAE4";
default = null;
description = ''
Background color to be used for GRUB to fill the areas the image isn't filling.
'';
};
timeoutStyle = mkOption {
default = "menu";
type = types.enum [
"menu"
"countdown"
"hidden"
];
description = ''
- `menu` shows the menu.
- `countdown` uses a text-mode countdown.
- `hidden` hides GRUB entirely.
When using a theme, the default value (`menu`) is appropriate for the graphical countdown.
When attempting to do flicker-free boot, `hidden` should be used.
See the [GRUB documentation section about `timeout_style`](https://www.gnu.org/software/grub/manual/grub/html_node/timeout.html).
::: {.note}
If this option is set to countdown or hidden [...] and ESC or F4 are pressed, or SHIFT is held down during that time, it will display the menu and wait for input.
:::
From: [Simple configuration handling page, under GRUB_TIMEOUT_STYLE](https://www.gnu.org/software/grub/manual/grub/html_node/Simple-configuration.html).
'';
};
entryOptions = mkOption {
default = "--class nixos --unrestricted";
type = types.nullOr types.str;
description = ''
Options applied to the primary NixOS menu entry.
'';
};
subEntryOptions = mkOption {
default = "--class nixos";
type = types.nullOr types.str;
description = ''
Options applied to the secondary NixOS submenu entry.
'';
};
theme = mkOption {
type = types.nullOr types.path;
example = literalExpression ''"''${pkgs.kdePackages.breeze-grub}/grub/themes/breeze"'';
default = null;
description = ''
Path to the grub theme to be used.
'';
};
splashMode = mkOption {
type = types.enum [
"normal"
"stretch"
];
default = "stretch";
description = ''
Whether to stretch the image or show the image in the top-left corner unstretched.
'';
};
font = mkOption {
type = types.nullOr types.path;
default = "${realGrub}/share/grub/unicode.pf2";
defaultText = literalExpression ''"''${pkgs.grub2}/share/grub/unicode.pf2"'';
description = ''
Path to a TrueType, OpenType, or pf2 font to be used by Grub.
'';
};
fontSize = mkOption {
type = types.nullOr types.int;
example = 16;
default = null;
description = ''
Font size for the grub menu. Ignored unless `font`
is set to a ttf or otf font.
'';
};
gfxmodeEfi = mkOption {
default = "auto";
example = "1024x768";
type = types.str;
description = ''
The gfxmode to pass to GRUB when loading a graphical boot interface under EFI.
'';
};
gfxmodeBios = mkOption {
default = "1024x768";
example = "auto";
type = types.str;
description = ''
The gfxmode to pass to GRUB when loading a graphical boot interface under BIOS.
'';
};
gfxpayloadEfi = mkOption {
default = "keep";
example = "text";
type = types.str;
description = ''
The gfxpayload to pass to GRUB when loading a graphical boot interface under EFI.
'';
};
gfxpayloadBios = mkOption {
default = "text";
example = "keep";
type = types.str;
description = ''
The gfxpayload to pass to GRUB when loading a graphical boot interface under BIOS.
'';
};
configurationLimit = mkOption {
default = 100;
example = 120;
type = types.int;
description = ''
Maximum of configurations in boot menu. GRUB has problems when
there are too many entries.
'';
};
copyKernels = mkOption {
default = false;
type = types.bool;
description = ''
Whether the GRUB menu builder should copy kernels and initial
ramdisks to /boot. This is done automatically if /boot is
on a different partition than /.
'';
};
default = mkOption {
default = "0";
type = types.either types.int types.str;
apply = toString;
description = ''
Index of the default menu item to be booted.
Can also be set to "saved", which will make GRUB select
the menu item that was used at the last boot.
'';
};
fsIdentifier = mkOption {
default = "uuid";
type = types.enum [
"uuid"
"label"
"provided"
];
description = ''
Determines how GRUB will identify devices when generating the
configuration file. A value of uuid / label signifies that grub
will always resolve the uuid or label of the device before using
it in the configuration. A value of provided means that GRUB will
use the device name as show in {command}`df` or
{command}`mount`. Note, zfs zpools / datasets are ignored
and will always be mounted using their labels.
'';
};
zfsSupport = mkOption {
default = false;
type = types.bool;
description = ''
Whether GRUB should be built against libzfs.
'';
};
zfsPackage = mkOption {
type = types.package;
internal = true;
default = pkgs.zfs;
defaultText = literalExpression "pkgs.zfs";
description = ''
Which ZFS package to use if `config.boot.loader.grub.zfsSupport` is true.
'';
};
efiSupport = mkOption {
default = false;
type = types.bool;
description = ''
Whether GRUB should be built with EFI support.
'';
};
efiInstallAsRemovable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to invoke `grub-install` with
`--removable`.
Unless you turn this on, GRUB will install itself somewhere in
`boot.loader.efi.efiSysMountPoint` (exactly where
depends on other config variables). If you've set
`boot.loader.efi.canTouchEfiVariables` *AND* you
are currently booted in UEFI mode, then GRUB will use
`efibootmgr` to modify the boot order in the
EFI variables of your firmware to include this location. If you are
*not* booted in UEFI mode at the time GRUB is being installed, the
NVRAM will not be modified, and your system will not find GRUB at
boot time. However, GRUB will still return success so you may miss
the warning that gets printed ("`efibootmgr: EFI variables
are not supported on this system.`").
If you turn this feature on, GRUB will install itself in a
special location within `efiSysMountPoint` (namely
`EFI/boot/boot$arch.efi`) which the firmwares
are hardcoded to try first, regardless of NVRAM EFI variables.
To summarize, turn this on if:
- You are installing NixOS and want it to boot in UEFI mode,
but you are currently booted in legacy mode
- You want to make a drive that will boot regardless of
the NVRAM state of the computer (like a USB "removable" drive)
- You simply dislike the idea of depending on NVRAM
state to make your drive bootable
'';
};
enableCryptodisk = mkOption {
default = false;
type = types.bool;
description = ''
Enable support for encrypted partitions. GRUB should automatically
unlock the correct encrypted partition and look for filesystems.
'';
};
forceInstall = mkOption {
default = false;
type = types.bool;
description = ''
Whether to try and forcibly install GRUB even if problems are
detected. It is not recommended to enable this unless you know what
you are doing.
'';
};
forcei686 = mkOption {
default = false;
type = types.bool;
description = ''
Whether to force the use of a ia32 boot loader on x64 systems. Required
to install and run NixOS on 64bit x86 systems with 32bit (U)EFI.
'';
};
};
};
###### implementation
config = mkMerge [
{ boot.loader.grub.splashImage = mkDefault defaultSplash; }
(mkIf (cfg.splashImage == defaultSplash) {
boot.loader.grub.backgroundColor = mkDefault "#2F302F";
boot.loader.grub.splashMode = mkDefault "normal";
})
(mkIf cfg.enable {
boot.loader.grub.devices = mkIf (cfg.device != "") [ cfg.device ];
boot.loader.grub.mirroredBoots = mkIf (cfg.devices != [ ]) [
{
path = "/boot";
inherit (cfg) devices;
inherit (efi) efiSysMountPoint;
}
];
boot.loader.supportsInitrdSecrets = true;
system.systemBuilderArgs.configurationName = cfg.configurationName;
system.systemBuilderCommands = ''
echo -n "$configurationName" > $out/configuration-name
'';
system.build.installBootLoader =
let
install-grub-pl = pkgs.replaceVars ./install-grub.pl {
utillinux = pkgs.util-linux;
btrfsprogs = pkgs.btrfs-progs;
inherit (config.system.nixos) distroName;
# targets of a replacement in code
bootPath = null;
bootRoot = null;
};
perl = pkgs.perl.withPackages (
p: with p; [
FileSlurp
FileCopyRecursive
XMLLibXML
XMLSAX
XMLSAXBase
ListCompare
JSON
]
);
in
pkgs.writeScript "install-grub.sh" (
''
#!${pkgs.runtimeShell}
set -e
${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
''
+ flip concatMapStrings cfg.mirroredBoots (args: ''
${perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@
'')
+ cfg.extraInstallCommands
);
system.build.grub = grub;
# Common attribute for boot loaders so only one of them can be
# set at once.
system.boot.loader.id = "grub";
environment.systemPackages = mkIf (grub != null) [ grub ];
boot.loader.grub.extraPrepareConfig = concatStrings (
mapAttrsToList (
fileName: sourcePath:
flip concatMapStrings cfg.mirroredBoots (
args:
let
efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint;
in
''
${pkgs.coreutils}/bin/install -Dp ${escapeShellArg sourcePath} ${escapeShellArg efiSysMountPoint}/${escapeShellArg fileName}
''
)
) config.boot.loader.grub.extraFiles
);
assertions = [
{
assertion = cfg.mirroredBoots != [ ];
message =
"You must set the option boot.loader.grub.devices or "
+ "'boot.loader.grub.mirroredBoots' to make the system bootable.";
}
{
assertion =
cfg.efiSupport
|| all (c: c < 2) (mapAttrsToList (n: c: if n == "nodev" then 0 else c) bootDeviceCounters);
message = "You cannot have duplicated devices in mirroredBoots";
}
{
assertion = cfg.efiInstallAsRemovable -> cfg.efiSupport;
message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn on boot.loader.grub.efiSupport";
}
{
assertion = cfg.efiInstallAsRemovable -> !config.boot.loader.efi.canTouchEfiVariables;
message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn off boot.loader.efi.canTouchEfiVariables";
}
{
assertion = !(options.boot.loader.grub.version.isDefined && cfg.version == 1);
message = "Support for version 0.9x of GRUB was removed after being unsupported upstream for around a decade";
}
]
++ flip concatMap cfg.mirroredBoots (
args:
[
{
assertion = args.devices != [ ];
message = "A boot path cannot have an empty devices string in ${args.path}";
}
{
assertion = hasPrefix "/" args.path;
message = "Boot paths must be absolute, not ${args.path}";
}
{
assertion = if args.efiSysMountPoint == null then true else hasPrefix "/" args.efiSysMountPoint;
message = "EFI paths must be absolute, not ${args.efiSysMountPoint}";
}
]
++ forEach args.devices (device: {
assertion = device == "nodev" || hasPrefix "/" device;
message = "GRUB devices must be absolute paths, not ${device} in ${args.path}";
})
);
})
(mkIf options.boot.loader.grub.version.isDefined {
warnings = [
''
The boot.loader.grub.version option does not have any effect anymore, please remove it from your configuration.
''
];
})
];
imports = [
(mkRemovedOptionModule [ "boot" "loader" "grub" "bootDevice" ] "")
(mkRenamedOptionModule [ "boot" "copyKernels" ] [ "boot" "loader" "grub" "copyKernels" ])
(mkRenamedOptionModule [ "boot" "extraGrubEntries" ] [ "boot" "loader" "grub" "extraEntries" ])
(mkRenamedOptionModule
[ "boot" "extraGrubEntriesBeforeNixos" ]
[ "boot" "loader" "grub" "extraEntriesBeforeNixOS" ]
)
(mkRenamedOptionModule [ "boot" "grubDevice" ] [ "boot" "loader" "grub" "device" ])
(mkRenamedOptionModule [ "boot" "bootMount" ] [ "boot" "loader" "grub" "bootDevice" ])
(mkRenamedOptionModule [ "boot" "grubSplashImage" ] [ "boot" "loader" "grub" "splashImage" ])
(mkRemovedOptionModule [ "boot" "loader" "grub" "trustedBoot" ] ''
Support for Trusted GRUB has been removed, because the project
has been retired upstream.
'')
(mkRemovedOptionModule [ "boot" "loader" "grub" "extraInitrd" ] ''
This option has been replaced with the bootloader agnostic
boot.initrd.secrets option. To migrate to the initrd secrets system,
extract the extraInitrd archive into your main filesystem:
# zcat /boot/extra_initramfs.gz | cpio -idvmD /etc/secrets/initrd
/path/to/secret1
/path/to/secret2
then replace boot.loader.grub.extraInitrd with boot.initrd.secrets:
boot.initrd.secrets = {
"/path/to/secret1" = "/etc/secrets/initrd/path/to/secret1";
"/path/to/secret2" = "/etc/secrets/initrd/path/to/secret2";
};
See the boot.initrd.secrets option documentation for more information.
'')
];
}

View File

@@ -0,0 +1,806 @@
use strict;
use warnings;
use Class::Struct;
use XML::LibXML;
use File::Basename;
use File::Path qw(make_path rmtree);
use File::stat;
use File::Copy;
use File::Copy::Recursive qw(rcopy pathrm);
use File::Slurp;
use File::Temp;
use JSON;
use File::Find;
require List::Compare;
use POSIX;
use Cwd;
# system.build.toplevel path
my $defaultConfig = $ARGV[1] or die;
# Grub config XML generated by grubConfig function in grub.nix
my $dom = XML::LibXML->load_xml(location => $ARGV[0]);
sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); }
sub getList {
my ($name) = @_;
my @list = ();
foreach my $entry ($dom->findnodes("/expr/attrs/attr[\@name = '$name']/list/string/\@value")) {
$entry = $entry->findvalue(".") or die;
push(@list, $entry);
}
return @list;
}
sub readFile {
my ($fn) = @_;
# enable slurp mode: read entire file in one go
local $/ = undef;
open my $fh, "<", $fn
or return;
my $s = <$fh>;
close $fh;
# disable slurp mode
local $/ = "\n";
chomp $s;
return $s;
}
sub writeFile {
my ($fn, $s) = @_;
open my $fh, ">", $fn or die "cannot create $fn: $!\n";
print $fh $s or die "cannot write to $fn: $!\n";
close $fh or die "cannot close $fn: $!\n";
}
sub runCommand {
open(my $fh, "-|", @_) or die "Failed to execute: $@_\n";
my @ret = $fh->getlines();
close $fh;
return ($?, @ret);
}
my $grub = get("grub");
my $grubTarget = get("grubTarget");
my $extraConfig = get("extraConfig");
my $extraPrepareConfig = get("extraPrepareConfig");
my $extraPerEntryConfig = get("extraPerEntryConfig");
my $extraEntries = get("extraEntries");
my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true";
my $splashImage = get("splashImage");
my $splashMode = get("splashMode");
my $entryOptions = get("entryOptions");
my $subEntryOptions = get("subEntryOptions");
my $backgroundColor = get("backgroundColor");
my $configurationLimit = int(get("configurationLimit"));
my $copyKernels = get("copyKernels") eq "true";
my $timeout = int(get("timeout"));
my $timeoutStyle = get("timeoutStyle");
my $defaultEntry = get("default");
my $fsIdentifier = get("fsIdentifier");
my $grubEfi = get("grubEfi");
my $grubTargetEfi = get("grubTargetEfi");
my $bootPath = get("bootPath");
my $storePath = get("storePath");
my $canTouchEfiVariables = get("canTouchEfiVariables");
my $efiInstallAsRemovable = get("efiInstallAsRemovable");
my $efiSysMountPoint = get("efiSysMountPoint");
my $gfxmodeEfi = get("gfxmodeEfi");
my $gfxmodeBios = get("gfxmodeBios");
my $gfxpayloadEfi = get("gfxpayloadEfi");
my $gfxpayloadBios = get("gfxpayloadBios");
my $bootloaderId = get("bootloaderId");
my $forceInstall = get("forceInstall");
my $font = get("font");
my $theme = get("theme");
my $saveDefault = $defaultEntry eq "saved";
$ENV{'PATH'} = get("path");
print STDERR "updating GRUB 2 menu...\n";
make_path("$bootPath/grub", { mode => 0700 });
# Discover whether the bootPath is on the same filesystem as / and
# /nix/store. If not, then all kernels and initrds must be copied to
# the bootPath.
if (stat($bootPath)->dev != stat("/nix/store")->dev) {
$copyKernels = 1;
}
# Discover information about the location of the bootPath
struct(Fs => {
device => '$',
type => '$',
mount => '$',
});
sub PathInMount {
my ($path, $mount) = @_;
my @splitMount = split /\//, $mount;
my @splitPath = split /\//, $path;
if ($#splitPath < $#splitMount) {
return 0;
}
for (my $i = 0; $i <= $#splitMount; $i++) {
if ($splitMount[$i] ne $splitPath[$i]) {
return 0;
}
}
return 1;
}
# Figure out what filesystem is used for the directory with init/initrd/kernel files
sub GetFs {
my ($dir) = @_;
my $bestFs = Fs->new(device => "", type => "", mount => "");
foreach my $fs (read_file("/proc/self/mountinfo")) {
chomp $fs;
my @fields = split / /, $fs;
my $mountPoint = $fields[4];
my @mountOptions = split /,/, $fields[5];
# Skip the optional fields.
my $n = 6; $n++ while $fields[$n] ne "-"; $n++;
my $fsType = $fields[$n];
my $device = $fields[$n + 1];
my @superOptions = split /,/, $fields[$n + 2];
# Skip the bind-mount on /nix/store.
next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions);
# Skip mount point generated by systemd-efi-boot-generator?
next if $fsType eq "autofs";
# Ensure this matches the intended directory
next unless PathInMount($dir, $mountPoint);
# Is it better than our current match?
if (length($mountPoint) > length($bestFs->mount)) {
# -d performs a stat, which can hang forever on network file systems,
# so we only make this call last, when it's likely that this is the mount point we need.
next unless -d $mountPoint;
$bestFs = Fs->new(device => $device, type => $fsType, mount => $mountPoint);
}
}
return $bestFs;
}
struct (Grub => {
path => '$',
search => '$',
});
my $driveid = 1;
sub GrubFs {
my ($dir) = @_;
my $fs = GetFs($dir);
my $path = substr($dir, length($fs->mount));
if (substr($path, 0, 1) ne "/") {
$path = "/$path";
}
my $search = "";
# ZFS is completely separate logic as zpools are always identified by a label
# or custom UUID
if ($fs->type eq 'zfs') {
my $sid = index($fs->device, '/');
if ($sid < 0) {
$search = '--label ' . $fs->device;
$path = '/@' . $path;
} else {
$search = '--label ' . substr($fs->device, 0, $sid);
$path = '/' . substr($fs->device, $sid) . '/@' . $path;
}
} else {
my %types = ('uuid' => '--fs-uuid', 'label' => '--label');
if ($fsIdentifier eq 'provided') {
# If the provided dev is identifying the partition using a label or uuid,
# we should get the label / uuid and do a proper search
my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/;
if ($#matches > 1) {
die "Too many matched devices"
} elsif ($#matches == 1) {
$search = "$types{$matches[0]} $matches[1]"
}
} else {
# Determine the identifying type
$search = $types{$fsIdentifier} . ' ';
# Based on the type pull in the identifier from the system
my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid", "-o", "export", @{[$fs->device]});
if ($status != 0) {
die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}";
}
my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/;
if ($#matches != 0) {
die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n"
}
$search .= $matches[0];
}
# BTRFS is a special case in that we need to fix the referenced path based on subvolumes
if ($fs->type eq 'btrfs') {
my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "show", @{[$fs->mount]});
if ($status != 0) {
die "Failed to retrieve subvolume info for @{[$fs->mount]}\n";
}
my @ids = join("\n", @id_info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s;
if ($#ids > 0) {
die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n"
} elsif ($#ids == 0) {
my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "list", @{[$fs->mount]});
if ($status != 0) {
die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n";
}
my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/;
if ($#paths > 0) {
die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n";
} elsif ($#paths != 0) {
die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n";
}
$path = "/$paths[0]$path";
}
}
}
if (not $search eq "") {
$search = "search --set=drive$driveid " . $search;
$path = "(\$drive$driveid)$path";
$driveid += 1;
}
return Grub->new(path => $path, search => $search);
}
my $grubBoot = GrubFs($bootPath);
my $grubStore;
if ($copyKernels == 0) {
$grubStore = GrubFs($storePath);
}
# Generate the header.
my $conf .= "# Automatically generated. DO NOT EDIT THIS FILE!\n";
my @users = ();
foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) {
my $name = $user->findvalue('@name') or die;
my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value');
my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value');
my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value');
my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value');
if ($hashedPasswordFile) {
open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!";
$hashedPassword = <$f>;
chomp $hashedPassword;
}
if ($passwordFile) {
open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!";
$password = <$f>;
chomp $password;
}
if ($hashedPassword) {
if (index($hashedPassword, "grub.pbkdf2.") == 0) {
$conf .= "\npassword_pbkdf2 $name $hashedPassword";
}
else {
die "Password hash for GRUB user '$name' is not valid!";
}
}
elsif ($password) {
$conf .= "\npassword $name $password";
}
else {
die "GRUB user '$name' has no password!";
}
push(@users, $name);
}
if (@users) {
$conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n";
}
if ($copyKernels == 0) {
$conf .= "
" . $grubStore->search;
}
# FIXME: should use grub-mkconfig.
my $defaultEntryText = $defaultEntry;
if ($saveDefault) {
$defaultEntryText = "\"\${saved_entry}\"";
}
$conf .= "
" . $grubBoot->search . "
if [ -s \$prefix/grubenv ]; then
load_env
fi
# grub-reboot sets a one-time saved entry, which we process here and
# then delete.
if [ \"\${next_entry}\" ]; then
set default=\"\${next_entry}\"
set next_entry=
save_env next_entry
set timeout=1
set boot_once=true
else
set default=$defaultEntryText
set timeout=$timeout
fi
set timeout_style=$timeoutStyle
function savedefault {
if [ -z \"\${boot_once}\"]; then
saved_entry=\"\${chosen}\"
save_env saved_entry
fi
}
# Setup the graphics stack for bios and efi systems
if [ \"\${grub_platform}\" = \"efi\" ]; then
insmod efi_gop
insmod efi_uga
else
insmod vbe
fi
";
if ($font) {
copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n";
$conf .= "
insmod font
if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
insmod gfxterm
if [ \"\${grub_platform}\" = \"efi\" ]; then
set gfxmode=$gfxmodeEfi
set gfxpayload=$gfxpayloadEfi
else
set gfxmode=$gfxmodeBios
set gfxpayload=$gfxpayloadBios
fi
terminal_output gfxterm
fi
";
}
if ($splashImage) {
# Keeps the image's extension.
my ($filename, $dirs, $suffix) = fileparse($splashImage, qr"\..[^.]*$");
# The module for jpg is jpeg.
if ($suffix eq ".jpg") {
$suffix = ".jpeg";
}
if ($backgroundColor) {
$conf .= "
background_color '$backgroundColor'
";
}
copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n";
$conf .= "
insmod " . substr($suffix, 1) . "
if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
set color_normal=white/black
set color_highlight=black/white
else
set menu_color_normal=cyan/blue
set menu_color_highlight=white/blue
fi
";
}
rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme";
if ($theme) {
# Copy theme
rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n";
# Detect which modules will need to be loaded
my $with_png = 0;
my $with_jpeg = 0;
find({ wanted => sub {
if ($_ =~ /\.png$/i) {
$with_png = 1;
}
elsif ($_ =~ /\.jpe?g$/i) {
$with_jpeg = 1;
}
}, no_chdir => 1 }, $theme);
if ($with_png) {
$conf .= "
insmod png
"
}
if ($with_jpeg) {
$conf .= "
insmod jpeg
"
}
$conf .= "
# Sets theme.
set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt
export theme
# Load theme fonts, if any
";
find( { wanted => sub {
if ($_ =~ /\.pf2$/i) {
$font = File::Spec->abs2rel($File::Find::name, $theme);
$conf .= "
loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font
";
}
}, no_chdir => 1 }, $theme );
}
$conf .= "$extraConfig\n";
# Generate the menu entries.
$conf .= "\n";
my %copied;
make_path("$bootPath/kernels", { mode => 0755 }) if $copyKernels;
sub copyToKernelsDir {
my ($path) = @_;
return $grubStore->path . substr($path, length("/nix/store")) unless $copyKernels;
$path =~ /\/nix\/store\/(.*)/ or die;
my $name = $1; $name =~ s/\//-/g;
my $dst = "$bootPath/kernels/$name";
# Don't copy the file if $dst already exists. This means that we
# have to create $dst atomically to prevent partially copied
# kernels or initrd if this script is ever interrupted.
if (! -e $dst) {
my $tmp = "$dst.tmp";
copy $path, $tmp or die "cannot copy $path to $tmp: $!\n";
rename $tmp, $dst or die "cannot rename $tmp to $dst: $!\n";
}
$copied{$dst} = 1;
return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name";
}
sub addEntry {
my ($name, $path, $options, $current) = @_;
return unless -e "$path/kernel" && -e "$path/initrd";
my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd"));
# Include second initrd with secrets
if (-e -x "$path/append-initrd-secrets") {
# Name the initrd secrets after the system from which they're derived.
my $systemName = basename(Cwd::abs_path("$path"));
my $initrdSecretsPath = "$bootPath/kernels/$systemName-secrets";
make_path(dirname($initrdSecretsPath), { mode => 0755 });
my $oldUmask = umask;
# Make sure initrd is not world readable (won't work if /boot is FAT)
umask 0137;
my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX");
if (system("$path/append-initrd-secrets", $initrdSecretsPathTemp) != 0) {
if ($current) {
die "failed to create initrd secrets $!\n";
} else {
say STDERR "warning: failed to create initrd secrets for \"$name\", an older generation";
say STDERR "note: this is normal after having removed or renamed a file in `boot.initrd.secrets`";
}
}
# Check whether any secrets were actually added
if (-e $initrdSecretsPathTemp && ! -z _) {
rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n";
$copied{$initrdSecretsPath} = 1;
$initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$systemName-secrets";
} else {
unlink $initrdSecretsPathTemp;
rmdir dirname($initrdSecretsPathTemp);
}
umask $oldUmask;
}
my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef;
# FIXME: $confName
my $kernelParams =
"init=" . Cwd::abs_path("$path/init") . " " .
readFile("$path/kernel-params");
my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : "";
$conf .= "menuentry \"$name\" " . $options . " {\n";
if ($saveDefault) {
$conf .= " savedefault\n";
}
$conf .= $grubBoot->search . "\n";
if ($copyKernels == 0) {
$conf .= $grubStore->search . "\n";
}
$conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig;
$conf .= " multiboot $xen $xenParams\n" if $xen;
$conf .= " " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n";
$conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n";
$conf .= "}\n\n";
}
sub addGeneration {
my ($name, $nameSuffix, $path, $options, $current) = @_;
# Do not search for grand children
my @links = sort (glob "$path/specialisation/*");
if ($current != 1 && scalar(@links) != 0) {
$conf .= "submenu \"$name$nameSuffix\" --class submenu {\n";
}
addEntry("$name" . (scalar(@links) == 0 ? "" : " - Default") . $nameSuffix, $path, $options, $current);
# Find all the children of the current default configuration
# Do not search for grand children
foreach my $link (@links) {
my $entryName = "";
my $cfgName = readFile("$link/configuration-name");
my $date = strftime("%F", localtime(lstat($link)->mtime));
my $version =
-e "$link/nixos-version"
? readFile("$link/nixos-version")
: basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
if ($cfgName) {
$entryName = $cfgName;
} else {
my $linkname = basename($link);
$entryName = "($linkname - $date - $version)";
}
addEntry("$name - $entryName", $link, "", 1);
}
if ($current != 1 && scalar(@links) != 0) {
$conf .= "}\n";
}
}
# Add default entries.
$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
addGeneration("@distroName@", "", $defaultConfig, $entryOptions, 1);
$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
my $grubBootPath = $grubBoot->path;
# extraEntries could refer to @bootRoot@, which we have to substitute
$conf =~ s/\@bootRoot\@/$grubBootPath/g;
# Emit submenus for all system profiles.
sub addProfile {
my ($profile, $description) = @_;
# Add entries for all generations of this profile.
$conf .= "submenu \"$description\" --class submenu {\n";
sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; }
my @links = sort
{ nrFromGen($b) <=> nrFromGen($a) }
(glob "$profile-*-link");
my $curEntry = 0;
foreach my $link (@links) {
last if $curEntry++ >= $configurationLimit;
if (! -e "$link/nixos-version") {
warn "skipping corrupt system profile entry $link\n";
next;
}
my $date = strftime("%F", localtime(lstat($link)->mtime));
my $version =
-e "$link/nixos-version"
? readFile("$link/nixos-version")
: basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
addGeneration("@distroName@ - Configuration " . nrFromGen($link), " ($date - $version)", $link, $subEntryOptions, 0);
}
$conf .= "}\n";
}
addProfile "/nix/var/nix/profiles/system", "@distroName@ - All configurations";
for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") {
my $name = basename($profile);
next unless $name =~ /^\w+$/;
addProfile $profile, "@distroName@ - Profile '$name'";
}
# extraPrepareConfig could refer to @bootPath@, which we have to substitute
$extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g;
# Run extraPrepareConfig in sh
if ($extraPrepareConfig ne "") {
system((get("shell"), "-c", $extraPrepareConfig));
}
# write the GRUB config.
my $confFile = "$bootPath/grub/grub.cfg";
my $tmpFile = $confFile . ".tmp";
writeFile($tmpFile, $conf);
# check whether to install GRUB EFI or not
sub getEfiTarget {
if (($grub ne "") && ($grubEfi ne "")) {
# EFI can only be installed when target is set;
# A target is also required then for non-EFI grub
if (($grubTarget eq "") || ($grubTargetEfi eq "")) { die }
else { return "both" }
} elsif (($grub ne "") && ($grubEfi eq "")) {
# TODO: It would be safer to disallow non-EFI grub installation if no target is given.
# If no target is given, then grub auto-detects the target which can lead to errors.
# E.g. it seems as if grub would auto-detect a EFI target based on the availability
# of a EFI partition.
# However, it seems as auto-detection is currently relied on for non-x86_64 and non-i386
# architectures in NixOS. That would have to be fixed in the nixos modules first.
return "no"
} elsif (($grub eq "") && ($grubEfi ne "")) {
# EFI can only be installed when target is set;
if ($grubTargetEfi eq "") { die }
else {return "only" }
} else {
# prevent an installation if neither grub nor grubEfi is given
return "neither"
}
}
my $efiTarget = getEfiTarget();
# Append entries detected by os-prober
if (get("useOSProber") eq "true") {
if ($saveDefault) {
# os-prober will read this to determine if "savedefault" should be added to generated entries
$ENV{'GRUB_SAVEDEFAULT'} = "true";
}
my $targetpackage = ($efiTarget eq "no") ? $grub : $grubEfi;
system(get("shell"), "-c", "pkgdatadir=$targetpackage/share/grub $targetpackage/etc/grub.d/30_os-prober >> $tmpFile");
}
# Atomically switch to the new config
rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile: $!\n";
# Remove obsolete files from $bootPath/kernels.
foreach my $fn (glob "$bootPath/kernels/*") {
next if defined $copied{$fn};
print STDERR "removing obsolete file $fn\n";
unlink $fn;
}
#
# Install GRUB if the parameters changed from the last time we installed it.
#
struct(GrubState => {
name => '$',
version => '$',
efi => '$',
devices => '$',
efiMountPoint => '$',
extraGrubInstallArgs => '@',
});
# If you add something to the state file, only add it to the end
# because it is read line-by-line.
sub readGrubState {
my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () );
open my $fh, "<", "$bootPath/grub/state" or return $defaultGrubState;
local $/ = "\n";
my $name = <$fh>;
chomp($name);
my $version = <$fh>;
chomp($version);
my $efi = <$fh>;
chomp($efi);
my $devices = <$fh>;
chomp($devices);
my $efiMountPoint = <$fh>;
chomp($efiMountPoint);
# Historically, arguments in the state file were one per each line, but that
# gets really messy when newlines are involved, structured arguments
# like lists are needed (they have to have a separator encoding), or even worse,
# when we need to remove a setting in the future. Thus, the 6th line is a JSON
# object that can store structured data, with named keys, and all new state
# should go in there.
my $jsonStateLine = <$fh>;
# For historical reasons we do not check the values above for un-definedness
# (that is, when the state file has too few lines and EOF is reached),
# because the above come from the first version of this logic and are thus
# guaranteed to be present.
$jsonStateLine = defined $jsonStateLine ? $jsonStateLine : '{}'; # empty JSON object
chomp($jsonStateLine);
if ($jsonStateLine eq "") {
$jsonStateLine = '{}'; # empty JSON object
}
my %jsonState = %{decode_json($jsonStateLine)};
my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : ();
close $fh;
my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs );
return $grubState
}
my @deviceTargets = getList('devices');
my $prevGrubState = readGrubState();
my @prevDeviceTargets = split/,/, $prevGrubState->devices;
my @extraGrubInstallArgs = getList('extraGrubInstallArgs');
my @prevExtraGrubInstallArgs = @{$prevGrubState->extraGrubInstallArgs};
my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference());
my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference());
my $nameDiffer = get("fullName") ne $prevGrubState->name;
my $versionDiffer = get("fullVersion") ne $prevGrubState->version;
my $efiDiffer = $efiTarget ne $prevGrubState->efi;
my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint;
if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") {
warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER";
$ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1";
}
my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
# install a symlink so that grub can detect the boot drive
my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!";
symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!";
# install non-EFI GRUB
if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
foreach my $dev (@deviceTargets) {
next if $dev eq "nodev";
print STDERR "installing the GRUB 2 boot loader on $dev...\n";
my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs);
if ($forceInstall eq "true") {
push @command, "--force";
}
if ($grubTarget ne "") {
push @command, "--target=$grubTarget";
}
(system @command) == 0 or die "$0: installation of GRUB on $dev failed: $!\n";
}
}
# install EFI GRUB
if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) {
print STDERR "installing the GRUB 2 boot loader into $efiSysMountPoint...\n";
my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs);
if ($forceInstall eq "true") {
push @command, "--force";
}
push @command, "--bootloader-id=$bootloaderId";
if ($canTouchEfiVariables ne "true") {
push @command, "--no-nvram";
push @command, "--removable" if $efiInstallAsRemovable eq "true";
}
(system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed: $!\n";
}
# update GRUB state file
if ($requireNewInstall != 0) {
# Temp file for atomic rename.
my $stateFile = "$bootPath/grub/state";
my $stateFileTmp = $stateFile . ".tmp";
open my $fh, ">", "$stateFileTmp" or die "cannot create $stateFileTmp: $!\n";
print $fh get("fullName"), "\n" or die;
print $fh get("fullVersion"), "\n" or die;
print $fh $efiTarget, "\n" or die;
print $fh join( ",", @deviceTargets ), "\n" or die;
print $fh $efiSysMountPoint, "\n" or die;
my %jsonState = (
extraGrubInstallArgs => \@extraGrubInstallArgs
);
my $jsonStateLine = encode_json(\%jsonState);
print $fh $jsonStateLine, "\n" or die;
close $fh or die;
# Atomically switch to the new state file
rename $stateFileTmp, $stateFile or die "cannot rename $stateFileTmp to $stateFile: $!\n";
}

View File

@@ -0,0 +1,65 @@
# This module adds a scripted iPXE entry to the GRUB boot menu.
{
config,
lib,
pkgs,
...
}:
with lib;
let
scripts = builtins.attrNames config.boot.loader.grub.ipxe;
grubEntry = name: ''
menuentry "iPXE - ${name}" {
linux16 @bootRoot@/ipxe.lkrn
initrd16 @bootRoot@/${name}.ipxe
}
'';
scriptFile =
name:
let
value = builtins.getAttr name config.boot.loader.grub.ipxe;
in
if builtins.typeOf value == "path" then value else builtins.toFile "${name}.ipxe" value;
in
{
options = {
boot.loader.grub.ipxe = mkOption {
type = types.attrsOf (types.either types.path types.str);
description = ''
Set of iPXE scripts available for
booting from the GRUB boot menu.
'';
default = { };
example = literalExpression ''
{ demo = '''
#!ipxe
dhcp
chain http://boot.ipxe.org/demo/boot.php
''';
}
'';
};
};
config = mkIf (builtins.length scripts != 0) {
boot.loader.grub.extraEntries = toString (map grubEntry scripts);
boot.loader.grub.extraFiles = {
"ipxe.lkrn" = "${pkgs.ipxe}/ipxe.lkrn";
}
// builtins.listToAttrs (
map (name: {
name = name + ".ipxe";
value = scriptFile name;
}) scripts
);
};
}

View File

@@ -0,0 +1,74 @@
# This module adds Memtest86+ to the GRUB boot menu.
{
config,
lib,
pkgs,
...
}:
with lib;
let
memtest86 = pkgs.memtest86plus;
cfg = config.boot.loader.grub.memtest86;
in
{
options = {
boot.loader.grub.memtest86 = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Make Memtest86+, a memory testing program, available from the GRUB
boot menu.
'';
};
params = mkOption {
default = [ ];
example = [ "console=ttyS0,115200" ];
type = types.listOf types.str;
description = ''
Parameters added to the Memtest86+ command line. As of memtest86+ 5.01
the following list of (apparently undocumented) parameters are
accepted:
- `console=...`, set up a serial console.
Examples:
`console=ttyS0`,
`console=ttyS0,9600` or
`console=ttyS0,115200n8`.
- `btrace`, enable boot trace.
- `maxcpus=N`, limit number of CPUs.
- `onepass`, run one pass and exit if there
are no errors.
- `tstlist=...`, list of tests to run.
Example: `0,1,2`.
- `cpumask=...`, set a CPU mask, to select CPUs
to use for testing.
This list of command line options was obtained by reading the
Memtest86+ source code.
'';
};
};
};
config = mkIf cfg.enable {
boot.loader.grub.extraEntries = ''
menuentry "Memtest86+" {
linux @bootRoot@/memtest.bin ${toString cfg.params}
}
'';
boot.loader.grub.extraFiles."memtest.bin" = "${memtest86}/memtest.bin";
};
}

View File

@@ -0,0 +1,88 @@
#! @bash@/bin/sh -e
shopt -s nullglob
export PATH=/empty:@path@
if test $# -ne 1; then
echo "Usage: init-script-builder.sh DEFAULT-CONFIG"
exit 1
fi
defaultConfig="$1"
[ "$(stat -f -c '%i' /)" = "$(stat -f -c '%i' /boot)" ] || {
# see grub-menu-builder.sh
echo "WARNING: /boot being on a different filesystem not supported by init-script-builder.sh"
}
target="/sbin/init"
targetOther="/boot/init-other-configurations-contents.txt"
tmp="$target.tmp"
tmpOther="$targetOther.tmp"
configurationCounter=0
# Add an entry to $targetOther
addEntry() {
local name="$1"
local path="$2"
local shortSuffix="$3"
configurationCounter=$((configurationCounter + 1))
local stage2=$path/init
content="$(
echo "#!/bin/sh"
echo "# $name"
echo "# created by init-script-builder.sh"
echo "exec $stage2"
)"
[ "$path" != "$defaultConfig" ] || {
echo "$content" > $tmp
echo "# older configurations: $targetOther" >> $tmp
chmod +x $tmp
}
echo -e "$content\n\n" >> $tmpOther
}
mkdir -p /boot /sbin
addEntry "@distroName@ - Default" $defaultConfig ""
# Add all generations of the system profile to the menu, in reverse
# (most recent to least recent) order.
for link in $((ls -d $defaultConfig/specialisation/* ) | sort -n); do
date=$(stat --printf="%y\n" $link | sed 's/\..*//')
addEntry "@distroName@ - variation" $link ""
done
for generation in $(
(cd /nix/var/nix/profiles && ls -d system-*-link) \
| sed 's/system-\([0-9]\+\)-link/\1/' \
| sort -n -r); do
link=/nix/var/nix/profiles/system-$generation-link
date=$(stat --printf="%y\n" $link | sed 's/\..*//')
if [ -d $link/kernel ]; then
kernelVersion=$(cd $(dirname $(readlink -f $link/kernel))/lib/modules && echo *)
suffix="($date - $kernelVersion)"
else
suffix="($date)"
fi
addEntry "@distroName@ - Configuration $generation $suffix" $link "$generation ($date)"
done
mv $tmpOther $targetOther
mv $tmp $target

View File

@@ -0,0 +1,62 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
initScriptBuilder = pkgs.replaceVarsWith {
src = ./init-script-builder.sh;
isExecutable = true;
replacements = {
inherit (pkgs) bash;
inherit (config.system.nixos) distroName;
path = lib.makeBinPath [
pkgs.coreutils
pkgs.gnused
pkgs.gnugrep
];
};
};
in
{
###### interface
options = {
boot.loader.initScript = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Some systems require a /sbin/init script which is started.
Or having it makes starting NixOS easier.
This applies to some kind of hosting services and user mode linux.
Additionally this script will create
/boot/init-other-configurations-contents.txt containing
contents of remaining configurations. You can copy paste them into
/sbin/init manually running a rescue system or such.
'';
};
};
};
###### implementation
config = mkIf config.boot.loader.initScript.enable {
system.build.installBootLoader = initScriptBuilder;
};
}

View File

@@ -0,0 +1,661 @@
#!@python3@/bin/python3 -B
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple
import datetime
import hashlib
import json
from ctypes import CDLL
import os
import psutil
import re
import shutil
import subprocess
import sys
import tempfile
import textwrap
@dataclass
class XenBootSpec:
"""Represent the bootspec extension for Xen dom0 kernels"""
efiPath: str
multibootPath: str
params: List[str]
version: str
@dataclass
class BootSpec:
system: str
init: str
kernel: str
kernelParams: List[str]
label: str
toplevel: str
specialisations: Dict[str, "BootSpec"]
xen: XenBootSpec | None
initrd: str | None = None
initrdSecrets: str | None = None
install_config = json.load(open('@configPath@', 'r'))
libc = CDLL("libc.so.6")
limine_install_dir: Optional[str] = None
can_use_direct_paths = False
paths: Dict[str, bool] = {}
def config(*path: str) -> Optional[Any]:
result = install_config
for component in path:
result = result[component]
return result
def get_system_path(profile: str = 'system', gen: Optional[str] = None, spec: Optional[str] = None) -> str:
basename = f'{profile}-{gen}-link' if gen is not None else profile
profiles_dir = '/nix/var/nix/profiles'
if profile == 'system':
result = os.path.join(profiles_dir, basename)
else:
result = os.path.join(profiles_dir, 'system-profiles', basename)
if spec is not None:
result = os.path.join(result, 'specialisation', spec)
return result
def get_profiles() -> List[str]:
profiles_dir = '/nix/var/nix/profiles/system-profiles/'
dirs = os.listdir(profiles_dir) if os.path.isdir(profiles_dir) else []
return [path for path in dirs if not path.endswith('-link')]
def get_gens(profile: str = 'system') -> List[Tuple[int, List[str]]]:
nix_env = os.path.join(str(config('nixPath')), 'bin', 'nix-env')
output = subprocess.check_output([
nix_env, '--list-generations',
'-p', get_system_path(profile),
'--option', 'build-users-group', '',
], universal_newlines=True)
gen_lines = output.splitlines()
gen_nums = [int(line.split()[0]) for line in gen_lines]
return [gen for gen in gen_nums][-config('maxGenerations'):]
def is_encrypted(device: str) -> bool:
for name in config('luksDevices'):
if os.readlink(os.path.join('/dev/mapper', name)) == os.readlink(device):
return True
return False
def is_fs_type_supported(fs_type: str) -> bool:
return fs_type.startswith('vfat')
def get_dest_file(path: str) -> str:
package_id = os.path.basename(os.path.dirname(path))
suffix = os.path.basename(path)
return f'{package_id}-{suffix}'
def get_dest_path(path: str, target: str) -> str:
dest_file = get_dest_file(path)
return os.path.join(str(limine_install_dir), target, dest_file)
def get_copied_path_uri(path: str, target: str) -> str:
result = ''
dest_file = get_dest_file(path)
dest_path = get_dest_path(path, target)
if not os.path.exists(dest_path):
copy_file(path, dest_path)
else:
paths[dest_path] = True
path_with_prefix = os.path.join('/limine', target, dest_file)
result = f'boot():{path_with_prefix}'
if config('validateChecksums'):
with open(path, 'rb') as file:
b2sum = hashlib.blake2b()
b2sum.update(file.read())
result += f'#{b2sum.hexdigest()}'
return result
def get_path_uri(path: str) -> str:
return get_copied_path_uri(path, "")
def get_file_uri(profile: str, gen: Optional[str], spec: Optional[str], name: str) -> str:
gen_path = get_system_path(profile, gen, spec)
path_in_store = os.path.realpath(os.path.join(gen_path, name))
return get_path_uri(path_in_store)
def get_kernel_uri(kernel_path: str) -> str:
return get_copied_path_uri(kernel_path, "kernels")
def bootjson_to_bootspec(bootjson: dict) -> BootSpec:
specialisations = bootjson['org.nixos.specialisation.v1']
specialisations = {k: bootjson_to_bootspec(v) for k, v in specialisations.items()}
xen = None
if 'org.xenproject.bootspec.v2' in bootjson:
xen = bootjson['org.xenproject.bootspec.v2']
return BootSpec(
**bootjson['org.nixos.bootspec.v1'],
specialisations=specialisations,
xen=xen,
)
def generate_xen_efi_files(
bootspec: BootSpec,
gen: str
) -> str:
"""Generate a Xen EFI xen.cfg file, and copy required files in place.
Assumes the bootspec has already been validated as having the requried
Xen keys.
Arguments:
bootspec -- the NixOS BootSpec requiring Xen EFI configuration
gen -- The system generation requiring Xen EFI configuration
Returns the path to the Xen EFI binary
"""
xen_efi_boot_path = get_copied_path_uri(bootspec.xen['efiPath'], f'xen/{gen}')
xen_efi_path = get_dest_path(bootspec.xen['efiPath'], f'xen/{gen}')
xen_efi_cfg_dir = os.path.dirname(xen_efi_path)
xen_efi_cfg_path = xen_efi_path[:-4] + '.cfg'
if not os.path.exists(xen_efi_cfg_dir):
os.makedirs(xen_efi_cfg_dir)
xen_efi_cfg = (
f'default=nixos{gen}\n\n' +
f'[nixos{gen}]\n'
)
# set xen dom0 parameters
if 'params' in bootspec.xen and len(bootspec.xen['params']) > 0:
xen_efi_cfg += 'options=' + ' '.join(bootspec.xen['params']).strip() + '\n'
# set kernel and copy in-place
xen_efi_kernel_path = get_dest_path(bootspec.kernel, f'xen/{gen}')
copy_file(bootspec.kernel, xen_efi_kernel_path)
xen_efi_cfg += (
'kernel=' + os.path.basename(xen_efi_kernel_path) + ' '
+ ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip()
+ '\n'
)
# set ramdisk and copy initrd in-place
if bootspec.initrd:
xen_efi_initrd_path = get_dest_path(bootspec.initrd, f'xen/{gen}')
copy_file(bootspec.initrd, xen_efi_initrd_path)
xen_efi_cfg += 'ramdisk=' + os.path.basename(xen_efi_initrd_path) + '\n'
with open(xen_efi_cfg_path, 'w') as xen_efi_cfg_file:
xen_efi_cfg_file.write(xen_efi_cfg)
return xen_efi_boot_path
def xen_config_entry(
levels: int, bootspec: BootSpec, xenVersion: str, gen: str, time: str, efi: bool
) -> str:
"""Generate EFI and BIOS entries for Xen dom0 kernels.
Arguments:
levels -- The number of Limine menu levels for entries
bootspec -- The NixOS BootSpec used for generating this Limine configuration
xenVersion -- The version of Xen the entry is generated for, from the boot extension
gen -- The system generation these entries are generated for
time -- The build time for the configuration
efi -- True if EFI protocol should be used for this entry
"""
# generate Xen menu label for the current generation
entry = '/' * levels + f'Generation {gen} with Xen {xenVersion}' + (' EFI\n' if efi else '\n')
entry += f'comment: Xen {xenVersion} {bootspec.label}, built on {time}\n'
# load Xen dom0 as the executable, using multiboot for EFI & BIOS
if (
efi and
'multibootPath' in bootspec.xen and
len(bootspec.xen['multibootPath']) > 0 and
os.path.exists(bootspec.xen['multibootPath'])
):
# Use the EFI protocol and generate Xen EFI configuration
# files and directories which are loaded by Xen's EFI binary
# directly.
# Ideally both EFI and BIOS booting would use multiboot2,
# however Limine's multiboot2 module has trouble finding
# an entry-point in Xen's multiboot binary, and multiboot1
# doesn't work under EFI.
# Upstream Limine issue #482
entry += 'protocol: efi\n'
entry += (
'path: ' + generate_xen_efi_files(bootspec, gen) + '\n'
)
elif (
'multibootPath' in bootspec.xen and
len(bootspec.xen['multibootPath']) > 0 and
os.path.exists(bootspec.xen['multibootPath'])
):
# Use multiboot1 if not generating an EFI entry, as multiboot2
# doesn't work under Limine for booting Xen.
# Upstream Limine issue #483
entry += 'protocol: multiboot\n'
entry += (
'path: ' + get_copied_path_uri(bootspec.xen['multibootPath'], f'xen/{gen}') + '\n'
)
# set params as the multiboot executable's parameters
if 'params' in bootspec.xen and len(bootspec.xen['params']) > 0:
# TODO: Understand why the first argument is ignored below?
# --- to work around first argument being ignored
entry += (
'cmdline: -- ' + ' '.join(bootspec.xen['params']).strip() + '\n'
)
# load the linux kernel as the second module
entry += 'module_path: ' + get_kernel_uri(bootspec.kernel) + '\n'
# set kernel parameters as the parameters to the first module
# TODO: Understand why the first argument is ignored below?
# --- to work around first argument being ignored
entry += (
'module_string: -- '
+ ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip()
+ '\n'
)
if bootspec.initrd:
# the final module is the initrd
entry += 'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n'
return entry
def config_entry(levels: int, bootspec: BootSpec, label: str, time: str) -> str:
entry = '/' * levels + label + '\n'
entry += 'protocol: linux\n'
entry += f'comment: {bootspec.label}, built on {time}\n'
entry += 'kernel_path: ' + get_kernel_uri(bootspec.kernel) + '\n'
entry += 'cmdline: ' + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() + '\n'
if bootspec.initrd:
entry += f'module_path: ' + get_kernel_uri(bootspec.initrd) + '\n'
if bootspec.initrdSecrets:
base_path = str(limine_install_dir) + '/kernels/'
initrd_secrets_path = base_path + os.path.basename(bootspec.toplevel) + '-secrets'
if not os.path.exists(base_path):
os.makedirs(base_path)
old_umask = os.umask(0o137)
initrd_secrets_path_temp = tempfile.mktemp(os.path.basename(bootspec.toplevel) + '-secrets')
if os.system(bootspec.initrdSecrets + " " + initrd_secrets_path_temp) != 0:
print(f'warning: failed to create initrd secrets for "{label}"', file=sys.stderr)
print(f'note: if this is an older generation there is nothing to worry about')
if os.path.exists(initrd_secrets_path_temp):
copy_file(initrd_secrets_path_temp, initrd_secrets_path)
os.unlink(initrd_secrets_path_temp)
entry += 'module_path: ' + get_kernel_uri(initrd_secrets_path) + '\n'
os.umask(old_umask)
return entry
def generate_config_entry(profile: str, gen: str, special: bool) -> str:
time = datetime.datetime.fromtimestamp(os.stat(get_system_path(profile,gen), follow_symlinks=False).st_mtime).strftime("%F %H:%M:%S")
boot_json = json.load(open(os.path.join(get_system_path(profile, gen), 'boot.json'), 'r'))
boot_spec = bootjson_to_bootspec(boot_json)
specialisation_list = boot_spec.specialisations.items()
depth = 2
entry = ""
# Xen, if configured, should be listed first for each generation
if boot_spec.xen and 'version' in boot_spec.xen:
xen_version = boot_spec.xen['version']
if config('efiSupport'):
entry += xen_config_entry(2, boot_spec, xen_version, gen, time, True)
entry += xen_config_entry(2, boot_spec, xen_version, gen, time, False)
if len(specialisation_list) > 0:
depth += 1
entry += '/' * (depth-1)
if special:
entry += '+'
entry += f'Generation {gen}' + '\n'
entry += config_entry(depth, boot_spec, f'Default', str(time))
else:
entry += config_entry(depth, boot_spec, f'Generation {gen}', str(time))
for spec, spec_boot_spec in specialisation_list:
entry += config_entry(depth, spec_boot_spec, f'{spec}', str(time))
return entry
def find_disk_device(part: str) -> str:
part = os.path.realpath(part)
part = part.removeprefix('/dev/')
disk = os.path.realpath(os.path.join('/sys', 'class', 'block', part))
disk = os.path.dirname(disk)
return os.path.join('/dev', os.path.basename(disk))
def find_mounted_device(path: str) -> str:
path = os.path.abspath(path)
while not os.path.ismount(path):
path = os.path.dirname(path)
devices = [x for x in psutil.disk_partitions() if x.mountpoint == path]
assert len(devices) == 1
return devices[0].device
def copy_file(from_path: str, to_path: str):
dirname = os.path.dirname(to_path)
if not os.path.exists(dirname):
os.makedirs(dirname)
shutil.copyfile(from_path, to_path + ".tmp")
os.rename(to_path + ".tmp", to_path)
paths[to_path] = True
def option_from_config(name: str, config_path: List[str], conversion: Callable[[str], str] | None = None) -> str:
if config(*config_path):
return f'{name}: {conversion(config(*config_path)) if conversion else config(*config_path)}\n'
return ''
def install_bootloader() -> None:
global limine_install_dir
boot_fs = None
for mount_point, fs in config('fileSystems').items():
if mount_point == '/boot':
boot_fs = fs
if config('efiSupport'):
limine_install_dir = os.path.join(str(config('efiMountPoint')), 'limine')
elif boot_fs and is_fs_type_supported(boot_fs['fsType']) and not is_encrypted(boot_fs['device']):
limine_install_dir = '/boot/limine'
else:
possible_causes = []
if not boot_fs:
possible_causes.append(f'/limine on the boot partition (not present)')
else:
is_boot_fs_type_ok = is_fs_type_supported(boot_fs['fsType'])
is_boot_fs_encrypted = is_encrypted(boot_fs['device'])
possible_causes.append(f'/limine on the boot partition ({is_boot_fs_type_ok=} {is_boot_fs_encrypted=})')
causes_str = textwrap.indent('\n'.join(possible_causes), ' - ')
raise Exception(textwrap.dedent('''
Could not find a valid place for Limine configuration files!'
Possible candidates that were ruled out:
''') + causes_str + textwrap.dedent('''
Limine cannot be installed on a system without an unencrypted
partition formatted as FAT.
'''))
if config('secureBoot', 'enable') and not config('secureBoot', 'createAndEnrollKeys') and not os.path.exists("/var/lib/sbctl"):
print("There are no sbctl secure boot keys present. Please generate some.")
sys.exit(1)
if not os.path.exists(limine_install_dir):
os.makedirs(limine_install_dir)
else:
for dir, dirs, files in os.walk(limine_install_dir, topdown=True):
for file in files:
paths[os.path.join(dir, file)] = False
limine_xen_dir = os.path.join(limine_install_dir, 'xen')
if os.path.exists(limine_xen_dir):
print(f'cleaning {limine_xen_dir}')
shutil.rmtree(limine_xen_dir)
os.makedirs(limine_xen_dir)
profiles = [('system', get_gens())]
for profile in get_profiles():
profiles += [(profile, get_gens(profile))]
timeout = config('timeout')
editor_enabled = 'yes' if config('enableEditor') else 'no'
hash_mismatch_panic = 'yes' if config('panicOnChecksumMismatch') else 'no'
last_gen = get_gens()[-1]
last_gen_json = json.load(open(os.path.join(get_system_path('system', last_gen), 'boot.json'), 'r'))
last_gen_boot_spec = bootjson_to_bootspec(last_gen_json)
config_file = str(config('extraConfig')) + '\n'
config_file += textwrap.dedent(f'''
timeout: {timeout}
editor_enabled: {editor_enabled}
hash_mismatch_panic: {hash_mismatch_panic}
graphics: yes
default_entry: {3 if len(last_gen_boot_spec.specialisations.items()) > 0 else 2}
''')
for wallpaper in config('style', 'wallpapers'):
config_file += f'''wallpaper: {get_copied_path_uri(wallpaper, 'wallpapers')}\n'''
config_file += option_from_config('wallpaper_style', ['style', 'wallpaperStyle'])
config_file += option_from_config('backdrop', ['style', 'backdrop'])
config_file += option_from_config('interface_resolution', ['style', 'interface', 'resolution'])
config_file += option_from_config('interface_branding', ['style', 'interface', 'branding'])
config_file += option_from_config('interface_branding_colour', ['style', 'interface', 'brandingColor'])
config_file += option_from_config('interface_help_hidden', ['style', 'interface', 'helpHidden'])
config_file += option_from_config('term_font_scale', ['style', 'graphicalTerminal', 'font', 'scale'])
config_file += option_from_config('term_font_spacing', ['style', 'graphicalTerminal', 'font', 'spacing'])
config_file += option_from_config('term_palette', ['style', 'graphicalTerminal', 'palette'])
config_file += option_from_config('term_palette_bright', ['style', 'graphicalTerminal', 'brightPalette'])
config_file += option_from_config('term_foreground', ['style', 'graphicalTerminal', 'foreground'])
config_file += option_from_config('term_background', ['style', 'graphicalTerminal', 'background'])
config_file += option_from_config('term_foreground_bright', ['style', 'graphicalTerminal', 'brightForeground'])
config_file += option_from_config('term_background_bright', ['style', 'graphicalTerminal', 'brightBackground'])
config_file += option_from_config('term_margin', ['style', 'graphicalTerminal', 'margin'])
config_file += option_from_config('term_margin_gradient', ['style', 'graphicalTerminal', 'marginGradient'])
config_file += textwrap.dedent('''
# NixOS boot entries start here
''')
for (profile, gens) in profiles:
group_name = 'default profile' if profile == 'system' else f"profile '{profile}'"
config_file += f'/+NixOS {group_name}\n'
isFirst = True
for gen in sorted(gens, key=lambda x: x, reverse=True):
config_file += generate_config_entry(profile, gen, isFirst)
isFirst = False
config_file_path = os.path.join(limine_install_dir, 'limine.conf')
config_file += '\n# NixOS boot entries end here\n\n'
config_file += str(config('extraEntries'))
with open(f"{config_file_path}.tmp", 'w') as file:
file.truncate()
file.write(config_file.strip())
file.flush()
os.fsync(file.fileno())
os.rename(f"{config_file_path}.tmp", config_file_path)
paths[config_file_path] = True
for dest_path, source_path in config('additionalFiles').items():
dest_path = os.path.join(limine_install_dir, dest_path)
copy_file(source_path, dest_path)
limine_binary = os.path.join(str(config('liminePath')), 'bin', 'limine')
cpu_family = config('hostArchitecture', 'family')
if config('efiSupport'):
boot_file = ""
if cpu_family == 'x86':
if config('hostArchitecture', 'bits') == 32:
boot_file = 'BOOTIA32.EFI'
elif config('hostArchitecture', 'bits') == 64:
boot_file = 'BOOTX64.EFI'
elif cpu_family == 'arm':
if config('hostArchitecture', 'arch') == 'armv8-a' and config('hostArchitecture', 'bits') == 64:
boot_file = 'BOOTAA64.EFI'
else:
raise Exception(f'Unsupported CPU arch: {config("hostArchitecture", "arch")}')
else:
raise Exception(f'Unsupported CPU family: {cpu_family}')
efi_path = os.path.join(str(config('liminePath')), 'share', 'limine', boot_file)
dest_path = os.path.join(str(config('efiMountPoint')), 'efi', 'boot' if config('efiRemovable') else 'limine', boot_file)
copy_file(efi_path, dest_path)
if config('enrollConfig'):
b2sum = hashlib.blake2b()
b2sum.update(config_file.strip().encode())
try:
subprocess.run([limine_binary, 'enroll-config', dest_path, b2sum.hexdigest()])
except:
print('error: failed to enroll limine config.', file=sys.stderr)
sys.exit(1)
if config('secureBoot', 'enable'):
sbctl = os.path.join(str(config('secureBoot', 'sbctl')), 'bin', 'sbctl')
if config('secureBoot', 'createAndEnrollKeys'):
print("TEST MODE: creating and enrolling keys")
try:
subprocess.run([sbctl, 'create-keys'])
except:
print('error: failed to create keys', file=sys.stderr)
sys.exit(1)
try:
subprocess.run([sbctl, 'enroll-keys', '--yes-this-might-brick-my-machine'])
except:
print('error: failed to enroll keys', file=sys.stderr)
sys.exit(1)
print('signing limine...')
try:
subprocess.run([sbctl, 'sign', dest_path])
except:
print('error: failed to sign limine', file=sys.stderr)
sys.exit(1)
if not config('efiRemovable') and not config('canTouchEfiVariables'):
print('warning: boot.loader.efi.canTouchEfiVariables is set to false while boot.loader.limine.efiInstallAsRemovable.\n This may render the system unbootable.')
if config('canTouchEfiVariables'):
if config('efiRemovable'):
print('note: boot.loader.limine.efiInstallAsRemovable is true, no need to add EFI entry.')
else:
efibootmgr = os.path.join(str(config('efiBootMgrPath')), 'bin', 'efibootmgr')
efi_partition = find_mounted_device(str(config('efiMountPoint')))
efi_disk = find_disk_device(efi_partition)
efibootmgr_output = subprocess.check_output([efibootmgr], stderr=subprocess.STDOUT, universal_newlines=True)
# Check the output of `efibootmgr` to find if limine is already installed and present in the boot record
limine_boot_entry = None
if matches := re.findall(r'Boot([0-9a-fA-F]{4})\*? Limine', efibootmgr_output):
limine_boot_entry = matches[0]
# If there's already a Limine entry, replace it
if limine_boot_entry:
boot_order = re.findall(r'BootOrder: ((?:[0-9a-fA-F]{4},?)*)', efibootmgr_output)[0]
efibootmgr_output = subprocess.check_output([
efibootmgr,
'-b', limine_boot_entry,
'-B',
], stderr=subprocess.STDOUT, universal_newlines=True)
efibootmgr_output = subprocess.check_output([
efibootmgr,
'-c',
'-b', limine_boot_entry,
'-d', efi_disk,
'-p', efi_partition.removeprefix(efi_disk).removeprefix('p'),
'-l', f'\\efi\\limine\\{boot_file}',
'-L', 'Limine',
'-o', boot_order,
], stderr=subprocess.STDOUT, universal_newlines=True)
else:
efibootmgr_output = subprocess.check_output([
efibootmgr,
'-c',
'-d', efi_disk,
'-p', efi_partition.removeprefix(efi_disk).removeprefix('p'),
'-l', f'\\efi\\limine\\{boot_file}',
'-L', 'Limine',
], stderr=subprocess.STDOUT, universal_newlines=True)
if config('biosSupport'):
if cpu_family != 'x86':
raise Exception(f'Unsupported CPU family for BIOS install: {cpu_family}')
limine_sys = os.path.join(str(config('liminePath')), 'share', 'limine', 'limine-bios.sys')
limine_sys_dest = os.path.join(limine_install_dir, 'limine-bios.sys')
copy_file(limine_sys, limine_sys_dest)
device = str(config('biosDevice'))
if device == 'nodev':
print("note: boot.loader.limine.biosSupport is set, but device is set to nodev, only the stage 2 bootloader will be installed.", file=sys.stderr)
return
limine_deploy_args: List[str] = [limine_binary, 'bios-install', device]
if config('partitionIndex'):
limine_deploy_args.append(str(config('partitionIndex')))
if config('force'):
limine_deploy_args.append('--force')
try:
subprocess.run(limine_deploy_args)
except:
raise Exception(
'Failed to deploy BIOS stage 1 Limine bootloader!\n' +
'You might want to try enabling the `boot.loader.limine.force` option.')
print("removing unused boot files...")
for path in paths:
if not paths[path] and os.path.exists(path):
os.remove(path)
def main() -> None:
try:
install_bootloader()
finally:
# Since fat32 provides little recovery facilities after a crash,
# it can leave the system in an unbootable state, when a crash/outage
# happens shortly after an update. To decrease the likelihood of this
# event sync the efi filesystem after each update.
rc = libc.syncfs(os.open(f"{str(config('efiMountPoint'))}", os.O_RDONLY))
if rc != 0:
print(f"could not sync {str(config('efiMountPoint'))}: {os.strerror(rc)}", file=sys.stderr)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,467 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.loader.limine;
efi = config.boot.loader.efi;
limineInstallConfig = pkgs.writeText "limine-install.json" (
builtins.toJSON {
nixPath = config.nix.package;
efiBootMgrPath = pkgs.efibootmgr;
liminePath = cfg.package;
efiMountPoint = efi.efiSysMountPoint;
fileSystems = config.fileSystems;
luksDevices = builtins.attrNames config.boot.initrd.luks.devices;
canTouchEfiVariables = efi.canTouchEfiVariables;
efiSupport = cfg.efiSupport;
efiRemovable = cfg.efiInstallAsRemovable;
secureBoot = cfg.secureBoot;
biosSupport = cfg.biosSupport;
biosDevice = cfg.biosDevice;
partitionIndex = cfg.partitionIndex;
force = cfg.force;
enrollConfig = cfg.enrollConfig;
style = cfg.style;
maxGenerations = if cfg.maxGenerations == null then 0 else cfg.maxGenerations;
hostArchitecture = pkgs.stdenv.hostPlatform.parsed.cpu;
timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else 10;
enableEditor = cfg.enableEditor;
extraConfig = cfg.extraConfig;
extraEntries = cfg.extraEntries;
additionalFiles = cfg.additionalFiles;
validateChecksums = cfg.validateChecksums;
panicOnChecksumMismatch = cfg.panicOnChecksumMismatch;
}
);
defaultWallpaper = pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath;
in
{
meta = {
inherit (pkgs.limine.meta) maintainers;
};
imports = [
(lib.mkRenamedOptionModule
[ "boot" "loader" "limine" "forceMbr" ]
[ "boot" "loader" "limine" "force" ]
)
];
options.boot.loader.limine = {
enable = lib.mkEnableOption "the Limine Bootloader";
package = lib.mkPackageOption pkgs "limine" { };
enableEditor = lib.mkEnableOption null // {
description = ''
Whether to allow editing the boot entries before booting them.
It is recommended to set this to false, as it allows gaining root
access by passing `init=/bin/sh` as a kernel parameter.
'';
};
maxGenerations = lib.mkOption {
default = null;
example = 50;
type = lib.types.nullOr lib.types.int;
description = ''
Maximum number of latest generations in the boot menu.
Useful to prevent boot partition of running out of disk space.
`null` means no limit i.e. all generations that were not
garbage collected yet.
'';
};
extraConfig = lib.mkOption {
default = "";
type = lib.types.lines;
example = lib.literalExpression ''
serial: yes
'';
description = ''
A string which is prepended to limine.conf. The config format can be found [here](https://github.com/limine-bootloader/limine/blob/trunk/CONFIG.md).
'';
};
extraEntries = lib.mkOption {
default = "";
type = lib.types.lines;
example = lib.literalExpression ''
/memtest86
protocol: chainload
path: boot():///efi/memtest86/memtest86.efi
'';
description = ''
A string which is appended to the end of limine.conf. The config format can be found [here](https://github.com/limine-bootloader/limine/blob/trunk/CONFIG.md).
'';
};
additionalFiles = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.path;
example = lib.literalExpression ''
{ "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; }
'';
description = ''
A set of files to be copied to {file}`/boot`. Each attribute name denotes the
destination file name in {file}`/boot`, while the corresponding attribute value
specifies the source file.
'';
};
validateChecksums = lib.mkEnableOption null // {
default = true;
description = ''
Whether to validate file checksums before booting.
'';
};
panicOnChecksumMismatch = lib.mkEnableOption null // {
description = ''
Whether or not checksum validation failure should be a fatal
error at boot time.
'';
};
efiSupport = lib.mkEnableOption null // {
default = pkgs.stdenv.hostPlatform.isEfi;
defaultText = lib.literalExpression "pkgs.stdenv.hostPlatform.isEfi";
description = ''
Whether or not to install the limine EFI files.
'';
};
efiInstallAsRemovable = lib.mkEnableOption null // {
default = !efi.canTouchEfiVariables;
defaultText = lib.literalExpression "!config.boot.loader.efi.canTouchEfiVariables";
description = ''
Whether or not to install the limine EFI files as removable.
See {option}`boot.loader.grub.efiInstallAsRemovable`
'';
};
biosSupport = lib.mkEnableOption null // {
default = !cfg.efiSupport && pkgs.stdenv.hostPlatform.isx86;
defaultText = lib.literalExpression "!config.boot.loader.limine.efiSupport && pkgs.stdenv.hostPlatform.isx86";
description = ''
Whether or not to install limine for BIOS.
'';
};
biosDevice = lib.mkOption {
default = "nodev";
type = lib.types.str;
description = ''
Device to install the BIOS version of limine on.
'';
};
partitionIndex = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.int;
description = ''
The 1-based index of the dedicated partition for limine's second stage.
'';
};
enrollConfig = lib.mkEnableOption null // {
default = cfg.panicOnChecksumMismatch;
defaultText = lib.literalExpression "boot.loader.limine.panicOnChecksumMismatch";
description = ''
Whether or not to enroll the config.
Only works on EFI!
'';
};
force = lib.mkEnableOption null // {
description = ''
Force installation even if the safety checks fail, use absolutely only if necessary!
'';
};
secureBoot = {
enable = lib.mkEnableOption null // {
description = ''
Whether to use sign the limine binary with sbctl.
::: {.note}
This requires you to already have generated the keys and enrolled them with {command}`sbctl`.
To create keys use {command}`sbctl create-keys`.
To enroll them first reset secure boot to "Setup Mode". This is device specific.
Then enroll them using {command}`sbctl enroll-keys -m -f`.
You can now rebuild your system with this option enabled.
Afterwards turn setup mode off and enable secure boot.
:::
'';
};
createAndEnrollKeys = lib.mkEnableOption null // {
internal = true;
description = ''
Creates secure boot signing keys and enrolls them during bootloader installation.
::: {.note}
This is used for automated nixos tests.
NOT INTENDED to be used on a real system.
:::
'';
};
sbctl = lib.mkPackageOption pkgs "sbctl" { };
};
style = {
wallpapers = lib.mkOption {
default = [ ];
example = lib.literalExpression "[ pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader.gnomeFilePath ]";
type = lib.types.listOf lib.types.path;
description = ''
A list of wallpapers.
If more than one is specified, a random one will be selected at boot.
'';
};
wallpaperStyle = lib.mkOption {
default = "streched";
type = lib.types.enum [
"centered"
"streched"
"tiled"
];
description = ''
How the wallpaper should be fit to the screen.
'';
};
backdrop = lib.mkOption {
default = null;
example = "7EBAE4";
type = lib.types.nullOr lib.types.str;
description = ''
Color to fill the rest of the screen with when wallpaper_style is centered in RRGGBB format.
'';
};
interface = {
resolution = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The resolution of the interface.
'';
};
branding = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The title at the top of the screen.
'';
};
brandingColor = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.int;
description = ''
Color index of the title at the top of the screen in the range of 0-7 (Limine defaults to 6 (cyan)).
'';
};
helpHidden = lib.mkEnableOption null // {
description = ''
Whether or not to hide the keybinds at the top of the screen.
'';
};
};
graphicalTerminal = {
font = {
scale = lib.mkOption {
default = null;
example = lib.literalExpression "2x2";
type = lib.types.nullOr lib.types.str;
description = ''
The scale of the font in the format <width>x<height>.
'';
};
spacing = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.int;
description = ''
The horizontal spacing between characters in pixels.
'';
};
};
palette = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
A ; seperated array of 8 colors in the format RRGGBB:
black, red, green, brown, blue, magenta, cyan, and gray.
'';
};
brightPalette = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
A ; seperated array of 8 colors in the format RRGGBB:
dark gray, bright red, bright green, yellow, bright blue, bright magenta, bright cyan, and white.
'';
};
foreground = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
Text foreground color (RRGGBB).
'';
};
background = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
Text background color (TTRRGGBB). TT is transparency.
'';
};
brightForeground = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
Text foreground bright color (RRGGBB).
'';
};
brightBackground = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
Text background bright color (RRGGBB).
'';
};
margin = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.int;
description = ''
The amount of margin around the terminal.
'';
};
marginGradient = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.int;
description = ''
The thickness in pixels for the margin around the terminal.
'';
};
};
};
};
config = lib.mkMerge [
{
boot.loader.limine.style.wallpapers = lib.mkDefault [ defaultWallpaper ];
}
(lib.mkIf (cfg.style.wallpapers == [ defaultWallpaper ]) {
boot.loader.limine.style.backdrop = lib.mkDefault "2F302F";
boot.loader.limine.style.wallpaperStyle = lib.mkDefault "streched";
})
(lib.mkIf cfg.enable {
assertions = [
{
assertion =
pkgs.stdenv.hostPlatform.isx86_64
|| pkgs.stdenv.hostPlatform.isi686
|| pkgs.stdenv.hostPlatform.isAarch64;
message = "Limine can only be installed on aarch64 & x86 platforms";
}
{
assertion = cfg.efiSupport || cfg.biosSupport;
message = "Both UEFI support and BIOS support for Limine are disabled, this will result in an unbootable system";
}
];
boot.loader.grub.enable = lib.mkDefault false;
boot.loader.supportsInitrdSecrets = true;
system = {
boot.loader.id = "limine";
build.installBootLoader = pkgs.replaceVarsWith {
src = ./limine-install.py;
isExecutable = true;
replacements = {
python3 = pkgs.python3.withPackages (python-packages: [ python-packages.psutil ]);
configPath = limineInstallConfig;
};
};
};
})
(lib.mkIf (cfg.enable && cfg.secureBoot.enable) {
assertions = [
{
assertion = cfg.enrollConfig;
message = "Disabling enrollConfig allows bypassing secure boot.";
}
{
assertion = cfg.validateChecksums;
message = "Disabling validateChecksums allows bypassing secure boot.";
}
{
assertion = cfg.panicOnChecksumMismatch;
message = "Disabling panicOnChecksumMismatch allows bypassing secure boot.";
}
{
assertion = cfg.efiSupport;
message = "Secure boot is only supported on EFI systems.";
}
];
boot.loader.limine.enrollConfig = true;
boot.loader.limine.validateChecksums = true;
boot.loader.limine.panicOnChecksumMismatch = true;
})
# Fwupd binary needs to be signed in secure boot mode
(lib.mkIf (cfg.enable && cfg.secureBoot.enable && config.services.fwupd.enable) {
systemd.services.fwupd = {
environment.FWUPD_EFIAPPDIR = "/run/fwupd-efi";
};
systemd.services.fwupd-efi = {
description = "Sign fwupd EFI app for secure boot";
wantedBy = [ "fwupd.service" ];
partOf = [ "fwupd.service" ];
before = [ "fwupd.service" ];
unitConfig.ConditionPathIsDirectory = "/var/lib/sbctl";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
RuntimeDirectory = "fwupd-efi";
};
script = ''
cp ${config.services.fwupd.package.fwupd-efi}/libexec/fwupd/efi/fwupd*.efi /run/fwupd-efi/
chmod +w /run/fwupd-efi/fwupd*.efi
${lib.getExe cfg.secureBoot.sbctl} sign /run/fwupd-efi/fwupd*.efi
'';
};
services.fwupd.uefiCapsuleSettings = {
DisableShimForSecureBoot = true;
};
})
];
}

View File

@@ -0,0 +1,20 @@
{ lib, ... }:
with lib;
{
imports = [
(mkRenamedOptionModule [ "boot" "loader" "grub" "timeout" ] [ "boot" "loader" "timeout" ])
(mkRenamedOptionModule [ "boot" "loader" "gummiboot" "timeout" ] [ "boot" "loader" "timeout" ])
];
options = {
boot.loader.timeout = mkOption {
default = 5;
type = types.nullOr types.int;
description = ''
Timeout (in seconds) until loader boots the default menu item. Use null if the loader menu should be displayed indefinitely.
'';
};
};
}

View File

@@ -0,0 +1,344 @@
#!@python3@/bin/python3 -B
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple
import datetime
import json
from ctypes import CDLL
import os
import psutil
import re
import shutil
import subprocess
import textwrap
refind_dir = None
libc = CDLL("libc.so.6")
install_config = json.load(open('@configPath@', 'r'))
def config(*path: str) -> Optional[Any]:
result = install_config
for component in path:
result = result[component]
return result
def get_system_path(profile: str = 'system', gen: Optional[str] = None, spec: Optional[str] = None) -> str:
basename = f'{profile}-{gen}-link' if gen is not None else profile
profiles_dir = '/nix/var/nix/profiles'
if profile == 'system':
result = os.path.join(profiles_dir, basename)
else:
result = os.path.join(profiles_dir, 'system-profiles', basename)
if spec is not None:
result = os.path.join(result, 'specialisation', spec)
return result
def get_profiles() -> List[str]:
profiles_dir = '/nix/var/nix/profiles/system-profiles/'
dirs = os.listdir(profiles_dir) if os.path.isdir(profiles_dir) else []
return [path for path in dirs if not path.endswith('-link')]
def get_gens(profile: str = 'system') -> List[Tuple[int, List[str]]]:
nix_env = os.path.join(config('nixPath'), 'bin', 'nix-env')
output = subprocess.check_output([
nix_env, '--list-generations',
'-p', get_system_path(profile),
'--option', 'build-users-group', '',
], universal_newlines=True)
gen_lines = output.splitlines()
gen_nums = [int(line.split()[0]) for line in gen_lines]
return [gen for gen in gen_nums][-config('maxGenerations'):]
def is_encrypted(device: str) -> bool:
for name, _ in config('luksDevices'):
if os.readlink(os.path.join('/dev/mapper', name)) == os.readlink(device):
return True
return False
def is_fs_type_supported(fs_type: str) -> bool:
return fs_type.startswith('vfat')
paths = {}
def get_copied_path_uri(path: str, target: str) -> str:
package_id = os.path.basename(os.path.dirname(path))
suffix = os.path.basename(path)
dest_file = f'{package_id}-{suffix}'
dest_path = os.path.join(refind_dir, target, dest_file)
if not os.path.exists(dest_path):
copy_file(path, dest_path)
else:
paths[dest_path] = True
return os.path.join('/efi/refind', target, dest_file)
def get_path_uri(path: str) -> str:
return get_copied_path_uri(path, "")
def get_file_uri(profile: str, gen: Optional[str], spec: Optional[str], name: str) -> str:
gen_path = get_system_path(profile, gen, spec)
path_in_store = os.path.realpath(os.path.join(gen_path, name))
return get_path_uri(path_in_store)
def get_kernel_uri(kernel_path: str) -> str:
return get_copied_path_uri(kernel_path, "kernels")
@dataclass
class BootSpec:
system: str
init: str
kernel: str
kernelParams: List[str]
label: str
toplevel: str
specialisations: Dict[str, "BootSpec"]
initrd: str | None = None
initrdSecrets: str | None = None
def bootjson_to_bootspec(bootjson: dict) -> BootSpec:
specialisations = bootjson['org.nixos.specialisation.v1']
specialisations = {k: bootjson_to_bootspec(v) for k, v in specialisations.items()}
return BootSpec(
**bootjson['org.nixos.bootspec.v1'],
specialisations=specialisations,
)
def config_entry(is_sub: bool, bootspec: BootSpec, label: str, time: str) -> str:
entry = ""
if is_sub:
entry += 'sub'
entry += f'menuentry "{label}" {{\n'
entry += ' loader ' + get_kernel_uri(bootspec.kernel) + '\n'
if bootspec.initrd:
entry += ' initrd ' + get_kernel_uri(bootspec.initrd) + '\n'
entry += ' options "' + ' '.join(['init=' + bootspec.init] + bootspec.kernelParams).strip() + '"\n'
entry += '}\n'
return entry
def generate_config_entry(profile: str, gen: str, special: bool, group_name: str) -> str:
time = datetime.datetime.fromtimestamp(os.stat(get_system_path(profile,gen), follow_symlinks=False).st_mtime).strftime("%F %H:%M:%S")
boot_json = json.load(open(os.path.join(get_system_path(profile, gen), 'boot.json'), 'r'))
boot_spec = bootjson_to_bootspec(boot_json)
specialisation_list = boot_spec.specialisations.items()
entry = ""
if len(specialisation_list) > 0:
entry += f'menuentry "NixOS {group_name} Generation {gen}" {{\n'
entry += config_entry(True, boot_spec, f'Default', str(time))
for spec, spec_boot_spec in specialisation_list:
entry += config_entry(True, spec_boot_spec, f'{spec}', str(time))
entry += '}\n'
else:
entry += config_entry(False, boot_spec, f'NixOS {group_name} Generation {gen}', str(time))
return entry
def find_disk_device(part: str) -> str:
part = os.path.realpath(part)
part = part.removeprefix('/dev/')
disk = os.path.realpath(os.path.join('/sys', 'class', 'block', part))
disk = os.path.dirname(disk)
return os.path.join('/dev', os.path.basename(disk))
def find_mounted_device(path: str) -> str:
path = os.path.abspath(path)
while not os.path.ismount(path):
path = os.path.dirname(path)
devices = [x for x in psutil.disk_partitions() if x.mountpoint == path]
assert len(devices) == 1
return devices[0].device
def copy_file(from_path: str, to_path: str):
dirname = os.path.dirname(to_path)
if not os.path.exists(dirname):
os.makedirs(dirname)
shutil.copyfile(from_path, to_path + ".tmp")
os.rename(to_path + ".tmp", to_path)
paths[to_path] = True
def install_bootloader() -> None:
global refind_dir
refind_dir = os.path.join(str(config('efiMountPoint')), 'efi', 'refind')
if not os.path.exists(refind_dir):
os.makedirs(refind_dir)
else:
for dir, dirs, files in os.walk(refind_dir, topdown=True):
for file in files:
paths[os.path.join(dir, file)] = False
profiles = [('system', get_gens())]
for profile in get_profiles():
profiles += [(profile, get_gens(profile))]
timeout = config('timeout')
last_gen = get_gens()[-1]
last_gen_json = json.load(open(os.path.join(get_system_path('system', last_gen), 'boot.json'), 'r'))
last_gen_boot_spec = bootjson_to_bootspec(last_gen_json)
config_file = str(config('extraConfig')) + '\n'
config_file += textwrap.dedent(f'''
timeout {timeout}
default_selection {3 if len(last_gen_boot_spec.specialisations.items()) > 0 else 2}
''')
config_file += textwrap.dedent('''
# NixOS boot entries start here
''')
for (profile, gens) in profiles:
group_name = 'default profile' if profile == 'system' else f"profile '{profile}'"
isFirst = True
for gen in sorted(gens, key=lambda x: x, reverse=True):
config_file += generate_config_entry(profile, gen, isFirst, group_name)
isFirst = False
config_file_path = os.path.join(refind_dir, 'refind.conf')
config_file += '\n# NixOS boot entries end here\n\n'
with open(f"{config_file_path}.tmp", 'w') as file:
file.truncate()
file.write(config_file.strip())
file.flush()
os.fsync(file.fileno())
os.rename(f"{config_file_path}.tmp", config_file_path)
paths[config_file_path] = True
for dest_path, source_path in config('additionalFiles').items():
dest_path = os.path.join(refind_dir, dest_path)
copy_file(source_path, dest_path)
cpu_family = config('hostArchitecture', 'family')
if cpu_family == 'x86':
if config('hostArchitecture', 'bits') == 32:
boot_file = 'BOOTIA32.EFI'
efi_file = 'refind_ia32.efi'
elif config('hostArchitecture', 'bits') == 64:
boot_file = 'BOOTX64.EFI'
efi_file = 'refind_x64.efi'
elif cpu_family == 'arm':
if config('hostArchitecture', 'arch') == 'armv8-a' and config('hostArchitecture', 'bits') == 64:
boot_file = 'BOOTAA64.EFI'
efi_file = 'refind_aa64.efi'
else:
raise Exception(f'Unsupported CPU arch: {config("hostArchitecture", "arch")}')
else:
raise Exception(f'Unsupported CPU family: {cpu_family}')
efi_path = os.path.join(config('refindPath'), 'share', 'refind', efi_file)
dest_path = os.path.join(config('efiMountPoint'), 'efi', 'boot' if config('efiRemovable') else 'refind', boot_file)
copy_file(efi_path, dest_path)
if not config('efiRemovable') and not config('canTouchEfiVariables'):
print('warning: boot.loader.efi.canTouchEfiVariables is set to false while boot.loader.limine.efiInstallAsRemovable.\n This may render the system unbootable.')
if config('canTouchEfiVariables'):
if config('efiRemovable'):
print('note: boot.loader.limine.efiInstallAsRemovable is true, no need to add EFI entry.')
else:
efibootmgr = os.path.join(str(config('efiBootMgrPath')), 'bin', 'efibootmgr')
efi_partition = find_mounted_device(str(config('efiMountPoint')))
efi_disk = find_disk_device(efi_partition)
efibootmgr_output = subprocess.check_output([efibootmgr], stderr=subprocess.STDOUT, universal_newlines=True)
# Check the output of `efibootmgr` to find if rEFInd is already installed and present in the boot record
refind_boot_entry = None
if matches := re.findall(r'Boot([0-9a-fA-F]{4})\*? rEFInd', efibootmgr_output):
refind_boot_entry = matches[0]
# If there's already a Limine entry, replace it
if refind_boot_entry:
boot_order = re.findall(r'BootOrder: ((?:[0-9a-fA-F]{4},?)*)', efibootmgr_output)[0]
efibootmgr_output = subprocess.check_output([
efibootmgr,
'-b', refind_boot_entry,
'-B',
], stderr=subprocess.STDOUT, universal_newlines=True)
efibootmgr_output = subprocess.check_output([
efibootmgr,
'-c',
'-b', refind_boot_entry,
'-d', efi_disk,
'-p', efi_partition.removeprefix(efi_disk).removeprefix('p'),
'-l', f'\\efi\\refind\\{boot_file}',
'-L', 'rEFInd',
'-o', boot_order,
], stderr=subprocess.STDOUT, universal_newlines=True)
else:
efibootmgr_output = subprocess.check_output([
efibootmgr,
'-c',
'-d', efi_disk,
'-p', efi_partition.removeprefix(efi_disk).removeprefix('p'),
'-l', f'\\efi\\refind\\{boot_file}',
'-L', 'rEFInd',
], stderr=subprocess.STDOUT, universal_newlines=True)
print("removing unused boot files...")
for path in paths:
if not paths[path]:
os.remove(path)
def main() -> None:
try:
install_bootloader()
finally:
# Since fat32 provides little recovery facilities after a crash,
# it can leave the system in an unbootable state, when a crash/outage
# happens shortly after an update. To decrease the likelihood of this
# event sync the efi filesystem after each update.
rc = libc.syncfs(os.open(f"{config('efiMountPoint')}", os.O_RDONLY))
if rc != 0:
print(f"could not sync {config('efiMountPoint')}: {os.strerror(rc)}", file=sys.stderr)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,117 @@
{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
mkIf
mkEnableOption
mkOption
literalExpression
types
;
cfg = config.boot.loader.refind;
efi = config.boot.loader.efi;
refindInstallConfig = pkgs.writeText "refind-install.json" (
builtins.toJSON {
nixPath = config.nix.package;
efiBootMgrPath = pkgs.efibootmgr;
refindPath = cfg.package;
efiMountPoint = efi.efiSysMountPoint;
fileSystems = config.fileSystems;
luksDevices = config.boot.initrd.luks.devices;
canTouchEfiVariables = efi.canTouchEfiVariables;
efiRemovable = cfg.efiInstallAsRemovable;
maxGenerations = if cfg.maxGenerations == null then 0 else cfg.maxGenerations;
hostArchitecture = pkgs.stdenv.hostPlatform.parsed.cpu;
timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else 10;
extraConfig = cfg.extraConfig;
additionalFiles = cfg.additionalFiles;
}
);
in
{
meta = {
inherit (pkgs.refind.meta) maintainers;
};
options = {
boot.loader.refind = {
enable = mkEnableOption "the rEFInd boot loader";
extraConfig = lib.mkOption {
default = "";
type = types.lines;
description = ''
A string which is prepended to refind.conf.
'';
};
package = lib.mkPackageOption pkgs "refind" { };
maxGenerations = lib.mkOption {
default = null;
example = 50;
type = types.nullOr types.int;
description = ''
Maximum number of latest generations in the boot menu.
Useful to prevent boot partition of running out of disk space.
`null` means no limit i.e. all generations that were not
garbage collected yet.
'';
};
additionalFiles = mkOption {
default = { };
type = types.attrsOf types.path;
example = literalExpression ''
{ "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; }
'';
description = ''
A set of files to be copied to {file}`/boot`. Each attribute name denotes the
destination file name in {file}`/boot`, while the corresponding attribute value
specifies the source file.
'';
};
efiInstallAsRemovable = mkEnableOption null // {
default = !efi.canTouchEfiVariables;
defaultText = literalExpression "!config.boot.loader.efi.canTouchEfiVariables";
description = ''
Whether or not to install the rEFInd EFI files as removable.
See {option}`boot.loader.grub.efiInstallAsRemovable`
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion =
pkgs.stdenv.hostPlatform.isx86_64
|| pkgs.stdenv.hostPlatform.isi686
|| pkgs.stdenv.hostPlatform.isAarch64;
message = "rEFInd can only be installed on aarch64 & x86 platforms";
}
{
assertion = pkgs.stdenv.hostPlatform.isEfi;
message = "rEFInd can only be installed on UEFI platforms";
}
];
# Common attribute for boot loaders so only one of them can be
# set at once.
system = {
boot.loader.id = "refind";
build.installBootLoader = pkgs.replaceVarsWith {
src = ./refind-install.py;
isExecutable = true;
replacements = {
python3 = pkgs.python3.withPackages (python-packages: [ python-packages.psutil ]);
configPath = refindInstallConfig;
};
};
};
};
}

View File

@@ -0,0 +1,452 @@
#! @python3@/bin/python3 -B
import argparse
import ctypes
import datetime
import errno
import os
import re
import shutil
import subprocess
import sys
import tempfile
import warnings
import json
from typing import NamedTuple, Any, Sequence
from dataclasses import dataclass
from pathlib import Path
# These values will be replaced with actual values during the package build
EFI_SYS_MOUNT_POINT = Path("@efiSysMountPoint@")
BOOT_MOUNT_POINT = Path("@bootMountPoint@")
LOADER_CONF = EFI_SYS_MOUNT_POINT / "loader/loader.conf" # Always stored on the ESP
NIXOS_DIR = Path("@nixosDir@".strip("/")) # Path relative to the XBOOTLDR or ESP mount point
TIMEOUT = "@timeout@"
EDITOR = "@editor@" == "1" # noqa: PLR0133
CONSOLE_MODE = "@consoleMode@"
BOOTSPEC_TOOLS = "@bootspecTools@"
DISTRO_NAME = "@distroName@"
NIX = "@nix@"
SYSTEMD = "@systemd@"
CONFIGURATION_LIMIT = int("@configurationLimit@")
REBOOT_FOR_BITLOCKER = bool("@rebootForBitlocker@")
CAN_TOUCH_EFI_VARIABLES = "@canTouchEfiVariables@"
GRACEFUL = "@graceful@"
COPY_EXTRA_FILES = "@copyExtraFiles@"
CHECK_MOUNTPOINTS = "@checkMountpoints@"
STORE_DIR = "@storeDir@"
@dataclass
class BootSpec:
init: Path
initrd: Path
kernel: Path
kernelParams: list[str] # noqa: N815
label: str
system: str
toplevel: Path
specialisations: dict[str, "BootSpec"]
sortKey: str # noqa: N815
devicetree: Path | None = None # noqa: N815
initrdSecrets: str | None = None # noqa: N815
libc = ctypes.CDLL("libc.so.6")
FILE = None | int
def run(cmd: Sequence[str | Path], stdout: FILE = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, check=True, text=True, stdout=stdout)
class SystemIdentifier(NamedTuple):
profile: str | None
generation: int
specialisation: str | None
def copy_if_not_exists(source: Path, dest: Path) -> None:
if not dest.exists():
tmpfd, tmppath = tempfile.mkstemp(dir=dest.parent, prefix=dest.name, suffix='.tmp.')
shutil.copyfile(source, tmppath)
os.fsync(tmpfd)
shutil.move(tmppath, dest)
def generation_dir(profile: str | None, generation: int) -> Path:
if profile:
return Path(f"/nix/var/nix/profiles/system-profiles/{profile}-{generation}-link")
else:
return Path(f"/nix/var/nix/profiles/system-{generation}-link")
def system_dir(profile: str | None, generation: int, specialisation: str | None) -> Path:
d = generation_dir(profile, generation)
if specialisation:
return d / "specialisation" / specialisation
else:
return d
BOOT_ENTRY = """title {title}
sort-key {sort_key}
version Generation {generation} {description}
linux {kernel}
initrd {initrd}
options {kernel_params}
"""
def generation_conf_filename(profile: str | None, generation: int, specialisation: str | None) -> str:
pieces = [
"nixos",
profile or None,
"generation",
str(generation),
f"specialisation-{specialisation}" if specialisation else None,
]
return "-".join(p for p in pieces if p) + ".conf"
def write_loader_conf(profile: str | None, generation: int, specialisation: str | None) -> None:
tmp = LOADER_CONF.with_suffix(".tmp")
with tmp.open('x') as f:
f.write(f"timeout {TIMEOUT}\n")
f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation))
if not EDITOR:
f.write("editor 0\n")
if REBOOT_FOR_BITLOCKER:
f.write("reboot-for-bitlocker yes\n")
f.write(f"console-mode {CONSOLE_MODE}\n")
f.flush()
os.fsync(f.fileno())
os.rename(tmp, LOADER_CONF)
def get_bootspec(profile: str | None, generation: int) -> BootSpec:
system_directory = system_dir(profile, generation, None)
boot_json_path = (system_directory / "boot.json").resolve()
if boot_json_path.is_file():
with boot_json_path.open("r") as f:
# check if json is well-formed, else throw error with filepath
try:
bootspec_json = json.load(f)
except ValueError as e:
print(f"error: Malformed Json: {e}, in {boot_json_path}", file=sys.stderr)
sys.exit(1)
else:
boot_json_str = run(
[
f"{BOOTSPEC_TOOLS}/bin/synthesize",
"--version",
"1",
system_directory,
"/dev/stdout",
],
stdout=subprocess.PIPE,
).stdout
bootspec_json = json.loads(boot_json_str)
return bootspec_from_json(bootspec_json)
def bootspec_from_json(bootspec_json: dict[str, Any]) -> BootSpec:
specialisations = bootspec_json['org.nixos.specialisation.v1']
specialisations = {k: bootspec_from_json(v) for k, v in specialisations.items()}
systemdBootExtension = bootspec_json.get('org.nixos.systemd-boot', {})
sortKey = systemdBootExtension.get('sortKey', 'nixos')
devicetree = systemdBootExtension.get('devicetree')
if devicetree:
devicetree = Path(devicetree)
main_json = bootspec_json['org.nixos.bootspec.v1']
for attr in ("kernel", "initrd", "toplevel"):
if attr in main_json:
main_json[attr] = Path(main_json[attr])
return BootSpec(
**main_json,
specialisations=specialisations,
sortKey=sortKey,
devicetree=devicetree,
)
def copy_from_file(file: Path, dry_run: bool = False) -> Path:
"""
Copy a file to the boot filesystem (XBOOTLDR if in use, otherwise ESP), basing the destination filename on the store path that's being copied from. Return the destination path, relative to the boot filesystem mountpoint.
"""
store_file_path = file.resolve()
suffix = store_file_path.name
store_subdir = store_file_path.relative_to(STORE_DIR).parts[0]
efi_file_path = NIXOS_DIR / (f"{suffix}.efi" if suffix == store_subdir else f"{store_subdir}-{suffix}.efi")
if not dry_run:
copy_if_not_exists(store_file_path, BOOT_MOUNT_POINT / efi_file_path)
return efi_file_path
def write_entry(profile: str | None, generation: int, specialisation: str | None,
machine_id: str | None, bootspec: BootSpec, current: bool) -> None:
if specialisation:
bootspec = bootspec.specialisations[specialisation]
kernel = copy_from_file(bootspec.kernel)
initrd = copy_from_file(bootspec.initrd)
devicetree = copy_from_file(bootspec.devicetree) if bootspec.devicetree is not None else None
title = "{name}{profile}{specialisation}".format(
name=DISTRO_NAME,
profile=" [" + profile + "]" if profile else "",
specialisation=" (%s)" % specialisation if specialisation else "")
try:
if bootspec.initrdSecrets is not None:
run([bootspec.initrdSecrets, BOOT_MOUNT_POINT / initrd])
except subprocess.CalledProcessError:
if current:
print("failed to create initrd secrets!", file=sys.stderr)
sys.exit(1)
else:
print("warning: failed to create initrd secrets "
f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
print("note: this is normal after having removed "
"or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
entry_file = BOOT_MOUNT_POINT / "loader/entries" / generation_conf_filename(profile, generation, specialisation)
tmp_path = entry_file.with_suffix(".tmp")
kernel_params = "init=%s " % bootspec.init
kernel_params = kernel_params + " ".join(bootspec.kernelParams)
build_time = int(system_dir(profile, generation, specialisation).stat().st_ctime)
build_date = datetime.datetime.fromtimestamp(build_time).strftime('%F')
with tmp_path.open("w") as f:
f.write(BOOT_ENTRY.format(title=title,
sort_key=bootspec.sortKey,
generation=generation,
kernel=f"/{kernel}",
initrd=f"/{initrd}",
kernel_params=kernel_params,
description=f"{bootspec.label}, built on {build_date}"))
if machine_id is not None:
f.write("machine-id %s\n" % machine_id)
if devicetree is not None:
f.write("devicetree %s\n" % devicetree)
f.flush()
os.fsync(f.fileno())
tmp_path.rename(entry_file)
def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
gen_list = run(
[
f"{NIX}/bin/nix-env",
"--list-generations",
"-p",
"/nix/var/nix/profiles/%s"
% ("system-profiles/" + profile if profile else "system"),
],
stdout=subprocess.PIPE,
).stdout
gen_lines = gen_list.split("\n")
gen_lines.pop()
configurationLimit = CONFIGURATION_LIMIT
configurations = [
SystemIdentifier(
profile=profile,
generation=int(line.split()[0]),
specialisation=None
)
for line in gen_lines
]
return configurations[-configurationLimit:]
def remove_old_entries(gens: list[SystemIdentifier]) -> None:
rex_profile = re.compile(r"^nixos-(.*)-generation-.*\.conf$")
rex_generation = re.compile(r"^nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$")
known_paths = []
for gen in gens:
bootspec = get_bootspec(gen.profile, gen.generation)
known_paths.append(copy_from_file(bootspec.kernel, True).name)
known_paths.append(copy_from_file(bootspec.initrd, True).name)
if bootspec.devicetree is not None:
known_paths.append(copy_from_file(bootspec.devicetree, True).name)
for path in (BOOT_MOUNT_POINT / "loader/entries").glob("nixos*-generation-[1-9]*.conf", case_sensitive=False):
if rex_profile.match(path.name):
prof = rex_profile.sub(r"\1", path.name)
else:
prof = None
try:
gen_number = int(rex_generation.sub(r"\1", path.name))
except ValueError:
continue
if (prof, gen_number, None) not in gens:
path.unlink()
for path in (BOOT_MOUNT_POINT / NIXOS_DIR).iterdir():
if path.name not in known_paths and not path.is_dir():
path.unlink()
def cleanup_esp() -> None:
for path in (EFI_SYS_MOUNT_POINT / "loader/entries").glob("nixos*"):
path.unlink()
nixos_dir = EFI_SYS_MOUNT_POINT / NIXOS_DIR
if nixos_dir.is_dir():
shutil.rmtree(nixos_dir)
def get_profiles() -> list[str]:
system_profiles = Path("/nix/var/nix/profiles/system-profiles/")
if system_profiles.is_dir():
return [x.name
for x in system_profiles.iterdir()
if not x.name.endswith("-link")]
else:
return []
def install_bootloader(args: argparse.Namespace) -> None:
try:
with open("/etc/machine-id") as machine_file:
machine_id = machine_file.readlines()[0].strip()
except IOError as e:
if e.errno != errno.ENOENT:
raise
machine_id = None
if os.getenv("NIXOS_INSTALL_GRUB") == "1":
warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning)
os.environ["NIXOS_INSTALL_BOOTLOADER"] = "1"
# flags to pass to bootctl install/update
bootctl_flags = []
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
bootctl_flags.append(f"--boot-path={BOOT_MOUNT_POINT}")
if CAN_TOUCH_EFI_VARIABLES != "1":
bootctl_flags.append("--no-variables")
if GRACEFUL == "1":
bootctl_flags.append("--graceful")
if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
# bootctl uses fopen() with modes "wxe" and fails if the file exists.
LOADER_CONF.unlink(missing_ok=True)
run(
[f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"]
+ bootctl_flags
+ ["install"]
)
else:
# Update bootloader to latest if needed
available_out = run(
[f"{SYSTEMD}/bin/bootctl", "--version"], stdout=subprocess.PIPE
).stdout.split()[2]
installed_out = run(
[f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}", "status"],
stdout=subprocess.PIPE,
).stdout
# See status_binaries() in systemd bootctl.c for code which generates this
# Matches
# Available Boot Loaders on ESP:
# ESP: /boot (/dev/disk/by-partuuid/9b39b4c4-c48b-4ebf-bfea-a56b2395b7e0)
# File: └─/EFI/systemd/systemd-bootx64.efi (systemd-boot 255.2)
# But also:
# Available Boot Loaders on ESP:
# ESP: /boot (/dev/disk/by-partuuid/9b39b4c4-c48b-4ebf-bfea-a56b2395b7e0)
# File: ├─/EFI/systemd/HashTool.efi
# └─/EFI/systemd/systemd-bootx64.efi (systemd-boot 255.2)
installed_match = re.search(r"^\W+.*/EFI/(?:BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$",
installed_out, re.IGNORECASE | re.MULTILINE)
available_match = re.search(r"^\((.*)\)$", available_out)
if installed_match is None:
raise Exception("Could not find any previously installed systemd-boot. If you are switching to systemd-boot from a different bootloader, you need to run `nixos-rebuild switch --install-bootloader`")
if available_match is None:
raise Exception("could not determine systemd-boot version")
installed_version = installed_match.group(1)
available_version = available_match.group(1)
if installed_version < available_version:
print("updating systemd-boot from %s to %s" % (installed_version, available_version), file=sys.stderr)
run(
[f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"]
+ bootctl_flags
+ ["update"]
)
(BOOT_MOUNT_POINT / NIXOS_DIR).mkdir(parents=True, exist_ok=True)
(BOOT_MOUNT_POINT / "loader/entries").mkdir(parents=True, exist_ok=True)
gens = get_generations()
for profile in get_profiles():
gens += get_generations(profile)
remove_old_entries(gens)
for gen in gens:
try:
bootspec = get_bootspec(gen.profile, gen.generation)
is_default = Path(bootspec.init).parent == Path(args.default_config)
write_entry(*gen, machine_id, bootspec, current=is_default)
for specialisation in bootspec.specialisations.keys():
write_entry(gen.profile, gen.generation, specialisation, machine_id, bootspec, current=is_default)
if is_default:
write_loader_conf(*gen)
except OSError as e:
# See https://github.com/NixOS/nixpkgs/issues/114552
if e.errno == errno.EINVAL:
profile = f"profile '{gen.profile}'" if gen.profile else "default profile"
print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
else:
raise e
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
# Cleanup any entries in ESP if xbootldrMountPoint is set.
# If the user later unsets xbootldrMountPoint, entries in XBOOTLDR will not be cleaned up
# automatically, as we don't have information about the mount point anymore.
cleanup_esp()
extra_files_dir = BOOT_MOUNT_POINT / NIXOS_DIR / ".extra-files"
for root, _, files in extra_files_dir.walk(top_down=False):
relative_root = root.relative_to(extra_files_dir)
actual_root = BOOT_MOUNT_POINT / relative_root
for file in files:
actual_file = actual_root / file
actual_file.unlink(missing_ok=True)
(root / file).unlink()
if not list(actual_root.iterdir()):
actual_root.rmdir()
root.rmdir()
extra_files_dir.mkdir(parents=True, exist_ok=True)
run([COPY_EXTRA_FILES])
def main() -> None:
parser = argparse.ArgumentParser(description=f"Update {DISTRO_NAME}-related systemd-boot files")
parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help=f"The default {DISTRO_NAME} config to boot")
args = parser.parse_args()
run([CHECK_MOUNTPOINTS])
try:
install_bootloader(args)
finally:
# Since fat32 provides little recovery facilities after a crash,
# it can leave the system in an unbootable state, when a crash/outage
# happens shortly after an update. To decrease the likelihood of this
# event sync the efi filesystem after each update.
rc = libc.syncfs(os.open(f"{BOOT_MOUNT_POINT}", os.O_RDONLY))
if rc != 0:
print(f"could not sync {BOOT_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr)
if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT:
rc = libc.syncfs(os.open(EFI_SYS_MOUNT_POINT, os.O_RDONLY))
if rc != 0:
print(f"could not sync {EFI_SYS_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,642 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.boot.loader.systemd-boot;
efi = config.boot.loader.efi;
# We check the source code in a derivation that does not depend on the
# system configuration so that most users don't have to redo the check and require
# the necessary dependencies.
checkedSource =
pkgs.runCommand "systemd-boot"
{
preferLocalBuild = true;
}
''
install -m755 -D ${./systemd-boot-builder.py} $out
${lib.getExe pkgs.buildPackages.mypy} \
--no-implicit-optional \
--disallow-untyped-calls \
--disallow-untyped-defs \
$out
'';
edk2ShellEspPath = "efi/edk2-uefi-shell/shell.efi";
systemdBootBuilder = pkgs.replaceVarsWith {
name = "systemd-boot";
dir = "bin";
src = checkedSource;
isExecutable = true;
replacements = rec {
inherit (builtins) storeDir;
inherit (pkgs) python3;
systemd = config.systemd.package;
bootspecTools = config.boot.bootspec.package;
nix = config.nix.package.out;
timeout = if config.boot.loader.timeout == null then "menu-force" else config.boot.loader.timeout;
configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit;
inherit (cfg)
consoleMode
graceful
editor
rebootForBitlocker
;
inherit (efi) efiSysMountPoint canTouchEfiVariables;
bootMountPoint =
if cfg.xbootldrMountPoint != null then cfg.xbootldrMountPoint else efi.efiSysMountPoint;
nixosDir = "/EFI/nixos";
inherit (config.system.nixos) distroName;
checkMountpoints = pkgs.writeShellScript "check-mountpoints" ''
fail() {
echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2
exit 1
}
${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint}
${lib.optionalString (cfg.xbootldrMountPoint != null)
"${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"
}
'';
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
empty_file=$(${pkgs.coreutils}/bin/mktemp)
${concatStrings (
mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n}
'') cfg.extraFiles
)}
${concatStrings (
mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n}
'') cfg.extraEntries
)}
'';
};
};
finalSystemdBootBuilder = pkgs.writeScript "install-systemd-boot.sh" ''
#!${pkgs.runtimeShell}
${systemdBootBuilder}/bin/systemd-boot "$@"
${cfg.extraInstallCommands}
'';
in
{
meta.maintainers = with lib.maintainers; [ julienmalka ];
imports = [
(mkRenamedOptionModule
[
"boot"
"loader"
"gummiboot"
"enable"
]
[
"boot"
"loader"
"systemd-boot"
"enable"
]
)
(lib.mkChangedOptionModule
[
"boot"
"loader"
"systemd-boot"
"memtest86"
"entryFilename"
]
[
"boot"
"loader"
"systemd-boot"
"memtest86"
"sortKey"
]
(config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename)
)
(lib.mkChangedOptionModule
[
"boot"
"loader"
"systemd-boot"
"netbootxyz"
"entryFilename"
]
[
"boot"
"loader"
"systemd-boot"
"netbootxyz"
"sortKey"
]
(config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename)
)
];
options.boot.loader.systemd-boot = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager.
For more information about systemd-boot:
<https://www.freedesktop.org/wiki/Software/systemd/systemd-boot/>
'';
};
sortKey = mkOption {
default = "nixos";
type = types.str;
description = ''
The sort key used for the NixOS bootloader entries.
This key determines sorting relative to non-NixOS entries.
See also <https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting>
This option can also be used to control the sorting of NixOS specialisations.
By default, specialisations inherit the sort key of their parent generation
and will have the same value for both the sort-key and the version (i.e. the generation number),
systemd-boot will therefore sort them based on their file name, meaning that
in your boot menu you will have each main generation directly followed by
its specialisations sorted alphabetically by their names.
If you want a different ordering for a specialisation, you can override
its sort-key which will cause the specialisation to be uncoupled from its
parent generation. It will then be sorted by its new sort-key just like
any other boot entry.
The sort-key is stored in the generation's bootspec, which means that
generations keep their sort-keys even if the original definition of the
generation was removed from the NixOS configuration.
It also means that updating the sort-key will only affect new generations,
while old ones will keep the sort-key that they were originally built with.
'';
};
editor = mkOption {
default = true;
type = types.bool;
description = ''
Whether to allow editing the kernel command-line before
boot. It is recommended to set this to false, as it allows
gaining root access by passing init=/bin/sh as a kernel
parameter. However, it is enabled by default for backwards
compatibility.
'';
};
xbootldrMountPoint = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
Where the XBOOTLDR partition is mounted.
If set, this partition will be used as $BOOT to store boot loader entries and extra files
instead of the EFI partition. As per the bootloader specification, it is recommended that
the EFI and XBOOTLDR partitions be mounted at `/efi` and `/boot`, respectively.
'';
};
configurationLimit = mkOption {
default = null;
example = 120;
type = types.nullOr types.int;
description = ''
Maximum number of latest generations in the boot menu.
Useful to prevent boot partition running out of disk space.
`null` means no limit i.e. all generations
that have not been garbage collected yet.
'';
};
installDeviceTree = mkOption {
default = with config.hardware.deviceTree; enable && name != null;
defaultText = ''with config.hardware.deviceTree; enable && name != null'';
description = ''
Install the devicetree blob specified by `config.hardware.deviceTree.name`
to the ESP and instruct systemd-boot to pass this DTB to linux.
'';
};
extraInstallCommands = mkOption {
default = "";
example = ''
default_cfg=$(cat /boot/loader/loader.conf | grep default | awk '{print $2}')
init_value=$(cat /boot/loader/entries/$default_cfg | grep init= | awk '{print $2}')
sed -i "s|@INIT@|$init_value|g" /boot/custom/config_with_placeholder.conf
'';
type = types.lines;
description = ''
Additional shell commands inserted in the bootloader installer
script after generating menu entries. It can be used to expand
on extra boot entries that cannot incorporate certain pieces of
information (such as the resulting `init=` kernel parameter).
'';
};
consoleMode = mkOption {
default = "keep";
type = types.enum [
"0"
"1"
"2"
"5"
"auto"
"max"
"keep"
];
description = ''
The resolution of the console. The following values are valid:
- `"0"`: Standard UEFI 80x25 mode
- `"1"`: 80x50 mode, not supported by all devices
- `"2"`: The first non-standard mode provided by the device firmware, if any
- `"5"`: Applicable for SteamDeck where this mode represent horizontal mode
- `"auto"`: Pick a suitable mode automatically using heuristics
- `"max"`: Pick the highest-numbered available mode
- `"keep"`: Keep the mode selected by firmware (the default)
'';
};
memtest86 = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Make Memtest86+ available from the systemd-boot menu. Memtest86+ is a
program for testing memory.
'';
};
sortKey = mkOption {
default = "o_memtest86";
type = types.str;
description = ''
`systemd-boot` orders the menu entries by their sort keys,
so if you want something to appear after all the NixOS entries,
it should start with {file}`o` or onwards.
See also {option}`boot.loader.systemd-boot.sortKey`.
'';
};
};
netbootxyz = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Make `netboot.xyz` available from the
`systemd-boot` menu. `netboot.xyz`
is a menu system that allows you to boot OS installers and
utilities over the network.
'';
};
sortKey = mkOption {
default = "o_netbootxyz";
type = types.str;
description = ''
`systemd-boot` orders the menu entries by their sort keys,
so if you want something to appear after all the NixOS entries,
it should start with {file}`o` or onwards.
See also {option}`boot.loader.systemd-boot.sortKey`.
'';
};
};
edk2-uefi-shell = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Make the EDK2 UEFI Shell available from the systemd-boot menu.
It can be used to manually boot other operating systems or for debugging.
'';
};
sortKey = mkOption {
type = types.str;
default = "o_edk2-uefi-shell";
description = ''
`systemd-boot` orders the menu entries by their sort keys,
so if you want something to appear after all the NixOS entries,
it should start with {file}`o` or onwards.
See also {option}`boot.loader.systemd-boot.sortKey`..
'';
};
};
extraEntries = mkOption {
type = types.attrsOf types.lines;
default = { };
example = literalExpression ''
{ "memtest86.conf" = '''
title Memtest86+
efi /efi/memtest86/memtest.efi
sort-key z_memtest
'''; }
'';
description = ''
Any additional entries you want added to the `systemd-boot` menu.
These entries will be copied to {file}`$BOOT/loader/entries`.
Each attribute name denotes the destination file name,
and the corresponding attribute value is the contents of the entry.
To control the ordering of the entry in the boot menu, use the sort-key
field, see
<https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting>
and {option}`boot.loader.systemd-boot.sortKey`.
'';
};
extraFiles = mkOption {
type = types.attrsOf types.path;
default = { };
example = literalExpression ''
{ "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; }
'';
description = ''
A set of files to be copied to {file}`$BOOT`.
Each attribute name denotes the destination file name in
{file}`$BOOT`, while the corresponding
attribute value specifies the source file.
'';
};
graceful = mkOption {
default = false;
type = types.bool;
description = ''
Invoke `bootctl install` with the `--graceful` option,
which ignores errors when EFI variables cannot be written or when the EFI System Partition
cannot be found. Currently only applies to random seed operations.
Only enable this option if `systemd-boot` otherwise fails to install, as the
scope or implication of the `--graceful` option may change in the future.
'';
};
rebootForBitlocker = mkOption {
default = false;
type = types.bool;
description = ''
Enable *EXPERIMENTAL* BitLocker support.
Try to detect BitLocker encrypted drives along with an active
TPM. If both are found and Windows Boot Manager is selected in
the boot menu, set the "BootNext" EFI variable and restart the
system. The firmware will then start Windows Boot Manager
directly, leaving the TPM PCRs in expected states so that
Windows can unseal the encryption key.
'';
};
windows = mkOption {
default = { };
description = ''
Make Windows bootable from systemd-boot. This option is not necessary when Windows and
NixOS use the same EFI System Partition (ESP). In that case, Windows will automatically be
detected by systemd-boot.
However, if Windows is installed on a separate drive or ESP, you can use this option to add
a menu entry for each installation manually.
The attribute name is used for the title of the menu entry and internal file names.
'';
example = literalExpression ''
{
"10".efiDeviceHandle = "HD0c3";
"11-ame" = {
title = "Windows 11 Ameliorated Edition";
efiDeviceHandle = "HD0b1";
};
"11-home" = {
title = "Windows 11 Home";
efiDeviceHandle = "FS1";
sortKey = "z_windows";
};
}
'';
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
efiDeviceHandle = mkOption {
type = types.str;
example = "HD1b3";
description = ''
The device handle of the EFI System Partition (ESP) where the Windows bootloader is
located. This is the device handle that the EDK2 UEFI Shell uses to load the
bootloader.
To find this handle, follow these steps:
1. Set {option}`boot.loader.systemd-boot.edk2-uefi-shell.enable` to `true`
2. Run `nixos-rebuild boot`
3. Reboot and select "EDK2 UEFI Shell" from the systemd-boot menu
4. Run `map -c` to list all consistent device handles
5. For each device handle (for example, `HD0c1`), run `ls HD0c1:\EFI`
6. If the output contains the directory `Microsoft`, you might have found the correct device handle
7. Run `HD0c1:\EFI\Microsoft\Boot\Bootmgfw.efi` to check if Windows boots correctly
8. If it does, this device handle is the one you need (in this example, `HD0c1`)
This option is required, there is no useful default.
'';
};
title = mkOption {
type = types.str;
example = "Michaelsoft Binbows";
default = "Windows ${name}";
defaultText = ''attribute name of this entry, prefixed with "Windows "'';
description = ''
The title of the boot menu entry.
'';
};
sortKey = mkOption {
type = types.str;
default = "o_windows_${name}";
defaultText = ''attribute name of this entry, prefixed with "o_windows_"'';
description = ''
`systemd-boot` orders the menu entries by their sort keys,
so if you want something to appear after all the NixOS entries,
it should start with {file}`o` or onwards.
See also {option}`boot.loader.systemd-boot.sortKey`..
'';
};
};
}
)
);
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = (hasPrefix "/" efi.efiSysMountPoint);
message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path";
}
{
assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint);
message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' must be an absolute path";
}
{
assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint;
message = "The XBOOTLDR mount point '${toString cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${toString efi.efiSysMountPoint}'";
}
{
assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
message = "This kernel does not support the EFI boot stub";
}
{
assertion =
cfg.installDeviceTree
-> config.hardware.deviceTree.enable
-> config.hardware.deviceTree.name != null;
message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set";
}
]
++ concatMap (filename: [
{
assertion = !(hasInfix "/" filename);
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
}
{
assertion = hasSuffix ".conf" filename;
message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension";
}
]) (builtins.attrNames cfg.extraEntries)
++ concatMap (filename: [
{
assertion = !(hasPrefix "/" filename);
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not begin with a slash";
}
{
assertion = !(hasInfix ".." filename);
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not reference the parent directory";
}
{
assertion = !(hasInfix "nixos/.extra-files" (toLower filename));
message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory";
}
]) (builtins.attrNames cfg.extraFiles)
++ concatMap (winVersion: [
{
assertion = lib.match "^[-_0-9A-Za-z]+$" winVersion != null;
message = "boot.loader.systemd-boot.windows.${winVersion} is invalid: key must only contain alphanumeric characters, hyphens, and underscores";
}
]) (builtins.attrNames cfg.windows);
boot.loader.grub.enable = mkDefault false;
boot.loader.supportsInitrdSecrets = true;
boot.loader.systemd-boot.extraFiles = mkMerge [
(mkIf cfg.memtest86.enable {
"efi/memtest86/memtest.efi" = "${pkgs.memtest86plus.efi}";
})
(mkIf cfg.netbootxyz.enable {
"efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
})
(mkIf (cfg.edk2-uefi-shell.enable || cfg.windows != { }) {
${edk2ShellEspPath} = "${pkgs.edk2-uefi-shell}/shell.efi";
})
];
boot.loader.systemd-boot.extraEntries = mkMerge (
[
(mkIf cfg.memtest86.enable {
"memtest86.conf" = ''
title Memtest86+
efi /efi/memtest86/memtest.efi
sort-key ${cfg.memtest86.sortKey}
'';
})
(mkIf cfg.netbootxyz.enable {
"netbootxyz.conf" = ''
title netboot.xyz
efi /efi/netbootxyz/netboot.xyz.efi
sort-key ${cfg.netbootxyz.sortKey}
'';
})
(mkIf cfg.edk2-uefi-shell.enable {
"edk2-uefi-shell.conf" = ''
title EDK2 UEFI Shell
efi /${edk2ShellEspPath}
sort-key ${cfg.edk2-uefi-shell.sortKey}
'';
})
]
++ (mapAttrsToList (winVersion: cfg: {
"windows_${winVersion}.conf" = ''
title ${cfg.title}
efi /${edk2ShellEspPath}
options -nointerrupt -nomap -noversion ${cfg.efiDeviceHandle}:EFI\Microsoft\Boot\Bootmgfw.efi
sort-key ${cfg.sortKey}
'';
}) cfg.windows)
);
boot.bootspec.extensions."org.nixos.systemd-boot" = {
inherit (config.boot.loader.systemd-boot) sortKey;
devicetree = lib.mkIf cfg.installDeviceTree "${config.hardware.deviceTree.package}/${config.hardware.deviceTree.name}";
};
system = {
build.installBootLoader = finalSystemdBootBuilder;
boot.loader.id = "systemd-boot";
requiredKernelConfig = with config.lib.kernelConfig; [
(isYes "EFI_STUB")
];
};
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
# A list of attrnames is coerced into an attrset of bools by
# setting the values to true.
attrNamesToTrue = types.coercedTo (types.listOf types.str) (
enabledList: lib.genAttrs enabledList (_attrName: true)
) (types.attrsOf types.bool);
in
{
###### interface
options = {
boot.modprobeConfig.enable =
mkEnableOption "modprobe config. This is useful for systems like containers which do not require a kernel"
// {
default = true;
};
boot.modprobeConfig.useUbuntuModuleBlacklist =
mkEnableOption "Ubuntu distro's module blacklist"
// {
default = true;
};
boot.blacklistedKernelModules = mkOption {
type = attrNamesToTrue;
default = { };
example = [
"cirrusfb"
"i2c_piix4"
];
description = ''
Set of names of kernel modules that should not be loaded
automatically by the hardware probing code. 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 blacklisted.
'';
apply = mods: lib.attrNames (lib.filterAttrs (_: v: v) mods);
};
boot.extraModprobeConfig = mkOption {
default = "";
example = ''
options parport_pc io=0x378 irq=7 dma=1
'';
description = ''
Any additional configuration to be appended to the generated
{file}`modprobe.conf`. This is typically used to
specify module options. See
{manpage}`modprobe.d(5)` for details.
'';
type = types.lines;
};
};
###### implementation
config = mkIf config.boot.modprobeConfig.enable {
environment.etc."modprobe.d/ubuntu.conf" =
mkIf config.boot.modprobeConfig.useUbuntuModuleBlacklist
{
source = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
};
environment.etc."modprobe.d/nixos.conf".text = ''
${flip concatMapStrings config.boot.blacklistedKernelModules (name: ''
blacklist ${name}
'')}
${config.boot.extraModprobeConfig}
'';
environment.etc."modprobe.d/debian.conf".source = pkgs.kmod-debian-aliases;
environment.etc."modprobe.d/systemd.conf".source =
"${config.systemd.package}/lib/modprobe.d/systemd.conf";
environment.systemPackages = [ pkgs.kmod ];
system.activationScripts.modprobe = stringAfter [ "specialfs" ] ''
# Allow the kernel to find our wrapped modprobe (which searches
# in the right location in the Nix store for kernel modules).
# We need this when the kernel (or some module) auto-loads a
# module.
echo ${pkgs.kmod}/bin/modprobe > /proc/sys/kernel/modprobe
'';
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.initrd.nix-store-veritysetup;
in
{
meta.maintainers = with lib.maintainers; [ nikstur ];
options.boot.initrd.nix-store-veritysetup = {
enable = lib.mkEnableOption "nix-store-veritysetup";
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.dmVerity.enable;
message = "nix-store-veritysetup requires dm-verity in the systemd initrd.";
}
];
boot.initrd.systemd = {
contents = {
"/etc/systemd/system-generators/nix-store-veritysetup-generator".source =
"${lib.getExe pkgs.nix-store-veritysetup-generator}";
};
storePaths = [
"${config.boot.initrd.systemd.package}/bin/systemd-escape"
];
};
};
}

View File

@@ -0,0 +1,38 @@
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <openssl/evp.h>
void hextorb(uint8_t* hex, uint8_t* rb)
{
while(sscanf(hex, "%2x", rb) == 1)
{
hex += 2;
rb += 1;
}
*rb = '\0';
}
int main(int argc, char** argv)
{
uint8_t k_user[2048];
uint8_t salt[2048];
uint8_t key[4096];
uint32_t key_length = atoi(argv[1]);
uint32_t iteration_count = atoi(argv[2]);
hextorb(argv[3], salt);
uint32_t salt_length = strlen(argv[3]) / 2;
fgets(k_user, 2048, stdin);
uint32_t k_user_length = strlen(k_user);
if(k_user[k_user_length - 1] == '\n') {
k_user[k_user_length - 1] = '\0';
}
PKCS5_PBKDF2_HMAC(k_user, k_user_length, salt, salt_length, iteration_count, EVP_sha512(), key_length, key);
fwrite(key, 1, key_length, stdout);
return 0;
}

View File

@@ -0,0 +1,29 @@
# tpm2-totp with Plymouth {#module-boot-plymouth-tpm2-totp}
[tpm2-totp](https://github.com/tpm2-software/tpm2-totp) attests the trustworthiness of a device against a human using time-based one-time passwords. This module uses a `tpm2-totp` configuration to display a TOTP at boot using Plymouth.
## Quick start {#module-boot-plymouth-tpm2-totp-quick-start}
### 1. Enable modules {#module-boot-plymouth-tpm2-totp-quick-start-enable}
```nix
{
boot.plymouth.tpm2-totp.enable = true;
# Plymouth and systemd initrd/stage-1 are required:
boot.plymouth.enable = true;
boot.initrd.systemd.enable = true;
}
```
Switch to the new configuration before proceeding to the next step.
### 2. Configure `tpm2-totp` {#module-boot-plymouth-tpm2-totp-quick-start-configure}
Generate a new TOTP secret and save the secret in your chosen authenticator app. See `man tpm2-totp` for commands and configuration examples.
More information, including security considerations, can be found in the `README.md` in the [tpm2-totp](https://github.com/tpm2-software/tpm2-totp) repository. Be sure to select the tag for the version of `tpm2-totp` you have installed.
### 3. Check configuration {#module-boot-plymouth-tpm2-totp-quick-start-check}
Reboot and you should see the TOTP appear on the Plymouth boot screen. The TOTP should match the code displayed in your authenticator app (or the code immediately before/after).

View File

@@ -0,0 +1,59 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.plymouth.tpm2-totp;
in
{
options.boot.plymouth.tpm2-totp = {
enable = lib.mkEnableOption "tpm2-totp using Plymouth" // {
description = "Whether to display a TOTP during boot using tpm2-totp and Plymouth.";
};
package = lib.mkPackageOption pkgs "tpm2-totp" { default = "tpm2-totp-with-plymouth"; };
};
meta = {
maintainers = with lib.maintainers; [ majiir ];
doc = ./plymouth-tpm2-totp.md;
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = "boot.plymouth.tpm2-totp is only supported with boot.initrd.systemd.";
}
];
environment.systemPackages = [
cfg.package
];
boot.initrd.systemd.storePaths = [
"${cfg.package}/libexec/tpm2-totp/plymouth-tpm2-totp"
"${cfg.package}/lib/libtpm2-totp.so.0"
"${cfg.package}/lib/libtpm2-totp.so.0.0.0"
];
# Based on https://github.com/tpm2-software/tpm2-totp/blob/9bcfdcbfdd42e0b2e1d7769852009608f889631c/dist/plymouth-tpm2-totp.service.in
boot.initrd.systemd.services.plymouth-tpm2-totp = {
description = "Display a TOTP during boot using Plymouth";
requires = [ "plymouth-start.service" ];
after = [
"plymouth-start.service"
"tpm2.target"
];
wantedBy = [ "sysinit.target" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "exec";
ExecStart = "${cfg.package}/libexec/tpm2-totp/plymouth-tpm2-totp";
};
};
};
}

View File

@@ -0,0 +1,417 @@
{
config,
lib,
options,
pkgs,
...
}:
with lib;
let
plymouth = pkgs.plymouth.override {
systemd = config.boot.initrd.systemd.package;
};
cfg = config.boot.plymouth;
opt = options.boot.plymouth;
nixosBreezePlymouth = pkgs.kdePackages.breeze-plymouth.override {
logoFile = cfg.logo;
logoName = "nixos";
osName = config.system.nixos.distroName;
osVersion = config.system.nixos.release;
};
plymouthLogos = pkgs.runCommand "plymouth-logos" { inherit (cfg) logo; } ''
mkdir -p $out
# For themes that are compiled with PLYMOUTH_LOGO_FILE
mkdir -p $out/etc/plymouth
ln -s $logo $out/etc/plymouth/logo.png
# Logo for bgrt theme
# Note this is technically an abuse of watermark for the bgrt theme
# See: https://gitlab.freedesktop.org/plymouth/plymouth/-/issues/95#note_813768
mkdir -p $out/share/plymouth/themes/spinner
ln -s $logo $out/share/plymouth/themes/spinner/watermark.png
# Logo for spinfinity theme
# See: https://gitlab.freedesktop.org/plymouth/plymouth/-/issues/106
mkdir -p $out/share/plymouth/themes/spinfinity
ln -s $logo $out/share/plymouth/themes/spinfinity/header-image.png
# Logo for catppuccin (two-step) theme
for flavour in mocha macchiato latte frappe
do
mkdir -p $out/share/plymouth/themes/catppuccin-"$flavour"
ln -s $logo $out/share/plymouth/themes/catppuccin-"$flavour"/header-image.png
done
'';
themesEnv = pkgs.buildEnv {
name = "plymouth-themes";
paths = [
plymouth
plymouthLogos
]
++ cfg.themePackages;
};
configFile = pkgs.writeText "plymouthd.conf" ''
[Daemon]
ShowDelay=0
DeviceTimeout=8
Theme=${cfg.theme}
${cfg.extraConfig}
'';
checkIfThemeExists = ''
# Check if the actual requested theme is here
if [[ ! -d ${themesEnv}/share/plymouth/themes/${cfg.theme} ]]; then
echo "The requested theme: ${cfg.theme} is not provided by any of the packages in boot.plymouth.themePackages"
echo "The following themes exist: $(ls ${themesEnv}/share/plymouth/themes/)"
exit 1
fi
'';
# 'emergency.serivce' and 'rescue.service' have
# 'ExecStartPre=-plymouth quit --wait', but 'plymouth' is not on
# their 'ExecSearchPath'. We could set 'ExecSearchPath', but it
# overrides 'DefaultEnvironment=PATH=...', which is trouble for the
# initrd shell. It's simpler to just reset 'ExecStartPre' with an
# empty string and then set it to exactly what we want.
preStartQuitFixup = {
serviceConfig.ExecStartPre = [
""
"${plymouth}/bin/plymouth quit --wait"
];
};
in
{
options = {
boot.plymouth = {
enable = mkEnableOption "Plymouth boot splash screen";
font = mkOption {
default = "${pkgs.dejavu_fonts.minimal}/share/fonts/truetype/DejaVuSans.ttf";
defaultText = literalExpression ''"''${pkgs.dejavu_fonts.minimal}/share/fonts/truetype/DejaVuSans.ttf"'';
type = types.path;
description = ''
Font file made available for displaying text on the splash screen.
'';
};
themePackages = mkOption {
default = lib.optional (cfg.theme == "breeze") nixosBreezePlymouth;
defaultText = literalMD ''
A NixOS branded variant of the breeze theme when
`config.${opt.theme} == "breeze"`, otherwise
`[ ]`.
'';
type = types.listOf types.package;
description = ''
Extra theme packages for plymouth.
'';
};
theme = mkOption {
default = "bgrt";
type = types.str;
description = ''
Splash screen theme.
'';
};
logo = mkOption {
type = types.path;
# Dimensions are 48x48 to match GDM logo
default = "${pkgs.nixos-icons}/share/icons/hicolor/48x48/apps/nix-snowflake-white.png";
defaultText = literalExpression ''"''${pkgs.nixos-icons}/share/icons/hicolor/48x48/apps/nix-snowflake-white.png"'';
example = literalExpression ''
pkgs.fetchurl {
url = "https://nixos.org/logo/nixos-hires.png";
sha256 = "1ivzgd7iz0i06y36p8m5w48fd8pjqwxhdaavc0pxs7w1g7mcy5si";
}
'';
description = ''
Logo which is displayed on the splash screen.
Currently supports PNG file format only.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Literal string to append to `configFile`
and the config file generated by the plymouth module.
'';
};
};
};
config = mkIf cfg.enable {
boot.kernelParams = [ "splash" ];
# To be discoverable by systemd.
environment.systemPackages = [ plymouth ];
environment.etc."plymouth/plymouthd.conf".source = configFile;
environment.etc."plymouth/plymouthd.defaults".source =
"${plymouth}/share/plymouth/plymouthd.defaults";
environment.etc."plymouth/logo.png".source = cfg.logo;
environment.etc."plymouth/themes".source = "${themesEnv}/share/plymouth/themes";
# XXX: Needed because we supply a different set of plugins in initrd.
environment.etc."plymouth/plugins".source = "${plymouth}/lib/plymouth";
systemd.tmpfiles.rules = [
"d /run/plymouth 0755 root root 0 -"
"L+ /run/plymouth/plymouthd.defaults - - - - /etc/plymouth/plymouthd.defaults"
"L+ /run/plymouth/themes - - - - /etc/plymouth/themes"
"L+ /run/plymouth/plugins - - - - /etc/plymouth/plugins"
];
systemd.packages = [ plymouth ];
systemd.services.plymouth-kexec.wantedBy = [ "kexec.target" ];
systemd.services.plymouth-halt.wantedBy = [ "halt.target" ];
systemd.services.plymouth-quit-wait.wantedBy = [ "multi-user.target" ];
systemd.services.plymouth-quit.wantedBy = [ "multi-user.target" ];
systemd.services.plymouth-poweroff.wantedBy = [ "poweroff.target" ];
systemd.services.plymouth-reboot.wantedBy = [ "reboot.target" ];
systemd.services.plymouth-read-write.wantedBy = [ "sysinit.target" ];
systemd.services.systemd-ask-password-plymouth.wantedBy = [ "sysinit.target" ];
systemd.paths.systemd-ask-password-plymouth.wantedBy = [ "sysinit.target" ];
# Prevent Plymouth taking over the screen during system updates.
systemd.services.plymouth-start.restartIfChanged = false;
systemd.services.rescue = preStartQuitFixup;
systemd.services.emergency = preStartQuitFixup;
boot.initrd.systemd = {
extraBin.plymouth = "${plymouth}/bin/plymouth"; # for the recovery shell
storePaths = [
"${lib.getBin config.boot.initrd.systemd.package}/bin/systemd-tty-ask-password-agent"
"${plymouth}/bin/plymouthd"
"${plymouth}/sbin/plymouthd"
];
packages = [ plymouth ]; # systemd units
services.rescue = preStartQuitFixup;
services.emergency = preStartQuitFixup;
contents = {
# Files
"/etc/plymouth/plymouthd.conf".source = configFile;
"/etc/plymouth/logo.png".source = cfg.logo;
"/etc/plymouth/plymouthd.defaults".source = "${plymouth}/share/plymouth/plymouthd.defaults";
# Directories
"/etc/plymouth/plugins".source = pkgs.runCommand "plymouth-initrd-plugins" { } (
checkIfThemeExists
+ ''
moduleName="$(sed -n 's,ModuleName *= *,,p' ${themesEnv}/share/plymouth/themes/${cfg.theme}/${cfg.theme}.plymouth)"
mkdir -p $out/renderers
# module might come from a theme
cp ${themesEnv}/lib/plymouth/*.so $out
cp ${plymouth}/lib/plymouth/renderers/*.so $out/renderers
# useless in the initrd, and adds several megabytes to the closure
rm $out/renderers/x11.so
''
);
"/etc/plymouth/themes".source = pkgs.runCommand "plymouth-initrd-themes" { } (
checkIfThemeExists
+ ''
mkdir -p $out/${cfg.theme}
cp -r ${themesEnv}/share/plymouth/themes/${cfg.theme}/* $out/${cfg.theme}
# Copy more themes if the theme depends on others
for theme in $(grep -hRo '/share/plymouth/themes/.*$' $out | xargs -n1 basename); do
if [[ -d "${themesEnv}/share/plymouth/themes/$theme" ]]; then
if [[ ! -d "$out/$theme" ]]; then
echo "Adding dependent theme: $theme"
mkdir -p "$out/$theme"
cp -r "${themesEnv}/share/plymouth/themes/$theme"/* "$out/$theme"
fi
else
echo "Missing theme dependency: $theme"
fi
done
# Fixup references
for theme in $out/*/*.plymouth; do
sed -i "s,${builtins.storeDir}/.*/share/plymouth/themes,$out," "$theme"
done
''
);
# Fonts
"/etc/plymouth/fonts".source = pkgs.runCommand "plymouth-initrd-fonts" { } ''
mkdir -p $out
cp ${escapeShellArg cfg.font} $out
'';
"/etc/fonts/fonts.conf".text = ''
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
<dir>/etc/plymouth/fonts</dir>
</fontconfig>
'';
};
# Properly enable units. These are the units that arch copies
services = {
plymouth-halt.wantedBy = [ "halt.target" ];
plymouth-kexec.wantedBy = [ "kexec.target" ];
plymouth-poweroff.wantedBy = [ "poweroff.target" ];
plymouth-quit-wait.wantedBy = [ "multi-user.target" ];
plymouth-quit.wantedBy = [ "multi-user.target" ];
plymouth-read-write.wantedBy = [ "sysinit.target" ];
plymouth-reboot.wantedBy = [ "reboot.target" ];
plymouth-start.wantedBy = [
"initrd-switch-root.target"
"sysinit.target"
];
plymouth-switch-root-initramfs.wantedBy = [
"halt.target"
"kexec.target"
"plymouth-switch-root-initramfs.service"
"poweroff.target"
"reboot.target"
];
plymouth-switch-root.wantedBy = [ "initrd-switch-root.target" ];
};
# Link in runtime files before starting
services.plymouth-start.preStart = ''
mkdir -p /run/plymouth
ln -sf /etc/plymouth/{plymouthd.defaults,themes,plugins} /run/plymouth/
'';
};
# Insert required udev rules. We take stage 2 systemd because the udev
# rules are only generated when building with logind.
boot.initrd.services.udev.packages = [
(pkgs.runCommand "initrd-plymouth-udev-rules" { } ''
mkdir -p $out/etc/udev/rules.d
cp ${config.systemd.package.out}/lib/udev/rules.d/{70-uaccess,71-seat}.rules $out/etc/udev/rules.d
sed -i '/loginctl/d' $out/etc/udev/rules.d/71-seat.rules
'')
];
boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) (
''
copy_bin_and_libs ${plymouth}/bin/plymouth
copy_bin_and_libs ${plymouth}/bin/plymouthd
''
+ checkIfThemeExists
+ ''
moduleName="$(sed -n 's,ModuleName *= *,,p' ${themesEnv}/share/plymouth/themes/${cfg.theme}/${cfg.theme}.plymouth)"
mkdir -p $out/lib/plymouth/renderers
# module might come from a theme
cp ${themesEnv}/lib/plymouth/*.so $out/lib/plymouth
cp ${plymouth}/lib/plymouth/renderers/*.so $out/lib/plymouth/renderers
# useless in the initrd, and adds several megabytes to the closure
rm $out/lib/plymouth/renderers/x11.so
mkdir -p $out/share/plymouth/themes
cp ${plymouth}/share/plymouth/plymouthd.defaults $out/share/plymouth
# Copy themes into working directory for patching
mkdir themes
# Use -L to copy the directories proper, not the symlinks to them.
# Copy all themes because they're not large assets, and bgrt depends on the ImageDir of
# the spinner theme.
cp -r -L ${themesEnv}/share/plymouth/themes/* themes
# Patch out any attempted references to the theme or plymouth's themes directory
chmod -R +w themes
find themes -type f | while read file
do
sed -i "s,${builtins.storeDir}/.*/share/plymouth/themes,$out/share/plymouth/themes,g" $file
done
# Install themes
cp -r themes/* $out/share/plymouth/themes
# Install logo
mkdir -p $out/etc/plymouth
cp -r -L ${themesEnv}/etc/plymouth $out/etc
# Setup font
mkdir -p $out/share/fonts
cp ${cfg.font} $out/share/fonts
mkdir -p $out/etc/fonts
cat > $out/etc/fonts/fonts.conf <<EOF
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
<dir>$out/share/fonts</dir>
</fontconfig>
EOF
''
);
boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
$out/bin/plymouthd --help >/dev/null
$out/bin/plymouth --help >/dev/null
'';
boot.initrd.extraUdevRulesCommands = mkIf (!config.boot.initrd.systemd.enable) ''
cp ${config.systemd.package}/lib/udev/rules.d/{70-uaccess,71-seat}.rules $out
sed -i '/loginctl/d' $out/71-seat.rules
'';
# We use `mkAfter` to ensure that LUKS password prompt would be shown earlier than the splash screen.
boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) (mkAfter ''
plymouth_enabled=1
for o in $(cat /proc/cmdline); do
case $o in
plymouth.enable=0)
plymouth_enabled=0
;;
esac
done
if [ "$plymouth_enabled" != 0 ]; then
mkdir -p /etc/plymouth
mkdir -p /run/plymouth
ln -s $extraUtils/etc/plymouth/logo.png /etc/plymouth/logo.png
ln -s ${configFile} /etc/plymouth/plymouthd.conf
ln -s $extraUtils/share/plymouth/plymouthd.defaults /run/plymouth/plymouthd.defaults
ln -s $extraUtils/share/plymouth/themes /run/plymouth/themes
ln -s $extraUtils/lib/plymouth /run/plymouth/plugins
ln -s $extraUtils/etc/fonts /etc/fonts
plymouthd --mode=boot --pid-file=/run/plymouth/pid --attach-to-session
plymouth show-splash
fi
'');
boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
if [ "$plymouth_enabled" != 0 ]; then
plymouth update-root-fs --new-root-dir="$targetRoot"
fi
'';
# `mkBefore` to ensure that any custom prompts would be visible.
boot.initrd.preFailCommands = mkIf (!config.boot.initrd.systemd.enable) (mkBefore ''
if [ "$plymouth_enabled" != 0 ]; then
plymouth quit --wait
fi
'');
};
}

View File

@@ -0,0 +1,248 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.resolved;
dnsmasqResolve = config.services.dnsmasq.enable && config.services.dnsmasq.resolveLocalQueries;
resolvedConf = ''
[Resolve]
${optionalString (
config.networking.nameservers != [ ]
) "DNS=${concatStringsSep " " config.networking.nameservers}"}
${optionalString (cfg.fallbackDns != null) "FallbackDNS=${concatStringsSep " " cfg.fallbackDns}"}
${optionalString (cfg.domains != [ ]) "Domains=${concatStringsSep " " cfg.domains}"}
LLMNR=${cfg.llmnr}
DNSSEC=${cfg.dnssec}
DNSOverTLS=${cfg.dnsovertls}
${config.services.resolved.extraConfig}
'';
in
{
options = {
services.resolved.enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to enable the systemd DNS resolver daemon, `systemd-resolved`.
Search for `services.resolved` to see all options.
'';
};
services.resolved.fallbackDns = mkOption {
default = null;
example = [
"8.8.8.8"
"2001:4860:4860::8844"
];
type = types.nullOr (types.listOf types.str);
description = ''
A list of IPv4 and IPv6 addresses to use as the fallback DNS servers.
If this option is null, a compiled-in list of DNS servers is used instead.
Setting this option to an empty list will override the built-in list to an empty list, disabling fallback.
'';
};
services.resolved.domains = mkOption {
default = config.networking.search;
defaultText = literalExpression "config.networking.search";
example = [ "example.com" ];
type = types.listOf types.str;
description = ''
A list of domains. These domains are used as search suffixes
when resolving single-label host names (domain names which
contain no dot), in order to qualify them into fully-qualified
domain names (FQDNs).
For compatibility reasons, if this setting is not specified,
the search domains listed in
{file}`/etc/resolv.conf` are used instead, if
that file exists and any domains are configured in it.
'';
};
services.resolved.llmnr = mkOption {
default = "true";
example = "false";
type = types.enum [
"true"
"resolve"
"false"
];
description = ''
Controls Link-Local Multicast Name Resolution support
(RFC 4795) on the local host.
If set to
- `"true"`: Enables full LLMNR responder and resolver support.
- `"false"`: Disables both.
- `"resolve"`: Only resolution support is enabled, but responding is disabled.
'';
};
services.resolved.dnssec = mkOption {
default = "false";
example = "true";
type = types.enum [
"true"
"allow-downgrade"
"false"
];
description = ''
If set to
- `"true"`:
all DNS lookups are DNSSEC-validated locally (excluding
LLMNR and Multicast DNS). Note that this mode requires a
DNS server that supports DNSSEC. If the DNS server does
not properly support DNSSEC all validations will fail.
- `"allow-downgrade"`:
DNSSEC validation is attempted, but if the server does not
support DNSSEC properly, DNSSEC mode is automatically
disabled. Note that this mode makes DNSSEC validation
vulnerable to "downgrade" attacks, where an attacker might
be able to trigger a downgrade to non-DNSSEC mode by
synthesizing a DNS response that suggests DNSSEC was not
supported.
- `"false"`: DNS lookups are not DNSSEC validated.
At the time of September 2023, systemd upstream advise
to disable DNSSEC by default as the current code
is not robust enough to deal with "in the wild" non-compliant
servers, which will usually give you a broken bad experience
in addition of insecure.
'';
};
services.resolved.dnsovertls = mkOption {
default = "false";
example = "true";
type = types.enum [
"true"
"opportunistic"
"false"
];
description = ''
If set to
- `"true"`:
all DNS lookups will be encrypted. This requires
that the DNS server supports DNS-over-TLS and
has a valid certificate. If the hostname was specified
via the `address#hostname` format in {option}`services.resolved.domains`
then the specified hostname is used to validate its certificate.
- `"opportunistic"`:
all DNS lookups will attempt to be encrypted, but will fallback
to unecrypted requests if the server does not support DNS-over-TLS.
Note that this mode does allow for a malicious party to conduct a
downgrade attack by immitating the DNS server and pretending to not
support encryption.
- `"false"`:
all DNS lookups are done unencrypted.
'';
};
services.resolved.extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra config to append to resolved.conf.
'';
};
boot.initrd.services.resolved.enable = mkOption {
default = config.boot.initrd.systemd.network.enable;
defaultText = "config.boot.initrd.systemd.network.enable";
description = ''
Whether to enable resolved for stage 1 networking.
Uses the toplevel 'services.resolved' options for 'resolved.conf'
'';
};
};
config = mkMerge [
(mkIf cfg.enable {
assertions = [
{
assertion = !config.networking.useHostResolvConf;
message = "Using host resolv.conf is not supported with systemd-resolved";
}
];
users.users.systemd-resolve.group = "systemd-resolve";
# add resolve to nss hosts database if enabled and nscd enabled
# system.nssModules is configured in nixos/modules/system/boot/systemd.nix
# added with order 501 to allow modules to go before with mkBefore
system.nssDatabases.hosts = (mkOrder 501 [ "resolve [!UNAVAIL=return]" ]);
systemd.additionalUpstreamSystemUnits = [
"systemd-resolved.service"
];
systemd.services.systemd-resolved = {
wantedBy = [ "sysinit.target" ];
aliases = [ "dbus-org.freedesktop.resolve1.service" ];
reloadTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
stopIfChanged = false;
};
environment.etc = {
"systemd/resolved.conf".text = resolvedConf;
# symlink the dynamic stub resolver of resolv.conf as recommended by upstream:
# https://www.freedesktop.org/software/systemd/man/systemd-resolved.html#/etc/resolv.conf
"resolv.conf".source = "/run/systemd/resolve/stub-resolv.conf";
}
// optionalAttrs dnsmasqResolve {
"dnsmasq-resolv.conf".source = "/run/systemd/resolve/resolv.conf";
};
# If networkmanager is enabled, ask it to interface with resolved.
networking.networkmanager.dns = "systemd-resolved";
networking.resolvconf.package = pkgs.systemd;
})
(mkIf config.boot.initrd.services.resolved.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = "'boot.initrd.services.resolved.enable' can only be enabled with systemd stage 1.";
}
];
boot.initrd.systemd = {
contents = {
"/etc/systemd/resolved.conf".text = resolvedConf;
};
tmpfiles.settings.systemd-resolved-stub."/etc/resolv.conf".L.argument =
"/run/systemd/resolve/stub-resolv.conf";
additionalUpstreamUnits = [ "systemd-resolved.service" ];
users.systemd-resolve = { };
groups.systemd-resolve = { };
storePaths = [ "${config.boot.initrd.systemd.package}/lib/systemd/systemd-resolved" ];
services.systemd-resolved = {
wantedBy = [ "sysinit.target" ];
aliases = [ "dbus-org.freedesktop.resolve1.service" ];
};
};
})
];
}

View File

@@ -0,0 +1,33 @@
{
config,
lib,
pkgs,
...
}:
{
# This unit saves the value of the system clock to the hardware
# clock on shutdown.
systemd.services.save-hwclock = {
description = "Save Hardware Clock";
wantedBy = [ "shutdown.target" ];
unitConfig = {
DefaultDependencies = false;
ConditionPathExists = "/dev/rtc";
ConditionPathIsReadWrite = "/etc/";
};
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.util-linux}/sbin/hwclock --systohc ${
if config.time.hardwareClockInLocalTime then "--localtime" else "--utc"
}";
};
};
boot.kernel.sysctl."kernel.poweroff_cmd" = "${config.systemd.package}/sbin/poweroff";
}

View File

@@ -0,0 +1,678 @@
#! @shell@
targetRoot=/mnt-root
console=tty1
verbose="@verbose@"
info() {
if [[ -n "$verbose" ]]; then
echo "$@"
fi
}
extraUtils="@extraUtils@"
export LD_LIBRARY_PATH=@extraUtils@/lib
export PATH=@extraUtils@/bin
ln -s @extraUtils@/bin /bin
# hardcoded in util-linux's mount helper search path `/run/wrappers/bin:/run/current-system/sw/bin:/sbin`
ln -s @extraUtils@/bin /sbin
# Copy the secrets to their needed location
if [ -d "@extraUtils@/secrets" ]; then
for secret in $(cd "@extraUtils@/secrets"; find . -type f); do
mkdir -p $(dirname "/$secret")
ln -s "@extraUtils@/secrets/$secret" "$secret"
done
fi
# Stop LVM complaining about fd3
export LVM_SUPPRESS_FD_WARNINGS=true
fail() {
if [ -n "$panicOnFail" ]; then exit 1; fi
@preFailCommands@
# If starting stage 2 failed, allow the user to repair the problem
# in an interactive shell.
cat <<EOF
An error occurred in stage 1 of the boot process, which must mount the
root filesystem on \`$targetRoot' and then start stage 2. Press one
of the following keys:
EOF
if [ -n "$allowShell" ]; then cat <<EOF
i) to launch an interactive shell
f) to start an interactive shell having pid 1 (needed if you want to
start stage 2's init manually)
EOF
fi
cat <<EOF
r) to reboot immediately
*) to ignore the error and continue
EOF
read -n 1 reply
if [ -n "$allowShell" -a "$reply" = f ]; then
exec setsid @shell@ -c "exec @shell@ < /dev/$console >/dev/$console 2>/dev/$console"
elif [ -n "$allowShell" -a "$reply" = i ]; then
echo "Starting interactive shell..."
setsid @shell@ -c "exec @shell@ < /dev/$console >/dev/$console 2>/dev/$console" || fail
elif [ "$reply" = r ]; then
echo "Rebooting..."
reboot -f
else
info "Continuing..."
fi
}
trap 'fail' 0
# Print a greeting.
info
info "<<< @distroName@ Stage 1 >>>"
info
# Make several required directories.
mkdir -p /etc/udev
touch /etc/fstab # to shut up mount
ln -s /proc/mounts /etc/mtab # to shut up mke2fs
touch /etc/udev/hwdb.bin # to shut up udev
touch /etc/initrd-release
# Function for waiting for device(s) to appear.
waitDevice() {
local device="$1"
# Split device string using ':' as a delimiter, bcachefs uses
# this for multi-device filesystems, i.e. /dev/sda1:/dev/sda2:/dev/sda3
local IFS
# bcachefs is the only known use for this at the moment
# Preferably, the 'UUID=' syntax should be enforced, but
# this is kept for compatibility reasons
if [ "$fsType" = bcachefs ]; then IFS=':'; fi
# USB storage devices tend to appear with some delay. It would be
# great if we had a way to synchronously wait for them, but
# alas... So just wait for a few seconds for the device to
# appear.
for dev in $device; do
if test ! -e $dev; then
echo -n "waiting for device $dev to appear..."
try=20
while [ $try -gt 0 ]; do
sleep 1
# also re-try lvm activation now that new block devices might have appeared
lvm vgchange -ay
# and tell udev to create nodes for the new LVs
udevadm trigger --action=add
if test -e $dev; then break; fi
echo -n "."
try=$((try - 1))
done
echo
[ $try -ne 0 ]
fi
done
}
# Create the mount point if required.
makeMountPoint() {
local device="$1"
local mountPoint="$2"
local options="$3"
local IFS=,
# If we're bind mounting a file, the mount point should also be a file.
if ! [ -d "$device" ]; then
for opt in $options; do
if [ "$opt" = bind ] || [ "$opt" = rbind ]; then
mkdir -p "$(dirname "/mnt-root$mountPoint")"
touch "/mnt-root$mountPoint"
return
fi
done
fi
mkdir -m 0755 -p "/mnt-root$mountPoint"
}
# Mount special file systems.
specialMount() {
local device="$1"
local mountPoint="$2"
local options="$3"
local fsType="$4"
mkdir -m 0755 -p "$mountPoint"
mount -n -t "$fsType" -o "$options" "$device" "$mountPoint"
}
source @earlyMountScript@
# Copy initrd secrets from /.initrd-secrets to their actual destinations
if [ -d "/.initrd-secrets" ]; then
#
# Secrets are named by their full destination pathname and stored
# under /.initrd-secrets/
#
for secret in $(cd "/.initrd-secrets"; find . -type f); do
mkdir -p $(dirname "/$secret")
cp "/.initrd-secrets/$secret" "$secret"
done
fi
# Log the script output to /dev/kmsg or /run/log/stage-1-init.log.
mkdir -p /tmp
mkfifo /tmp/stage-1-init.log.fifo
logOutFd=8 && logErrFd=9
eval "exec $logOutFd>&1 $logErrFd>&2"
if test -w /dev/kmsg; then
tee -i < /tmp/stage-1-init.log.fifo /proc/self/fd/"$logOutFd" | while read -r line; do
if test -n "$line"; then
echo "<7>stage-1-init: [$(date)] $line" > /dev/kmsg
fi
done &
else
mkdir -p /run/log
tee -i < /tmp/stage-1-init.log.fifo /run/log/stage-1-init.log &
fi
exec > /tmp/stage-1-init.log.fifo 2>&1
# Process the kernel command line.
export stage2Init=/init
for o in $(cat /proc/cmdline); do
case $o in
console=*)
set -- $(IFS==; echo $o)
params=$2
set -- $(IFS=,; echo $params)
console=$1
;;
init=*)
set -- $(IFS==; echo $o)
stage2Init=$2
;;
boot.persistence=*)
set -- $(IFS==; echo $o)
persistence=$2
;;
boot.persistence.opt=*)
set -- $(IFS==; echo $o)
persistence_opt=$2
;;
boot.trace|debugtrace)
# Show each command.
set -x
;;
boot.shell_on_fail)
allowShell=1
;;
boot.debug1|debug1) # stop right away
allowShell=1
fail
;;
boot.debug1devices) # stop after loading modules and creating device nodes
allowShell=1
debug1devices=1
;;
boot.debug1mounts) # stop after mounting file systems
allowShell=1
debug1mounts=1
;;
boot.panic_on_fail|stage1panic=1)
panicOnFail=1
;;
root=*)
# If a root device is specified on the kernel command
# line, make it available through the symlink /dev/root.
# Recognise LABEL= and UUID= to support UNetbootin.
set -- $(IFS==; echo $o)
if [ $2 = "LABEL" ]; then
root="/dev/disk/by-label/$3"
elif [ $2 = "UUID" ]; then
root="/dev/disk/by-uuid/$3"
else
root=$2
fi
ln -s "$root" /dev/root
;;
copytoram)
copytoram=1
;;
findiso=*)
# if an iso name is supplied, try to find the device where
# the iso resides on
set -- $(IFS==; echo $o)
isoPath=$2
;;
esac
done
# Set hostid before modules are loaded.
# This is needed by the spl/zfs modules.
@setHostId@
# Load the required kernel modules.
echo @extraUtils@/bin/modprobe > /proc/sys/kernel/modprobe
for i in @kernelModules@; do
info "loading module $(basename $i)..."
modprobe $i
done
# Create device nodes in /dev.
@preDeviceCommands@
info "running udev..."
ln -sfn /proc/self/fd /dev/fd
ln -sfn /proc/self/fd/0 /dev/stdin
ln -sfn /proc/self/fd/1 /dev/stdout
ln -sfn /proc/self/fd/2 /dev/stderr
mkdir -p /etc/systemd
ln -sfn @linkUnits@ /etc/systemd/network
mkdir -p /etc/udev
ln -sfn @udevRules@ /etc/udev/rules.d
mkdir -p /dev/.mdadm
systemd-udevd --daemon
udevadm trigger --action=add
udevadm settle
# XXX: Use case usb->lvm will still fail, usb->luks->lvm is covered
@preLVMCommands@
info "starting device mapper and LVM..."
lvm vgchange -ay
if test -n "$debug1devices"; then fail; fi
@postDeviceCommands@
# Check the specified file system, if appropriate.
checkFS() {
local device="$1"
local fsType="$2"
# Only check block devices.
if [ ! -b "$device" ]; then return 0; fi
# Don't check ROM filesystems.
if [ "$fsType" = iso9660 -o "$fsType" = udf ]; then return 0; fi
# Don't check resilient COWs as they validate the fs structures at mount time
if [ "$fsType" = btrfs -o "$fsType" = zfs -o "$fsType" = bcachefs ]; then return 0; fi
# Skip fsck for apfs as the fsck utility does not support repairing the filesystem (no -a option)
if [ "$fsType" = apfs ]; then return 0; fi
# Skip fsck for nilfs2 - not needed by design and no fsck tool for this filesystem.
if [ "$fsType" = nilfs2 ]; then return 0; fi
# Skip fsck for inherently readonly filesystems.
if [ "$fsType" = squashfs ]; then return 0; fi
# Skip fsck.erofs because it is still experimental.
if [ "$fsType" = erofs ]; then return 0; fi
# If we couldn't figure out the FS type, then skip fsck.
if [ "$fsType" = auto ]; then
echo 'cannot check filesystem with type "auto"!'
return 0
fi
# Device might be already mounted manually
# e.g. NBD-device or the host filesystem of the file which contains encrypted root fs
if mount | grep -q "^$device on "; then
echo "skip checking already mounted $device"
return 0
fi
# Optionally, skip fsck on journaling filesystems. This option is
# a hack - it's mostly because e2fsck on ext3 takes much longer to
# recover the journal than the ext3 implementation in the kernel
# does (minutes versus seconds).
if test -z "@checkJournalingFS@" -a \
\( "$fsType" = ext3 -o "$fsType" = ext4 -o "$fsType" = reiserfs \
-o "$fsType" = xfs -o "$fsType" = jfs -o "$fsType" = f2fs \)
then
return 0
fi
echo "checking $device..."
fsck -V -a "$device"
fsckResult=$?
if test $(($fsckResult | 2)) = $fsckResult; then
echo "fsck finished, rebooting..."
sleep 3
reboot -f
fi
if test $(($fsckResult | 4)) = $fsckResult; then
echo "$device has unrepaired errors, please fix them manually."
fail
fi
if test $fsckResult -ge 8; then
echo "fsck on $device failed."
fail
fi
return 0
}
escapeFstab() {
local original="$1"
# Replace space
local escaped="${original// /\\040}"
# Replace tab
echo "${escaped//$'\t'/\\011}"
}
# Function for mounting a file system.
mountFS() {
local device="$1"
local mountPoint="$2"
local options="$3"
local fsType="$4"
if [ "$fsType" = auto ]; then
fsType=$(blkid -o value -s TYPE "$device")
if [ -z "$fsType" ]; then fsType=auto; fi
fi
# Filter out x- options, which busybox doesn't do yet.
local optionsFiltered="$(IFS=,; for i in $options; do if [ "${i:0:2}" != "x-" ]; then echo -n $i,; fi; done)"
# Prefix (lower|upper|work)dir with /mnt-root (overlayfs)
local optionsPrefixed="$( echo "${optionsFiltered%,}" | sed -E 's#\<(lowerdir|upperdir|workdir)=#\1=/mnt-root#g' )"
echo "$device /mnt-root$mountPoint $fsType $optionsPrefixed" >> /etc/fstab
checkFS "$device" "$fsType"
# Create backing directories for overlayfs
if [ "$fsType" = overlay ]; then
for i in upper work; do
dir="$( echo "$optionsPrefixed" | grep -o "${i}dir=[^,]*" )"
mkdir -m 0700 -p "${dir##*=}"
done
fi
info "mounting $device on $mountPoint..."
makeMountPoint "$device" "$mountPoint" "$optionsPrefixed"
# For ZFS and CIFS mounts, retry a few times before giving up.
# We do this for ZFS as a workaround for issue NixOS/nixpkgs#25383.
local n=0
while true; do
mount "/mnt-root$mountPoint" && break
if [ \( "$fsType" != cifs -a "$fsType" != zfs \) -o "$n" -ge 10 ]; then fail; break; fi
echo "retrying..."
sleep 1
n=$((n + 1))
done
# For bind mounts, busybox has a tendency to ignore options, which can be a
# security issue (e.g. "nosuid"). Remounting the partition seems to fix the
# issue. This should only be done for bind and rbind mountpoints because other
# filesystems may have limitations or may not support remount.
local IFS=,
for opt in $options; do
if [[ "$opt" = bind || "$opt" = rbind ]]; then
mount "/mnt-root$mountPoint" -o "remount,$optionsPrefixed"
break
fi
done
unset IFS
[ "$mountPoint" == "/" ] &&
[ -f "/mnt-root/etc/NIXOS_LUSTRATE" ] &&
lustrateRoot "/mnt-root"
true
}
lustrateRoot () {
local root="$1"
echo
echo -e "\e[1;33m<<< @distroName@ is now lustrating the root filesystem (cruft goes to /old-root) >>>\e[0m"
echo
mkdir -m 0755 -p "$root/old-root.tmp"
echo
echo "Moving impurities out of the way:"
for d in "$root"/*
do
[ "$d" == "$root/nix" ] && continue
[ "$d" == "$root/boot" ] && continue # Don't render the system unbootable
[ "$d" == "$root/old-root.tmp" ] && continue
mv -v "$d" "$root/old-root.tmp"
done
# Use .tmp to make sure subsequent invocations don't clash
mv -v "$root/old-root.tmp" "$root/old-root"
mkdir -m 0755 -p "$root/etc"
touch "$root/etc/NIXOS"
exec 4< "$root/old-root/etc/NIXOS_LUSTRATE"
echo
echo "Restoring selected impurities:"
while read -u 4 keeper; do
dirname="$(dirname "$keeper")"
mkdir -m 0755 -p "$root/$dirname"
cp -av "$root/old-root/$keeper" "$root/$keeper"
done
exec 4>&-
}
if test -e /sys/power/resume -a -e /sys/power/disk; then
if test -n "@resumeDevice@" && waitDevice "@resumeDevice@"; then
resumeDev="@resumeDevice@"
resumeInfo="$(udevadm info -q property "$resumeDev" )"
else
for sd in @resumeDevices@; do
# Try to detect resume device. According to Ubuntu bug:
# https://bugs.launchpad.net/ubuntu/+source/pm-utils/+bug/923326/comments/1
# when there are multiple swap devices, we can't know where the hibernate
# image will reside. We can check all of them for swsuspend blkid.
if waitDevice "$sd"; then
resumeInfo="$(udevadm info -q property "$sd")"
if [ "$(echo "$resumeInfo" | sed -n 's/^ID_FS_TYPE=//p')" = "swsuspend" ]; then
resumeDev="$sd"
break
fi
fi
done
fi
if test -n "$resumeDev"; then
resumeMajor="$(echo "$resumeInfo" | sed -n 's/^MAJOR=//p')"
resumeMinor="$(echo "$resumeInfo" | sed -n 's/^MINOR=//p')"
echo "$resumeMajor:$resumeMinor" > /sys/power/resume 2> /dev/null || echo "failed to resume..."
fi
fi
@postResumeCommands@
# If we have a path to an iso file, find the iso and link it to /dev/root
if [ -n "$isoPath" ]; then
mkdir -p /findiso
for delay in 5 10; do
blkid | while read -r line; do
device=$(echo "$line" | sed 's/:.*//')
type=$(echo "$line" | sed 's/.*TYPE="\([^"]*\)".*/\1/')
mount -t "$type" "$device" /findiso
if [ -e "/findiso$isoPath" ]; then
ln -sf "/findiso$isoPath" /dev/root
break 2
else
umount /findiso
fi
done
sleep "$delay"
done
fi
# Try to find and mount the root device.
mkdir -p $targetRoot
exec 3< @fsInfo@
while read -u 3 mountPoint; do
read -u 3 device
read -u 3 fsType
read -u 3 options
# !!! Really quick hack to support bind mounts, i.e., where the
# "device" should be taken relative to /mnt-root, not /. Assume
# that every device that starts with / but doesn't start with /dev
# is a bind mount.
pseudoDevice=
case $device in
/dev/*)
;;
//*)
# Don't touch SMB/CIFS paths.
pseudoDevice=1
;;
/*)
device=/mnt-root$device
;;
*)
# Not an absolute path; assume that it's a pseudo-device
# like an NFS path (e.g. "server:/path").
pseudoDevice=1
;;
esac
if test -z "$pseudoDevice" && ! waitDevice "$device"; then
# If it doesn't appear, try to mount it anyway (and
# probably fail). This is a fallback for non-device "devices"
# that we don't properly recognise.
echo "Timed out waiting for device $device, trying to mount anyway."
fi
# Wait once more for the udev queue to empty, just in case it's
# doing something with $device right now.
udevadm settle
# If copytoram is enabled: skip mounting the ISO and copy its content to a tmpfs.
if [ -n "$copytoram" ] && [ "$device" = /dev/root ] && [ "$mountPoint" = /iso ]; then
fsType=$(blkid -o value -s TYPE "$device")
fsSize=$(blockdev --getsize64 "$device" || stat -Lc '%s' "$device")
mkdir -p /tmp-iso
mount -t "$fsType" /dev/root /tmp-iso
mountFS tmpfs /iso size="$fsSize" tmpfs
echo "copying ISO contents to RAM..."
cp -r /tmp-iso/* /mnt-root/iso/
umount /tmp-iso
rmdir /tmp-iso
if [ -n "$isoPath" ] && [ $fsType = "iso9660" ] && mountpoint -q /findiso; then
umount /findiso
fi
continue
fi
if [ "$mountPoint" = / ] && [ "$device" = tmpfs ] && [ ! -z "$persistence" ]; then
echo persistence...
waitDevice "$persistence"
echo enabling persistence...
mountFS "$persistence" "$mountPoint" "$persistence_opt" "auto"
continue
fi
mountFS "$device" "$(escapeFstab "$mountPoint")" "$(escapeFstab "$options")" "$fsType"
done
exec 3>&-
@postMountCommands@
# Emit a udev rule for /dev/root to prevent systemd from complaining.
if [ -e /mnt-root/iso ]; then
eval $(udevadm info --export --export-prefix=ROOT_ --device-id-of-file=/mnt-root/iso)
else
eval $(udevadm info --export --export-prefix=ROOT_ --device-id-of-file=$targetRoot)
fi
if [ "$ROOT_MAJOR" -a "$ROOT_MINOR" -a "$ROOT_MAJOR" != 0 ]; then
mkdir -p /run/udev/rules.d
echo 'ACTION=="add|change", SUBSYSTEM=="block", ENV{MAJOR}=="'$ROOT_MAJOR'", ENV{MINOR}=="'$ROOT_MINOR'", SYMLINK+="root"' > /run/udev/rules.d/61-dev-root-link.rules
fi
# Stop udevd.
udevadm control --exit
# Reset the logging file descriptors.
# Do this just before pkill, which will kill the tee process.
exec 1>&$logOutFd 2>&$logErrFd
eval "exec $logOutFd>&- $logErrFd>&-"
# Kill any remaining processes, just to be sure we're not taking any
# with us into stage 2. But keep storage daemons like unionfs-fuse.
#
# Storage daemons are distinguished by an @ in front of their command line:
# https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/
for pid in $(pgrep -v -f '^@'); do
# Make sure we don't kill kernel processes, see #15226 and:
# http://stackoverflow.com/questions/12213445/identifying-kernel-threads
readlink "/proc/$pid/exe" &> /dev/null || continue
# Try to avoid killing ourselves.
[ $pid -eq $$ ] && continue
kill -9 "$pid"
done
if test -n "$debug1mounts"; then fail; fi
# Restore /proc/sys/kernel/modprobe to its original value.
echo /sbin/modprobe > /proc/sys/kernel/modprobe
# Start stage 2. `switch_root' deletes all files in the ramfs on the
# current root. The path has to be valid in the chroot not outside.
if [ ! -e "$targetRoot/$stage2Init" ]; then
stage2Check=${stage2Init}
while [ "$stage2Check" != "${stage2Check%/*}" ] && [ ! -L "$targetRoot/$stage2Check" ]; do
stage2Check=${stage2Check%/*}
done
if [ ! -L "$targetRoot/$stage2Check" ]; then
echo "stage 2 init script ($targetRoot/$stage2Init) not found"
fail
fi
fi
mkdir -m 0755 -p $targetRoot/proc $targetRoot/sys $targetRoot/dev $targetRoot/run
mount --move /proc $targetRoot/proc
mount --move /sys $targetRoot/sys
mount --move /dev $targetRoot/dev
mount --move /run $targetRoot/run
exec env -i $(type -P switch_root) "$targetRoot" "$stage2Init"
fail # should never be reached

View File

@@ -0,0 +1,809 @@
# This module builds the initial ramdisk, which contains an init
# script that performs the first stage of booting the system: it loads
# the modules necessary to mount the root file system, then calls the
# init in the root file system to start the second boot stage.
{
config,
options,
lib,
utils,
pkgs,
...
}:
with lib;
let
udev = config.systemd.package;
kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
# Determine the set of modules that we need to mount the root FS.
modulesClosure = pkgs.makeModulesClosure {
rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules;
kernel = config.system.modulesTree;
firmware = config.hardware.firmware;
allowMissing = config.boot.initrd.allowMissingModules;
inherit (config.boot.initrd) extraFirmwarePaths;
};
# The initrd only has to mount `/` or any FS marked as necessary for
# booting (such as the FS containing `/nix/store`, or an FS needed for
# mounting `/`, like `/` on a loopback).
fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
# Determine whether zfs-mount(8) is needed.
zfsRequiresMountHelper = any (fs: lib.elem "zfsutil" fs.options) fileSystems;
# A utility for enumerating the shared-library dependencies of a program
findLibs = pkgs.buildPackages.writeShellScriptBin "find-libs" ''
set -euo pipefail
declare -A seen
left=()
patchelf="${pkgs.buildPackages.patchelf}/bin/patchelf"
function add_needed {
rpath="$($patchelf --print-rpath $1)"
dir="$(dirname $1)"
for lib in $($patchelf --print-needed $1); do
left+=("$lib" "$rpath" "$dir")
done
}
add_needed "$1"
while [ ''${#left[@]} -ne 0 ]; do
next=''${left[0]}
rpath=''${left[1]}
ORIGIN=''${left[2]}
left=("''${left[@]:3}")
if [ -z ''${seen[$next]+x} ]; then
seen[$next]=1
# Ignore the dynamic linker which for some reason appears as a DT_NEEDED of glibc but isn't in glibc's RPATH.
case "$next" in
ld*.so.?) continue;;
esac
IFS=: read -ra paths <<< $rpath
res=
for path in "''${paths[@]}"; do
path=$(eval "echo $path")
if [ -f "$path/$next" ]; then
res="$path/$next"
echo "$res"
add_needed "$res"
break
fi
done
if [ -z "$res" ]; then
echo "Couldn't satisfy dependency $next" >&2
exit 1
fi
fi
done
'';
# Some additional utilities needed in stage 1, like mount, lvm, fsck
# etc. We don't want to bring in all of those packages, so we just
# copy what we need. Instead of using statically linked binaries,
# we just copy what we need from Glibc and use patchelf to make it
# work.
extraUtils =
pkgs.runCommand "extra-utils"
{
nativeBuildInputs = with pkgs.buildPackages; [
nukeReferences
bintools
];
allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd
}
''
set +o pipefail
mkdir -p $out/bin $out/lib
ln -s $out/bin $out/sbin
copy_bin_and_libs () {
[ -f "$out/bin/$(basename $1)" ] && rm "$out/bin/$(basename $1)"
cp -pdv $1 $out/bin
}
# Copy BusyBox.
for BIN in ${pkgs.busybox}/{s,}bin/*; do
copy_bin_and_libs $BIN
done
${optionalString zfsRequiresMountHelper ''
# Filesystems using the "zfsutil" option are mounted regardless of the
# mount.zfs(8) helper, but it is required to ensure that ZFS properties
# are used as mount options.
#
# BusyBox does not use the ZFS helper in the first place.
# util-linux searches /sbin/ as last path for helpers (stage-1-init.sh
# must symlink it to the store PATH).
# Without helper program, both `mount`s silently fails back to internal
# code, using default options and effectively ignore security relevant
# ZFS properties such as `setuid=off` and `exec=off` (unless manually
# duplicated in `fileSystems.*.options`, defeating "zfsutil"'s purpose).
copy_bin_and_libs ${lib.getOutput "mount" pkgs.util-linux}/bin/mount
copy_bin_and_libs ${config.boot.zfs.package}/bin/mount.zfs
''}
# Copy some util-linux stuff.
copy_bin_and_libs ${pkgs.util-linux}/sbin/blkid
# Copy dmsetup and lvm.
copy_bin_and_libs ${getBin pkgs.lvm2}/bin/dmsetup
copy_bin_and_libs ${getBin pkgs.lvm2}/bin/lvm
# Copy udev.
copy_bin_and_libs ${udev}/bin/udevadm
cp ${lib.getLib udev.kmod}/lib/libkmod.so* $out/lib
copy_bin_and_libs ${udev}/lib/systemd/systemd-sysctl
for BIN in ${udev}/lib/udev/*_id; do
copy_bin_and_libs $BIN
done
# systemd-udevd is only a symlink to udevadm these days
ln -sf udevadm $out/bin/systemd-udevd
# Copy modprobe.
copy_bin_and_libs ${pkgs.kmod}/bin/kmod
ln -sf kmod $out/bin/modprobe
# Copy multipath.
${optionalString config.services.multipath.enable ''
copy_bin_and_libs ${config.services.multipath.package}/bin/multipath
copy_bin_and_libs ${config.services.multipath.package}/bin/multipathd
# Copy lib/multipath manually.
cp -rpv ${config.services.multipath.package}/lib/multipath $out/lib
''}
# Copy secrets if needed.
#
# TODO: move out to a separate script; see #85000.
${optionalString (!config.boot.loader.supportsInitrdSecrets) (
concatStringsSep "\n" (
mapAttrsToList (
dest: source:
let
source' = if source == null then dest else source;
in
''
mkdir -p $(dirname "$out/secrets/${dest}")
# Some programs (e.g. ssh) doesn't like secrets to be
# symlinks, so we use `cp -L` here to match the
# behaviour when secrets are natively supported.
cp -Lr ${source'} "$out/secrets/${dest}"
''
) config.boot.initrd.secrets
)
)}
${config.boot.initrd.extraUtilsCommands}
# Copy ld manually since it isn't detected correctly
cp -pv ${pkgs.stdenv.cc.libc.out}/lib/ld*.so.? $out/lib
# Copy all of the needed libraries in a consistent order so
# duplicates are resolved the same way.
find $out/bin $out/lib -type f | sort | while read BIN; do
echo "Copying libs for executable $BIN"
for LIB in $(${findLibs}/bin/find-libs $BIN); do
TGT="$out/lib/$(basename $LIB)"
if [ ! -f "$TGT" ]; then
SRC="$(readlink -e $LIB)"
cp -pdv "$SRC" "$TGT"
fi
done
done
# Strip binaries further than normal.
chmod -R u+w $out
stripDirs "$STRIP" "$RANLIB" "lib bin" "-s"
# Run patchelf to make the programs refer to the copied libraries.
find $out/bin $out/lib -type f | while read i; do
nuke-refs -e $out $i
done
find $out/bin -type f | while read i; do
echo "patching $i..."
patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true
done
find $out/lib -type f \! -name 'ld*.so.?' | while read i; do
echo "patching $i..."
patchelf --set-rpath $out/lib $i
done
if [ -z "${toString (pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform)}" ]; then
# Make sure that the patchelf'ed binaries still work.
echo "testing patched programs..."
$out/bin/ash -c 'echo hello world' | grep "hello world"
${
if zfsRequiresMountHelper then
''
$out/bin/mount -V 1>&1 | grep -q "mount from util-linux"
$out/bin/mount.zfs -h 2>&1 | grep -q "Usage: mount.zfs"
''
else
''
$out/bin/mount --help 2>&1 | grep -q "BusyBox"
''
}
$out/bin/blkid -V 2>&1 | grep -q 'libblkid'
$out/bin/udevadm --version
$out/bin/dmsetup --version 2>&1 | tee -a log | grep -q "version:"
LVM_SYSTEM_DIR=$out $out/bin/lvm version 2>&1 | tee -a log | grep -q "LVM"
${optionalString config.services.multipath.enable ''
($out/bin/multipath || true) 2>&1 | grep -q 'need to be root'
($out/bin/multipathd || true) 2>&1 | grep -q 'need to be root'
''}
${config.boot.initrd.extraUtilsCommandsTest}
fi
''; # */
# Networkd link files are used early by udev to set up interfaces early.
# This must be done in stage 1 to avoid race conditions between udev and
# network daemons.
linkUnits =
pkgs.runCommand "link-units"
{
allowedReferences = [ extraUtils ];
preferLocalBuild = true;
}
(
''
mkdir -p $out
cp -v ${udev}/lib/systemd/network/*.link $out/
''
+ (
let
links = filterAttrs (n: v: hasSuffix ".link" n) config.systemd.network.units;
files = mapAttrsToList (n: v: "${v.unit}/${n}") links;
in
concatMapStringsSep "\n" (file: "cp -v ${file} $out/") files
)
);
udevRules =
pkgs.runCommand "udev-rules"
{
allowedReferences = [ extraUtils ];
preferLocalBuild = true;
}
''
mkdir -p $out
cp -v ${udev}/lib/udev/rules.d/60-cdrom_id.rules $out/
cp -v ${udev}/lib/udev/rules.d/60-persistent-storage.rules $out/
cp -v ${udev}/lib/udev/rules.d/75-net-description.rules $out/
cp -v ${udev}/lib/udev/rules.d/80-drivers.rules $out/
cp -v ${udev}/lib/udev/rules.d/80-net-setup-link.rules $out/
cp -v ${pkgs.lvm2}/lib/udev/rules.d/*.rules $out/
${config.boot.initrd.extraUdevRulesCommands}
for i in $out/*.rules; do
substituteInPlace $i \
--replace ata_id ${extraUtils}/bin/ata_id \
--replace scsi_id ${extraUtils}/bin/scsi_id \
--replace cdrom_id ${extraUtils}/bin/cdrom_id \
--replace ${pkgs.coreutils}/bin/basename ${extraUtils}/bin/basename \
--replace ${pkgs.util-linux}/bin/blkid ${extraUtils}/bin/blkid \
--replace ${getBin pkgs.lvm2}/bin ${extraUtils}/bin \
--replace ${pkgs.mdadm}/sbin ${extraUtils}/sbin \
--replace ${pkgs.bash}/bin/sh ${extraUtils}/bin/sh \
--replace ${udev} ${extraUtils}
done
# Work around a bug in QEMU, which doesn't implement the "READ
# DISC INFORMATION" SCSI command:
# https://bugzilla.redhat.com/show_bug.cgi?id=609049
# As a result, `cdrom_id' doesn't print
# ID_CDROM_MEDIA_TRACK_COUNT_DATA, which in turn prevents the
# /dev/disk/by-label symlinks from being created. We need these
# in the NixOS installation CD, so use ID_CDROM_MEDIA in the
# corresponding udev rules for now. This was the behaviour in
# udev <= 154. See also
# https://www.spinics.net/lists/hotplug/msg03935.html
substituteInPlace $out/60-persistent-storage.rules \
--replace ID_CDROM_MEDIA_TRACK_COUNT_DATA ID_CDROM_MEDIA
''; # */
# The init script of boot stage 1 (loading kernel modules for
# mounting the root FS).
bootStage1 = pkgs.replaceVarsWith {
src = ./stage-1-init.sh;
isExecutable = true;
postInstall = ''
echo checking syntax
# check both with bash
${pkgs.buildPackages.bash}/bin/sh -n $target
# and with ash shell, just in case
${pkgs.buildPackages.busybox}/bin/ash -n $target
'';
replacements = {
shell = "${extraUtils}/bin/ash";
inherit linkUnits udevRules extraUtils;
inherit (config.boot) resumeDevice;
inherit (config.system.nixos) distroName;
inherit (config.system.build) earlyMountScript;
inherit (config.boot.initrd)
checkJournalingFS
verbose
preLVMCommands
preDeviceCommands
postDeviceCommands
postResumeCommands
postMountCommands
preFailCommands
kernelModules
;
resumeDevices = map (sd: if sd ? device then sd.device else "/dev/disk/by-label/${sd.label}") (
filter (
sd:
hasPrefix "/dev/" sd.device
&& !sd.randomEncryption.enable
# Don't include zram devices
&& !(hasPrefix "/dev/zram" sd.device)
) config.swapDevices
);
fsInfo =
let
f = fs: [
fs.mountPoint
(if fs.device != null then fs.device else "/dev/disk/by-label/${fs.label}")
fs.fsType
(builtins.concatStringsSep "," fs.options)
];
in
pkgs.writeText "initrd-fsinfo" (concatStringsSep "\n" (concatMap f fileSystems));
setHostId = optionalString (config.networking.hostId != null) ''
hi="${config.networking.hostId}"
${
if pkgs.stdenv.hostPlatform.isBigEndian then
''
echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > /etc/hostid
''
else
''
echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > /etc/hostid
''
}
'';
};
};
# The closure of the init script of boot stage 1 is what we put in
# the initial RAM disk.
initialRamdisk = pkgs.makeInitrd {
name = "initrd-${kernel-name}";
inherit (config.boot.initrd) compressor compressorArgs prepend;
contents = [
{
object = bootStage1;
symlink = "/init";
}
{
object = "${modulesClosure}/lib";
symlink = "/lib";
}
{
object = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
symlink = "/etc/modprobe.d/ubuntu.conf";
}
{
object = config.environment.etc."modprobe.d/nixos.conf".source;
symlink = "/etc/modprobe.d/nixos.conf";
}
{
object = pkgs.kmod-debian-aliases;
symlink = "/etc/modprobe.d/debian.conf";
}
]
++ lib.optionals config.services.multipath.enable [
{
object =
pkgs.runCommand "multipath.conf"
{
src = config.environment.etc."multipath.conf".text;
preferLocalBuild = true;
}
''
target=$out
printf "$src" > $out
substituteInPlace $out \
--replace ${config.services.multipath.package}/lib ${extraUtils}/lib
'';
symlink = "/etc/multipath.conf";
}
]
++ (lib.mapAttrsToList (symlink: options: {
inherit symlink;
object = options.source;
}) config.boot.initrd.extraFiles);
};
# Script to add secret files to the initrd at bootloader update time
initialRamdiskSecretAppender =
let
compressorExe = initialRamdisk.compressorExecutableFunction pkgs;
in
pkgs.writeScriptBin "append-initrd-secrets" ''
#!${pkgs.bash}/bin/bash -e
function usage {
echo "USAGE: $0 INITRD_FILE" >&2
echo "Appends this configuration's secrets to INITRD_FILE" >&2
}
if [ $# -ne 1 ]; then
usage
exit 1
fi
if [ "$1"x = "--helpx" ]; then
usage
exit 0
fi
${lib.optionalString (config.boot.initrd.secrets == { }) "exit 0"}
export PATH=${pkgs.coreutils}/bin:${pkgs.cpio}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
function cleanup {
if [ -n "$tmp" -a -d "$tmp" ]; then
rm -fR "$tmp"
fi
}
trap cleanup EXIT
tmp=$(mktemp -d ''${TMPDIR:-/tmp}/initrd-secrets.XXXXXXXXXX)
${lib.concatStringsSep "\n" (
mapAttrsToList (
dest: source:
let
source' = if source == null then dest else toString source;
in
''
mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
cp -a ${source'} "$tmp/.initrd-secrets/${dest}"
''
) config.boot.initrd.secrets
)}
# mindepth 1 so that we don't change the mode of /
(cd "$tmp" && find . -mindepth 1 | xargs touch -amt 197001010000 && find . -mindepth 1 -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null) | \
${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
'';
in
{
options = {
boot.resumeDevice = mkOption {
type = types.str;
default = "";
example = "/dev/sda3";
description = ''
Device for manual resume attempt during boot. This should be used primarily
if you want to resume from file. If left empty, the swap partitions are used.
Specify here the device where the file resides.
You should also use {var}`boot.kernelParams` to specify
`«resume_offset»`.
'';
};
boot.initrd.enable = mkOption {
type = types.bool;
default = !config.boot.isContainer;
defaultText = literalExpression "!config.boot.isContainer";
description = ''
Whether to enable the NixOS initial RAM disk (initrd). This may be
needed to perform some initialisation tasks (like mounting
network/encrypted file systems) before continuing the boot process.
'';
};
boot.initrd.extraFiles = mkOption {
default = { };
type = types.attrsOf (
types.submodule {
options = {
source = mkOption {
type = types.package;
description = "The object to make available inside the initrd.";
};
};
}
);
description = ''
Extra files to link and copy in to the initrd.
'';
};
boot.initrd.prepend = mkOption {
default = [ ];
type = types.listOf types.str;
description = ''
Other initrd files to prepend to the final initrd we are building.
'';
};
boot.initrd.extraFirmwarePaths = mkOption {
default = [ ];
type = types.listOf types.str;
description = ''
Other firmware files (relative to `"''${config.hardware.firmware}/lib/firmware"`) to include in the final initrd we are building.
'';
};
boot.initrd.checkJournalingFS = mkOption {
default = true;
type = types.bool;
description = ''
Whether to run {command}`fsck` on journaling filesystems such as ext3.
'';
};
boot.initrd.preLVMCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands to be executed immediately before LVM discovery.
'';
};
boot.initrd.preDeviceCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands to be executed before udev is started to create
device nodes.
'';
};
boot.initrd.postDeviceCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands to be executed immediately after stage 1 of the
boot has loaded kernel modules and created device nodes in
{file}`/dev`.
'';
};
boot.initrd.postResumeCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands to be executed immediately after attempting to resume.
'';
};
boot.initrd.postMountCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands to be executed immediately after the stage 1
filesystems have been mounted.
'';
};
boot.initrd.preFailCommands = mkOption {
default = "";
type = types.lines;
description = ''
Shell commands to be executed before the failure prompt is shown.
'';
};
boot.initrd.extraUtilsCommands = mkOption {
internal = true;
default = "";
type = types.lines;
description = ''
Shell commands to be executed in the builder of the
extra-utils derivation. This can be used to provide
additional utilities in the initial ramdisk.
'';
};
boot.initrd.extraUtilsCommandsTest = mkOption {
internal = true;
default = "";
type = types.lines;
description = ''
Shell commands to be executed in the builder of the
extra-utils derivation after patchelf has done its
job. This can be used to test additional utilities
copied in extraUtilsCommands.
'';
};
boot.initrd.extraUdevRulesCommands = mkOption {
internal = true;
default = "";
type = types.lines;
description = ''
Shell commands to be executed in the builder of the
udev-rules derivation. This can be used to add
additional udev rules in the initial ramdisk.
'';
};
boot.initrd.compressor = mkOption {
default = (
if lib.versionAtLeast config.boot.kernelPackages.kernel.version "5.9" then "zstd" else "gzip"
);
defaultText = literalMD "`zstd` if the kernel supports it (5.9+), `gzip` if not";
type = types.either types.str (types.functionTo types.str);
description = ''
The compressor to use on the initrd image. May be any of:
- The name of one of the predefined compressors, see {file}`pkgs/build-support/kernel/initrd-compressor-meta.nix` for the definitions.
- A function which, given the nixpkgs package set, returns the path to a compressor tool, e.g. `pkgs: "''${pkgs.pigz}/bin/pigz"`
- (not recommended, because it does not work when cross-compiling) the full path to a compressor tool, e.g. `"''${pkgs.pigz}/bin/pigz"`
The given program should read data from stdin and write it to stdout compressed.
'';
example = "xz";
};
boot.initrd.compressorArgs = mkOption {
default = null;
type = types.nullOr (types.listOf types.str);
description = "Arguments to pass to the compressor for the initrd image, or null to use the compressor's defaults.";
};
boot.initrd.secrets = mkOption {
default = { };
type = types.attrsOf (types.nullOr types.path);
description = ''
Secrets to append to the initrd. The attribute name is the
path the secret should have inside the initrd, the value
is the path it should be copied from (or null for the same
path inside and out).
Note that `nixos-rebuild switch` will generate the initrd
also for past generations, so if secrets are moved or deleted
you will also have to garbage collect the generations that
use those secrets.
'';
example = literalExpression ''
{ "/etc/dropbear/dropbear_rsa_host_key" =
./secret-dropbear-key;
}
'';
};
boot.initrd.supportedFilesystems = mkOption {
default = { };
inherit (options.boot.supportedFilesystems) example type description;
};
boot.initrd.verbose = mkOption {
default = true;
type = types.bool;
description = ''
Verbosity of the initrd. Please note that disabling verbosity removes
only the mandatory messages generated by the NixOS scripts. For a
completely silent boot, you might also want to set the two following
configuration options:
- `boot.consoleLogLevel = 0;`
- `boot.kernelParams = [ "quiet" "udev.log_level=3" ];`
'';
};
boot.loader.supportsInitrdSecrets = mkOption {
internal = true;
default = false;
type = types.bool;
description = ''
Whether the bootloader setup runs append-initrd-secrets.
If not, any needed secrets must be copied into the initrd
and thus added to the store.
'';
};
fileSystems = mkOption {
type =
with lib.types;
attrsOf (submodule {
options.neededForBoot = mkOption {
default = false;
type = types.bool;
description = ''
If set, this file system will be mounted in the initial ramdisk.
Note that the file system will always be mounted in the initial
ramdisk if its mount point is one of the following:
${concatStringsSep ", " (forEach utils.pathsNeededForBoot (i: "{file}`${i}`"))}.
'';
};
});
};
};
config = mkIf config.boot.initrd.enable {
assertions = [
{
assertion = !config.boot.initrd.systemd.enable -> any (fs: fs.mountPoint == "/") fileSystems;
message = "The fileSystems option does not specify your root file system.";
}
{
assertion =
let
inherit (config.boot) resumeDevice;
in
resumeDevice == "" || builtins.substring 0 1 resumeDevice == "/";
message =
"boot.resumeDevice has to be an absolute path." + " Old \"x:y\" style is no longer supported.";
}
# TODO: remove when #85000 is fixed
{
assertion =
!config.boot.loader.supportsInitrdSecrets
-> all (
source: builtins.isPath source || (builtins.isString source && hasPrefix builtins.storeDir source)
) (attrValues config.boot.initrd.secrets);
message = ''
boot.initrd.secrets values must be unquoted paths when
using a bootloader that doesn't natively support initrd
secrets, e.g.:
boot.initrd.secrets = {
"/etc/secret" = /path/to/secret;
};
Note that this will result in all secrets being stored
world-readable in the Nix store!
'';
}
];
system.build = mkMerge [
{
inherit
bootStage1
initialRamdiskSecretAppender
extraUtils
modulesClosure
;
}
# generated in nixos/modules/system/boot/systemd/initrd.nix
(mkIf (!config.boot.initrd.systemd.enable) { inherit initialRamdisk; })
];
system.requiredKernelConfig = with config.lib.kernelConfig; [
(isYes "TMPFS")
(isYes "BLK_DEV_INITRD")
];
boot.initrd.supportedFilesystems = map (fs: fs.fsType) fileSystems;
};
imports = [
(mkRenamedOptionModule [ "boot" "initrd" "mdadmConf" ] [ "boot" "swraid" "mdadmConf" ])
];
}

View File

@@ -0,0 +1,159 @@
#! @shell@
systemConfig=@systemConfig@
export HOME=/root PATH="@path@"
if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
# Process the kernel command line.
for o in $(</proc/cmdline); do
case $o in
boot.debugtrace)
# Show each command.
set -x
;;
esac
done
# Print a greeting.
echo
echo -e "\e[1;32m<<< @distroName@ Stage 2 >>>\e[0m"
echo
# Normally, stage 1 mounts the root filesystem read/writable.
# However, in some environments, stage 2 is executed directly, and the
# root is read-only. So make it writable here.
if [ -z "$container" ]; then
mount -n -o remount,rw none /
fi
fi
# Likewise, stage 1 mounts /proc, /dev and /sys, so if we don't have a
# stage 1, we need to do that here.
if [ ! -e /proc/1 ]; then
specialMount() {
local device="$1"
local mountPoint="$2"
local options="$3"
local fsType="$4"
# We must not overwrite this mount because it's bind-mounted
# from stage 1's /run
if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] && [ "${mountPoint}" = /run ]; then
return
fi
install -m 0755 -d "$mountPoint"
mount -n -t "$fsType" -o "$options" "$device" "$mountPoint"
}
source @earlyMountScript@
fi
if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] || [ ! -c /dev/kmsg ] ; then
echo "booting system configuration ${systemConfig}"
else
echo "booting system configuration $systemConfig" > /dev/kmsg
fi
# Give /nix/store the defined mount options.
# Typically, this should be:
# - 'ro' to enforce immutability of the Nix store
# - 'nosuid' to enforce no suid binaries make it into the store and get executed by accident.
# suid-binaries should only exist in /run/wrappers.
# If an attacker can make the nix builder produce suid binaries in the store, those should be useless.
# Another example is tampering with the store from an outside system.
# - 'nodev' to enforce no device files in the store
# Note that we can't use "chown root:nixbld" here
# because users/groups might not exist yet.
# Silence chown/chmod to fail gracefully on a readonly filesystem
# like squashfs.
chown -f 0:30000 /nix/store
chmod -f 1775 /nix/store
missing_opts=() # stores the missing mount options that still need to be applied to the nix store
current_opts="$(findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store)"
for mount_opt in @nixStoreMountOpts@ ; do
# #375257: Ensure that we pick the "top" (i.e. last) mount so we don't get a false positive for a lower mount.
# matches '$opt', foo,$opt', '$opt,foo', 'foo,$opt,bar'
# crucially, it does not match 'foo$opt', otherwise e.g. 'errors=remount-ro' would yield false positives for 'ro'
if ! [[ "$current_opts" =~ (^|,)"$mount_opt"(,|$) ]]; then
missing_opts+=("$mount_opt")
fi
done
# only change the mount options if any need changing
if [[ ${#missing_opts[@]} != 0 ]]; then
if [ -z "$container" ]; then
mount --bind /nix/store /nix/store
else
mount --rbind /nix/store /nix/store
fi
# apply the missing mount options
mount -o remount,"$(IFS=, ; echo "${missing_opts[*]}")",bind /nix/store
fi
if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
# Use /etc/resolv.conf supplied by systemd-nspawn, if applicable.
if [ -n "@useHostResolvConf@" ] && [ -e /etc/resolv.conf ]; then
resolvconf -m 1000 -a host </etc/resolv.conf
fi
# Log the script output to /dev/kmsg or /run/log/stage-2-init.log.
# Only at this point are all the necessary prerequisites ready for these commands.
exec {logOutFd}>&1 {logErrFd}>&2
if test -w /dev/kmsg; then
exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do
if test -n "$line"; then
echo "<7>stage-2-init: $line" > /dev/kmsg
fi
done) 2>&1
else
mkdir -p /run/log
exec > >(tee -i /run/log/stage-2-init.log) 2>&1
fi
fi
# Required by the activation script
install -m 0755 -d /etc
if [ ! -h "/etc/nixos" ]; then
install -m 0755 -d /etc/nixos
fi
install -m 01777 -d /tmp
# Run the script that performs all configuration activation that does
# not have to be done at boot time.
echo "running activation script..."
$systemConfig/activate
# Record the boot configuration.
ln -sfn "$systemConfig" /run/booted-system
# Run any user-specified commands.
@shell@ @postBootCommands@
# No need to restore the stdout/stderr streams we never redirected and
# especially no need to start systemd
if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
# Reset the logging file descriptors.
exec 1>&$logOutFd 2>&$logErrFd
exec {logOutFd}>&- {logErrFd}>&-
# Start systemd in a clean environment.
echo "starting systemd..."
exec @systemdExecutable@ "$@"
fi

View File

@@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
useHostResolvConf = config.networking.resolvconf.enable && config.networking.useHostResolvConf;
bootStage2 = pkgs.replaceVarsWith {
src = ./stage-2-init.sh;
isExecutable = true;
replacements = {
shell = "${pkgs.bash}/bin/bash";
systemConfig = null; # replaced in ../activation/top-level.nix
inherit (config.boot) systemdExecutable;
nixStoreMountOpts = lib.concatStringsSep " " (map lib.escapeShellArg config.boot.nixStoreMountOpts);
inherit (config.system.nixos) distroName;
inherit useHostResolvConf;
inherit (config.system.build) earlyMountScript;
path = lib.makeBinPath (
[
pkgs.coreutils
pkgs.util-linux
]
++ lib.optional useHostResolvConf pkgs.openresolv
);
postBootCommands = pkgs.writeText "local-cmds" ''
${config.boot.postBootCommands}
${config.powerManagement.powerUpCommands}
'';
};
};
in
{
imports = [
(lib.mkRemovedOptionModule
[
"boot"
"readOnlyNixStore"
]
"Please use the `boot.nixStoreMountOpts' option to define mount options for the Nix store, including 'ro'"
)
];
options = {
boot = {
postBootCommands = mkOption {
default = "";
example = "rm -f /var/log/messages";
type = types.lines;
description = ''
Shell commands to be executed just before systemd is started.
'';
};
nixStoreMountOpts = mkOption {
type = types.listOf types.nonEmptyStr;
default = [
"ro"
"nodev"
"nosuid"
];
description = ''
Defines the mount options used on a bind mount for the {file}`/nix/store`.
This affects the whole system except the nix store daemon, which will undo the bind mount.
`ro` enforces immutability of the Nix store.
The store daemon should already not put device mappers or suid binaries in the store,
meaning `nosuid` and `nodev` enforce what should already be the case.
'';
};
systemdExecutable = mkOption {
default = "/run/current-system/systemd/lib/systemd/systemd";
type = types.str;
description = ''
The program to execute to start systemd.
'';
};
extraSystemdUnitPaths = mkOption {
default = [ ];
type = types.listOf types.str;
description = ''
Additional paths that get appended to the SYSTEMD_UNIT_PATH environment variable
that can contain mutable unit files.
'';
};
};
};
config = {
system.build.bootStage2 = bootStage2;
};
}

View File

@@ -0,0 +1,98 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
requiredStratisFilesystems = lib.attrsets.filterAttrs (
_: x: utils.fsNeededForBoot x && x.stratis.poolUuid != null
) config.fileSystems;
in
{
options = { };
config = lib.mkIf (requiredStratisFilesystems != { }) {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = "stratis root fs requires systemd stage 1";
}
];
boot.initrd = {
systemd = {
storePaths = [
"${pkgs.stratisd}/lib/udev/stratis-base32-decode"
"${pkgs.stratisd}/lib/udev/stratis-str-cmp"
"${pkgs.lvm2.bin}/bin/dmsetup"
"${pkgs.stratisd}/libexec/stratisd-min"
"${pkgs.stratisd.initrd}/bin/stratis-rootfs-setup"
];
packages = [ pkgs.stratisd.initrd ];
extraBin = {
thin_check = "${pkgs."thin-provisioning-tools"}/bin/thin_check";
thin_repair = "${pkgs."thin-provisioning-tools"}/bin/thin_repair";
thin_metadata_size = "${pkgs."thin-provisioning-tools"}/bin/thin_metadata_size";
stratis-min = "${pkgs.stratisd}/bin/stratis-min";
};
services = lib.attrsets.mapAttrs' (mountPoint: fileSystem: {
name = "stratis-setup-${fileSystem.stratis.poolUuid}";
value = {
description = "setup for Stratis root filesystem";
unitConfig.DefaultDependencies = "no";
conflicts = [
"shutdown.target"
"initrd-switch-root.target"
];
onFailure = [ "emergency.target" ];
unitConfig.OnFailureJobMode = "isolate";
wants = [
"stratisd-min.service"
"plymouth-start.service"
];
wantedBy = [ "initrd.target" ];
after = [
"paths.target"
"plymouth-start.service"
"stratisd-min.service"
];
before = [
"initrd.target"
"shutdown.target"
"initrd-switch-root.target"
];
environment.STRATIS_ROOTFS_UUID = fileSystem.stratis.poolUuid;
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.stratisd.initrd}/bin/stratis-rootfs-setup";
RemainAfterExit = "yes";
};
};
}) requiredStratisFilesystems;
};
availableKernelModules = [
"dm-thin-pool"
"dm-crypt"
]
++ [
"aes"
"aes_generic"
"blowfish"
"twofish"
"serpent"
"cbc"
"xts"
"lrw"
"sha1"
"sha256"
"sha512"
"af_alg"
"algif_skcipher"
];
services.udev.packages = [
pkgs.stratisd.initrd
pkgs.lvm2
];
};
};
}

View File

@@ -0,0 +1,829 @@
{
config,
lib,
pkgs,
utils,
...
}:
with utils;
with systemdUtils.unitOptions;
with lib;
let
cfg = config.systemd;
inherit (systemdUtils.lib)
generateUnits
targetToUnit
serviceToUnit
socketToUnit
timerToUnit
pathToUnit
mountToUnit
automountToUnit
sliceToUnit
settingsToSections
;
upstreamSystemUnits = [
# Targets.
"basic.target"
"sysinit.target"
"sockets.target"
"exit.target"
"graphical.target"
"multi-user.target"
"network.target"
"network-pre.target"
"network-online.target"
"nss-lookup.target"
"nss-user-lookup.target"
"time-sync.target"
"first-boot-complete.target"
]
++ optionals cfg.package.withCryptsetup [
"cryptsetup.target"
"cryptsetup-pre.target"
"remote-cryptsetup.target"
]
++ [
"sigpwr.target"
"timers.target"
"paths.target"
"rpcbind.target"
# Rescue mode.
"rescue.target"
"rescue.service"
# systemd-debug-generator
"debug-shell.service"
# Udev.
"systemd-udevd-control.socket"
"systemd-udevd-kernel.socket"
"systemd-udevd.service"
"systemd-udev-settle.service"
]
++ (optional (!config.boot.isContainer) "systemd-udev-trigger.service")
++ [
# hwdb.bin is managed by NixOS
# "systemd-hwdb-update.service"
# Hardware (started by udev when a relevant device is plugged in).
"sound.target"
"bluetooth.target"
"printer.target"
"smartcard.target"
# Kernel module loading.
"systemd-modules-load.service"
"kmod-static-nodes.service"
"modprobe@.service"
# Filesystems.
"systemd-fsck@.service"
"systemd-fsck-root.service"
"systemd-growfs@.service"
"systemd-growfs-root.service"
"systemd-remount-fs.service"
"systemd-pstore.service"
"local-fs.target"
"local-fs-pre.target"
"remote-fs.target"
"remote-fs-pre.target"
"swap.target"
"dev-hugepages.mount"
"dev-mqueue.mount"
"sys-fs-fuse-connections.mount"
]
++ (optional (!config.boot.isContainer) "sys-kernel-config.mount")
++ [
"sys-kernel-debug.mount"
"sys-kernel-tracing.mount"
# Maintaining state across reboots.
"systemd-random-seed.service"
]
++ optionals cfg.package.withBootloader [
"systemd-boot-random-seed.service"
"systemd-bless-boot.service"
]
++ [
"systemd-backlight@.service"
"systemd-rfkill.service"
"systemd-rfkill.socket"
"boot-complete.target"
# Hibernate / suspend.
"hibernate.target"
"suspend.target"
"suspend-then-hibernate.target"
"sleep.target"
"hybrid-sleep.target"
"systemd-hibernate.service"
]
++ (lib.optional cfg.package.withEfi "systemd-hibernate-clear.service")
++ [
"systemd-hybrid-sleep.service"
"systemd-suspend.service"
"systemd-suspend-then-hibernate.service"
# Reboot stuff.
"reboot.target"
"systemd-reboot.service"
"poweroff.target"
"systemd-poweroff.service"
"halt.target"
"systemd-halt.service"
"shutdown.target"
"umount.target"
"final.target"
"kexec.target"
"systemd-kexec.service"
]
++ lib.optional cfg.package.withUtmp "systemd-update-utmp.service"
++ [
# Password entry.
"systemd-ask-password-console.path"
"systemd-ask-password-console.service"
"systemd-ask-password-wall.path"
"systemd-ask-password-wall.service"
# Varlink APIs
]
++ lib.optionals cfg.package.withBootloader [
"systemd-bootctl@.service"
"systemd-bootctl.socket"
]
++ [
"systemd-creds@.service"
"systemd-creds.socket"
]
++ lib.optional cfg.package.withTpm2Units [
"systemd-pcrlock@.service"
"systemd-pcrlock.socket"
]
++ [
# Slices / containers.
"slices.target"
]
++ optionals cfg.package.withImportd [
"systemd-importd.service"
]
++ optionals cfg.package.withMachined [
"machine.slice"
"machines.target"
"systemd-machined.service"
]
++ [
"systemd-nspawn@.service"
# Misc.
"systemd-sysctl.service"
"systemd-machine-id-commit.service"
]
++ optionals cfg.package.withTimedated [
"dbus-org.freedesktop.timedate1.service"
"systemd-timedated.service"
]
++ optionals cfg.package.withLocaled [
"dbus-org.freedesktop.locale1.service"
"systemd-localed.service"
]
++ optionals cfg.package.withHostnamed [
"dbus-org.freedesktop.hostname1.service"
"systemd-hostnamed.service"
"systemd-hostnamed.socket"
]
++ optionals cfg.package.withPortabled [
"dbus-org.freedesktop.portable1.service"
"systemd-portabled.service"
]
++ [
"systemd-exit.service"
"systemd-update-done.service"
# Capsule support
"capsule@.service"
"capsule.slice"
]
++ cfg.additionalUpstreamSystemUnits;
upstreamSystemWants = [
"sysinit.target.wants"
"sockets.target.wants"
"local-fs.target.wants"
"multi-user.target.wants"
"timers.target.wants"
];
proxy_env = config.networking.proxy.envVars;
in
{
###### interface
options.systemd = {
package = mkPackageOption pkgs "systemd" { };
enableStrictShellChecks = mkEnableOption "" // {
description = ''
Whether to run `shellcheck` on the generated scripts for systemd
units.
When enabled, all systemd scripts generated by NixOS will be checked
with `shellcheck` and any errors or warnings will cause the build to
fail.
This affects all scripts that have been created through the `script`,
`reload`, `preStart`, `postStart`, `preStop` and `postStop` options for
systemd services. This does not affect command lines passed directly
to `ExecStart`, `ExecReload`, `ExecStartPre`, `ExecStartPost`,
`ExecStop` or `ExecStopPost`.
It therefore also does not affect systemd units that are coming from
packages and that are not defined through the NixOS config. This option
is disabled by default, and although some services have already been
fixed, it is still likely that you will encounter build failures when
enabling this.
We encourage people to enable this option when they are willing and
able to submit fixes for potential build failures to Nixpkgs. The
option can also be enabled or disabled for individual services using
the `enableStrictShellChecks` option on the service itself, which will
take precedence over the global setting.
'';
};
units = mkOption {
description = "Definition of systemd units; see {manpage}`systemd.unit(5)`.";
default = { };
type = systemdUtils.types.units;
};
packages = mkOption {
default = [ ];
type = types.listOf types.package;
example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
description = "Packages providing systemd units and hooks.";
};
targets = mkOption {
default = { };
type = systemdUtils.types.targets;
description = "Definition of systemd target units; see {manpage}`systemd.target(5)`";
};
services = mkOption {
default = { };
type = systemdUtils.types.services;
description = "Definition of systemd service units; see {manpage}`systemd.service(5)`.";
};
sockets = mkOption {
default = { };
type = systemdUtils.types.sockets;
description = "Definition of systemd socket units; see {manpage}`systemd.socket(5)`.";
};
timers = mkOption {
default = { };
type = systemdUtils.types.timers;
description = "Definition of systemd timer units; see {manpage}`systemd.timer(5)`.";
};
paths = mkOption {
default = { };
type = systemdUtils.types.paths;
description = "Definition of systemd path units; see {manpage}`systemd.path(5)`.";
};
mounts = mkOption {
default = [ ];
type = systemdUtils.types.mounts;
description = ''
Definition of systemd mount units; see {manpage}`systemd.mount(5)`.
This is a list instead of an attrSet, because systemd mandates
the names to be derived from the `where` attribute.
'';
};
automounts = mkOption {
default = [ ];
type = systemdUtils.types.automounts;
description = ''
Definition of systemd automount units; see {manpage}`systemd.automount(5)`.
This is a list instead of an attrSet, because systemd mandates
the names to be derived from the `where` attribute.
'';
};
slices = mkOption {
default = { };
type = systemdUtils.types.slices;
description = "Definition of slice configurations; see {manpage}`systemd.slice(5)`.";
};
generators = mkOption {
type = types.attrsOf types.path;
default = { };
example = {
systemd-gpt-auto-generator = "/dev/null";
};
description = ''
Definition of systemd generators; see {manpage}`systemd.generator(5)`.
For each `NAME = VALUE` pair of the attrSet, a link is generated from
`/etc/systemd/system-generators/NAME` to `VALUE`.
'';
};
shutdown = mkOption {
type = types.attrsOf types.path;
default = { };
description = ''
Definition of systemd shutdown executables.
For each `NAME = VALUE` pair of the attrSet, a link is generated from
`/etc/systemd/system-shutdown/NAME` to `VALUE`.
'';
};
defaultUnit = mkOption {
default = "multi-user.target";
type = types.str;
description = ''
Default unit started when the system boots; see {manpage}`systemd.special(7)`.
'';
};
ctrlAltDelUnit = mkOption {
default = "reboot.target";
type = types.str;
example = "poweroff.target";
description = ''
Target that should be started when Ctrl-Alt-Delete is pressed;
see {manpage}`systemd.special(7)`.
'';
};
globalEnvironment = mkOption {
type =
with types;
attrsOf (
nullOr (oneOf [
str
path
package
])
);
default = { };
example = {
TZ = "CET";
};
description = ''
Environment variables passed to *all* systemd units.
'';
};
managerEnvironment = mkOption {
type =
with types;
attrsOf (
nullOr (oneOf [
str
path
package
])
);
default = { };
example = {
SYSTEMD_LOG_LEVEL = "debug";
};
description = ''
Environment variables of PID 1. These variables are
*not* passed to started units.
'';
};
settings.Manager = mkOption {
default = { };
defaultText = lib.literalExpression ''
{
DefaultIOAccounting = true;
DefaultIPAccounting = true;
}
'';
type = lib.types.submodule {
freeformType = types.attrsOf unitOption;
};
example = {
WatchdogDevice = "/dev/watchdog";
RuntimeWatchdogSec = "30s";
RebootWatchdogSec = "10min";
KExecWatchdogSec = "5min";
};
description = ''
Options for the global systemd service manager. See {manpage}`systemd-system.conf(5)` man page
for available options.
'';
};
sleep.extraConfig = mkOption {
default = "";
type = types.lines;
example = "HibernateDelaySec=1h";
description = ''
Extra config options for systemd sleep state logic.
See {manpage}`sleep.conf.d(5)` man page for available options.
'';
};
additionalUpstreamSystemUnits = mkOption {
default = [ ];
type = types.listOf types.str;
example = [
"debug-shell.service"
"systemd-quotacheck.service"
];
description = ''
Additional units shipped with systemd that shall be enabled.
'';
};
suppressedSystemUnits = mkOption {
default = [ ];
type = types.listOf types.str;
example = [ "systemd-backlight@.service" ];
description = ''
A list of units to skip when generating system systemd configuration directory. This has
priority over upstream units, {option}`systemd.units`, and
{option}`systemd.additionalUpstreamSystemUnits`. The main purpose of this is to
prevent a upstream systemd unit from being added to the initrd with any modifications made to it
by other NixOS modules.
'';
};
};
###### implementation
config = {
warnings =
let
mkOneNetOnlineWarn =
typeStr: name: def:
lib.optional (
lib.elem "network-online.target" def.after
&& !(lib.elem "network-online.target" (def.wants ++ def.requires ++ def.bindsTo))
) "${name}.${typeStr} is ordered after 'network-online.target' but doesn't depend on it";
mkNetOnlineWarns =
typeStr: defs: lib.concatLists (lib.mapAttrsToList (mkOneNetOnlineWarn typeStr) defs);
mkMountNetOnlineWarns =
typeStr: defs: lib.concatLists (map (m: mkOneNetOnlineWarn typeStr m.what m) defs);
in
concatLists (
mapAttrsToList (
name: service:
let
type = service.serviceConfig.Type or "";
restart = service.serviceConfig.Restart or "no";
hasDeprecated = builtins.hasAttr "StartLimitInterval" service.serviceConfig;
in
concatLists [
(optional (type == "oneshot" && (restart == "always" || restart == "on-success"))
"Service '${name}.service' with 'Type=oneshot' cannot have 'Restart=always' or 'Restart=on-success'"
)
(optional hasDeprecated "Service '${name}.service' uses the attribute 'StartLimitInterval' in the Service section, which is deprecated. See https://github.com/NixOS/nixpkgs/issues/45786.")
(optional (service.reloadIfChanged && service.reloadTriggers != [ ])
"Service '${name}.service' has both 'reloadIfChanged' and 'reloadTriggers' set. This is probably not what you want, because 'reloadTriggers' behave the same whay as 'restartTriggers' if 'reloadIfChanged' is set."
)
]
) cfg.services
)
++ (mkNetOnlineWarns "target" cfg.targets)
++ (mkNetOnlineWarns "service" cfg.services)
++ (mkNetOnlineWarns "socket" cfg.sockets)
++ (mkNetOnlineWarns "timer" cfg.timers)
++ (mkNetOnlineWarns "path" cfg.paths)
++ (mkMountNetOnlineWarns "mount" cfg.mounts)
++ (mkMountNetOnlineWarns "automount" cfg.automounts)
++ (mkNetOnlineWarns "slice" cfg.slices);
assertions = concatLists (
mapAttrsToList (
name: service:
map
(message: {
assertion = false;
inherit message;
})
(concatLists [
(optional
(
(builtins.elem "network-interfaces.target" service.after)
|| (builtins.elem "network-interfaces.target" service.wants)
)
"Service '${name}.service' is using the deprecated target network-interfaces.target, which no longer exists. Using network.target is recommended instead."
)
])
) cfg.services
);
system.build.units = cfg.units;
system.nssModules = [ cfg.package.out ];
system.nssDatabases = {
hosts = (
mkMerge [
(mkOrder 400 [ "mymachines" ]) # 400 to ensure it comes before resolve (which is 501)
(mkOrder 999 [ "myhostname" ]) # after files (which is 998), but before regular nss modules
]
);
passwd = (
mkMerge [
(mkAfter [ "systemd" ])
]
);
group = (
mkMerge [
(mkAfter [ "[success=merge] systemd" ]) # need merge so that NSS won't stop at file-based groups
]
);
shadow = (
mkMerge [
(mkAfter [ "systemd" ])
]
);
};
environment.systemPackages = [ cfg.package ];
environment.etc =
let
# generate contents for /etc/systemd/${dir} from attrset of links and packages
hooks =
dir: links:
pkgs.runCommand "${dir}"
{
preferLocalBuild = true;
packages = cfg.packages;
}
''
set -e
mkdir -p $out
for package in $packages
do
for hook in $package/lib/systemd/${dir}/*
do
ln -s $hook $out/
done
done
${concatStrings (mapAttrsToList (exec: target: "ln -s ${target} $out/${exec};\n") links)}
'';
enabledUpstreamSystemUnits = filter (n: !elem n cfg.suppressedSystemUnits) upstreamSystemUnits;
enabledUnits = filterAttrs (n: v: !elem n cfg.suppressedSystemUnits) cfg.units;
in
{
"systemd/system".source = generateUnits {
type = "system";
units = enabledUnits;
upstreamUnits = enabledUpstreamSystemUnits;
upstreamWants = upstreamSystemWants;
};
"systemd/system.conf".text = settingsToSections cfg.settings;
"systemd/sleep.conf".text = ''
[Sleep]
${cfg.sleep.extraConfig}
'';
"systemd/user-generators" = {
source = hooks "user-generators" cfg.user.generators;
};
"systemd/system-generators" = {
source = hooks "system-generators" cfg.generators;
};
"systemd/system-shutdown" = {
source = hooks "system-shutdown" cfg.shutdown;
};
# Ignore all other preset files so systemd doesn't try to enable/disable
# units during runtime.
"systemd/system-preset/00-nixos.preset".text = ''
ignore *
'';
"systemd/user-preset/00-nixos.preset".text = ''
ignore *
'';
};
services.dbus.enable = true;
users.users.systemd-network = {
uid = config.ids.uids.systemd-network;
group = "systemd-network";
};
users.groups.systemd-network.gid = config.ids.gids.systemd-network;
users.users.systemd-resolve = {
uid = config.ids.uids.systemd-resolve;
group = "systemd-resolve";
};
users.groups.systemd-resolve.gid = config.ids.gids.systemd-resolve;
# Target for charon send-keys to hook into.
users.groups.keys.gid = config.ids.gids.keys;
systemd.targets.keys = {
description = "Security Keys";
unitConfig.X-StopOnReconfiguration = true;
};
# This target only exists so that services ordered before sysinit.target
# are restarted in the correct order, notably BEFORE the other services,
# when switching configurations.
systemd.targets.sysinit-reactivation = {
description = "Reactivate sysinit units";
};
systemd.units =
let
withName = cfgToUnit: cfg: lib.nameValuePair cfg.name (cfgToUnit cfg);
in
mapAttrs' (_: withName pathToUnit) cfg.paths
// mapAttrs' (_: withName serviceToUnit) cfg.services
// mapAttrs' (_: withName sliceToUnit) cfg.slices
// mapAttrs' (_: withName socketToUnit) cfg.sockets
// mapAttrs' (_: withName targetToUnit) cfg.targets
// mapAttrs' (_: withName timerToUnit) cfg.timers
// listToAttrs (map (withName mountToUnit) cfg.mounts)
// listToAttrs (map (withName automountToUnit) cfg.automounts);
# Environment of PID 1
systemd.managerEnvironment = {
# Doesn't contain systemd itself - everything works so it seems to use the compiled-in value for its tools
# util-linux is needed for the main fsck utility wrapping the fs-specific ones
PATH = lib.makeBinPath (
config.system.fsPackages
++ [ cfg.package.util-linux ]
# systemd-ssh-generator needs sshd in PATH
++ lib.optional config.services.openssh.enable config.services.openssh.package
);
LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive";
TZDIR = "/etc/zoneinfo";
# If SYSTEMD_UNIT_PATH ends with an empty component (":"), the usual unit load path will be appended to the contents of the variable
SYSTEMD_UNIT_PATH = lib.mkIf (
config.boot.extraSystemdUnitPaths != [ ]
) "${builtins.concatStringsSep ":" config.boot.extraSystemdUnitPaths}:";
};
systemd.settings.Manager = {
ManagerEnvironment = lib.concatStringsSep " " (
lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment
);
DefaultIOAccounting = lib.mkDefault true;
DefaultIPAccounting = lib.mkDefault true;
};
system.requiredKernelConfig = map config.lib.kernelConfig.isEnabled [
"DEVTMPFS"
"CGROUPS"
"INOTIFY_USER"
"SIGNALFD"
"TIMERFD"
"EPOLL"
"NET"
"SYSFS"
"PROC_FS"
"FHANDLE"
"CRYPTO_USER_API_HASH"
"CRYPTO_HMAC"
"CRYPTO_SHA256"
"DMIID"
"AUTOFS_FS"
"TMPFS_POSIX_ACL"
"TMPFS_XATTR"
"SECCOMP"
];
# Generate timer units for all services that have a startAt value.
systemd.timers = mapAttrs (name: service: {
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = service.startAt;
}) (filterAttrs (name: service: service.enable && service.startAt != [ ]) cfg.services);
# Some overrides to upstream units.
systemd.services."systemd-backlight@".restartIfChanged = false;
systemd.services."systemd-fsck@".restartIfChanged = false;
systemd.services."systemd-fsck@".path = [ pkgs.util-linux ] ++ config.system.fsPackages;
systemd.services."systemd-makefs@" = {
restartIfChanged = false;
path = [ pkgs.util-linux ] ++ config.system.fsPackages;
# Since there is no /etc/systemd/system/systemd-makefs@.service
# file, the units generated in /run/systemd/generator would
# override anything we put here. But by forcing the use of a
# drop-in in /etc, it does apply.
overrideStrategy = "asDropin";
};
systemd.services."systemd-mkswap@" = {
restartIfChanged = false;
path = [ pkgs.util-linux ];
overrideStrategy = "asDropin";
};
systemd.services.systemd-random-seed.restartIfChanged = false;
systemd.services.systemd-remount-fs.restartIfChanged = false;
systemd.services.systemd-update-utmp.restartIfChanged = false;
systemd.services.systemd-udev-settle.restartIfChanged = false; # Causes long delays in nixos-rebuild
systemd.targets.local-fs.unitConfig.X-StopOnReconfiguration = true;
systemd.targets.remote-fs.unitConfig.X-StopOnReconfiguration = true;
systemd.services.systemd-importd.environment = proxy_env;
systemd.services.systemd-pstore.wantedBy = [ "sysinit.target" ]; # see #81138
# NixOS has kernel modules in a different location, so override that here.
systemd.services.kmod-static-nodes.unitConfig.ConditionFileNotEmpty = [
"" # required to unset the previous value!
"/run/booted-system/kernel-modules/lib/modules/%v/modules.devname"
];
# Don't bother with certain units in containers.
systemd.services.systemd-remount-fs.unitConfig.ConditionVirtualization = "!container";
# Increase numeric PID range (set directly instead of copying a one-line file from systemd)
# https://github.com/systemd/systemd/pull/12226
boot.kernel.sysctl."kernel.pid_max" = mkIf pkgs.stdenv.hostPlatform.is64bit (lib.mkDefault 4194304);
services.logrotate.settings = {
"/var/log/btmp" = mapAttrs (_: mkDefault) {
frequency = "monthly";
rotate = 1;
create = "0660 root ${config.users.groups.utmp.name}";
minsize = "1M";
};
"/var/log/wtmp" = mapAttrs (_: mkDefault) {
frequency = "monthly";
rotate = 1;
create = "0664 root ${config.users.groups.utmp.name}";
minsize = "1M";
};
};
# run0 is supposed to authenticate the user via polkit and then run a command. Without this next
# part, run0 would fail to run the command even if authentication is successful and the user has
# permission to run the command. This next part is only enabled if polkit is enabled because the
# error that were trying to avoid cant possibly happen if polkit isnt enabled. When polkit isnt
# enabled, run0 will fail before it even tries to run the command.
security.pam.services = mkIf config.security.polkit.enable {
systemd-run0 = {
# Upstream config: https://github.com/systemd/systemd/blob/main/src/run/systemd-run0.in
setLoginUid = true;
pamMount = false;
};
};
};
# FIXME: Remove these eventually.
imports = [
(mkRenamedOptionModule [ "boot" "systemd" "sockets" ] [ "systemd" "sockets" ])
(mkRenamedOptionModule [ "boot" "systemd" "targets" ] [ "systemd" "targets" ])
(mkRenamedOptionModule [ "boot" "systemd" "services" ] [ "systemd" "services" ])
(mkRenamedOptionModule [ "jobs" ] [ "systemd" "services" ])
(mkRemovedOptionModule [ "systemd" "generator-packages" ] "Use systemd.packages instead.")
(mkRemovedOptionModule [ "systemd" "enableUnifiedCgroupHierarchy" ] ''
In 256 support for cgroup v1 ('legacy' and 'hybrid' hierarchies) is now considered obsolete and systemd by default will refuse to boot under it.
To forcibly reenable cgroup v1 support, you can set boot.kernelParams = [ "systemd.unified_cgroup_hierarchy=0" "SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1" ].
NixOS does not officially support this configuration and might cause your system to be unbootable in future versions. You are on your own.
'')
(mkRemovedOptionModule [ "systemd" "extraConfig" ] "Use systemd.settings.Manager instead.")
(lib.mkRenamedOptionModule
[ "systemd" "watchdog" "device" ]
[ "systemd" "settings" "Manager" "WatchdogDevice" ]
)
(lib.mkRenamedOptionModule
[ "systemd" "watchdog" "runtimeTime" ]
[ "systemd" "settings" "Manager" "RuntimeWatchdogSec" ]
)
(lib.mkRenamedOptionModule
[ "systemd" "watchdog" "rebootTime" ]
[ "systemd" "settings" "Manager" "RebootWatchdogSec" ]
)
(lib.mkRenamedOptionModule
[ "systemd" "watchdog" "kexecTime" ]
[ "systemd" "settings" "Manager" "KExecWatchdogSec" ]
)
(mkRemovedOptionModule [
"systemd"
"enableCgroupAccounting"
] "To disable cgroup accounting, disable systemd.settings.Manager.*Accounting directly.")
];
}

View File

@@ -0,0 +1,87 @@
{
config,
lib,
pkgs,
utils,
...
}:
with lib;
let
cfg = config.systemd.coredump;
systemd = config.systemd.package;
in
{
options = {
systemd.coredump.enable = mkOption {
default = true;
type = types.bool;
description = ''
Whether core dumps should be processed by
{command}`systemd-coredump`. If disabled, core dumps
appear in the current directory of the crashing process.
'';
};
systemd.coredump.extraConfig = mkOption {
default = "";
type = types.lines;
example = "Storage=journal";
description = ''
Extra config options for systemd-coredump. See {manpage}`coredump.conf(5)` man page
for available options.
'';
};
};
config = mkMerge [
(mkIf cfg.enable {
systemd.additionalUpstreamSystemUnits = [
"systemd-coredump.socket"
"systemd-coredump@.service"
];
environment.etc = {
"systemd/coredump.conf".text = ''
[Coredump]
${cfg.extraConfig}
'';
# install provided sysctl snippets
"sysctl.d/50-coredump.conf".source =
# Fix systemd-coredump error caused by truncation of `kernel.core_pattern`
# when the `systemd` derivation name is too long. This works by substituting
# the path to `systemd` with a symlink that has a constant-length path.
#
# See: https://github.com/NixOS/nixpkgs/issues/213408
pkgs.substitute {
src = "${systemd}/example/sysctl.d/50-coredump.conf";
substitutions = [
"--replace-fail"
"${systemd}"
"${pkgs.symlinkJoin {
name = "systemd";
paths = [ systemd ];
}}"
];
};
"sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
};
users.users.systemd-coredump = {
uid = config.ids.uids.systemd-coredump;
group = "systemd-coredump";
};
users.groups.systemd-coredump = { };
})
(mkIf (!cfg.enable) {
boot.kernel.sysctl."kernel.core_pattern" = mkDefault "core";
})
];
}

View File

@@ -0,0 +1,61 @@
{ config, lib, ... }:
let
cfg = config.boot.initrd.systemd.dmVerity;
in
{
options = {
boot.initrd.systemd.dmVerity = {
enable = lib.mkEnableOption "dm-verity" // {
description = ''
Mount verity-protected block devices in the initrd.
Enabling this option allows to use `systemd-veritysetup` and
`systemd-veritysetup-generator` in the initrd.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = ''
'boot.initrd.systemd.dmVerity.enable' requires 'boot.initrd.systemd.enable' to be enabled.
'';
}
];
boot.initrd = {
availableKernelModules = [
"dm_mod"
"dm_verity"
];
# dm-verity needs additional udev rules from LVM to work.
services.lvm.enable = true;
# The additional targets and store paths allow users to integrate verity-protected devices
# through the systemd tooling.
systemd = {
additionalUpstreamUnits = [
"veritysetup-pre.target"
"veritysetup.target"
"remote-veritysetup.target"
];
storePaths = [
"${config.boot.initrd.systemd.package}/lib/systemd/systemd-veritysetup"
"${config.boot.initrd.systemd.package}/lib/systemd/system-generators/systemd-veritysetup-generator"
];
};
};
};
meta.maintainers = with lib.maintainers; [
msanft
nikstur
willibutz
];
}

View File

@@ -0,0 +1,32 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.boot.initrd.systemd;
in
{
options = {
boot.initrd.systemd.fido2.enable = lib.mkEnableOption "systemd FIDO2 support" // {
default = cfg.package.withFido2;
defaultText = lib.literalExpression "config.boot.initrd.systemd.package.withFido2";
};
};
config = lib.mkIf cfg.fido2.enable {
boot.initrd.services.udev.packages = [
# TODO: Add a better way to include upstream rules files.
(pkgs.runCommand "udev-fido2" { } ''
mkdir -p $out/lib/udev/rules.d/
cp ${cfg.package}/lib/udev/rules.d/60-fido-id.rules $out/lib/udev/rules.d/60-fido-id.rules
'')
];
boot.initrd.systemd.storePaths = [
"${pkgs.systemd}/lib/udev/fido_id"
"${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so"
"${pkgs.libfido2}/lib/libfido2.so.1"
];
};
}

View File

@@ -0,0 +1,94 @@
{
config,
lib,
utils,
...
}:
let
cfg = config.services.homed;
in
{
options.services.homed = {
enable = lib.mkEnableOption "systemd home area/user account manager";
promptOnFirstBoot =
lib.mkEnableOption ''
interactively prompting for user creation on first boot
''
// {
default = true;
};
settings.Home = lib.mkOption {
default = { };
type = lib.types.submodule {
freeformType = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption;
};
example = {
DefaultStorage = "luks";
DefaultFileSystemType = "btrfs";
};
description = ''
Options for systemd-homed. See {manpage}`homed.conf(5)` man page for
available options.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.services.nscd.enable;
message = ''
systemd-homed requires the use of the systemd nss module.
services.nscd.enable must be set to true.
'';
}
];
systemd.additionalUpstreamSystemUnits = [
"systemd-homed.service"
"systemd-homed-activate.service"
"systemd-homed-firstboot.service"
];
# homed exposes SSH public keys and other user metadata using userdb
services.userdbd = {
enable = true;
enableSSHSupport = lib.mkDefault config.services.openssh.enable;
};
# Enable creation and mounting of LUKS home areas with all filesystems
# supported by systemd-homed.
boot.supportedFilesystems = [
"btrfs"
"ext4"
"xfs"
];
environment.etc."systemd/homed.conf".text = ''
[Home]
${utils.systemdUtils.lib.attrsToSection cfg.settings.Home}
'';
systemd.services = {
systemd-homed = {
# These packages are required to manage home areas with LUKS storage
path = config.system.fsPackages;
aliases = [ "dbus-org.freedesktop.home1.service" ];
wantedBy = [ "multi-user.target" ];
};
systemd-homed-activate = {
wantedBy = [ "systemd-homed.service" ];
};
systemd-homed-firstboot = {
enable = cfg.promptOnFirstBoot;
wantedBy = [ "systemd-homed.service" ];
};
};
};
}

View File

@@ -0,0 +1,51 @@
{
config,
pkgs,
lib,
...
}:
{
config = lib.mkIf (config.boot.initrd.enable && config.boot.initrd.systemd.enable) {
# Copy secrets into the initrd if they cannot be appended
boot.initrd.systemd.contents = lib.mkIf (!config.boot.loader.supportsInitrdSecrets) (
lib.mapAttrs' (
dest: source:
lib.nameValuePair "/.initrd-secrets/${dest}" { source = if source == null then dest else source; }
) config.boot.initrd.secrets
);
# Copy secrets to their respective locations
boot.initrd.systemd.services.initrd-nixos-copy-secrets =
lib.mkIf (config.boot.initrd.secrets != { })
{
description = "Copy secrets into place";
# Run as early as possible
wantedBy = [ "sysinit.target" ];
before = [
"cryptsetup-pre.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
# We write the secrets to /.initrd-secrets and move them because this allows
# secrets to be written to /run. If we put the secret directly to /run and
# drop this service, we'd mount the /run tmpfs over the secret, making it
# invisible in stage 2.
script = ''
for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
mkdir -p "$(dirname "/$secret")"
cp "/.initrd-secrets/$secret" "/$secret"
done
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
# The script needs this
boot.initrd.systemd.extraBin.find = "${pkgs.findutils}/bin/find";
};
}

View File

@@ -0,0 +1,765 @@
{
lib,
options,
config,
utils,
pkgs,
...
}:
with lib;
let
inherit (utils) systemdUtils escapeSystemdPath;
inherit (systemdUtils.unitOptions) unitOption;
inherit (systemdUtils.lib)
generateUnits
pathToUnit
serviceToUnit
sliceToUnit
socketToUnit
targetToUnit
timerToUnit
mountToUnit
automountToUnit
settingsToSections
;
cfg = config.boot.initrd.systemd;
upstreamUnits = [
"basic.target"
"ctrl-alt-del.target"
"debug-shell.service"
"emergency.service"
"emergency.target"
"final.target"
"halt.target"
"initrd-cleanup.service"
"initrd-fs.target"
"initrd-parse-etc.service"
"initrd-root-device.target"
"initrd-root-fs.target"
"initrd-switch-root.service"
"initrd-switch-root.target"
"initrd.target"
"kexec.target"
"kmod-static-nodes.service"
"local-fs-pre.target"
"local-fs.target"
"modprobe@.service"
"multi-user.target"
"paths.target"
"poweroff.target"
"reboot.target"
"rescue.service"
"rescue.target"
"rpcbind.target"
"shutdown.target"
"sigpwr.target"
"slices.target"
"sockets.target"
"swap.target"
"sysinit.target"
"sys-kernel-config.mount"
"syslog.socket"
"systemd-ask-password-console.path"
"systemd-ask-password-console.service"
"systemd-fsck@.service"
"systemd-halt.service"
"systemd-hibernate-resume.service"
"systemd-journald-audit.socket"
"systemd-journald-dev-log.socket"
"systemd-journald.service"
"systemd-journald.socket"
"systemd-kexec.service"
"systemd-modules-load.service"
"systemd-poweroff.service"
"systemd-reboot.service"
"systemd-sysctl.service"
"timers.target"
"umount.target"
"systemd-bsod.service"
]
++ cfg.additionalUpstreamUnits;
upstreamWants = [
"sysinit.target.wants"
];
enabledUpstreamUnits = filter (n: !elem n cfg.suppressedUnits) upstreamUnits;
enabledUnits = filterAttrs (n: v: !elem n cfg.suppressedUnits) cfg.units;
jobScripts = concatLists (
mapAttrsToList (_: unit: unit.jobScripts or [ ]) (filterAttrs (_: v: v.enable) cfg.services)
);
stage1Units = generateUnits {
type = "initrd";
units = enabledUnits;
upstreamUnits = enabledUpstreamUnits;
inherit upstreamWants;
inherit (cfg) packages package;
};
kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
initrdBinEnv = pkgs.buildEnv {
name = "initrd-bin-env";
paths = map getBin cfg.initrdBin;
pathsToLink = [
"/bin"
"/sbin"
];
# Make sure sbin and bin have the same contents, and add extraBin
postBuild = ''
find $out/bin -maxdepth 1 -type l -print0 | xargs --null cp --no-dereference --no-clobber -t $out/sbin/
find $out/sbin -maxdepth 1 -type l -print0 | xargs --null cp --no-dereference --no-clobber -t $out/bin/
${concatStringsSep "\n" (
mapAttrsToList (n: v: ''
ln -sf '${v}' $out/bin/'${n}'
ln -sf '${v}' $out/sbin/'${n}'
'') cfg.extraBin
)}
'';
};
initialRamdisk = pkgs.makeInitrdNG {
name = "initrd-${kernel-name}";
inherit (config.boot.initrd) compressor compressorArgs prepend;
contents = lib.filter (
{ source, enable, ... }: (!lib.elem source cfg.suppressedStorePaths) && enable
) cfg.storePaths;
};
in
{
imports = [
(lib.mkRemovedOptionModule [ "boot" "initrd" "systemd" "strip" ] ''
The option to strip ELF files in initrd has been removed.
It only saved ~1MiB of initramfs size, but caused a few issues
like unloadable kernel modules.
'')
(lib.mkRemovedOptionModule [
"boot"
"initrd"
"systemd"
"extraConfig"
] "Use boot.initrd.systemd.settings.Manager instead.")
];
options.boot.initrd.systemd = {
enable = mkEnableOption "systemd in initrd" // {
description = ''
Whether to enable systemd in initrd. The unit options such as
{option}`boot.initrd.systemd.services` are the same as their
stage 2 counterparts such as {option}`systemd.services`,
except that `restartTriggers` and `reloadTriggers` are not
supported.
'';
};
package = lib.mkOption {
type = lib.types.package;
default = config.systemd.package;
defaultText = lib.literalExpression "config.systemd.package";
description = ''
The systemd package to use.
'';
};
settings.Manager = mkOption {
default = { };
defaultText = lib.literalExpression ''
{
DefaultEnvironment = "PATH=/bin:/sbin";
}
'';
type = lib.types.submodule {
freeformType = types.attrsOf unitOption;
};
example = {
WatchdogDevice = "/dev/watchdog";
RuntimeWatchdogSec = "30s";
RebootWatchdogSec = "10min";
KExecWatchdogSec = "5min";
};
description = ''
Options for the global systemd service manager used in initrd. See {manpage}`systemd-system.conf(5)` man page
for available options.
'';
};
managerEnvironment = mkOption {
type =
with types;
attrsOf (
nullOr (oneOf [
str
path
package
])
);
default = { };
defaultText = ''
{
PATH = "/bin:/sbin";
}
'';
example = {
SYSTEMD_LOG_LEVEL = "debug";
};
description = ''
Environment variables of PID 1. These variables are
*not* passed to started units.
'';
};
contents = mkOption {
description = "Set of files that have to be linked into the initrd";
example = literalExpression ''
{
"/etc/machine-id".source = /etc/machine-id;
}
'';
default = { };
type = utils.systemdUtils.types.initrdContents;
};
storePaths = mkOption {
description = ''
Store paths to copy into the initrd as well.
'';
type = utils.systemdUtils.types.initrdStorePath;
default = [ ];
};
extraBin = mkOption {
description = ''
Tools to add to /bin
'';
example = literalExpression ''
{
umount = ''${pkgs.util-linux}/bin/umount;
}
'';
type = types.attrsOf types.path;
default = { };
};
suppressedStorePaths = mkOption {
description = ''
Store paths specified in the storePaths option that
should not be copied.
'';
type = types.listOf types.singleLineStr;
default = [ ];
};
root = lib.mkOption {
type = lib.types.enum [
"fstab"
"gpt-auto"
];
default = "fstab";
example = "gpt-auto";
description = ''
Controls how systemd will interpret the root FS in initrd. See
{manpage}`kernel-command-line(7)`. NixOS currently does not
allow specifying the root file system itself this
way. Instead, the `fstab` value is used in order to interpret
the root file system specified with the `fileSystems` option.
'';
};
emergencyAccess = mkOption {
type =
with types;
oneOf [
bool
(nullOr (passwdEntry str))
];
description = ''
Set to true for unauthenticated emergency access, and false or
null for no emergency access.
Can also be set to a hashed super user password to allow
authenticated access to the emergency mode.
For emergency access after initrd, use `${options.systemd.enableEmergencyMode}` instead.
'';
default = false;
};
initrdBin = mkOption {
type = types.listOf types.package;
default = [ ];
description = ''
Packages to include in /bin for the stage 1 emergency shell.
'';
};
additionalUpstreamUnits = mkOption {
default = [ ];
type = types.listOf types.str;
example = [
"debug-shell.service"
"systemd-quotacheck.service"
];
description = ''
Additional units shipped with systemd that shall be enabled.
'';
};
suppressedUnits = mkOption {
default = [ ];
type = types.listOf types.str;
example = [ "systemd-backlight@.service" ];
description = ''
A list of units to skip when generating system systemd configuration directory. This has
priority over upstream units, {option}`boot.initrd.systemd.units`, and
{option}`boot.initrd.systemd.additionalUpstreamUnits`. The main purpose of this is to
prevent a upstream systemd unit from being added to the initrd with any modifications made to it
by other NixOS modules.
'';
};
units = mkOption {
description = "Definition of systemd units.";
default = { };
visible = "shallow";
type = systemdUtils.types.units;
};
packages = mkOption {
default = [ ];
type = types.listOf types.package;
example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
description = "Packages providing systemd units and hooks.";
};
targets = mkOption {
default = { };
visible = "shallow";
type = systemdUtils.types.initrdTargets;
description = "Definition of systemd target units.";
};
services = mkOption {
default = { };
type = systemdUtils.types.initrdServices;
visible = "shallow";
description = "Definition of systemd service units.";
};
sockets = mkOption {
default = { };
type = systemdUtils.types.initrdSockets;
visible = "shallow";
description = "Definition of systemd socket units.";
};
timers = mkOption {
default = { };
type = systemdUtils.types.initrdTimers;
visible = "shallow";
description = "Definition of systemd timer units.";
};
paths = mkOption {
default = { };
type = systemdUtils.types.initrdPaths;
visible = "shallow";
description = "Definition of systemd path units.";
};
mounts = mkOption {
default = [ ];
type = systemdUtils.types.initrdMounts;
visible = "shallow";
description = ''
Definition of systemd mount units.
This is a list instead of an attrSet, because systemd mandates the names to be derived from
the 'where' attribute.
'';
};
automounts = mkOption {
default = [ ];
type = systemdUtils.types.automounts;
visible = "shallow";
description = ''
Definition of systemd automount units.
This is a list instead of an attrSet, because systemd mandates the names to be derived from
the 'where' attribute.
'';
};
slices = mkOption {
default = { };
type = systemdUtils.types.slices;
visible = "shallow";
description = "Definition of slice configurations.";
};
};
config = mkIf (config.boot.initrd.enable && cfg.enable) {
assertions = [
{
assertion =
cfg.root == "fstab" -> any (fs: fs.mountPoint == "/") (builtins.attrValues config.fileSystems);
message = "The fileSystems option does not specify your root file system.";
}
]
++
map
(name: {
assertion = lib.attrByPath name (throw "impossible") config.boot.initrd == "";
message = ''
systemd stage 1 does not support 'boot.initrd.${lib.concatStringsSep "." name}'. Please
convert it to analogous systemd units in 'boot.initrd.systemd'.
Definitions:
${lib.concatMapStringsSep "\n" ({ file, ... }: " - ${file}")
(lib.attrByPath name (throw "impossible") options.boot.initrd).definitionsWithLocations
}
'';
})
[
[ "preFailCommands" ]
[ "preDeviceCommands" ]
[ "preLVMCommands" ]
[ "postDeviceCommands" ]
[ "postResumeCommands" ]
[ "postMountCommands" ]
[ "extraUdevRulesCommands" ]
[ "extraUtilsCommands" ]
[ "extraUtilsCommandsTest" ]
[
"network"
"postCommands"
]
];
system.build = { inherit initialRamdisk; };
boot.initrd.availableKernelModules = [
# systemd needs this for some features
"autofs"
# systemd-cryptenroll
]
++ lib.optional cfg.package.withEfi "efivarfs";
boot.kernelParams = [
"root=${config.boot.initrd.systemd.root}"
]
++ lib.optional (config.boot.resumeDevice != "") "resume=${config.boot.resumeDevice}"
# `systemd` mounts root in initrd as read-only unless "rw" is on the kernel command line.
# For NixOS activation to succeed, we need to have root writable in initrd.
++ lib.optional (config.boot.initrd.systemd.root == "gpt-auto") "rw";
boot.initrd.systemd = {
# bashInteractive is easier to use and also required by debug-shell.service
initrdBin = [
pkgs.bashInteractive
pkgs.coreutils
cfg.package
]
++ lib.optional (config.system.build.kernel.config.isYes "MODULES") cfg.package.kmod;
extraBin = {
less = "${pkgs.less}/bin/less";
mount = "${cfg.package.util-linux}/bin/mount";
umount = "${cfg.package.util-linux}/bin/umount";
fsck = "${cfg.package.util-linux}/bin/fsck";
};
managerEnvironment.PATH = "/bin:/sbin";
settings.Manager.ManagerEnvironment = lib.concatStringsSep " " (
lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment
);
settings.Manager.DefaultEnvironment = "PATH=/bin:/sbin";
contents = {
"/init".source = "${cfg.package}/lib/systemd/systemd";
"/etc/systemd/system".source = stage1Units;
"/etc/systemd/system.conf".text = settingsToSections cfg.settings;
# We can use either ! or * to lock the root account in the
# console, but some software like OpenSSH won't even allow you
# to log in with an SSH key if you use ! so we use * instead
"/etc/shadow".text =
let
ea = cfg.emergencyAccess;
access = ea != null && !(isBool ea && !ea);
passwd = if isString ea then ea else "";
in
"root:${if access then passwd else "*"}:::::::";
"/bin".source = "${initrdBinEnv}/bin";
"/sbin".source = "${initrdBinEnv}/sbin";
"/usr/bin".source = "${initrdBinEnv}/bin";
"/usr/sbin".source = "${initrdBinEnv}/sbin";
"/etc/os-release".source = config.boot.initrd.osRelease;
"/etc/initrd-release".source = config.boot.initrd.osRelease;
# For systemd-journald's _HOSTNAME field; needs to be set early, cannot be backfilled.
"/etc/hostname".text = config.networking.hostName;
}
// optionalAttrs (config.environment.etc ? "modprobe.d/nixos.conf") {
"/etc/modprobe.d/nixos.conf".source = config.environment.etc."modprobe.d/nixos.conf".source;
}
// optionalAttrs (with config.system.build.kernel.config; isSet "MODULES" -> isYes "MODULES") {
"/lib".source = "${config.system.build.modulesClosure}/lib";
"/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules;
"/etc/sysctl.d/nixos.conf".text = "kernel.modprobe = /sbin/modprobe";
"/etc/modprobe.d/systemd.conf".source = "${cfg.package}/lib/modprobe.d/systemd.conf";
"/etc/modprobe.d/ubuntu.conf".source = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
"/etc/modprobe.d/debian.conf".source = pkgs.kmod-debian-aliases;
};
storePaths = [
# systemd tooling
"${cfg.package}/lib/systemd/systemd-executor"
"${cfg.package}/lib/systemd/systemd-fsck"
"${cfg.package}/lib/systemd/systemd-hibernate-resume"
"${cfg.package}/lib/systemd/systemd-journald"
"${cfg.package}/lib/systemd/systemd-makefs"
"${cfg.package}/lib/systemd/systemd-modules-load"
"${cfg.package}/lib/systemd/systemd-remount-fs"
"${cfg.package}/lib/systemd/systemd-shutdown"
"${cfg.package}/lib/systemd/systemd-sulogin-shell"
"${cfg.package}/lib/systemd/systemd-sysctl"
"${cfg.package}/lib/systemd/systemd-bsod"
"${cfg.package}/lib/systemd/systemd-sysroot-fstab-check"
# generators
"${cfg.package}/lib/systemd/system-generators/systemd-debug-generator"
"${cfg.package}/lib/systemd/system-generators/systemd-fstab-generator"
"${cfg.package}/lib/systemd/system-generators/systemd-gpt-auto-generator"
"${cfg.package}/lib/systemd/system-generators/systemd-hibernate-resume-generator"
"${cfg.package}/lib/systemd/system-generators/systemd-run-generator"
# utilities needed by systemd
"${cfg.package.util-linux}/bin/mount"
"${cfg.package.util-linux}/bin/umount"
"${cfg.package.util-linux}/bin/sulogin"
# required for services generated with writeShellScript and friends
pkgs.runtimeShell
# some tools like xfs still want the sh symlink
"${pkgs.bashNonInteractive}/bin"
# so NSS can look up usernames
"${pkgs.glibc}/lib/libnss_files.so.2"
# Resolving sysroot symlinks without code exec
"${config.system.nixos-init.package}/bin/chroot-realpath"
# Find the etc paths
"${config.system.nixos-init.package}/bin/find-etc"
]
++ lib.optionals config.system.nixos-init.enable [
"${config.system.nixos-init.package}/bin/initrd-init"
]
++ jobScripts
++ map (c: builtins.removeAttrs c [ "text" ]) (builtins.attrValues cfg.contents);
targets.initrd.aliases = [ "default.target" ];
units =
mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit v)) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit v)) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit v)) cfg.slices
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit v)) cfg.sockets
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit v)) cfg.targets
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit v)) cfg.timers
// listToAttrs (
map (
v:
let
n = escapeSystemdPath v.where;
in
nameValuePair "${n}.mount" (mountToUnit v)
) cfg.mounts
)
// listToAttrs (
map (
v:
let
n = escapeSystemdPath v.where;
in
nameValuePair "${n}.automount" (automountToUnit v)
) cfg.automounts
);
services.initrd-find-nixos-closure = lib.mkIf (!config.system.nixos-init.enable) {
description = "Find NixOS closure";
unitConfig = {
RequiresMountsFor = "/sysroot/nix/store";
DefaultDependencies = false;
};
before = [
"initrd.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
requiredBy = [ "initrd.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = # bash
''
set -uo pipefail
export PATH="/bin:${
lib.makeBinPath [
cfg.package.util-linux
config.system.nixos-init.package
]
}"
# Figure out what closure to boot
closure=
for o in $(< /proc/cmdline); do
case $o in
init=*)
IFS="=" read -r -a initParam <<< "$o"
closure="''${initParam[1]}"
;;
esac
done
# Sanity check
if [ -z "''${closure:-}" ]; then
echo 'No init= parameter on the kernel command line' >&2
exit 1
fi
# Resolve symlinks in the init parameter. We need this for some boot loaders
# (e.g. boot.loader.generationsDir).
closure="$(chroot-realpath /sysroot "$closure")"
# Assume the directory containing the init script is the closure.
closure="$(dirname "$closure")"
ln --symbolic "$closure" /nixos-closure
# If we are not booting a NixOS closure (e.g. init=/bin/sh),
# we don't know what root to prepare so we don't do anything
if ! [ -x "/sysroot$(readlink "/sysroot$closure/prepare-root" || echo "$closure/prepare-root")" ]; then
echo "NEW_INIT=''${initParam[1]}" > /etc/switch-root.conf
echo "$closure does not look like a NixOS installation - not activating"
exit 0
fi
echo 'NEW_INIT=' > /etc/switch-root.conf
'';
};
# We need to propagate /run for things like /run/booted-system
# and /run/current-system.
mounts = [
{
where = "/sysroot/run";
what = "/run";
options = "rbind";
unitConfig = {
# See the comment on the mount unit for /run/etc-metadata
DefaultDependencies = false;
};
requiredBy = [ "initrd-fs.target" ];
before = [ "initrd-fs.target" ];
}
];
services.initrd-nixos-activation = lib.mkIf (!config.system.nixos-init.enable) {
after = [ "initrd-switch-root.target" ];
requiredBy = [ "initrd-switch-root.service" ];
before = [ "initrd-switch-root.service" ];
unitConfig.DefaultDependencies = false;
unitConfig = {
AssertPathExists = "/etc/initrd-release";
RequiresMountsFor = [
"/sysroot/run"
];
};
serviceConfig.Type = "oneshot";
description = "NixOS Activation";
script = # bash
''
set -uo pipefail
export PATH="/bin:${cfg.package.util-linux}/bin"
closure="$(realpath /nixos-closure)"
# Initialize the system
export IN_NIXOS_SYSTEMD_STAGE1=true
exec chroot /sysroot "$closure/prepare-root"
'';
};
services.initrd-switch-root =
if config.system.nixos-init.enable then
{
path = [
cfg.package
cfg.package.util-linux
config.system.nixos-init.package
];
environment = {
FIRMWARE = "${config.hardware.firmware}/lib/firmware";
MODPROBE_BINARY = "${pkgs.kmod}/bin/modprobe";
NIX_STORE_MOUNT_OPTS = lib.concatStringsSep "," config.boot.nixStoreMountOpts;
}
// lib.optionalAttrs (config.environment.usrbinenv != null) {
ENV_BINARY = config.environment.usrbinenv;
}
// lib.optionalAttrs (config.environment.binsh != null) {
SH_BINARY = config.environment.binsh;
};
serviceConfig = {
ExecStart = [
""
"${config.system.nixos-init.package}/bin/initrd-init"
];
};
}
else
# This will either call systemctl with the new init as the last parameter (which
# is the case when not booting a NixOS system) or with an empty string, causing
# systemd to bypass its verification code that checks whether the next file is a systemd
# and using its compiled-in value
{
serviceConfig = {
EnvironmentFile = "-/etc/switch-root.conf";
ExecStart = [
""
''systemctl --no-block switch-root /sysroot "''${NEW_INIT}"''
];
};
};
services.panic-on-fail = {
wantedBy = [ "emergency.target" ];
unitConfig = {
DefaultDependencies = false;
ConditionKernelCommandLine = [
"|boot.panic_on_fail"
"|stage1panic"
];
};
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/echo c";
StandardOutput = "file:/proc/sysrq-trigger";
};
};
};
};
}

View File

@@ -0,0 +1,147 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.journald.gateway;
cliArgs = lib.cli.toGNUCommandLineShell { } {
# If either of these are null / false, they are not passed in the command-line
inherit (cfg)
cert
key
trust
system
user
merge
;
};
in
{
meta.maintainers = [ lib.maintainers.raitobezarius ];
options.services.journald.gateway = {
enable = lib.mkEnableOption "the HTTP gateway to the journal";
port = lib.mkOption {
default = 19531;
type = lib.types.port;
description = ''
The port to listen to.
'';
};
cert = lib.mkOption {
default = null;
type = with lib.types; nullOr str;
description = ''
The path to a file or `AF_UNIX` stream socket to read the server
certificate from.
The certificate must be in PEM format. This option switches
`systemd-journal-gatewayd` into HTTPS mode and must be used together
with {option}`services.journald.gateway.key`.
'';
};
key = lib.mkOption {
default = null;
type = with lib.types; nullOr str;
description = ''
Specify the path to a file or `AF_UNIX` stream socket to read the
secret server key corresponding to the certificate specified with
{option}`services.journald.gateway.cert` from.
The key must be in PEM format.
This key should not be world-readable, and must be readably by the
`systemd-journal-gateway` user.
'';
};
trust = lib.mkOption {
default = null;
type = with lib.types; nullOr str;
description = ''
Specify the path to a file or `AF_UNIX` stream socket to read a CA
certificate from.
The certificate must be in PEM format.
Setting this option enforces client certificate checking.
'';
};
system = lib.mkOption {
default = true;
type = lib.types.bool;
description = ''
Serve entries from system services and the kernel.
This has the same meaning as `--system` for {manpage}`journalctl(1)`.
'';
};
user = lib.mkOption {
default = true;
type = lib.types.bool;
description = ''
Serve entries from services for the current user.
This has the same meaning as `--user` for {manpage}`journalctl(1)`.
'';
};
merge = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Serve entries interleaved from all available journals, including other
machines.
This has the same meaning as `--merge` option for
{manpage}`journalctl(1)`.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
# This prevents the weird case were disabling "system" and "user"
# actually enables both because the cli flags are not present.
assertion = cfg.system || cfg.user;
message = ''
systemd-journal-gatewayd cannot serve neither "system" nor "user"
journals.
'';
}
];
systemd.additionalUpstreamSystemUnits = [
"systemd-journal-gatewayd.socket"
"systemd-journal-gatewayd.service"
];
users.users.systemd-journal-gateway.uid = config.ids.uids.systemd-journal-gateway;
users.users.systemd-journal-gateway.group = "systemd-journal-gateway";
users.groups.systemd-journal-gateway.gid = config.ids.gids.systemd-journal-gateway;
systemd.services.systemd-journal-gatewayd.serviceConfig.ExecStart = [
# Clear the default command line
""
"${pkgs.systemd}/lib/systemd/systemd-journal-gatewayd ${cliArgs}"
];
systemd.sockets.systemd-journal-gatewayd = {
wantedBy = [ "sockets.target" ];
listenStreams = [
# Clear the default port
""
(toString cfg.port)
];
};
};
}

View File

@@ -0,0 +1,174 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.journald.remote;
format = pkgs.formats.systemd;
cliArgs = lib.cli.toGNUCommandLineShell { } {
inherit (cfg) output;
# "-3" specifies the file descriptor from the .socket unit.
"listen-${cfg.listen}" = "-3";
};
in
{
meta.maintainers = [ lib.maintainers.raitobezarius ];
options.services.journald.remote = {
enable = lib.mkEnableOption "receiving systemd journals from the network";
listen = lib.mkOption {
default = "https";
type = lib.types.enum [
"https"
"http"
];
description = ''
Which protocol to listen to.
'';
};
output = lib.mkOption {
default = "/var/log/journal/remote/";
type = lib.types.str;
description = ''
The location of the output journal.
In case the output file is not specified, journal files will be created
underneath the selected directory. Files will be called
{file}`remote-hostname.journal`, where the `hostname` part is the
escaped hostname of the source endpoint of the connection, or the
numerical address if the hostname cannot be determined.
'';
};
port = lib.mkOption {
default = 19532;
type = lib.types.port;
description = ''
The port to listen to.
Note that this option is used only if
{option}`services.journald.upload.listen` is configured to be either
"https" or "http".
'';
};
settings = lib.mkOption {
default = { };
description = ''
Configuration in the journal-remote configuration file. See
{manpage}`journal-remote.conf(5)` for available options.
'';
type = lib.types.submodule {
freeformType = format.type;
options.Remote = {
Seal = lib.mkOption {
default = false;
example = true;
type = lib.types.bool;
description = ''
Periodically sign the data in the journal using Forward Secure
Sealing.
'';
};
SplitMode = lib.mkOption {
default = "host";
example = "none";
type = lib.types.enum [
"host"
"none"
];
description = ''
With "host", a separate output file is used, based on the
hostname of the other endpoint of a connection. With "none", only
one output journal file is used.
'';
};
ServerKeyFile = lib.mkOption {
default = "/etc/ssl/private/journal-remote.pem";
type = lib.types.str;
description = ''
A path to a SSL secret key file in PEM format.
Note that due to security reasons, `systemd-journal-remote` will
refuse files from the world-readable `/nix/store`. This file
should be readable by the "" user.
This option can be used with `listen = "https"`. If the path
refers to an `AF_UNIX` stream socket in the file system a
connection is made to it and the key read from it.
'';
};
ServerCertificateFile = lib.mkOption {
default = "/etc/ssl/certs/journal-remote.pem";
type = lib.types.str;
description = ''
A path to a SSL certificate file in PEM format.
This option can be used with `listen = "https"`. If the path
refers to an `AF_UNIX` stream socket in the file system a
connection is made to it and the certificate read from it.
'';
};
TrustedCertificateFile = lib.mkOption {
default = "/etc/ssl/ca/trusted.pem";
type = lib.types.str;
description = ''
A path to a SSL CA certificate file in PEM format, or `all`.
If `all` is set, then client certificate checking will be
disabled.
This option can be used with `listen = "https"`. If the path
refers to an `AF_UNIX` stream socket in the file system a
connection is made to it and the certificate read from it.
'';
};
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.additionalUpstreamSystemUnits = [
"systemd-journal-remote.service"
"systemd-journal-remote.socket"
];
systemd.services.systemd-journal-remote.serviceConfig.ExecStart = [
# Clear the default command line
""
"${pkgs.systemd}/lib/systemd/systemd-journal-remote ${cliArgs}"
];
systemd.sockets.systemd-journal-remote = {
wantedBy = [ "sockets.target" ];
listenStreams = [
# Clear the default port
""
(toString cfg.port)
];
};
# User and group used by systemd-journal-remote.service
users.groups.systemd-journal-remote = { };
users.users.systemd-journal-remote = {
isSystemUser = true;
group = "systemd-journal-remote";
};
environment.etc."systemd/journal-remote.conf".source =
format.generate "journal-remote.conf" cfg.settings;
};
}

View File

@@ -0,0 +1,116 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.journald.upload;
format = pkgs.formats.systemd;
in
{
meta.maintainers = [ lib.maintainers.raitobezarius ];
options.services.journald.upload = {
enable = lib.mkEnableOption "uploading the systemd journal to a remote server";
settings = lib.mkOption {
default = { };
description = ''
Configuration for journal-upload. See {manpage}`journal-upload.conf(5)`
for available options.
'';
type = lib.types.submodule {
freeformType = format.type;
options.Upload = {
URL = lib.mkOption {
type = lib.types.str;
example = "https://192.168.1.1";
description = ''
The URL to upload the journal entries to.
See the description of `--url=` option in
{manpage}`systemd-journal-upload(8)` for the description of
possible values.
'';
};
ServerKeyFile = lib.mkOption {
type = with lib.types; nullOr str;
example = lib.literalExpression "./server-key.pem";
# Since systemd-journal-upload uses a DynamicUser, permissions must
# be done using groups
description = ''
SSL key in PEM format.
In contrary to what the name suggests, this option configures the
client private key sent to the remote journal server.
This key should not be world-readable, and must be readably by
the `systemd-journal` group.
'';
default = null;
};
ServerCertificateFile = lib.mkOption {
type = with lib.types; nullOr str;
example = lib.literalExpression "./server-ca.pem";
description = ''
SSL CA certificate in PEM format.
In contrary to what the name suggests, this option configures the
client certificate sent to the remote journal server.
'';
default = null;
};
TrustedCertificateFile = lib.mkOption {
type = with lib.types; nullOr str;
example = lib.literalExpression "./ca";
description = ''
SSL CA certificate.
This certificate will be used to check the remote journal HTTPS
server certificate.
'';
default = null;
};
NetworkTimeoutSec = lib.mkOption {
type = with lib.types; nullOr str;
example = "1s";
description = ''
When network connectivity to the server is lost, this option
configures the time to wait for the connectivity to get restored.
If the server is not reachable over the network for the
configured time, `systemd-journal-upload` exits. Takes a value in
seconds (or in other time units if suffixed with "ms", "min",
"h", etc). For details, see {manpage}`systemd.time(5)`.
'';
default = null;
};
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.additionalUpstreamSystemUnits = [ "systemd-journal-upload.service" ];
systemd.services."systemd-journal-upload" = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "always";
# To prevent flooding the server in case the server is struggling
RestartSec = "3sec";
};
};
environment.etc."systemd/journal-upload.conf".source =
format.generate "journal-upload.conf" cfg.settings;
};
}

View File

@@ -0,0 +1,168 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.journald;
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "journald" "enableHttpGateway" ]
[ "services" "journald" "gateway" "enable" ]
)
];
options = {
services.journald.console = lib.mkOption {
default = "";
type = lib.types.str;
description = "If non-empty, write log messages to the specified TTY device.";
};
services.journald.rateLimitInterval = lib.mkOption {
default = "30s";
type = lib.types.str;
description = ''
Configures the rate limiting interval that is applied to all
messages generated on the system. This rate limiting is applied
per-service, so that two services which log do not interfere with
each other's limit. The value may be specified in the following
units: s, min, h, ms, us. To turn off any kind of rate limiting,
set either value to 0.
See {option}`services.journald.rateLimitBurst` for important
considerations when setting this value.
'';
};
services.journald.storage = lib.mkOption {
default = "persistent";
type = lib.types.enum [
"persistent"
"volatile"
"auto"
"none"
];
description = ''
Controls where to store journal data. See
{manpage}`journald.conf(5)` for further information.
'';
};
services.journald.rateLimitBurst = lib.mkOption {
default = 10000;
type = lib.types.int;
description = ''
Configures the rate limiting burst limit (number of messages per
interval) that is applied to all messages generated on the system.
This rate limiting is applied per-service, so that two services
which log do not interfere with each other's limit.
Note that the effective rate limit is multiplied by a factor derived
from the available free disk space for the journal as described on
{manpage}`journald.conf(5)`.
Note that the total amount of logs stored is limited by journald settings
such as `SystemMaxUse`, which defaults to 10% the file system size
(capped at max 4GB), and `SystemKeepFree`, which defaults to 15% of the
file system size.
It is thus recommended to compute what period of time that you will be
able to store logs for when an application logs at full burst rate.
With default settings for log lines that are 100 Bytes long, this can
amount to just a few hours.
'';
};
services.journald.audit = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.bool;
description = ''
If enabled systemd-journald will turn on auditing on start-up.
If disabled it will turn it off. If unset it will neither enable nor disable it, leaving the previous state unchanged.
NixOS defaults to leaving this unset as enabling audit without auditd running leads to spamming /dev/kmesg with random messages
and if you enable auditd then auditd is responsible for turning auditing on.
If you want to have audit logs in journald and do not mind audit logs also ending up in /dev/kmesg you can set this option to true.
If you want to for some ununderstandable reason disable auditing if auditd enabled it then you can set this option to false.
It is of NixOS' opinion that setting this to false is definitely the wrong thing to do - but it's an option.
'';
};
services.journald.extraConfig = lib.mkOption {
default = "";
type = lib.types.lines;
example = "Storage=volatile";
description = ''
Extra config options for systemd-journald. See {manpage}`journald.conf(5)`
for available options.
'';
};
services.journald.forwardToSyslog = lib.mkOption {
default = config.services.rsyslogd.enable || config.services.syslog-ng.enable;
defaultText = lib.literalExpression "services.rsyslogd.enable || services.syslog-ng.enable";
type = lib.types.bool;
description = ''
Whether to forward log messages to syslog.
'';
};
};
config = {
systemd.additionalUpstreamSystemUnits = [
"systemd-journald.socket"
"systemd-journald@.socket"
"systemd-journald-varlink@.socket"
"systemd-journald.service"
"systemd-journald@.service"
"systemd-journal-flush.service"
"systemd-journal-catalog-update.service"
"systemd-journald-sync@.service"
"systemd-journald-audit.socket"
"systemd-journald-dev-log.socket"
"syslog.socket"
];
systemd.sockets.systemd-journald-audit.wantedBy = [
"systemd-journald.service"
"sockets.target"
];
environment.etc = {
"systemd/journald.conf".text = ''
[Journal]
Storage=${cfg.storage}
RateLimitInterval=${cfg.rateLimitInterval}
RateLimitBurst=${toString cfg.rateLimitBurst}
${lib.optionalString (cfg.console != "") ''
ForwardToConsole=yes
TTYPath=${cfg.console}
''}
${lib.optionalString (cfg.forwardToSyslog) ''
ForwardToSyslog=yes
''}
Audit=${utils.systemdUtils.lib.toOption cfg.audit}
${cfg.extraConfig}
'';
};
users.groups.systemd-journal.gid = config.ids.gids.systemd-journal;
systemd.services.systemd-journal-flush.restartIfChanged = false;
systemd.services.systemd-journald.restartTriggers = [
config.environment.etc."systemd/journald.conf".source
];
systemd.services.systemd-journald.stopIfChanged = false;
systemd.services."systemd-journald@".restartTriggers = [
config.environment.etc."systemd/journald.conf".source
];
systemd.services."systemd-journald@".stopIfChanged = false;
};
}

View File

@@ -0,0 +1,108 @@
{
config,
lib,
utils,
...
}:
{
options.services.logind = {
settings.Login = lib.mkOption {
description = ''
Settings option for systemd-logind.
See {manpage}`logind.conf(5)` for available options.
'';
type = lib.types.submodule {
freeformType = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption;
options.KillUserProcesses = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Specifies whether the processes of a user should be killed
when the user logs out. If true, the scope unit corresponding
to the session and all processes inside that scope will be
terminated. If false, the scope is "abandoned"
(see {manpage}`systemd.scope(5)`),
and processes are not killed.
See {manpage}`logind.conf(5)` for more details.
Defaulted to false in nixpkgs because many tools that rely on
persistent user processeslike `tmux`, `screen`, `mosh`, `VNC`,
`nohup`, and more would break by the systemd-default behavior.
'';
};
};
default = { };
example = {
KillUserProcesses = false;
HandleLidSwitch = "ignore";
};
};
};
config = {
systemd.additionalUpstreamSystemUnits = [
"systemd-logind.service"
"autovt@.service"
"systemd-user-sessions.service"
]
++ lib.optionals config.systemd.package.withImportd [
"dbus-org.freedesktop.import1.service"
]
++ lib.optionals config.systemd.package.withMachined [
"dbus-org.freedesktop.machine1.service"
]
++ lib.optionals config.systemd.package.withPortabled [
"dbus-org.freedesktop.portable1.service"
]
++ [
"dbus-org.freedesktop.login1.service"
"user@.service"
"user-runtime-dir@.service"
];
environment.etc."systemd/logind.conf".text =
utils.systemdUtils.lib.settingsToSections config.services.logind.settings;
# Restarting systemd-logind breaks X11
# - upstream commit: https://cgit.freedesktop.org/xorg/xserver/commit/?id=dc48bd653c7e101
# - systemd announcement: https://github.com/systemd/systemd/blob/22043e4317ecd2bc7834b48a6d364de76bb26d91/NEWS#L103-L112
# - this might be addressed in the future by xorg
#systemd.services.systemd-logind.restartTriggers = [ config.environment.etc."systemd/logind.conf".source ];
systemd.services.systemd-logind.restartIfChanged = false;
systemd.services.systemd-logind.stopIfChanged = false;
# The user-runtime-dir@ service is managed by systemd-logind we should not touch it or else we break the users' sessions.
systemd.services."user-runtime-dir@".stopIfChanged = false;
systemd.services."user-runtime-dir@".restartIfChanged = false;
};
imports =
let
settingsRename =
old: new:
lib.mkRenamedOptionModule
[ "services" "logind" old ]
[ "services" "logind" "settings" "Login" new ];
in
[
(lib.mkRemovedOptionModule [
"services"
"logind"
"extraConfig"
] "Use services.logind.settings.Login instead.")
(settingsRename "killUserProcesses" "KillUserProcesses")
(settingsRename "powerKey" "HandlePowerKey")
(settingsRename "powerKeyLongPress" "HandlePowerKeyLongPress")
(settingsRename "rebootKey" "HandleRebootKey")
(settingsRename "rebootKeyLongPress" "HandleRebootKeyLongPress")
(settingsRename "suspendKey" "HandleSuspendKey")
(settingsRename "suspendKeyLongPress" "HandleSuspendKeyLongPress")
(settingsRename "hibernateKey" "HandleHibernateKey")
(settingsRename "hibernateKeyLongPress" "HandleHibernateKeyLongPress")
(settingsRename "lidSwitch" "HandleLidSwitch")
(settingsRename "lidSwitchExternalPower" "HandleLidSwitchExternalPower")
(settingsRename "lidSwitchDocked" "HandleLidSwitchDocked")
];
}

View File

@@ -0,0 +1,208 @@
{
config,
lib,
pkgs,
utils,
...
}:
with utils.systemdUtils.unitOptions;
with utils.systemdUtils.lib;
with lib;
let
cfg = config.systemd.nspawn;
checkExec = checkUnitConfig "Exec" [
(assertOnlyFields [
"Boot"
"ProcessTwo"
"Parameters"
"Environment"
"User"
"WorkingDirectory"
"PivotRoot"
"Capability"
"DropCapability"
"NoNewPrivileges"
"KillSignal"
"Personality"
"MachineID"
"PrivateUsers"
"NotifyReady"
"SystemCallFilter"
"LimitCPU"
"LimitFSIZE"
"LimitDATA"
"LimitSTACK"
"LimitCORE"
"LimitRSS"
"LimitNOFILE"
"LimitAS"
"LimitNPROC"
"LimitMEMLOCK"
"LimitLOCKS"
"LimitSIGPENDING"
"LimitMSGQUEUE"
"LimitNICE"
"LimitRTPRIO"
"LimitRTTIME"
"OOMScoreAdjust"
"CPUAffinity"
"Hostname"
"ResolvConf"
"Timezone"
"LinkJournal"
"Ephemeral"
"AmbientCapability"
])
(assertValueOneOf "Boot" boolValues)
(assertValueOneOf "ProcessTwo" boolValues)
(assertValueOneOf "NotifyReady" boolValues)
];
checkFiles = checkUnitConfig "Files" [
(assertOnlyFields [
"ReadOnly"
"Volatile"
"Bind"
"BindReadOnly"
"TemporaryFileSystem"
"Overlay"
"OverlayReadOnly"
"PrivateUsersChown"
"BindUser"
"Inaccessible"
"PrivateUsersOwnership"
])
(assertValueOneOf "ReadOnly" boolValues)
(assertValueOneOf "Volatile" (boolValues ++ [ "state" ]))
(assertValueOneOf "PrivateUsersChown" boolValues)
(assertValueOneOf "PrivateUsersOwnership" [
"off"
"chown"
"map"
"auto"
])
];
checkNetwork = checkUnitConfig "Network" [
(assertOnlyFields [
"Private"
"VirtualEthernet"
"VirtualEthernetExtra"
"Interface"
"MACVLAN"
"IPVLAN"
"Bridge"
"Zone"
"Port"
])
(assertValueOneOf "Private" boolValues)
(assertValueOneOf "VirtualEthernet" boolValues)
];
instanceOptions = {
options = (getAttrs [ "enable" ] sharedOptions) // {
execConfig = mkOption {
default = { };
example = {
Parameters = "/bin/sh";
};
type = types.addCheck (types.attrsOf unitOption) checkExec;
description = ''
Each attribute in this set specifies an option in the
`[Exec]` section of this unit. See
{manpage}`systemd.nspawn(5)` for details.
'';
};
filesConfig = mkOption {
default = { };
example = {
Bind = [ "/home/alice" ];
};
type = types.addCheck (types.attrsOf unitOption) checkFiles;
description = ''
Each attribute in this set specifies an option in the
`[Files]` section of this unit. See
{manpage}`systemd.nspawn(5)` for details.
'';
};
networkConfig = mkOption {
default = { };
example = {
Private = false;
};
type = types.addCheck (types.attrsOf unitOption) checkNetwork;
description = ''
Each attribute in this set specifies an option in the
`[Network]` section of this unit. See
{manpage}`systemd.nspawn(5)` for details.
'';
};
};
};
instanceToUnit =
name: def:
let
base = {
text = ''
[Exec]
${attrsToSection def.execConfig}
[Files]
${attrsToSection def.filesConfig}
[Network]
${attrsToSection def.networkConfig}
'';
}
// def;
in
base // { unit = makeUnit name base; };
in
{
options = {
systemd.nspawn = mkOption {
default = { };
type = with types; attrsOf (submodule instanceOptions);
description = "Definition of systemd-nspawn configurations.";
};
};
config =
let
units = mapAttrs' (
n: v:
let
nspawnFile = "${n}.nspawn";
in
nameValuePair nspawnFile (instanceToUnit nspawnFile v)
) cfg;
in
mkMerge [
(mkIf (cfg != { }) {
environment.etc."systemd/nspawn".source = mkIf (cfg != { }) (generateUnits {
allowCollisions = false;
type = "nspawn";
inherit units;
upstreamUnits = [ ];
upstreamWants = [ ];
});
})
{
systemd.targets.multi-user.wants = [ "machines.target" ];
systemd.services."systemd-nspawn@".environment = {
SYSTEMD_NSPAWN_UNIFIED_HIERARCHY = mkDefault "1";
};
}
];
}

View File

@@ -0,0 +1,89 @@
{
config,
lib,
utils,
...
}:
let
cfg = config.systemd.oomd;
in
{
imports = [
(lib.mkRenamedOptionModule
[ "systemd" "oomd" "enableUserServices" ]
[ "systemd" "oomd" "enableUserSlices" ]
)
(lib.mkRenamedOptionModule [ "systemd" "oomd" "extraConfig" ] [ "systemd" "oomd" "settings" "OOM" ])
];
options.systemd.oomd = {
enable = lib.mkEnableOption "the `systemd-oomd` OOM killer" // {
default = true;
};
# Fedora enables the first and third option by default. See the 10-oomd-* files here:
# https://src.fedoraproject.org/rpms/systemd/tree/806c95e1c70af18f81d499b24cd7acfa4c36ffd6
enableRootSlice = lib.mkEnableOption "oomd on the root slice (`-.slice`)";
enableSystemSlice = lib.mkEnableOption "oomd on the system slice (`system.slice`)";
enableUserSlices = lib.mkEnableOption "oomd on all user slices (`user@.slice`) and all user owned slices";
settings.OOM = lib.mkOption {
description = ''
Settings option for systemd-oomd.
See {manpage}`oomd.conf(5)` for available options.
'';
type = lib.types.submodule {
freeformType = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption;
};
default = { };
example = {
DefaultMemoryPressureLimit = "60%";
};
};
};
config = lib.mkIf cfg.enable {
systemd.additionalUpstreamSystemUnits = [
"systemd-oomd.service"
"systemd-oomd.socket"
];
systemd.services.systemd-oomd.after = [
"swap.target" # TODO: drop after systemd v258
"systemd-sysusers.service" # TODO: drop after systemd v257.8
];
systemd.services.systemd-oomd.wantedBy = [ "multi-user.target" ];
environment.etc."systemd/oomd.conf".text = utils.systemdUtils.lib.settingsToSections cfg.settings;
users.users.systemd-oom = {
description = "systemd-oomd service user";
group = "systemd-oom";
isSystemUser = true;
};
users.groups.systemd-oom = { };
systemd.slices."-".sliceConfig = lib.mkIf cfg.enableRootSlice {
ManagedOOMMemoryPressure = "kill";
ManagedOOMMemoryPressureLimit = lib.mkDefault "80%";
};
systemd.slices."system".sliceConfig = lib.mkIf cfg.enableSystemSlice {
ManagedOOMMemoryPressure = "kill";
ManagedOOMMemoryPressureLimit = lib.mkDefault "80%";
};
systemd.slices."user".sliceConfig = lib.mkIf cfg.enableUserSlices {
ManagedOOMMemoryPressure = "kill";
ManagedOOMMemoryPressureLimit = lib.mkDefault "80%";
};
systemd.user.units."slice" = lib.mkIf cfg.enableUserSlices {
text = ''
[Slice]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=80%
'';
overrideStrategy = "asDropin";
};
};
}

View File

@@ -0,0 +1,220 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.systemd.repart;
initrdCfg = config.boot.initrd.systemd.repart;
format = pkgs.formats.ini { listsAsDuplicateKeys = true; };
definitionsDirectory = utils.systemdUtils.lib.definitions "repart.d" format (
lib.mapAttrs (_n: v: { Partition = v; }) cfg.partitions
);
partitionAssertions = lib.mapAttrsToList (
fileName: definition:
let
inherit (utils.systemdUtils.lib) GPTMaxLabelLength;
labelLength = builtins.stringLength definition.Label;
in
{
assertion = definition ? Label -> GPTMaxLabelLength >= labelLength;
message = ''
The partition label '${definition.Label}' defined for '${fileName}' is ${toString labelLength}
characters long, but the maximum label length supported by systemd is ${toString GPTMaxLabelLength}.
'';
}
) cfg.partitions;
in
{
options = {
boot.initrd.systemd.repart = {
enable = lib.mkEnableOption "systemd-repart" // {
description = ''
Grow and add partitions to a partition table at boot time in the initrd.
systemd-repart only works with GPT partition tables.
To run systemd-repart after the initrd, see
`options.systemd.repart.enable`.
'';
};
device = lib.mkOption {
type = with lib.types; nullOr str;
description = ''
The device to operate on.
If `device == null`, systemd-repart will operate on the device
backing the root partition. So in order to dynamically *create* the
root partition in the initrd you need to set a device.
'';
default = null;
example = "/dev/vda";
};
empty = lib.mkOption {
type = lib.types.enum [
"refuse"
"allow"
"require"
"force"
"create"
];
description = ''
Controls how to operate on empty devices that contain no partition table yet.
See {manpage}`systemd-repart(8)` for details.
'';
example = "require";
default = "refuse";
};
discard = lib.mkOption {
type = lib.types.bool;
description = ''
Controls whether to issue the BLKDISCARD I/O control command on the
space taken up by any added partitions or on the space in between them.
Usually, it's a good idea to issue this request since it tells the underlying
hardware that the covered blocks shall be considered empty, improving performance.
See {manpage}`systemd-repart(8)` for details.
'';
default = true;
};
extraArgs = lib.mkOption {
description = ''
Extra command-line arguments to pass to systemd-repart.
See {manpage}`systemd-repart(8)` for all available options.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
systemd.repart = {
enable = lib.mkEnableOption "systemd-repart" // {
description = ''
Grow and add partitions to a partition table.
systemd-repart only works with GPT partition tables.
To run systemd-repart while in the initrd, see
`options.boot.initrd.systemd.repart.enable`.
'';
};
partitions = lib.mkOption {
type =
with lib.types;
attrsOf (
attrsOf (oneOf [
str
int
bool
(listOf str)
])
);
default = { };
example = {
"10-root" = {
Type = "root";
};
"20-home" = {
Type = "home";
SizeMinBytes = "512M";
SizeMaxBytes = "2G";
};
};
description = ''
Specify partitions as a set of the names of the definition files as the
key and the partition configuration as its value. The partition
configuration can use all upstream options. See {manpage}`repart.d(5)`
for all available options.
'';
};
};
};
config = lib.mkIf (cfg.enable || initrdCfg.enable) {
assertions = [
{
assertion = initrdCfg.enable -> config.boot.initrd.systemd.enable;
message = ''
'boot.initrd.systemd.repart.enable' requires 'boot.initrd.systemd.enable' to be enabled.
'';
}
]
++ partitionAssertions;
# systemd-repart uses loopback devices for partition creation
boot.initrd.availableKernelModules = lib.optional initrdCfg.enable "loop";
boot.initrd.systemd = lib.mkIf initrdCfg.enable {
additionalUpstreamUnits = [
"systemd-repart.service"
];
storePaths = [
"${config.boot.initrd.systemd.package}/bin/systemd-repart"
];
contents."/etc/repart.d".source = definitionsDirectory;
# Override defaults in upstream unit.
services.systemd-repart =
let
deviceUnit = "${utils.escapeSystemdPath initrdCfg.device}.device";
in
{
# systemd-repart tries to create directories in /var/tmp by default to
# store large temporary files that benefit from persistence on disk. In
# the initrd, however, /var/tmp does not provide more persistence than
# /tmp, so we re-use it here.
environment."TMPDIR" = "/tmp";
serviceConfig = {
ExecStart = [
" " # required to unset the previous value.
# When running in the initrd, systemd-repart by default searches
# for definition files in /sysroot or /sysusr. We tell it to look
# in the initrd itself.
''
${config.boot.initrd.systemd.package}/bin/systemd-repart \
--definitions=/etc/repart.d \
--dry-run=no \
--empty=${initrdCfg.empty} \
--discard=${lib.boolToString initrdCfg.discard} \
${utils.escapeSystemdExecArgs initrdCfg.extraArgs} \
${lib.optionalString (initrdCfg.device != null) initrdCfg.device}
''
];
};
# systemd-repart needs to run after /sysroot (or /sysuser, but we
# don't have it) has been mounted because otherwise it cannot
# determine the device (i.e disk) to operate on. If you want to run
# systemd-repart without /sysroot (i.e. to create the root
# partition), you have to explicitly tell it which device to operate
# on. The service then needs to be ordered to run after this device
# is available.
requires = lib.mkIf (initrdCfg.device != null) [ deviceUnit ];
after = if initrdCfg.device == null then [ "sysroot.mount" ] else [ deviceUnit ];
};
};
environment.etc = lib.mkIf cfg.enable {
"repart.d".source = definitionsDirectory;
};
systemd = lib.mkIf cfg.enable {
additionalUpstreamSystemUnits = [
"systemd-repart.service"
];
};
};
meta.maintainers = with lib.maintainers; [ nikstur ];
}

View File

@@ -0,0 +1,80 @@
{
config,
lib,
utils,
pkgs,
...
}:
let
cfg = config.systemd.shutdownRamfs;
ramfsContents = pkgs.writeText "shutdown-ramfs-contents.json" (builtins.toJSON cfg.storePaths);
in
{
options.systemd.shutdownRamfs = {
enable = lib.mkEnableOption "pivoting back to an initramfs for shutdown" // {
default = true;
};
contents = lib.mkOption {
description = "Set of files that have to be linked into the shutdown ramfs";
example = lib.literalExpression ''
{
"/lib/systemd/system-shutdown/zpool-sync-shutdown".source = writeShellScript "zpool" "exec ''${zfs}/bin/zpool sync"
}
'';
type = utils.systemdUtils.types.initrdContents;
};
storePaths = lib.mkOption {
description = ''
Store paths to copy into the shutdown ramfs as well.
'';
type = utils.systemdUtils.types.initrdStorePath;
default = [ ];
};
};
config = lib.mkIf cfg.enable {
systemd.shutdownRamfs.contents = {
"/shutdown".source = "${config.systemd.package}/lib/systemd/systemd-shutdown";
"/etc/initrd-release".source = config.environment.etc.os-release.source;
"/etc/os-release".source = config.environment.etc.os-release.source;
};
systemd.shutdownRamfs.storePaths = [
pkgs.runtimeShell
"${pkgs.coreutils}/bin"
]
++ map (c: builtins.removeAttrs c [ "text" ]) (builtins.attrValues cfg.contents);
systemd.mounts = [
{
what = "tmpfs";
where = "/run/initramfs";
type = "tmpfs";
options = "mode=0700";
}
];
systemd.services.generate-shutdown-ramfs = {
description = "Generate shutdown ramfs";
wantedBy = [ "shutdown.target" ];
before = [ "shutdown.target" ];
unitConfig = {
DefaultDependencies = false;
RequiresMountsFor = "/run/initramfs";
ConditionFileIsExecutable = [
"!/run/initramfs/shutdown"
];
};
serviceConfig = {
Type = "oneshot";
ProtectSystem = "strict";
ReadWritePaths = "/run/initramfs";
ExecStart = "${pkgs.makeInitrdNGTool}/bin/make-initrd-ng ${ramfsContents} /run/initramfs";
};
};
};
}

View File

@@ -0,0 +1,159 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.systemd.sysupdate;
format = pkgs.formats.ini { listToValue = toString; };
# TODO: Switch back to using utils.systemdUtils.lib.definitions once
# https://github.com/systemd/systemd/pull/38187 is resolved. Also ensure
# utils.systemdUtils.lib.definitions is capable of setting a custom file
# suffix.
sysupdateTransfers = lib.mapAttrs' (name: value: {
name = "sysupdate.d/${name}.transfer";
value.source = format.generate "${name}.transfer" value;
}) cfg.transfers;
in
{
options.systemd.sysupdate = {
enable = lib.mkEnableOption "systemd-sysupdate" // {
description = ''
Atomically update the host OS, container images, portable service
images or other sources.
If enabled, updates are triggered in regular intervals via a
`systemd.timer` unit.
Please see {manpage}`systemd-sysupdate(8)` for more details.
'';
};
timerConfig = utils.systemdUtils.unitOptions.timerOptions.options.timerConfig // {
default = { };
description = ''
The timer configuration for performing the update.
By default, the upstream configuration is used:
<https://github.com/systemd/systemd/blob/main/units/systemd-sysupdate.timer>
'';
};
reboot = {
enable = lib.mkEnableOption "automatically rebooting after an update" // {
description = ''
Whether to automatically reboot after an update.
If set to `true`, the system will automatically reboot via a
`systemd.timer` unit but only after a new version was installed.
This uses a unit completely separate from the one performing the
update because it is typically advisable to download updates
regularly while the system is up, but delay reboots until the
appropriate time (i.e. typically at night).
Set this to `false` if you do not want to reboot after an update. This
is useful when you update a container image or another source where
rebooting is not necessary in order to finalize the update.
'';
};
timerConfig = utils.systemdUtils.unitOptions.timerOptions.options.timerConfig // {
default = { };
description = ''
The timer configuration for rebooting after an update.
By default, the upstream configuration is used:
<https://github.com/systemd/systemd/blob/main/units/systemd-sysupdate-reboot.timer>
'';
};
};
transfers = lib.mkOption {
type = with lib.types; attrsOf format.type;
default = { };
example = {
"10-uki" = {
Transfer = {
ProtectVersion = "%A";
};
Source = {
Type = "url-file";
Path = "https://download.example.com/";
MatchPattern = [
"nixos_@v+@l-@d.efi"
"nixos_@v+@l.efi"
"nixos_@v.efi"
];
};
Target = {
Type = "regular-file";
Path = "/EFI/Linux";
PathRelativeTo = "boot";
MatchPattern = ''
nixos_@v+@l-@d.efi"; \
nixos_@v+@l.efi \
nixos_@v.efi
'';
Mode = "0444";
TriesLeft = 3;
TriesDone = 0;
InstancesMax = 2;
};
};
};
description = ''
Specify transfers as a set of the names of the transfer files as the
key and the configuration as its value. The configuration can use all
upstream options. See {manpage}`sysupdate.d(5)`
for all available options.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.systemd.package.withSysupdate;
message = "Cannot enable systemd-sysupdate with systemd package not built with sysupdate support";
}
];
systemd.additionalUpstreamSystemUnits = [
"systemd-sysupdate.service"
"systemd-sysupdate.timer"
"systemd-sysupdate-reboot.service"
"systemd-sysupdate-reboot.timer"
"systemd-sysupdated.service"
];
systemd.services.systemd-sysupdated.aliases = [ "dbus-org.freedesktop.sysupdate1.service" ];
systemd.timers = {
"systemd-sysupdate" = {
wantedBy = [ "timers.target" ];
timerConfig = cfg.timerConfig;
};
"systemd-sysupdate-reboot" = lib.mkIf cfg.reboot.enable {
wantedBy = [ "timers.target" ];
timerConfig = cfg.reboot.timerConfig;
};
};
environment.etc = sysupdateTransfers;
};
meta.maintainers = with lib.maintainers; [
nikstur
jmbaur
];
}

View File

@@ -0,0 +1,209 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.systemd.sysusers;
userCfg = config.users;
systemUsers = lib.filterAttrs (_username: opts: opts.enable && !opts.isNormalUser) userCfg.users;
sysusersConfig = pkgs.writeTextDir "00-nixos.conf" ''
# Type Name ID GECOS Home directory Shell
# Users
${lib.concatLines (
lib.mapAttrsToList (
username: opts:
let
uid = if opts.uid == null then "/var/lib/nixos/uid/${username}" else toString opts.uid;
in
''u ${username} ${uid}:${opts.group} "${opts.description}" ${opts.home} ${utils.toShellPath opts.shell}''
) systemUsers
)}
# Groups
${lib.concatLines (
lib.mapAttrsToList (
groupname: opts:
''g ${groupname} ${
if opts.gid == null then "/var/lib/nixos/gid/${groupname}" else toString opts.gid
}''
) userCfg.groups
)}
# Group membership
${lib.concatStrings (
lib.mapAttrsToList (
groupname: opts: (lib.concatMapStrings (username: "m ${username} ${groupname}\n")) opts.members
) userCfg.groups
)}
'';
immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
# The location of the password files when using an immutable /etc.
immutablePasswordFilesLocation = "/var/lib/nixos/etc";
passwordFilesLocation = if immutableEtc then immutablePasswordFilesLocation else "/etc";
# The filenames created by systemd-sysusers.
passwordFiles = [
"passwd"
"group"
"shadow"
"gshadow"
];
in
{
options = {
# This module doesn't set it's own user options but reuses the ones from
# users-groups.nix
systemd.sysusers = {
enable = lib.mkEnableOption "systemd-sysusers" // {
description = ''
If enabled, users are created with systemd-sysusers instead of with
the custom `update-users-groups.pl` script.
Note: This is experimental.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.system.activationScripts.users == "";
message = "system.activationScripts.users has to be empty to use systemd-sysusers";
}
]
++ (lib.mapAttrsToList (username: opts: {
assertion = opts.enable -> !opts.isNormalUser;
message = "${username} is a normal user. systemd-sysusers doesn't create normal users, only system users.";
}) userCfg.users)
++ lib.mapAttrsToList (username: opts: {
assertion =
(opts.password == opts.initialPassword || opts.password == null)
&& (opts.hashedPassword == opts.initialHashedPassword || opts.hashedPassword == null);
message = "user '${username}' uses password or hashedPassword. systemd-sysupdate only supports initial passwords. It'll never update your passwords.";
}) systemUsers;
systemd = {
# Create home directories, do not create /var/empty even if that's a user's
# home.
tmpfiles.settings.home-directories = lib.mapAttrs' (
username: opts:
lib.nameValuePair opts.home {
d = {
mode = opts.homeMode;
user = username;
group = opts.group;
};
}
) (lib.filterAttrs (_username: opts: opts.home != "/var/empty") systemUsers);
# Create uid/gid marker files for those without an explicit id
tmpfiles.settings.nixos-uid = lib.mapAttrs' (
username: opts:
lib.nameValuePair "/var/lib/nixos/uid/${username}" {
f = {
user = username;
};
}
) (lib.filterAttrs (_username: opts: opts.uid == null) systemUsers);
tmpfiles.settings.nixos-gid = lib.mapAttrs' (
groupname: opts:
lib.nameValuePair "/var/lib/nixos/gid/${groupname}" {
f = {
group = groupname;
};
}
) (lib.filterAttrs (_groupname: opts: opts.gid == null) userCfg.groups);
additionalUpstreamSystemUnits = [
"systemd-sysusers.service"
];
services.systemd-sysusers = {
# Enable switch-to-configuration to restart the service.
unitConfig.ConditionNeedsUpdate = [ "" ];
requiredBy = [ "sysinit-reactivation.target" ];
before = [ "sysinit-reactivation.target" ];
restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ];
serviceConfig = {
# When we have an immutable /etc we cannot write the files directly
# to /etc so we write it to a different directory and symlink them
# into /etc.
#
# We need to explicitly list the config file, otherwise
# systemd-sysusers cannot find it when we also pass another flag.
ExecStart = lib.mkIf immutableEtc [
""
"${config.systemd.package}/bin/systemd-sysusers --root ${builtins.dirOf immutablePasswordFilesLocation} /etc/sysusers.d/00-nixos.conf"
];
# Make the source files writable before executing sysusers.
ExecStartPre = lib.mkIf (!userCfg.mutableUsers) (
lib.map (file: "-${pkgs.util-linux}/bin/umount ${passwordFilesLocation}/${file}") passwordFiles
);
# Make the source files read-only after sysusers has finished.
ExecStartPost = lib.mkIf (!userCfg.mutableUsers) (
lib.map (
file:
"${pkgs.util-linux}/bin/mount --bind -o ro ${passwordFilesLocation}/${file} ${passwordFilesLocation}/${file}"
) passwordFiles
);
LoadCredential = lib.mapAttrsToList (
username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}"
) (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) systemUsers);
SetCredential =
(lib.mapAttrsToList (
username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}"
) (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) systemUsers))
++ (lib.mapAttrsToList (
username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}"
) (lib.filterAttrs (_username: opts: opts.initialPassword != null) systemUsers));
};
};
};
environment.etc = lib.mkMerge [
{
"sysusers.d".source = sysusersConfig;
}
# Statically create the symlinks to immutablePasswordFilesLocation when
# using an immutable /etc because we will not be able to do it at
# runtime!
(lib.mkIf immutableEtc (
lib.listToAttrs (
lib.map (
file:
lib.nameValuePair file {
source = "${immutablePasswordFilesLocation}/${file}";
mode = "direct-symlink";
}
) passwordFiles
)
))
];
};
meta.maintainers = with lib.maintainers; [ nikstur ];
}

View File

@@ -0,0 +1,427 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.systemd.tmpfiles;
initrdCfg = config.boot.initrd.systemd.tmpfiles;
systemd = config.systemd.package;
attrsWith' =
placeholder: elemType:
types.attrsWith {
inherit elemType placeholder;
};
escapeArgument = lib.strings.escapeC [
"\t"
"\n"
"\r"
" "
"\\"
];
settingsOption = {
description = ''
Declare systemd-tmpfiles rules to create, delete, and clean up volatile
and temporary files and directories.
Even though the service is called `*tmp*files` you can also create
persistent files.
'';
example = {
"10-mypackage" = {
"/var/lib/my-service/statefolder".d = {
mode = "0755";
user = "root";
group = "root";
};
};
};
default = { };
type = attrsWith' "config-name" (
attrsWith' "path" (
attrsWith' "tmpfiles-type" (
types.submodule (
{ name, config, ... }:
{
options.type = mkOption {
type = types.str;
default = name;
defaultText = "tmpfiles-type";
example = "d";
description = ''
The type of operation to perform on the file.
The type consists of a single letter and optionally one or more
modifier characters.
Please see the upstream documentation for the available types and
more details:
{manpage}`tmpfiles.d(5)`
'';
};
options.mode = mkOption {
type = types.str;
default = "-";
example = "0755";
description = ''
The file access mode to use when creating this file or directory.
'';
};
options.user = mkOption {
type = types.str;
default = "-";
example = "root";
description = ''
The user of the file.
This may either be a numeric ID or a user/group name.
If omitted or when set to `"-"`, the user and group of the user who
invokes systemd-tmpfiles is used.
'';
};
options.group = mkOption {
type = types.str;
default = "-";
example = "root";
description = ''
The group of the file.
This may either be a numeric ID or a user/group name.
If omitted or when set to `"-"`, the user and group of the user who
invokes systemd-tmpfiles is used.
'';
};
options.age = mkOption {
type = types.str;
default = "-";
example = "10d";
description = ''
Delete a file when it reaches a certain age.
If a file or directory is older than the current time minus the age
field, it is deleted.
If set to `"-"` no automatic clean-up is done.
'';
};
options.argument = mkOption {
type = types.str;
default = "";
example = "";
description = ''
An argument whose meaning depends on the type of operation.
Please see the upstream documentation for the meaning of this
parameter in different situations:
{manpage}`tmpfiles.d(5)`
'';
};
}
)
)
)
);
};
# generates a single entry for a tmpfiles.d rule
settingsEntryToRule = path: entry: ''
'${entry.type}' '${path}' '${entry.mode}' '${entry.user}' '${entry.group}' '${entry.age}' ${escapeArgument entry.argument}
'';
# generates a list of tmpfiles.d rules from the attrs (paths) under tmpfiles.settings.<name>
pathsToRules = mapAttrsToList (
path: types: concatStrings (mapAttrsToList (_type: settingsEntryToRule path) types)
);
mkRuleFileContent = paths: concatStrings (pathsToRules paths);
in
{
options = {
systemd.tmpfiles.rules = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "d /tmp 1777 root root 10d" ];
description = ''
Rules for creation, deletion and cleaning of volatile and temporary files
automatically. See
{manpage}`tmpfiles.d(5)`
for the exact format.
'';
};
systemd.tmpfiles.settings = mkOption settingsOption;
boot.initrd.systemd.tmpfiles.settings = mkOption (
settingsOption
// {
description = ''
Similar to {option}`systemd.tmpfiles.settings` but the rules are
only applied by systemd-tmpfiles before `initrd-switch-root.target`.
See {manpage}`bootup(7)`.
'';
}
);
systemd.tmpfiles.packages = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression "[ pkgs.lvm2 ]";
apply = map getLib;
description = ''
List of packages containing {command}`systemd-tmpfiles` rules.
All files ending in .conf found in
{file}`«pkg»/lib/tmpfiles.d`
will be included.
If this folder does not exist or does not contain any files an error will be returned instead.
If a {file}`lib` output is available, rules are searched there and only there.
If there is no {file}`lib` output it will fall back to {file}`out`
and if that does not exist either, the default output will be used.
'';
};
};
config = {
warnings =
let
paths = lib.filter (path: path != null && lib.hasPrefix "/etc/tmpfiles.d/" path) (
map (path: path.target) config.boot.initrd.systemd.storePaths
);
in
lib.optional (lib.length paths > 0) (
lib.concatStringsSep " " [
"Files inside /etc/tmpfiles.d in the initrd need to be created with"
"boot.initrd.systemd.tmpfiles.settings."
"Creating them by hand using boot.initrd.systemd.contents or"
"boot.initrd.systemd.storePaths will lead to errors in the future."
"Found these problematic files: ${lib.concatStringsSep ", " paths}"
]
)
++ (lib.flatten (
lib.mapAttrsToList (
name: paths:
lib.mapAttrsToList (
path: entries:
lib.mapAttrsToList (
type': entry:
lib.optional (lib.match ''.*\\([nrt]|x[0-9A-Fa-f]{2}).*'' entry.argument != null) (
lib.concatStringsSep " " [
"The argument option of ${name}.${type'}.${path} appears to"
"contain escape sequences, which will be escaped again."
"Unescape them if this is not intended: \"${entry.argument}\""
]
)
) entries
) paths
) cfg.settings
));
systemd.additionalUpstreamSystemUnits = [
"systemd-tmpfiles-clean.service"
"systemd-tmpfiles-clean.timer"
"systemd-tmpfiles-setup-dev-early.service"
"systemd-tmpfiles-setup-dev.service"
"systemd-tmpfiles-setup.service"
];
systemd.additionalUpstreamUserUnits = [
"systemd-tmpfiles-clean.service"
"systemd-tmpfiles-clean.timer"
"systemd-tmpfiles-setup.service"
];
# Allow systemd-tmpfiles to be restarted by switch-to-configuration. This
# service is not pulled into the normal boot process. It only exists for
# switch-to-configuration.
#
# This needs to be a separate unit because it does not execute
# systemd-tmpfiles with `--boot` as that is supposed to only be executed
# once at boot time.
#
# Keep this aligned with the upstream `systemd-tmpfiles-setup.service` unit.
systemd.services."systemd-tmpfiles-resetup" = {
description = "Re-setup tmpfiles on a system that is already running.";
requiredBy = [ "sysinit-reactivation.target" ];
after = [
"local-fs.target"
"systemd-sysusers.service"
"systemd-journald.service"
];
before = [
"sysinit-reactivation.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];
restartTriggers = [ config.environment.etc."tmpfiles.d".source ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "systemd-tmpfiles --create --remove --exclude-prefix=/dev";
SuccessExitStatus = "DATAERR CANTCREAT";
ImportCredential = [
"tmpfiles.*"
"loging.motd"
"login.issue"
"network.hosts"
"ssh.authorized_keys.root"
];
RestrictSUIDSGID = false;
};
};
environment.etc = {
"tmpfiles.d".source =
(pkgs.symlinkJoin {
name = "tmpfiles.d";
paths = map (p: p + "/lib/tmpfiles.d") cfg.packages;
postBuild = ''
for i in $(cat $pathsPath); do
(test -d "$i" && test $(ls "$i"/*.conf | wc -l) -ge 1) || (
echo "ERROR: The path '$i' from systemd.tmpfiles.packages contains no *.conf files."
exit 1
)
done
''
+ concatMapStrings (
name:
optionalString (hasPrefix "tmpfiles.d/" name) ''
rm -f $out/${removePrefix "tmpfiles.d/" name}
''
) config.system.build.etc.passthru.targets;
})
+ "/*";
"mtab" = {
mode = "direct-symlink";
source = "/proc/mounts";
};
};
systemd.tmpfiles.packages = [
# Default tmpfiles rules provided by systemd
(pkgs.runCommand "systemd-default-tmpfiles" { } ''
mkdir -p $out/lib/tmpfiles.d
cd $out/lib/tmpfiles.d
ln -s "${systemd}/example/tmpfiles.d/home.conf"
ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf"
ln -s "${systemd}/example/tmpfiles.d/portables.conf"
ln -s "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf"
ln -s "${systemd}/example/tmpfiles.d/systemd.conf"
ln -s "${systemd}/example/tmpfiles.d/systemd-nologin.conf"
ln -s "${systemd}/example/tmpfiles.d/systemd-nspawn.conf"
ln -s "${systemd}/example/tmpfiles.d/systemd-tmp.conf"
ln -s "${systemd}/example/tmpfiles.d/tmp.conf"
ln -s "${systemd}/example/tmpfiles.d/var.conf"
ln -s "${systemd}/example/tmpfiles.d/x11.conf"
'')
# User-specified tmpfiles rules
(pkgs.writeTextFile {
name = "nixos-tmpfiles.d";
destination = "/lib/tmpfiles.d/00-nixos.conf";
text = ''
# This file is created automatically and should not be modified.
# Please change the option systemd.tmpfiles.rules instead.
${concatStringsSep "\n" cfg.rules}
'';
})
]
++ (mapAttrsToList (
name: paths: pkgs.writeTextDir "lib/tmpfiles.d/${name}.conf" (mkRuleFileContent paths)
) cfg.settings);
systemd.tmpfiles.rules = [
"d /run/lock 0755 root root - -"
"d /var/db 0755 root root - -"
"L /var/lock - - - - ../run/lock"
]
++ lib.optionals config.nix.enable [
"d /nix/var 0755 root root - -"
"L+ /nix/var/nix/gcroots/booted-system 0755 root root - /run/booted-system"
]
# Boot-time cleanup
++ [
"R! /etc/group.lock - - - - -"
"R! /etc/passwd.lock - - - - -"
"R! /etc/shadow.lock - - - - -"
]
++ lib.optionals config.nix.enable [
"R! /nix/var/nix/gcroots/tmp - - - - -"
"R! /nix/var/nix/temproots - - - - -"
];
boot.initrd.systemd = {
additionalUpstreamUnits = [
"systemd-tmpfiles-setup-dev-early.service"
"systemd-tmpfiles-setup-dev.service"
"systemd-tmpfiles-setup.service"
];
# override to exclude the prefix /sysroot, because it is not necessarily set up when the unit starts
services.systemd-tmpfiles-setup.serviceConfig = {
ExecStart = [
""
"systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev --exclude-prefix=/sysroot"
];
};
# sets up files under the prefix /sysroot, after the hierarchy is available and before nixos activation
services.systemd-tmpfiles-setup-sysroot = {
description = "Create Volatile Files and Directories in the Real Root";
after = [ "initrd-fs.target" ];
before = [
"initrd.target"
"shutdown.target"
"initrd-switch-root.target"
];
conflicts = [
"shutdown.target"
"initrd-switch-root.target"
];
wantedBy = [ "initrd.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev --prefix=/sysroot";
SuccessExitStatus = [ "DATAERR CANTCREAT" ];
ImportCredential = [
"tmpfiles.*"
"login.motd"
"login.issue"
"network.hosts"
"ssh.authorized_keys.root"
];
};
unitConfig = {
DefaultDependencies = false;
RefuseManualStop = true;
};
};
contents."/etc/tmpfiles.d" = mkIf (initrdCfg.settings != { }) {
source = pkgs.linkFarm "initrd-tmpfiles.d" (
mapAttrsToList (name: paths: {
name = "${name}.conf";
path = pkgs.writeText "${name}.conf" (mkRuleFileContent paths);
}) initrdCfg.settings
);
};
};
};
}

View File

@@ -0,0 +1,81 @@
{
lib,
config,
pkgs,
...
}:
{
meta.maintainers = [ lib.maintainers.elvishjerricco ];
imports = [
(lib.mkRenamedOptionModule
[
"boot"
"initrd"
"systemd"
"enableTpm2"
]
[
"boot"
"initrd"
"systemd"
"tpm2"
"enable"
]
)
];
options = {
systemd.tpm2.enable = lib.mkEnableOption "systemd TPM2 support" // {
default = config.systemd.package.withTpm2Units;
defaultText = "systemd.package.withTpm2Units";
};
boot.initrd.systemd.tpm2.enable = lib.mkEnableOption "systemd initrd TPM2 support" // {
default = config.boot.initrd.systemd.package.withTpm2Units;
defaultText = "boot.initrd.systemd.package.withTpm2Units";
};
};
# TODO: pcrphase, pcrextend, pcrfs, pcrmachine
config = lib.mkMerge [
# Stage 2
(
let
cfg = config.systemd;
in
lib.mkIf cfg.tpm2.enable {
systemd.additionalUpstreamSystemUnits = [
"tpm2.target"
"systemd-tpm2-setup-early.service"
"systemd-tpm2-setup.service"
];
}
)
# Stage 1
(
let
cfg = config.boot.initrd.systemd;
in
lib.mkIf (cfg.enable && cfg.tpm2.enable) {
boot.initrd.systemd.additionalUpstreamUnits = [
"tpm2.target"
"systemd-tpm2-setup-early.service"
];
boot.initrd.availableKernelModules = [
"tpm-tis"
]
++ lib.optional (
!(pkgs.stdenv.hostPlatform.isRiscV64 || pkgs.stdenv.hostPlatform.isArmv7)
) "tpm-crb";
boot.initrd.systemd.storePaths = [
pkgs.tpm2-tss
"${cfg.package}/lib/systemd/systemd-tpm2-setup"
"${cfg.package}/lib/systemd/system-generators/systemd-tpm2-generator"
];
}
)
];
}

View File

@@ -0,0 +1,269 @@
{
config,
lib,
pkgs,
utils,
...
}:
with utils;
with systemdUtils.unitOptions;
with lib;
let
cfg = config.systemd.user;
systemd = config.systemd.package;
inherit (systemdUtils.lib)
makeUnit
generateUnits
targetToUnit
serviceToUnit
sliceToUnit
socketToUnit
timerToUnit
pathToUnit
;
upstreamUserUnits = [
"app.slice"
"background.slice"
"basic.target"
"bluetooth.target"
"capsule@.target"
"default.target"
"exit.target"
"graphical-session-pre.target"
"graphical-session.target"
"paths.target"
"printer.target"
"session.slice"
"shutdown.target"
"smartcard.target"
"sockets.target"
"sound.target"
"systemd-exit.service"
"timers.target"
"xdg-desktop-autostart.target"
]
++ config.systemd.additionalUpstreamUserUnits;
writeTmpfiles =
{
rules,
user ? null,
}:
let
suffix = optionalString (user != null) "-${user}";
in
pkgs.writeTextFile {
name = "nixos-user-tmpfiles.d${suffix}";
destination = "/etc/xdg/user-tmpfiles.d/00-nixos${suffix}.conf";
text = ''
# This file is created automatically and should not be modified.
# Please change the options systemd.user.tmpfiles instead.
${concatStringsSep "\n" rules}
'';
};
in
{
options = {
systemd.user.extraConfig = mkOption {
default = "";
type = types.lines;
example = "DefaultCPUAccounting=yes";
description = ''
Extra config options for systemd user instances. See {manpage}`systemd-user.conf(5)` for
available options.
'';
};
systemd.user.units = mkOption {
description = "Definition of systemd per-user units.";
default = { };
type = systemdUtils.types.units;
};
systemd.user.paths = mkOption {
default = { };
type = systemdUtils.types.paths;
description = "Definition of systemd per-user path units.";
};
systemd.user.services = mkOption {
default = { };
type = systemdUtils.types.services;
description = "Definition of systemd per-user service units.";
};
systemd.user.slices = mkOption {
default = { };
type = systemdUtils.types.slices;
description = "Definition of systemd per-user slice units.";
};
systemd.user.sockets = mkOption {
default = { };
type = systemdUtils.types.sockets;
description = "Definition of systemd per-user socket units.";
};
systemd.user.targets = mkOption {
default = { };
type = systemdUtils.types.targets;
description = "Definition of systemd per-user target units.";
};
systemd.user.timers = mkOption {
default = { };
type = systemdUtils.types.timers;
description = "Definition of systemd per-user timer units.";
};
systemd.user.tmpfiles = {
enable =
(mkEnableOption "systemd user units systemd-tmpfiles-setup.service and systemd-tmpfiles-clean.timer")
// {
default = true;
example = false;
};
rules = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "D %C - - - 7d" ];
description = ''
Global user rules for creation, deletion and cleaning of volatile and
temporary files automatically. See
{manpage}`tmpfiles.d(5)`
for the exact format.
'';
};
users = mkOption {
description = ''
Per-user rules for creation, deletion and cleaning of volatile and
temporary files automatically.
'';
default = { };
type = types.attrsOf (
types.submodule {
options = {
rules = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "D %C - - - 7d" ];
description = ''
Per-user rules for creation, deletion and cleaning of volatile and
temporary files automatically. See
{manpage}`tmpfiles.d(5)`
for the exact format.
'';
};
};
}
);
};
};
systemd.user.generators = mkOption {
type = types.attrsOf types.path;
default = { };
example = {
systemd-gpt-auto-generator = "/dev/null";
};
description = ''
Definition of systemd generators; see {manpage}`systemd.generator(5)`.
For each `NAME = VALUE` pair of the attrSet, a link is generated from
`/etc/systemd/user-generators/NAME` to `VALUE`.
'';
};
systemd.additionalUpstreamUserUnits = mkOption {
default = [ ];
type = types.listOf types.str;
example = [ ];
description = ''
Additional units shipped with systemd that should be enabled for per-user systemd instances.
'';
internal = true;
};
};
config = {
systemd.additionalUpstreamSystemUnits = [
"user.slice"
];
environment.etc = {
"systemd/user".source = generateUnits {
type = "user";
inherit (cfg) units;
upstreamUnits = upstreamUserUnits;
upstreamWants = [ ];
};
"systemd/user.conf".text = ''
[Manager]
${cfg.extraConfig}
'';
};
systemd.user.units =
mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit v)) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit v)) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit v)) cfg.slices
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit v)) cfg.sockets
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit v)) cfg.targets
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit v)) cfg.timers;
systemd.user.timers = {
# enable systemd user tmpfiles
systemd-tmpfiles-clean.wantedBy = optional cfg.tmpfiles.enable "timers.target";
}
# Generate timer units for all services that have a startAt value.
// (mapAttrs (name: service: {
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = service.startAt;
}) (filterAttrs (name: service: service.startAt != [ ]) cfg.services));
# Provide the systemd-user PAM service, required to run systemd
# user instances.
security.pam.services.systemd-user = {
# Ensure that pam_systemd gets included. This is special-cased
# in systemd to provide XDG_RUNTIME_DIR.
startSession = true;
# Disable pam_mount in systemd-user to prevent it from being called
# multiple times during login, because it will prevent pam_mount from
# unmounting the previously mounted volumes.
pamMount = false;
};
# Some overrides to upstream units.
systemd.services."user@".restartIfChanged = false;
systemd.services.systemd-user-sessions.restartIfChanged = false; # Restart kills all active sessions.
# enable systemd user tmpfiles
systemd.user.services.systemd-tmpfiles-setup.wantedBy = optional cfg.tmpfiles.enable "basic.target";
# /run/current-system/sw/etc/xdg is in systemd's $XDG_CONFIG_DIRS so we can
# write the tmpfiles.d rules for everyone there
environment.systemPackages = optional (cfg.tmpfiles.rules != [ ]) (writeTmpfiles {
inherit (cfg.tmpfiles) rules;
});
# /etc/profiles/per-user/$USER/etc/xdg is in systemd's $XDG_CONFIG_DIRS so
# we can write a single user's tmpfiles.d rules there
users.users = mapAttrs (user: cfg': {
packages = optional (cfg'.rules != [ ]) (writeTmpfiles {
inherit (cfg') rules;
inherit user;
});
}) cfg.tmpfiles.users;
system.userActivationScripts.tmpfiles = ''
${config.systemd.package}/bin/systemd-tmpfiles --user --create --remove
'';
};
}

View File

@@ -0,0 +1,84 @@
{ config, lib, ... }:
let
cfg = config.services.userdbd;
# List of system users that will be incorrectly treated as regular/normal
# users by userdb.
highSystemUsers = lib.filter (
user: user.enable && user.isSystemUser && (lib.defaultTo 0 user.uid) >= 1000 && user.uid != 65534
) (lib.attrValues config.users.users);
in
{
options.services.userdbd = {
enable = lib.mkEnableOption ''
the systemd JSON user/group record lookup service
'';
enableSSHSupport = lib.mkEnableOption ''
exposing OpenSSH public keys defined in userdb. Be aware that this
enables modifying public keys at runtime, either by users managed by
{option}`services.homed`, or globally via drop-in files
'';
silenceHighSystemUsers = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = "Silence warning about system users with high UIDs.";
visible = false;
};
};
config = lib.mkIf cfg.enable {
assertions = lib.singleton {
assertion = cfg.enableSSHSupport -> config.security.enableWrappers;
message = "OpenSSH userdb integration requires security wrappers.";
};
warnings = lib.optional (lib.length highSystemUsers > 0 && !cfg.silenceHighSystemUsers) ''
The following system users have UIDs higher than 1000:
${lib.concatLines (lib.map (user: user.name) highSystemUsers)}
These users will be recognized by systemd-userdb as "regular" users, not
"system" users. This will affect programs that query regular users, such
as systemd-homed, which will not run the first boot user creation flow,
as regular users already exist.
To fix this issue, please remove or redefine these system users to have
UIDs below 1000. For Nix build users, it's possible to adjust the base
build user ID using the `ids.uids.nixbld` option, however care must be
taken to avoid collisions with UIDs of other services. Alternatively, you
may enable the `auto-allocate-uids` experimental feature and option in
the Nix configuration to avoid creating these users, however please note
that this option is experimental and subject to change.
Alternatively, to acknowledge and silence this warning, set
`services.userdbd.silenceHighSystemUsers` to true.
'';
systemd.additionalUpstreamSystemUnits = [
"systemd-userdbd.socket"
"systemd-userdbd.service"
];
systemd.sockets.systemd-userdbd.wantedBy = [ "sockets.target" ];
# OpenSSH requires AuthorizedKeysCommand to be owned only by root.
# Referencing `userdbctl` directly from the Nix store won't work, as
# `/nix/store` is owned by the `nixbld` group.
security.wrappers = lib.mkIf cfg.enableSSHSupport {
userdbctl = {
owner = "root";
group = "root";
source = lib.getExe' config.systemd.package "userdbctl";
};
};
services.openssh = lib.mkIf cfg.enableSSHSupport {
authorizedKeysCommand = "/run/wrappers/bin/userdbctl ssh-authorized-keys %u";
authorizedKeysCommandUser = "root";
};
};
}

View File

@@ -0,0 +1,119 @@
{ config, lib, ... }:
with lib;
let
cfg = config.services.timesyncd;
in
{
options = {
services.timesyncd = with types; {
enable = mkOption {
default = !config.boot.isContainer;
defaultText = literalExpression "!config.boot.isContainer";
type = bool;
description = ''
Enables the systemd NTP client daemon.
'';
};
servers = mkOption {
default = null;
type = nullOr (listOf str);
description = ''
The set of NTP servers from which to synchronise.
Setting this option to an empty list will write `NTP=` to the
`timesyncd.conf` file as opposed to setting this option to null which
will remove `NTP=` entirely.
See {manpage}`timesyncd.conf(5)` for details.
'';
};
fallbackServers = mkOption {
default = config.networking.timeServers;
defaultText = literalExpression "config.networking.timeServers";
type = nullOr (listOf str);
description = ''
The set of fallback NTP servers from which to synchronise.
Setting this option to an empty list will write `FallbackNTP=` to the
`timesyncd.conf` file as opposed to setting this option to null which
will remove `FallbackNTP=` entirely.
See {manpage}`timesyncd.conf(5)` for details.
'';
};
extraConfig = mkOption {
default = "";
type = lines;
example = ''
PollIntervalMaxSec=180
'';
description = ''
Extra config options for systemd-timesyncd. See
{manpage}`timesyncd.conf(5)` for available options.
'';
};
};
};
config = mkIf cfg.enable {
systemd.additionalUpstreamSystemUnits = [ "systemd-timesyncd.service" ];
systemd.services.systemd-timesyncd = {
wantedBy = [ "sysinit.target" ];
aliases = [ "dbus-org.freedesktop.timesync1.service" ];
restartTriggers = [ config.environment.etc."systemd/timesyncd.conf".source ];
# systemd-timesyncd disables DNSSEC validation in the nss-resolve module by setting SYSTEMD_NSS_RESOLVE_VALIDATE to 0 in the unit file.
# This is required in order to solve the chicken-and-egg problem when DNSSEC validation needs the correct time to work, but to set the
# correct time, we need to connect to an NTP server, which usually requires resolving its hostname.
# In order for nss-resolve to be able to read this environment variable we patch systemd-timesyncd to disable NSCD and use NSS modules directly.
# This means that systemd-timesyncd needs to have NSS modules path in LD_LIBRARY_PATH. When systemd-resolved is disabled we still need to set
# NSS module path so that systemd-timesyncd keeps using other NSS modules that are configured in the system.
environment.LD_LIBRARY_PATH = config.system.nssModules.path;
preStart = (
# Ensure that we have some stored time to prevent
# systemd-timesyncd to resort back to the fallback time. If
# the file doesn't exist we assume that our current system
# clock is good enough to provide an initial value.
''
if ! [ -f /var/lib/systemd/timesync/clock ]; then
test -d /var/lib/systemd/timesync || mkdir -p /var/lib/systemd/timesync
touch /var/lib/systemd/timesync/clock
fi
''
+
# workaround an issue of systemd-timesyncd not starting due to upstream systemd reverting their dynamic users changes
# - https://github.com/NixOS/nixpkgs/pull/61321#issuecomment-492423742
# - https://github.com/systemd/systemd/issues/12131
(lib.optionalString (versionOlder config.system.stateVersion "19.09") ''
if [ -L /var/lib/systemd/timesync ]; then
rm /var/lib/systemd/timesync
mv /var/lib/private/systemd/timesync /var/lib/systemd/timesync
fi
'')
);
};
environment.etc."systemd/timesyncd.conf".text = ''
[Time]
''
+ optionalString (cfg.servers != null) ''
NTP=${concatStringsSep " " cfg.servers}
''
+ optionalString (cfg.fallbackServers != null) ''
FallbackNTP=${concatStringsSep " " cfg.fallbackServers}
''
+ cfg.extraConfig;
users.users.systemd-timesync = {
uid = config.ids.uids.systemd-timesync;
group = "systemd-timesync";
};
users.groups.systemd-timesync.gid = config.ids.gids.systemd-timesync;
};
}

View File

@@ -0,0 +1,87 @@
{ config, lib, ... }:
let
cfg = config.boot.tmp;
in
{
imports = [
(lib.mkRenamedOptionModule [ "boot" "cleanTmpDir" ] [ "boot" "tmp" "cleanOnBoot" ])
(lib.mkRenamedOptionModule [ "boot" "tmpOnTmpfs" ] [ "boot" "tmp" "useTmpfs" ])
(lib.mkRenamedOptionModule [ "boot" "tmpOnTmpfsSize" ] [ "boot" "tmp" "tmpfsSize" ])
];
options = {
boot.tmp = {
cleanOnBoot = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to delete all files in {file}`/tmp` during boot.
'';
};
tmpfsSize = lib.mkOption {
type = lib.types.oneOf [
lib.types.str
lib.types.ints.positive
];
default = "50%";
description = ''
Size of tmpfs in percentage.
Percentage is defined by systemd.
'';
};
tmpfsHugeMemoryPages = lib.mkOption {
type = lib.types.enum [
"never"
"always"
"within_size"
"advise"
];
default = "never";
example = "within_size";
description = ''
never - Do not allocate huge memory pages. This is the default.
always - Attempt to allocate huge memory page every time a new page is needed.
within_size - Only allocate huge memory pages if it will be fully within i_size. Also respect madvise(2) hints. Recommended.
advise - Only allocate huge memory pages if requested with madvise(2).
'';
};
useTmpfs = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to mount a tmpfs on {file}`/tmp` during boot.
::: {.note}
Large Nix builds can fail if the mounted tmpfs is not large enough.
In such a case either increase the tmpfsSize or disable this option.
:::
'';
};
};
};
config = {
# When changing remember to update /tmp mount in virtualisation/qemu-vm.nix
systemd.mounts = lib.mkIf cfg.useTmpfs [
{
what = "tmpfs";
where = "/tmp";
type = "tmpfs";
mountConfig.Options = lib.concatStringsSep "," [
"mode=1777"
"strictatime"
"rw"
"nosuid"
"nodev"
"size=${toString cfg.tmpfsSize}"
"huge=${cfg.tmpfsHugeMemoryPages}"
];
}
];
systemd.tmpfiles.rules = lib.optional cfg.cleanOnBoot "D! /tmp 1777 root root";
};
}

View File

@@ -0,0 +1,117 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.uki;
inherit (pkgs.stdenv.hostPlatform) efiArch;
format = pkgs.formats.ini { };
in
{
options = {
boot.uki = {
name = lib.mkOption {
type = lib.types.str;
description = "Name of the UKI";
};
version = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = config.system.image.version;
defaultText = lib.literalExpression "config.system.image.version";
description = "Version of the image or generation the UKI belongs to";
};
tries = lib.mkOption {
type = lib.types.nullOr lib.types.ints.unsigned;
default = null;
description = ''
Number of boot attempts before this UKI is considered bad.
If no tries are specified (the default) automatic boot assessment remains inactive.
See documentation on [Automatic Boot Assessment](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/) and
[boot counting](https://uapi-group.org/specifications/specs/boot_loader_specification/#boot-counting)
for more information.
'';
};
settings = lib.mkOption {
type = format.type;
description = ''
The configuration settings for ukify. These control what the UKI
contains and how it is built.
'';
};
configFile = lib.mkOption {
type = lib.types.path;
description = ''
The configuration file passed to {manpage}`ukify(1)` to create the UKI.
By default this configuration file is created from {option}`boot.uki.settings`.
'';
};
};
system.boot.loader.ukiFile = lib.mkOption {
type = lib.types.str;
internal = true;
description = "Name of the UKI file";
};
};
config = {
boot.uki.name = lib.mkOptionDefault (
if config.system.image.id != null then config.system.image.id else "nixos"
);
boot.uki.settings = {
UKI = {
Linux = lib.mkOptionDefault "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
Initrd = lib.mkOptionDefault "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
Cmdline = lib.mkOptionDefault "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}";
Stub = lib.mkOptionDefault "${pkgs.systemd}/lib/systemd/boot/efi/linux${efiArch}.efi.stub";
Uname = lib.mkOptionDefault "${config.boot.kernelPackages.kernel.modDirVersion}";
OSRelease = lib.mkOptionDefault "@${config.system.build.etc}/etc/os-release";
# This is needed for cross compiling.
EFIArch = lib.mkOptionDefault efiArch;
}
//
lib.optionalAttrs (config.hardware.deviceTree.enable && config.hardware.deviceTree.name != null)
{
DeviceTree = lib.mkOptionDefault "${config.hardware.deviceTree.package}/${config.hardware.deviceTree.name}";
};
};
boot.uki.configFile = lib.mkOptionDefault (format.generate "ukify.conf" cfg.settings);
system.boot.loader.ukiFile =
let
name = config.boot.uki.name;
version = config.boot.uki.version;
versionInfix = if version != null then "_${version}" else "";
triesInfix = if cfg.tries != null then "+${builtins.toString cfg.tries}" else "";
in
name + versionInfix + triesInfix + ".efi";
system.build.uki = pkgs.runCommand config.system.boot.loader.ukiFile { } ''
mkdir -p $out
${pkgs.buildPackages.systemdUkify}/lib/systemd/ukify build \
--config=${cfg.configFile} \
--output="$out/${config.system.boot.loader.ukiFile}"
'';
};
meta.maintainers = with lib.maintainers; [ nikstur ];
}

View File

@@ -0,0 +1,102 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.initrd.unl0kr;
settingsFormat = pkgs.formats.ini { };
in
{
options.boot.initrd.unl0kr = {
enable = lib.mkEnableOption "unl0kr in initrd" // {
description = ''Whether to enable the unl0kr on-screen keyboard in initrd to unlock LUKS.'';
};
package = lib.mkPackageOption pkgs "buffybox" { };
allowVendorDrivers = lib.mkEnableOption "load optional drivers" // {
description = ''Whether to load additional drivers for certain vendors (I.E: Wacom, Intel, etc.)'';
};
settings = lib.mkOption {
description = ''
Configuration for `unl0kr`.
See `unl0kr.conf(5)` for supported values.
Alternatively, visit `https://gitlab.postmarketos.org/postmarketOS/buffybox/-/blob/3.2.0/unl0kr/unl0kr.conf`
'';
example = lib.literalExpression ''
{
general.animations = true;
general.backend = "drm";
theme = {
default = "pmos-dark";
alternate = "pmos-light";
};
}
'';
default = { };
type = lib.types.submodule { freeformType = settingsFormat.type; };
};
};
config = lib.mkIf cfg.enable {
meta.maintainers = with lib.maintainers; [ hustlerone ];
assertions = [
{
assertion = cfg.enable -> config.boot.initrd.systemd.enable;
message = "boot.initrd.unl0kr is only supported with boot.initrd.systemd.";
}
];
warnings = lib.mkMerge [
(lib.mkIf (config.hardware.amdgpu.initrd.enable) [
''Use early video loading at your risk. It's not guaranteed to work with unl0kr.''
])
(lib.mkIf (config.boot.plymouth.enable) [
''Upstream clearly intends unl0kr to not run with Plymouth. Good luck''
])
];
boot.initrd.availableKernelModules =
lib.optionals cfg.enable [
"hid-multitouch"
"hid-generic"
"usbhid"
"i2c-designware-core"
"i2c-designware-platform"
"i2c-hid-acpi"
"usbtouchscreen"
"evdev"
"psmouse"
]
++ lib.optionals cfg.allowVendorDrivers [
"intel_lpss_pci"
"elo"
"wacom"
];
boot.initrd.systemd = {
contents."/etc/unl0kr.conf".source = settingsFormat.generate "unl0kr.conf" cfg.settings;
storePaths = with pkgs; [
libinput
xkeyboard_config
(lib.getExe' cfg.package "unl0kr")
"${cfg.package}/libexec/unl0kr-agent"
];
packages = [
pkgs.buffybox
];
paths.unl0kr-agent.wantedBy = [ "local-fs-pre.target" ];
};
};
}

View File

@@ -0,0 +1,51 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.uvesafb;
inherit (lib)
mkIf
mkEnableOption
mkOption
types
;
in
{
options = {
boot.uvesafb = {
enable = mkEnableOption "uvesafb";
gfx-mode = mkOption {
type = types.str;
default = "1024x768-32";
description = "Screen resolution in modedb format. See [uvesafb](https://docs.kernel.org/fb/uvesafb.html) and [modedb](https://docs.kernel.org/fb/modedb.html) documentation for more details. The default value is a sensible default but may be not ideal for all setups.";
};
v86d.package = mkOption {
type = types.package;
description = "Which v86d package to use with uvesafb";
defaultText = ''
config.boot.kernelPackages.v86d.overrideAttrs (old: {
hardeningDisable = [ "all" ];
})'';
default = config.boot.kernelPackages.v86d.overrideAttrs (old: {
hardeningDisable = [ "all" ];
});
};
};
};
config = mkIf cfg.enable {
boot.initrd = {
kernelModules = [ "uvesafb" ];
extraFiles."/usr/v86d".source = cfg.v86d.package;
};
boot.kernelParams = [
"video=uvesafb:mode:${cfg.gfx-mode},mtrr:3,ywrap"
''uvesafb.v86d="${cfg.v86d.package}/bin/v86d"''
];
};
}

View File

@@ -0,0 +1,105 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.boot.tmp;
in
{
options = {
boot.tmp = {
useZram = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Whether to mount a zram device on {file}`/tmp` during boot.
::: {.note}
Large Nix builds can fail if the mounted zram device is not large enough.
In such a case either increase the zramSettings.zram-size or disable this option.
:::
'';
};
zramSettings = {
zram-size = lib.mkOption {
type = lib.types.str;
default = "ram * 0.5";
example = "min(ram / 2, 4096)";
description = ''
The size of the zram device, as a function of MemTotal, both in MB.
For example, if the machine has 1 GiB, and zram-size=ram/4,
then the zram device will have 256 MiB.
Fractions in the range 0.10.5 are recommended
See: <https://github.com/systemd/zram-generator/blob/main/zram-generator.conf.example>
'';
};
compression-algorithm = lib.mkOption {
type = lib.types.str;
default = "zstd";
example = "lzo-rle";
description = ''
The compression algorithm to use for the zram device.
See: <https://github.com/systemd/zram-generator/blob/main/zram-generator.conf.example>
'';
};
fs-type = lib.mkOption {
type = lib.types.str;
default = "ext4";
example = "ext2";
description = ''
The file system to put on the device.
See: <https://github.com/systemd/zram-generator/blob/main/zram-generator.conf.example>
'';
};
options = lib.mkOption {
type = lib.types.str;
default = "X-mount.mode=1777,discard";
description = ''
By default, file systems and swap areas are trimmed on-the-go
by setting "discard".
Setting this to the empty string clears the option.
See: <https://github.com/systemd/zram-generator/blob/main/zram-generator.conf.example>
'';
};
};
};
};
config = lib.mkIf (cfg.useZram) {
assertions = [
{
assertion = !cfg.useTmpfs;
message = "boot.tmp.useTmpfs is unnecessary if useZram=true";
}
];
services.zram-generator.enable = true;
services.zram-generator.settings =
let
cfgz = cfg.zramSettings;
in
{
"zram${toString (if config.zramSwap.enable then config.zramSwap.swapDevices else 0)}" = {
mount-point = "/tmp";
zram-size = cfgz.zram-size;
compression-algorithm = cfgz.compression-algorithm;
options = cfgz.options;
fs-type = cfgz.fs-type;
};
};
systemd.services."systemd-zram-setup@".path = [ pkgs.util-linux ] ++ config.system.fsPackages;
};
}

View File

@@ -0,0 +1,23 @@
{ lib, ... }:
let
inherit (lib) mkOption types;
in
{
options = {
system.build = mkOption {
default = { };
description = ''
Attribute set of derivations used to set up the system.
'';
type = types.submoduleWith {
modules = [
{
freeformType = with types; lazyAttrsOf (uniq unspecified);
}
];
};
};
};
}

View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Build a composefs dump from a Json config
See the man page of composefs-dump for details about the format:
https://github.com/containers/composefs/blob/main/man/composefs-dump.md
Ensure to check the file with the check script when you make changes to it:
./check-build-composefs-dump.sh ./build-composefs_dump.py
"""
import glob
import json
import os
import sys
from enum import Enum
from pathlib import Path
from typing import Any
Attrs = dict[str, Any]
class FileType(Enum):
"""The filetype as defined by the `st_mode` stat field in octal
You can check the st_mode stat field of a path in Python with
`oct(os.stat("/path/").st_mode)`
"""
directory = "4"
file = "10"
symlink = "12"
class ComposefsPath:
path: str
size: int
filetype: FileType
mode: str
uid: str
gid: str
payload: str
rdev: str = "0"
nlink: int = 1
mtime: str = "1.0"
content: str = "-"
digest: str = "-"
def __init__(
self,
attrs: Attrs,
size: int,
filetype: FileType,
mode: str,
payload: str,
path: str | None = None,
):
if path is None:
path = attrs["target"]
self.path = path
self.size = size
self.filetype = filetype
self.mode = mode
self.uid = attrs["uid"]
self.gid = attrs["gid"]
self.payload = payload
def write_line(self) -> str:
line_list = [
str(self.path),
str(self.size),
f"{self.filetype.value}{self.mode}",
str(self.nlink),
str(self.uid),
str(self.gid),
str(self.rdev),
str(self.mtime),
str(self.payload),
str(self.content),
str(self.digest),
]
return " ".join(line_list)
def eprint(*args: Any, **kwargs: Any) -> None:
print(*args, **kwargs, file=sys.stderr)
def normalize_path(path: str) -> str:
return str("/" + os.path.normpath(path).lstrip("/"))
def leading_directories(path: str) -> list[str]:
"""Return the leading directories of path
Given the path "alsa/conf.d/50-pipewire.conf", for example, this function
returns `[ "alsa", "alsa/conf.d" ]`.
"""
parents = list(Path(path).parents)
parents.reverse()
# remove the implicit `.` from the start of a relative path or `/` from an
# absolute path
del parents[0]
return [str(i) for i in parents]
def add_leading_directories(
target: str, attrs: Attrs, paths: dict[str, ComposefsPath]
) -> None:
"""Add the leading directories of a target path to the composefs paths
mkcomposefs expects that all leading directories are explicitly listed in
the dump file. Given the path "alsa/conf.d/50-pipewire.conf", for example,
this function adds "alsa" and "alsa/conf.d" to the composefs paths.
"""
path_components = leading_directories(target)
for component in path_components:
composefs_path = ComposefsPath(
attrs,
path=component,
size=4096,
filetype=FileType.directory,
mode="0755",
payload="-",
)
paths[component] = composefs_path
def main() -> None:
"""Build a composefs dump from a Json config
This config describes the files that the final composefs image is supposed
to contain.
"""
config_file = sys.argv[1]
if not config_file:
eprint("No config file was supplied.")
sys.exit(1)
with open(config_file, "rb") as f:
config = json.load(f)
if not config:
eprint("Config is empty.")
sys.exit(1)
eprint("Building composefs dump...")
paths: dict[str, ComposefsPath] = {}
for attrs in config:
# Normalize the target path to work around issues in how targets are
# declared in `environment.etc`.
attrs["target"] = normalize_path(attrs["target"])
target = attrs["target"]
source = attrs["source"]
mode = attrs["mode"]
if "*" in source: # Path with globbing
glob_sources = glob.glob(source)
for glob_source in glob_sources:
basename = os.path.basename(glob_source)
glob_target = f"{target}/{basename}"
composefs_path = ComposefsPath(
attrs,
path=glob_target,
size=100,
filetype=FileType.symlink,
mode="0777",
payload=glob_source,
)
paths[glob_target] = composefs_path
add_leading_directories(glob_target, attrs, paths)
else: # Without globbing
if mode == "symlink" or mode == "direct-symlink":
composefs_path = ComposefsPath(
attrs,
# A high approximation of the size of a symlink
size=100,
filetype=FileType.symlink,
mode="0777",
payload=source,
)
elif os.path.isdir(source):
composefs_path = ComposefsPath(
attrs,
size=4096,
filetype=FileType.directory,
mode=mode,
payload=source,
)
else:
composefs_path = ComposefsPath(
attrs,
size=os.stat(source).st_size,
filetype=FileType.file,
mode=mode,
# payload needs to be relative path in this case
payload=target.lstrip("/"),
)
paths[target] = composefs_path
add_leading_directories(target, attrs, paths)
composefs_dump = ["/ 4096 40755 1 0 0 0 0.0 - - -"] # Root directory
for key in sorted(paths):
composefs_path = paths[key]
eprint(composefs_path.path)
composefs_dump.append(composefs_path.write_line())
print("\n".join(composefs_dump))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,8 @@
#! /usr/bin/env nix-shell
#! nix-shell -i bash -p black ruff mypy
file=$1
black --check --diff $file
ruff --line-length 88 $file
mypy --strict $file

View File

@@ -0,0 +1,168 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [ ./etc.nix ];
config = lib.mkMerge [
{
system.activationScripts.etc = lib.stringAfter [
"users"
"groups"
"specialfs"
] config.system.build.etcActivationCommands;
}
(lib.mkIf config.system.etc.overlay.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = "`system.etc.overlay.enable` requires `boot.initrd.systemd.enable`";
}
{
assertion =
(!config.system.etc.overlay.mutable)
-> (config.systemd.sysusers.enable || config.services.userborn.enable);
message = "`!system.etc.overlay.mutable` requires `systemd.sysusers.enable` or `services.userborn.enable`";
}
{
assertion =
(config.system.switch.enable)
-> (lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.6");
message = "switchable systems with `system.etc.overlay.enable` require a newer kernel, at least version 6.6";
}
];
boot.initrd.availableKernelModules = [
"loop"
"erofs"
"overlay"
];
system.requiredKernelConfig = with config.lib.kernelConfig; [
(isEnabled "EROFS_FS")
];
boot.initrd.systemd = {
mounts = [
{
where = "/run/nixos-etc-metadata";
what = "/etc-metadata-image";
type = "erofs";
options = "loop,ro,nodev,nosuid";
unitConfig = {
# Since this unit depends on the nix store being mounted, it cannot
# be a dependency of local-fs.target, because if it did, we'd have
# local-fs.target ordered after the nix store mount which would cause
# things like network.target to only become active after the nix store
# has been mounted.
# This breaks for instance setups where sshd needs to be up before
# any encrypted disks can be mounted.
DefaultDependencies = false;
RequiresMountsFor = [
"/sysroot/nix/store"
];
};
requires = [
config.boot.initrd.systemd.services.initrd-find-etc.name
];
after = [
config.boot.initrd.systemd.services.initrd-find-etc.name
];
requiredBy = [ "initrd-fs.target" ];
before = [ "initrd-fs.target" ];
}
{
where = "/sysroot/etc";
what = "overlay";
type = "overlay";
options = lib.concatStringsSep "," (
[
"nodev"
"nosuid"
"relatime"
"redirect_dir=on"
"metacopy=on"
"lowerdir=/run/nixos-etc-metadata::/etc-basedir"
]
++ lib.optionals config.system.etc.overlay.mutable [
"rw"
"upperdir=/sysroot/.rw-etc/upper"
"workdir=/sysroot/.rw-etc/work"
]
++ lib.optionals (!config.system.etc.overlay.mutable) [
"ro"
]
);
requiredBy = [ "initrd-fs.target" ];
before = [ "initrd-fs.target" ];
requires = [
config.boot.initrd.systemd.services.initrd-find-etc.name
]
++ lib.optionals config.system.etc.overlay.mutable [
config.boot.initrd.systemd.services."rw-etc".name
];
after = [
config.boot.initrd.systemd.services.initrd-find-etc.name
]
++ lib.optionals config.system.etc.overlay.mutable [
config.boot.initrd.systemd.services."rw-etc".name
];
unitConfig = {
RequiresMountsFor = [
"/sysroot/nix/store"
"/run/nixos-etc-metadata"
];
DefaultDependencies = false;
};
}
];
services = lib.mkMerge [
(lib.mkIf config.system.etc.overlay.mutable {
rw-etc = {
requiredBy = [ "initrd-fs.target" ];
before = [ "initrd-fs.target" ];
unitConfig = {
DefaultDependencies = false;
RequiresMountsFor = "/sysroot";
};
serviceConfig = {
Type = "oneshot";
ExecStart = ''
/bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work
'';
};
};
})
{
initrd-find-etc = {
description = "Find the path to the etc metadata image and based dir";
before = [ "shutdown.target" ];
conflicts = [ "shutdown.target" ];
requiredBy = [ "initrd.target" ];
path = [ config.system.nixos-init.package ];
unitConfig = {
DefaultDependencies = false;
RequiresMountsFor = "/sysroot/nix/store";
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${config.system.nixos-init.package}/bin/find-etc";
};
};
}
];
};
})
];
}

View File

@@ -0,0 +1,411 @@
# Management of static files in /etc.
{
config,
lib,
pkgs,
...
}:
let
etc' = lib.filter (f: f.enable) (lib.attrValues config.environment.etc);
etc =
pkgs.runCommandLocal "etc"
{
# This is needed for the systemd module
passthru.targets = map (x: x.target) etc';
} # sh
''
set -euo pipefail
makeEtcEntry() {
src="$1"
target="$2"
mode="$3"
user="$4"
group="$5"
if [[ "$src" = *'*'* ]]; then
# If the source name contains '*', perform globbing.
mkdir -p "$out/etc/$target"
for fn in $src; do
ln -s "$fn" "$out/etc/$target/"
done
else
mkdir -p "$out/etc/$(dirname "$target")"
if ! [ -e "$out/etc/$target" ]; then
ln -s "$src" "$out/etc/$target"
else
echo "duplicate entry $target -> $src"
if [ "$(readlink "$out/etc/$target")" != "$src" ]; then
echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src"
ret=1
fi
fi
if [ "$mode" != symlink ]; then
echo "$mode" > "$out/etc/$target.mode"
echo "$user" > "$out/etc/$target.uid"
echo "$group" > "$out/etc/$target.gid"
fi
fi
}
mkdir -p "$out/etc"
${lib.concatMapStringsSep "\n" (
etcEntry:
lib.escapeShellArgs [
"makeEtcEntry"
# Force local source paths to be added to the store
"${etcEntry.source}"
etcEntry.target
etcEntry.mode
etcEntry.user
etcEntry.group
]
) etc'}
'';
etcHardlinks = lib.filter (f: f.mode != "symlink" && f.mode != "direct-symlink") etc';
in
{
imports = [ ../build.nix ];
###### interface
options = {
system.etc.overlay = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Mount `/etc` as an overlayfs instead of generating it via a perl script.
Note: This is currently experimental. Only enable this option if you're
confident that you can recover your system if it breaks.
'';
};
mutable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only).
If this is false, only the immutable lowerdir is mounted. If it is
true, a writable upperdir is mounted on top.
'';
};
};
environment.etc = lib.mkOption {
default = { };
example = lib.literalExpression ''
{ example-configuration-file =
{ source = "/nix/store/.../etc/dir/file.conf.example";
mode = "0440";
};
"default/useradd".text = "GROUP=100 ...";
}
'';
description = ''
Set of files that have to be linked in {file}`/etc`.
'';
type =
with lib.types;
attrsOf (
submodule (
{
name,
config,
options,
...
}:
{
options = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether this /etc file should be generated. This
option allows specific /etc files to be disabled.
'';
};
target = lib.mkOption {
type = lib.types.str;
description = ''
Name of symlink (relative to
{file}`/etc`). Defaults to the attribute
name.
'';
};
text = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.lines;
description = "Text of the file.";
};
source = lib.mkOption {
type = lib.types.path;
description = "Path of the source file.";
};
mode = lib.mkOption {
type = lib.types.str;
default = "symlink";
example = "0600";
description = ''
If set to something else than `symlink`,
the file is copied instead of symlinked, with the given
file mode.
'';
};
uid = lib.mkOption {
default = 0;
type = lib.types.int;
description = ''
UID of created file. Only takes effect when the file is
copied (that is, the mode is not 'symlink').
'';
};
gid = lib.mkOption {
default = 0;
type = lib.types.int;
description = ''
GID of created file. Only takes effect when the file is
copied (that is, the mode is not 'symlink').
'';
};
user = lib.mkOption {
default = "+${toString config.uid}";
type = lib.types.str;
description = ''
User name of file owner.
Only takes effect when the file is copied (that is, the
mode is not `symlink`).
When `services.userborn.enable`, this option has no effect.
You have to assign a `uid` instead. Otherwise this option
takes precedence over `uid`.
'';
};
group = lib.mkOption {
default = "+${toString config.gid}";
type = lib.types.str;
description = ''
Group name of file owner.
Only takes effect when the file is copied (that is, the
mode is not `symlink`).
When `services.userborn.enable`, this option has no effect.
You have to assign a `gid` instead. Otherwise this option
takes precedence over `gid`.
'';
};
};
config = {
target = lib.mkDefault name;
source = lib.mkIf (config.text != null) (
let
name' = "etc-" + lib.replaceStrings [ "/" ] [ "-" ] name;
in
lib.mkDerivedConfig options.text (pkgs.writeText name')
);
};
}
)
);
};
};
###### implementation
config = {
system.build.etc = etc;
system.build.etcActivationCommands =
let
etcOverlayOptions = lib.concatStringsSep "," (
[
"relatime"
"redirect_dir=on"
"metacopy=on"
]
++ lib.optionals config.system.etc.overlay.mutable [
"upperdir=/.rw-etc/upper"
"workdir=/.rw-etc/work"
]
);
in
if config.system.etc.overlay.enable then
#bash
''
# This script atomically remounts /etc when switching configuration.
# On a (re-)boot this should not run because /etc is mounted via a
# systemd mount unit instead.
# The activation script can also be called in cases where we didn't have
# an initrd though, like for instance when using nixos-enter,
# so we cannot assume that /etc has already been mounted.
#
# To a large extent this mimics what composefs does. Because
# it's relatively simple, however, we avoid the composefs dependency.
# Since this script is not idempotent, it should not run when etc hasn't
# changed.
if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]] && [[ "${config.system.build.etc}/etc" != "$(readlink -f /run/current-system/etc)" ]]; then
echo "remounting /etc..."
${lib.optionalString config.system.etc.overlay.mutable ''
# These directories are usually created in initrd,
# but we need to create them here when we're called directly,
# for instance by nixos-enter
mkdir --parents /.rw-etc/upper /.rw-etc/work
chmod 0755 /.rw-etc /.rw-etc/upper /.rw-etc/work
''}
tmpMetadataMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc-metadata.XXXXXXXXXX)
mount --type erofs --options ro,nodev,nosuid ${config.system.build.etcMetadataImage} $tmpMetadataMount
# There was no previous /etc mounted. This happens when we're called
# directly without an initrd, like with nixos-enter.
if ! mountpoint -q /etc; then
mount --type overlay \
--options nodev,nosuid,lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
overlay /etc
else
# Mount the new /etc overlay to a temporary private mount.
# This needs the indirection via a private bind mount because you
# cannot move shared mounts.
tmpEtcMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc.XXXXXXXXXX)
mount --bind --make-private $tmpEtcMount $tmpEtcMount
mount --type overlay \
--options nodev,nosuid,lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
overlay $tmpEtcMount
# Before moving the new /etc overlay under the old /etc, we have to
# move mounts on top of /etc to the new /etc mountpoint.
findmnt /etc --submounts --list --noheading --kernel --output TARGET | while read -r mountPoint; do
if [[ "$mountPoint" = "/etc" ]]; then
continue
fi
tmpMountPoint="$tmpEtcMount/''${mountPoint:5}"
${
if config.system.etc.overlay.mutable then
''
if [[ -f "$mountPoint" ]]; then
touch "$tmpMountPoint"
elif [[ -d "$mountPoint" ]]; then
mkdir -p "$tmpMountPoint"
fi
''
else
''
if [[ ! -e "$tmpMountPoint" ]]; then
echo "Skipping undeclared mountpoint in environment.etc: $mountPoint"
continue
fi
''
}
mount --bind "$mountPoint" "$tmpMountPoint"
done
# Move the new temporary /etc mount underneath the current /etc mount.
#
# This should eventually use util-linux to perform this move beneath,
# however, this functionality is not yet in util-linux. See this
# tracking issue: https://github.com/util-linux/util-linux/issues/2604
${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc
# Unmount the top /etc mount to atomically reveal the new mount.
umount --lazy --recursive /etc
# Unmount the temporary mount
umount --lazy "$tmpEtcMount"
rmdir "$tmpEtcMount"
fi
# Unmount old metadata mounts
# For some reason, `findmnt /tmp --submounts` does not show the nested
# mounts. So we'll just find all mounts of type erofs and filter on the
# name of the mountpoint.
findmnt --type erofs --list --kernel --output TARGET | while read -r mountPoint; do
if [[ ("$mountPoint" =~ ^/run/nixos-etc-metadata\..{10}$ || "$mountPoint" =~ ^/run/nixos-etc-metadata$ ) &&
"$mountPoint" != "$tmpMetadataMount" ]]; then
umount --lazy "$mountPoint"
rmdir "$mountPoint"
fi
done
fi
''
else
''
# Set up the statically computed bits of /etc.
echo "setting up /etc..."
${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
'';
system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } ''
set -euo pipefail
makeEtcEntry() {
src="$1"
target="$2"
mkdir -p "$out/$(dirname "$target")"
cp "$src" "$out/$target"
}
mkdir -p "$out"
${lib.concatMapStringsSep "\n" (
etcEntry:
lib.escapeShellArgs [
"makeEtcEntry"
# Force local source paths to be added to the store
"${etcEntry.source}"
etcEntry.target
]
) etcHardlinks}
'';
system.build.etcMetadataImage =
let
etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc');
etcDump = pkgs.runCommandLocal "etc-dump" { } ''
${lib.getExe pkgs.buildPackages.python3} ${./build-composefs-dump.py} ${etcJson} > $out
'';
in
pkgs.runCommandLocal "etc-metadata.erofs"
{
nativeBuildInputs = with pkgs.buildPackages; [
composefs
erofs-utils
];
}
''
mkcomposefs --from-file ${etcDump} $out
fsck.erofs $out
'';
};
}

View File

@@ -0,0 +1,159 @@
use strict;
use File::Find;
use File::Copy;
use File::Path;
use File::Basename;
use File::Slurp;
my $etc = $ARGV[0] or die;
my $static = "/etc/static";
sub atomicSymlink {
my ($source, $target) = @_;
my $tmp = "$target.tmp";
unlink $tmp;
symlink $source, $tmp or return 0;
if (rename $tmp, $target) {
return 1;
} else {
unlink $tmp;
return 0;
}
}
# Atomically update /etc/static to point at the etc files of the
# current configuration.
atomicSymlink $etc, $static or die;
# Returns 1 if the argument points to the files in /etc/static. That
# means either argument is a symlink to a file in /etc/static or a
# directory with all children being static.
sub isStatic {
my $path = shift;
if (-l $path) {
my $target = readlink $path;
return substr($target, 0, length "/etc/static/") eq "/etc/static/";
}
if (-d $path) {
opendir DIR, "$path" or return 0;
my @names = readdir DIR or die;
closedir DIR;
foreach my $name (@names) {
next if $name eq "." || $name eq "..";
unless (isStatic("$path/$name")) {
return 0;
}
}
return 1;
}
return 0;
}
# Remove dangling symlinks that point to /etc/static. These are
# configuration files that existed in a previous configuration but not
# in the current one. For efficiency, don't look under /etc/nixos
# (where all the NixOS sources live).
sub cleanup {
if ($File::Find::name eq "/etc/nixos") {
$File::Find::prune = 1;
return;
}
if (-l $_) {
my $target = readlink $_;
if (substr($target, 0, length $static) eq $static) {
my $x = "/etc/static/" . substr($File::Find::name, length "/etc/");
unless (-l $x) {
print STDERR "removing obsolete symlink $File::Find::name...\n";
unlink "$_";
}
}
}
}
find(\&cleanup, "/etc");
# Use /etc/.clean to keep track of copied files.
my @oldCopied = read_file("/etc/.clean", chomp => 1, err_mode => 'quiet');
open CLEAN, ">>/etc/.clean";
# For every file in the etc tree, create a corresponding symlink in
# /etc to /etc/static. The indirection through /etc/static is to make
# switching to a new configuration somewhat more atomic.
my %created;
my @copied;
sub link {
my $fn = substr $File::Find::name, length($etc) + 1 or next;
# nixos-enter sets up /etc/resolv.conf as a bind mount, so skip it.
if ($fn eq "resolv.conf" and $ENV{'IN_NIXOS_ENTER'}) {
return;
}
my $target = "/etc/$fn";
File::Path::make_path(dirname $target);
$created{$fn} = 1;
# Rename doesn't work if target is directory.
if (-l $_ && -d $target) {
if (isStatic $target) {
rmtree $target or warn;
} else {
warn "$target directory contains user files. Symlinking may fail.";
}
}
if (-e "$_.mode") {
my $mode = read_file("$_.mode"); chomp $mode;
if ($mode eq "direct-symlink") {
atomicSymlink readlink("$static/$fn"), $target or warn "could not create symlink $target";
} else {
my $uid = read_file("$_.uid"); chomp $uid;
my $gid = read_file("$_.gid"); chomp $gid;
copy "$static/$fn", "$target.tmp" or warn;
$uid = getpwnam $uid unless $uid =~ /^\+/;
$gid = getgrnam $gid unless $gid =~ /^\+/;
chown int($uid), int($gid), "$target.tmp" or warn;
chmod oct($mode), "$target.tmp" or warn;
unless (rename "$target.tmp", $target) {
warn "could not create target $target";
unlink "$target.tmp";
}
}
push @copied, $fn;
print CLEAN "$fn\n";
} elsif (-l "$_") {
atomicSymlink "$static/$fn", $target or warn "could not create symlink $target";
}
}
find(\&link, $etc);
# Delete files that were copied in a previous version but not in the
# current.
foreach my $fn (@oldCopied) {
if (!defined $created{$fn}) {
$fn = "/etc/$fn";
print STDERR "removing obsolete file $fn...\n";
unlink "$fn";
}
}
# Rewrite /etc/.clean.
close CLEAN;
write_file("/etc/.clean", map { "$_\n" } sort @copied);
# Create /etc/NIXOS tag if not exists.
# When /etc is not on a persistent filesystem, it will be wiped after reboot,
# so we need to check and re-create it during activation.
open TAG, ">>/etc/NIXOS";
close TAG;

View File

@@ -0,0 +1,86 @@
{
lib,
coreutils,
fakechroot,
fakeroot,
evalMinimalConfig,
pkgsModule,
runCommand,
util-linux,
vmTools,
writeText,
}:
let
node = evalMinimalConfig (
{ config, ... }:
{
imports = [
pkgsModule
../etc/etc.nix
];
environment.etc."passwd" = {
text = passwdText;
};
environment.etc."hosts" = {
text = hostsText;
mode = "0751";
};
}
);
passwdText = ''
root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash
'';
hostsText = ''
127.0.0.1 localhost
::1 localhost
# testing...
'';
in
lib.recurseIntoAttrs {
test-etc-vm = vmTools.runInLinuxVM (
runCommand "test-etc-vm" { } ''
mkdir -p /etc
${node.config.system.build.etcActivationCommands}
set -x
[[ -L /etc/passwd ]]
diff /etc/passwd ${writeText "expected-passwd" passwdText}
[[ 751 = $(stat --format %a /etc/hosts) ]]
diff /etc/hosts ${writeText "expected-hosts" hostsText}
set +x
touch $out
''
);
# fakeroot is behaving weird
test-etc-fakeroot =
runCommand "test-etc"
{
nativeBuildInputs = [
fakeroot
fakechroot
# for chroot
coreutils
# fakechroot needs getopt, which is provided by util-linux
util-linux
];
fakeRootCommands = ''
mkdir -p /etc
${node.config.system.build.etcActivationCommands}
diff /etc/hosts ${writeText "expected-hosts" hostsText}
touch $out
'';
}
''
mkdir fake-root
export FAKECHROOT_EXCLUDE_PATH=/dev:/proc:/sys:${builtins.storeDir}:$out
if [ -e "$NIX_ATTRS_SH_FILE" ]; then
export FAKECHROOT_EXCLUDE_PATH=$FAKECHROOT_EXCLUDE_PATH:$NIX_ATTRS_SH_FILE
fi
fakechroot fakeroot chroot $PWD/fake-root bash -e -c '
if [ -e "$NIX_ATTRS_SH_FILE" ]; then . "$NIX_ATTRS_SH_FILE"; fi
source $stdenv/setup
eval "$fakeRootCommands"
'
'';
}

View File

@@ -0,0 +1,145 @@
# Modular Services
This directory defines a modular service infrastructure for NixOS.
See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md).
[Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services
# Design decision log
## Initial design
- `system.services.<name>`. Alternatives considered
- `systemServices`: similar to does not allow importing a composition of services into `system`. Not sure if that's a good idea in the first place, but I've kept the possibility open.
- `services.abstract`: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system.
Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all.
- `services.modular`: only slightly better than `services.abstract`, but still weird
- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521
- For now, do not add an `enable` option, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)?
- Move all process options into a `process` option tree. Putting this at the root is messy, because we also have sub-services at that level. Those are rather distinct. Grouping them "by kind" should raise fewer questions.
- `modules/system/service/systemd/system.nix` has `system` twice. Not great, but
- they have different meanings
1. These are system-provided modules, provided by the configuration manager
2. `systemd/system` configures SystemD _system units_.
- This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially
## Configuration Data (`configData`) Design
Without a mechanism for adding files, all configuration had to go through `process.*`, requiring process restarts even when those would have been avoidable.
Many services implement automatic reloading or reloading on e.g. `SIGUSR1`, but those mechanisms need files to read. `configData` provides such files.
### Naming and Terminology
- **`configData` instead of `environment.etc`**: The name `configData` is service manager agnostic. While systemd system services can use `/etc`, other service managers may expose configuration data differently (e.g., different directory, relative paths).
- **`path` attribute**: Each `configData` entry automatically gets a `path` attribute set by the service manager implementation, allowing services to reference the location of their configuration files. These paths themselves are not subject to change from generation to generation; only their contents are.
- **`name` attribute**: In `environment.etc` this would be `target` but that's confusing, especially for symlinks, as it's not the symlink's target.
### Service Manager Integration
- **Portable base**: The `configData` interface is declared in `portable/config-data.nix`, making it available to all service manager implementations.
- **Systemd integration**: The systemd implementation (`systemd/system.nix`) maps `configData` entries to `environment.etc` entries under `/etc/system-services/`.
- **Path computation**: `systemd/config-data-path.nix` recursively computes unique paths for services and sub-services (e.g., `/etc/system-services/webserver/` vs `/etc/system-services/webserver-api/`).
Fun fact: for the module system it is a completely normal module, despite its recursive definition.
If we parameterize `/etc/system-services`, it will have to become an `importApply` style module nonetheless (function returning module).
- **Simple attribute structure**: Unlike `environment.etc`, `configData` uses a simpler structure with just `enable`, `name`, `text`, `source`, and `path` attributes. Complex ownership options were omitted for simplicity and portability.
Per-service user creation is still TBD.
## No `pkgs` module argument
The modular service infrastructure avoids exposing `pkgs` as a module argument to service modules. Instead, derivations and builder functions are provided through lexical closure, making dependency relationships explicit and avoiding uncertainty about where dependencies come from.
### Benefits
- **Explicit dependencies**: Services declare what they need rather than implicitly depending on `pkgs`
- **No interference**: Service modules can be reused in different contexts without assuming a specific `pkgs` instance. An unexpected `pkgs` version is not a failure mode anymore.
- **Clarity**: With fewer ways to do things, there's no ambiguity about where dependencies come from (from the module, not the OS or service manager)
### Implementation
- **Portable layer**: Service modules in `portable/` do not receive `pkgs` as a module argument. Any required derivations must be provided by the caller.
- **Systemd integration**: The `systemd/system.nix` module imports `config-data.nix` as a function, providing `pkgs` in lexical closure:
```nix
(import ../portable/config-data.nix { inherit pkgs; })
```
- **Service modules**:
1. Should explicitly declare their package dependencies as options rather than using `pkgs` defaults:
```nix
{
# Bad: uses pkgs module argument
foo.package = mkOption {
default = pkgs.python3;
# ...
};
}
```
```nix
{
# Good: caller provides the package
foo.package = mkOption {
type = types.package;
description = "Python package to use";
defaultText = lib.literalMD "The package that provided this module.";
};
}
```
2. `passthru.services` can still provide a complete module using the package's lexical scope, making the module truly self-contained:
**Package (`package.nix`):**
```nix
{
lib,
writeScript,
runtimeShell,
# ... other dependencies
}:
stdenv.mkDerivation (finalAttrs: {
# ... package definition
passthru.services.default = {
imports = [
(lib.modules.importApply ./service.nix {
inherit writeScript runtimeShell;
})
];
someService.package = finalAttrs.finalPackage;
};
})
```
**Service module (`service.nix`):**
```nix
# Non-module dependencies (importApply)
{ writeScript, runtimeShell }:
# Service module
{
lib,
config,
options,
...
}:
{
# Service definition using writeScript, runtimeShell from lexical scope
process.argv = [
(writeScript "wrapper" ''
#!${runtimeShell}
# ... wrapper logic
'')
# ... other args
];
}
```

View File

@@ -0,0 +1,65 @@
# Tests in: ../../../../tests/modular-service-etc/test.nix
# This file is a function that returns a module.
pkgs:
{
lib,
name,
config,
options,
...
}:
let
inherit (lib) mkOption types;
in
{
options = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether this configuration file should be generated.
This option allows specific configuration files to be disabled.
'';
};
name = mkOption {
type = types.str;
description = ''
Name of the configuration file (relative to the service's configuration directory). Defaults to the attribute name.
'';
};
path = mkOption {
type = types.str;
readOnly = true;
description = ''
The actual path where this configuration file will be available.
This is determined by the service manager implementation.
On NixOS it is an absolute path.
Other service managers may provide a relative path, in order to be unprivileged and/or relocatable.
'';
};
text = mkOption {
default = null;
type = types.nullOr types.lines;
description = "Text content of the configuration file.";
};
source = mkOption {
type = types.path;
description = "Path of the source file.";
};
};
config = {
name = lib.mkDefault name;
source = lib.mkIf (config.text != null) (
let
name' = "service-configdata-" + lib.replaceStrings [ "/" ] [ "-" ] name;
in
lib.mkDerivedConfig options.text (pkgs.writeText name')
);
};
}

View File

@@ -0,0 +1,47 @@
# Tests in: ../../../../tests/modular-service-etc/test.nix
# Non-modular context provided by the modular services integration.
{ pkgs }:
# Configuration data support for portable services
# This module provides configData for services, enabling configuration reloading
# without terminating and restarting the service process.
{
lib,
...
}:
let
inherit (lib) mkOption types;
inherit (lib.modules) importApply;
in
{
options = {
configData = mkOption {
default = { };
example = lib.literalExpression ''
{
"server.conf" = {
text = '''
port = 8080
workers = 4
''';
};
"ssl/cert.pem" = {
source = ./cert.pem;
};
}
'';
description = ''
Configuration data files for the service
These files are made available to the service and can be updated without restarting the service process, enabling configuration reloading.
The service manager implementation determines how these files are exposed to the service (e.g., via a specific directory path).
This path is available in the `path` sub-option for each `configData.<name>` entry.
This is particularly useful for services that support configuration reloading via signals (e.g., SIGHUP) or which pick up changes automatically, so that no downtime is required in order to reload the service.
'';
type = types.lazyAttrsOf (types.submodule (importApply ./config-data-item.nix pkgs));
};
};
}

View File

@@ -0,0 +1,77 @@
{ lib, ... }:
let
inherit (lib)
concatLists
mapAttrsToList
showOption
types
;
in
rec {
flattenMapServicesConfigToList =
f: loc: config:
f loc config
++ concatLists (
mapAttrsToList (
k: v:
flattenMapServicesConfigToList f (
loc
++ [
"services"
k
]
) v
) config.services
);
getWarnings = flattenMapServicesConfigToList (
loc: config: map (msg: "in ${showOption loc}: ${msg}") config.warnings
);
getAssertions = flattenMapServicesConfigToList (
loc: config:
map (ass: {
message = "in ${showOption loc}: ${ass.message}";
assertion = ass.assertion;
}) config.assertions
);
/**
This is the entrypoint for the portable part of modular services.
It provides the various options that are consumed by service manager implementations.
# Inputs
`serviceManagerPkgs`: A Nixpkgs instance which will be used for built-in logic such as converting `configData.<path>.text` to a store path.
`extraRootModules`: Modules to be loaded into the "root" service submodule, but not into its sub-`services`. That's the modules' own responsibility.
`extraRootSpecialArgs`: Fixed module arguments that are provided in a similar manner to `extraRootModules`.
# Output
An attribute set.
`serviceSubmodule`: a Module System option type which is a `submodule` with the portable modules and this function's inputs loaded into it.
*/
configure =
{
serviceManagerPkgs,
extraRootModules ? [ ],
extraRootSpecialArgs ? { },
}:
let
modules = [
(lib.modules.importApply ./service.nix { pkgs = serviceManagerPkgs; })
];
serviceSubmodule = types.submoduleWith {
class = "service";
modules = modules ++ extraRootModules;
specialArgs = extraRootSpecialArgs;
};
in
{
inherit serviceSubmodule;
};
}

Some files were not shown because too many files have changed in this diff Show More