uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead

https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948
this can do it nicely.

Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
Anton Arapov 2021-04-03 12:58:10 +02:00 committed by Alan Daniels
commit 56de2bcd43
30691 changed files with 3076956 additions and 0 deletions

View file

@ -0,0 +1,272 @@
# 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}
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
# Prevent the current configuration from being garbage-collected.
mkdir -p /nix/var/nix/gcroots
ln -sfn /run/current-system /nix/var/nix/gcroots/current-system
exit $_status
'';
path = with pkgs; map getBin
[ coreutils
gnugrep
findutils
getent
stdenv.cc.libc # nscd in update-users-groups.pl
shadow
nettools # needed for hostname
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
<literal>NIXOS_ACTION</literal> being set to <literal>dry-activate
</literal>. 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.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</command>, 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 = literalDocBook "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</command>, it's important that they are
idempotent and fast.
'';
type = with types; attrsOf (scriptType false);
apply = set: {
script = ''
unset 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 env(1) executable that is linked system-wide to
<literal>/usr/bin/env</literal>.
'';
};
};
###### implementation
config = {
system.activationScripts.stdio = ""; # obsolete
system.activationScripts.var =
''
# Various log/runtime directories.
mkdir -m 1777 -p /var/tmp
# Empty, immutable home directory of many system accounts.
mkdir -p /var/empty
# Make sure it's really empty
${pkgs.e2fsprogs}/bin/chattr -f -i /var/empty || true
find /var/empty -mindepth 1 -delete
chmod 0555 /var/empty
chown root:root /var/empty
${pkgs.e2fsprogs}/bin/chattr -f +i /var/empty || true
'';
system.activationScripts.usrbinenv = if config.environment.usrbinenv != null
then ''
mkdir -m 0755 -p /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
rmdir --ignore-fail-on-non-empty /usr/bin /usr
'';
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 -m 0755 -p "$mountPoint"
fi
mount -t "$fsType" -o "$options" "$device" "$mountPoint"
}
source ${config.system.build.earlyMountScript}
'';
systemd.user = {
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,8 @@
{ lib, ... }:
with lib;
{
boot.loader.grub.device = mkOverride 0 "nodev";
specialisation = mkOverride 0 {};
}

View file

@ -0,0 +1,932 @@
#! @perl@/bin/perl
# Issue #166838 uncovered a situation in which a configuration not suitable
# for the target architecture caused a cryptic error message instead of
# a clean failure. Due to this mismatch, the perl interpreter in the shebang
# line wasn't able to be executed, causing this script to be misinterpreted
# as a shell script.
#
# Let's detect this situation to give a more meaningful error
# message. The following two lines are carefully written to be both valid Perl
# and Bash.
printf "Perl script erroneously interpreted as shell script,\ndoes target platform match nixpkgs.crossSystem platform?\n" && exit 1
if 0;
use strict;
use warnings;
use Config::IniFiles;
use File::Path qw(make_path);
use File::Basename;
use File::Slurp qw(read_file write_file edit_file);
use Net::DBus;
use Sys::Syslog qw(:standard :macros);
use Cwd qw(abs_path);
## no critic(ControlStructures::ProhibitDeepNests)
## no critic(ErrorHandling::RequireCarping)
## no critic(CodeLayout::ProhibitParensWithBuiltins)
## no critic(Variables::ProhibitPunctuationVars, Variables::RequireLocalizedPunctuationVars)
## no critic(InputOutput::RequireCheckedSyscalls, InputOutput::RequireBracedFileHandleWithPrint, InputOutput::RequireBriefOpen)
## no critic(ValuesAndExpressions::ProhibitNoisyQuotes, ValuesAndExpressions::ProhibitMagicNumbers, ValuesAndExpressions::ProhibitEmptyQuotes, ValuesAndExpressions::ProhibitInterpolationOfLiterals)
## no critic(RegularExpressions::ProhibitEscapedMetacharacters)
# System closure path to switch to
my $out = "@out@";
# Path to the directory containing systemd tools of the old system
my $cur_systemd = abs_path("/run/current-system/sw/bin");
# Path to the systemd store path of the new system
my $new_systemd = "@systemd@";
# To be robust against interruption, record what units need to be started etc.
# We read these files again every time this script starts to make sure we continue
# where the old (interrupted) script left off.
my $start_list_file = "/run/nixos/start-list";
my $restart_list_file = "/run/nixos/restart-list";
my $reload_list_file = "/run/nixos/reload-list";
# Parse restart/reload requests by the activation script.
# Activation scripts may write newline-separated units to the restart
# file and switch-to-configuration will handle them. While
# `stopIfChanged = true` is ignored, switch-to-configuration will
# handle `restartIfChanged = false` and `reloadIfChanged = true`.
# This is the same as specifying a restart trigger in the NixOS module.
#
# The reload file asks the script to reload a unit. This is the same as
# specifying a reload trigger in the NixOS module and can be ignored if
# the unit is restarted in this activation.
my $restart_by_activation_file = "/run/nixos/activation-restart-list";
my $reload_by_activation_file = "/run/nixos/activation-reload-list";
my $dry_restart_by_activation_file = "/run/nixos/dry-activation-restart-list";
my $dry_reload_by_activation_file = "/run/nixos/dry-activation-reload-list";
# The action that is to be performed (like switch, boot, test, dry-activate)
# Also exposed via environment variable from now on
my $action = shift(@ARGV);
$ENV{NIXOS_ACTION} = $action;
# Expose the locale archive as an environment variable for systemctl and the activation script
if ("@localeArchive@" ne "") {
$ENV{LOCALE_ARCHIVE} = "@localeArchive@";
}
if (!defined($action) || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) {
print STDERR <<"EOF";
Usage: $0 [switch|boot|test]
switch: make the configuration the boot default and activate now
boot: make the configuration the boot default
test: activate the configuration, but don\'t make it the boot default
dry-activate: show what would be done if this configuration were activated
EOF
exit(1);
}
# This is a NixOS installation if it has /etc/NIXOS or a proper
# /etc/os-release.
if (!-f "/etc/NIXOS" && (read_file("/etc/os-release", err_mode => "quiet") // "") !~ /^ID="?nixos"?/msx) {
die("This is not a NixOS installation!\n");
}
make_path("/run/nixos", { mode => oct(755) });
openlog("nixos", "", LOG_USER);
# Install or update the bootloader.
if ($action eq "switch" || $action eq "boot") {
chomp(my $install_boot_loader = <<'EOFBOOTLOADER');
@installBootLoader@
EOFBOOTLOADER
system("$install_boot_loader $out") == 0 or exit 1;
}
# Just in case the new configuration hangs the system, do a sync now.
if (($ENV{"NIXOS_NO_SYNC"} // "") ne "1") {
system("@coreutils@/bin/sync", "-f", "/nix/store");
}
if ($action eq "boot") {
exit(0);
}
# Check if we can activate the new configuration.
my $cur_init_interface_version = read_file("/run/current-system/init-interface-version", err_mode => "quiet") // "";
my $new_init_interface_version = read_file("$out/init-interface-version");
if ($new_init_interface_version ne $cur_init_interface_version) {
print STDERR <<'EOF';
Warning: the new NixOS configuration has an init that is
incompatible with the current configuration. The new configuration
won't take effect until you reboot the system.
EOF
exit(100);
}
# Ignore SIGHUP so that we're not killed if we're running on (say)
# virtual console 1 and we restart the "tty1" unit.
$SIG{PIPE} = "IGNORE";
# Asks the currently running systemd instance via dbus which units are active.
# Returns a hash where the key is the name of each unit and the value a hash
# of load, state, substate.
sub get_active_units {
my $mgr = Net::DBus->system->get_service("org.freedesktop.systemd1")->get_object("/org/freedesktop/systemd1");
my $units = $mgr->ListUnitsByPatterns([], []);
my $res = {};
for my $item (@{$units}) {
my ($id, $description, $load_state, $active_state, $sub_state,
$following, $unit_path, $job_id, $job_type, $job_path) = @{$item};
if ($following ne "") {
next;
}
if ($job_id == 0 and $active_state eq "inactive") {
next;
}
$res->{$id} = { load => $load_state, state => $active_state, substate => $sub_state };
}
return $res;
}
# Asks the currently running systemd instance whether a unit is currently active.
# Takes the name of the unit as an argument and returns a bool whether the unit is active or not.
sub unit_is_active {
my ($unit_name) = @_;
my $mgr = Net::DBus->system->get_service("org.freedesktop.systemd1")->get_object("/org/freedesktop/systemd1");
my $units = $mgr->ListUnitsByNames([$unit_name]);
if (scalar(@{$units}) == 0) {
return 0;
}
my $active_state = $units->[0]->[3];
return $active_state eq "active" || $active_state eq "activating";
}
# Parse a fstab file, given its path.
# Returns a tuple of filesystems and swaps.
#
# Filesystems is a hash of mountpoint and { device, fsType, options }
# Swaps is a hash of device and { options }
sub parse_fstab {
my ($filename) = @_;
my ($fss, $swaps);
foreach my $line (read_file($filename, err_mode => "quiet")) {
chomp($line);
$line =~ s/^\s*\#.*//msx;
if ($line =~ /^\s*$/msx) {
next;
}
my @xs = split(/\s+/msx, $line);
if ($xs[2] eq "swap") {
$swaps->{$xs[0]} = { options => $xs[3] // "" };
} else {
$fss->{$xs[1]} = { device => $xs[0], fsType => $xs[2], options => $xs[3] // "" };
}
}
return ($fss, $swaps);
}
# This subroutine takes a single ini file that specified systemd configuration
# like unit configuration and parses it into a hash where the keys are the sections
# of the unit file and the values are hashes themselves. These hashes have the unit file
# keys as their keys (left side of =) and an array of all values that were set as their
# values. If a value is empty (for example `ExecStart=`), then all current definitions are
# removed.
#
# Instead of returning the hash, this subroutine takes a hashref to return the data in. This
# allows calling the subroutine multiple times with the same hash to parse override files.
sub parse_systemd_ini {
my ($unit_contents, $path) = @_;
# Tie the ini file to a hash for easier access
tie(my %file_contents, "Config::IniFiles", (-file => $path, -allowempty => 1, -allowcontinue => 1)); ## no critic(Miscellanea::ProhibitTies)
# Copy over all sections
foreach my $section_name (keys(%file_contents)) {
if ($section_name eq "Install") {
# Skip the [Install] section because it has no relevant keys for us
next;
}
# Copy over all keys
foreach my $ini_key (keys(%{$file_contents{$section_name}})) {
# Ensure the value is an array so it's easier to work with
my $ini_value = $file_contents{$section_name}{$ini_key};
my @ini_values;
if (ref($ini_value) eq "ARRAY") {
@ini_values = @{$ini_value};
} else {
@ini_values = $ini_value;
}
# Go over all values
for my $ini_value (@ini_values) {
# If a value is empty, it's an override that tells us to clean the value
if ($ini_value eq "") {
delete $unit_contents->{$section_name}->{$ini_key};
next;
}
push(@{$unit_contents->{$section_name}->{$ini_key}}, $ini_value);
}
}
}
return;
}
# This subroutine takes the path to a systemd configuration file (like a unit configuration),
# parses it, and returns a hash that contains the contents. The contents of this hash are
# explained in the `parse_systemd_ini` subroutine. Neither the sections nor the keys inside
# the sections are consistently sorted.
#
# If a directory with the same basename ending in .d exists next to the unit file, it will be
# assumed to contain override files which will be parsed as well and handled properly.
sub parse_unit {
my ($unit_path) = @_;
# Parse the main unit and all overrides
my %unit_data;
# Replace \ with \\ so glob() still works with units that have a \ in them
# Valid characters in unit names are ASCII letters, digits, ":", "-", "_", ".", and "\"
$unit_path =~ s/\\/\\\\/gmsx;
foreach (glob("${unit_path}{,.d/*.conf}")) {
parse_systemd_ini(\%unit_data, "$_")
}
return %unit_data;
}
# Checks whether a specified boolean in a systemd unit is true
# or false, with a default that is applied when the value is not set.
sub parse_systemd_bool {
my ($unit_config, $section_name, $bool_name, $default) = @_;
my @values = @{$unit_config->{$section_name}{$bool_name} // []};
# Return default if value is not set
if ((scalar(@values) < 1) || (not defined($values[-1]))) {
return $default;
}
# If value is defined multiple times, use the last definition
my $last_value = $values[-1];
# These are valid values as of systemd.syntax(7)
return $last_value eq "1" || $last_value eq "yes" || $last_value eq "true" || $last_value eq "on";
}
# Writes a unit name into a given file to be more resilient against
# crashes of the script. Does nothing when the action is dry-activate.
sub record_unit {
my ($fn, $unit) = @_;
if ($action ne "dry-activate") {
write_file($fn, { append => 1 }, "$unit\n");
}
return;
}
# The opposite of record_unit, removes a unit name from a file
sub unrecord_unit {
my ($fn, $unit) = @_;
if ($action ne "dry-activate") {
edit_file(sub { s/^$unit\n//msx }, $fn);
}
return;
}
# Compare the contents of two unit files and return whether the unit
# needs to be restarted or reloaded. If the units differ, the service
# is restarted unless the only difference is `X-Reload-Triggers` in the
# `Unit` section. If this is the only modification, the unit is reloaded
# instead of restarted.
# Returns:
# - 0 if the units are equal
# - 1 if the units are different and a restart action is required
# - 2 if the units are different and a reload action is required
sub compare_units { ## no critic(Subroutines::ProhibitExcessComplexity)
my ($cur_unit, $new_unit) = @_;
my $ret = 0;
# Keys to ignore in the [Unit] section
my %unit_section_ignores = map { $_ => 1 } qw(
X-Reload-Triggers
Description Documentation
OnFailure OnSuccess OnFailureJobMode
IgnoreOnIsolate StopWhenUnneeded
RefuseManualStart RefuseManualStop
AllowIsolate CollectMode
SourcePath
);
my $comp_array = sub {
my ($a, $b) = @_;
return join("\0", @{$a}) eq join("\0", @{$b});
};
# Comparison hash for the sections
my %section_cmp = map { $_ => 1 } keys(%{$new_unit});
# Iterate over the sections
foreach my $section_name (keys(%{$cur_unit})) {
# Missing section in the new unit?
if (not exists($section_cmp{$section_name})) {
# If the [Unit] section was removed, make sure that only keys
# were in it that are ignored
if ($section_name eq "Unit") {
foreach my $ini_key (keys(%{$cur_unit->{"Unit"}})) {
if (not defined($unit_section_ignores{$ini_key})) {
return 1;
}
}
next; # check the next section
} else {
return 1;
}
if ($section_name eq "Unit" and %{$cur_unit->{"Unit"}} == 1 and defined(%{$cur_unit->{"Unit"}}{"X-Reload-Triggers"})) {
# If a new [Unit] section was removed that only contained X-Reload-Triggers,
# do nothing.
next;
} else {
return 1;
}
}
delete $section_cmp{$section_name};
# Comparison hash for the section contents
my %ini_cmp = map { $_ => 1 } keys(%{$new_unit->{$section_name}});
# Iterate over the keys of the section
foreach my $ini_key (keys(%{$cur_unit->{$section_name}})) {
delete $ini_cmp{$ini_key};
my @cur_value = @{$cur_unit->{$section_name}{$ini_key}};
# If the key is missing in the new unit, they are different...
if (not $new_unit->{$section_name}{$ini_key}) {
# ... unless the key that is now missing is one of the ignored keys
if ($section_name eq "Unit" and defined($unit_section_ignores{$ini_key})) {
next;
}
return 1;
}
my @new_value = @{$new_unit->{$section_name}{$ini_key}};
# If the contents are different, the units are different
if (not $comp_array->(\@cur_value, \@new_value)) {
# Check if only the reload triggers changed or one of the ignored keys
if ($section_name eq "Unit") {
if ($ini_key eq "X-Reload-Triggers") {
$ret = 2;
next;
} elsif (defined($unit_section_ignores{$ini_key})) {
next;
}
}
return 1;
}
}
# A key was introduced that was missing in the previous unit
if (%ini_cmp) {
if ($section_name eq "Unit") {
foreach my $ini_key (keys(%ini_cmp)) {
if ($ini_key eq "X-Reload-Triggers") {
$ret = 2;
} elsif (defined($unit_section_ignores{$ini_key})) {
next;
} else {
return 1;
}
}
} else {
return 1;
}
};
}
# A section was introduced that was missing in the previous unit
if (%section_cmp) {
if (%section_cmp == 1 and defined($section_cmp{"Unit"})) {
foreach my $ini_key (keys(%{$new_unit->{"Unit"}})) {
if (not defined($unit_section_ignores{$ini_key})) {
return 1;
} elsif ($ini_key eq "X-Reload-Triggers") {
$ret = 2;
}
}
} else {
return 1;
}
}
return $ret;
}
# Called when a unit exists in both the old systemd and the new system and the units
# differ. This figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutines::ProhibitExcessComplexity)
my ($unit, $base_name, $new_unit_file, $new_unit_info, $active_cur, $units_to_stop, $units_to_start, $units_to_reload, $units_to_restart, $units_to_skip) = @_;
if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/msx || $unit =~ /\.slice$/msx) {
# Do nothing. These cannot be restarted directly.
# Slices and Paths don't have to be restarted since
# properties (resource limits and inotify watches)
# seem to get applied on daemon-reload.
} elsif ($unit =~ /\.mount$/msx) {
# Reload the changed mount unit to force a remount.
# FIXME: only reload when Options= changed, restart otherwise
$units_to_reload->{$unit} = 1;
record_unit($reload_list_file, $unit);
} elsif ($unit =~ /\.socket$/msx) {
# FIXME: do something?
# Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
# Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
# More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
} else {
my %new_unit_info = $new_unit_info ? %{$new_unit_info} : parse_unit($new_unit_file);
if (parse_systemd_bool(\%new_unit_info, "Service", "X-ReloadIfChanged", 0) and not $units_to_restart->{$unit} and not $units_to_stop->{$unit}) {
$units_to_reload->{$unit} = 1;
record_unit($reload_list_file, $unit);
}
elsif (!parse_systemd_bool(\%new_unit_info, "Service", "X-RestartIfChanged", 1) || parse_systemd_bool(\%new_unit_info, "Unit", "RefuseManualStop", 0) || parse_systemd_bool(\%new_unit_info, "Unit", "X-OnlyManualStart", 0)) {
$units_to_skip->{$unit} = 1;
} else {
# It doesn't make sense to stop and start non-services because
# they can't have ExecStop=
if (!parse_systemd_bool(\%new_unit_info, "Service", "X-StopIfChanged", 1) || $unit !~ /\.service$/msx) {
# This unit should be restarted instead of
# stopped and started.
$units_to_restart->{$unit} = 1;
record_unit($restart_list_file, $unit);
# Remove from units to reload so we don't restart and reload
if ($units_to_reload->{$unit}) {
delete $units_to_reload->{$unit};
unrecord_unit($reload_list_file, $unit);
}
} else {
# If this unit is socket-activated, then stop the
# socket unit(s) as well, and restart the
# socket(s) instead of the service.
my $socket_activated = 0;
if ($unit =~ /\.service$/msx) {
my @sockets = split(/\s+/msx, join(" ", @{$new_unit_info{Service}{Sockets} // []}));
if (scalar(@sockets) == 0) {
@sockets = ("$base_name.socket");
}
foreach my $socket (@sockets) {
if (defined($active_cur->{$socket})) {
# We can now be sure this is a socket-activate unit
$units_to_stop->{$socket} = 1;
# Only restart sockets that actually
# exist in new configuration:
if (-e "$out/etc/systemd/system/$socket") {
$units_to_start->{$socket} = 1;
if ($units_to_start eq $units_to_restart) {
record_unit($restart_list_file, $socket);
} else {
record_unit($start_list_file, $socket);
}
$socket_activated = 1;
}
# Remove from units to reload so we don't restart and reload
if ($units_to_reload->{$unit}) {
delete $units_to_reload->{$unit};
unrecord_unit($reload_list_file, $unit);
}
}
}
}
# If the unit is not socket-activated, record
# that this unit needs to be started below.
# We write this to a file to ensure that the
# service gets restarted if we're interrupted.
if (!$socket_activated) {
$units_to_start->{$unit} = 1;
if ($units_to_start eq $units_to_restart) {
record_unit($restart_list_file, $unit);
} else {
record_unit($start_list_file, $unit);
}
}
$units_to_stop->{$unit} = 1;
# Remove from units to reload so we don't restart and reload
if ($units_to_reload->{$unit}) {
delete $units_to_reload->{$unit};
unrecord_unit($reload_list_file, $unit);
}
}
}
}
return;
}
# Figure out what units need to be stopped, started, restarted or reloaded.
my (%units_to_stop, %units_to_skip, %units_to_start, %units_to_restart, %units_to_reload);
my %units_to_filter; # units not shown
%units_to_start = map { $_ => 1 }
split(/\n/msx, read_file($start_list_file, err_mode => "quiet") // "");
%units_to_restart = map { $_ => 1 }
split(/\n/msx, read_file($restart_list_file, err_mode => "quiet") // "");
%units_to_reload = map { $_ => 1 }
split(/\n/msx, read_file($reload_list_file, err_mode => "quiet") // "");
my $active_cur = get_active_units();
while (my ($unit, $state) = each(%{$active_cur})) {
my $base_unit = $unit;
my $cur_unit_file = "/etc/systemd/system/$base_unit";
my $new_unit_file = "$out/etc/systemd/system/$base_unit";
# Detect template instances.
if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$base_unit = "$1\@.$2";
$cur_unit_file = "/etc/systemd/system/$base_unit";
$new_unit_file = "$out/etc/systemd/system/$base_unit";
}
my $base_name = $base_unit;
$base_name =~ s/\.[[:lower:]]*$//msx;
if (-e $cur_unit_file && ($state->{state} eq "active" || $state->{state} eq "activating")) {
if (! -e $new_unit_file || abs_path($new_unit_file) eq "/dev/null") {
my %cur_unit_info = parse_unit($cur_unit_file);
if (parse_systemd_bool(\%cur_unit_info, "Unit", "X-StopOnRemoval", 1)) {
$units_to_stop{$unit} = 1;
}
}
elsif ($unit =~ /\.target$/msx) {
my %new_unit_info = parse_unit($new_unit_file);
# Cause all active target units to be restarted below.
# This should start most changed units we stop here as
# well as any new dependencies (including new mounts and
# swap devices). FIXME: the suspend target is sometimes
# active after the system has resumed, which probably
# should not be the case. Just ignore it.
if ($unit ne "suspend.target" && $unit ne "hibernate.target" && $unit ne "hybrid-sleep.target") {
if (!(parse_systemd_bool(\%new_unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%new_unit_info, "Unit", "X-OnlyManualStart", 0))) {
$units_to_start{$unit} = 1;
record_unit($start_list_file, $unit);
# Don't spam the user with target units that always get started.
$units_to_filter{$unit} = 1;
}
}
# Stop targets that have X-StopOnReconfiguration set.
# This is necessary to respect dependency orderings
# involving targets: if unit X starts after target Y and
# target Y starts after unit Z, then if X and Z have both
# changed, then X should be restarted after Z. However,
# if target Y is in the "active" state, X and Z will be
# restarted at the same time because X's dependency on Y
# is already satisfied. Thus, we need to stop Y first.
# Stopping a target generally has no effect on other units
# (unless there is a PartOf dependency), so this is just a
# bookkeeping thing to get systemd to do the right thing.
if (parse_systemd_bool(\%new_unit_info, "Unit", "X-StopOnReconfiguration", 0)) {
$units_to_stop{$unit} = 1;
}
}
else {
my %cur_unit_info = parse_unit($cur_unit_file);
my %new_unit_info = parse_unit($new_unit_file);
my $diff = compare_units(\%cur_unit_info, \%new_unit_info);
if ($diff == 1) {
handle_modified_unit($unit, $base_name, $new_unit_file, \%new_unit_info, $active_cur, \%units_to_stop, \%units_to_start, \%units_to_reload, \%units_to_restart, \%units_to_skip);
} elsif ($diff == 2 and not $units_to_restart{$unit}) {
$units_to_reload{$unit} = 1;
record_unit($reload_list_file, $unit);
}
}
}
}
# Converts a path to the name of a systemd mount unit that would be responsible
# for mounting this path.
sub path_to_unit_name {
my ($path) = @_;
# Use current version of systemctl binary before daemon is reexeced.
open(my $cmd, "-|", "$cur_systemd/systemd-escape", "--suffix=mount", "-p", $path)
or die "Unable to escape $path!\n";
my $escaped = do { local $/ = undef; <$cmd> };
chomp($escaped);
close($cmd) or die("Unable to close systemd-escape pipe");
return $escaped;
}
# Compare the previous and new fstab to figure out which filesystems
# need a remount or need to be unmounted. New filesystems are mounted
# automatically by starting local-fs.target. FIXME: might be nicer if
# we generated units for all mounts; then we could unify this with the
# unit checking code above.
my ($cur_fss, $cur_swaps) = parse_fstab("/etc/fstab");
my ($new_fss, $new_swaps) = parse_fstab("$out/etc/fstab");
foreach my $mount_point (keys(%{$cur_fss})) {
my $cur = $cur_fss->{$mount_point};
my $new = $new_fss->{$mount_point};
my $unit = path_to_unit_name($mount_point);
if (!defined($new)) {
# Filesystem entry disappeared, so unmount it.
$units_to_stop{$unit} = 1;
} elsif ($cur->{fsType} ne $new->{fsType} || $cur->{device} ne $new->{device}) {
# Filesystem type or device changed, so unmount and mount it.
$units_to_stop{$unit} = 1;
$units_to_start{$unit} = 1;
record_unit($start_list_file, $unit);
} elsif ($cur->{options} ne $new->{options}) {
# Mount options changes, so remount it.
$units_to_reload{$unit} = 1;
record_unit($reload_list_file, $unit);
}
}
# Also handles swap devices.
foreach my $device (keys(%{$cur_swaps})) {
my $cur = $cur_swaps->{$device};
my $new = $new_swaps->{$device};
if (!defined($new)) {
# Swap entry disappeared, so turn it off. Can't use
# "systemctl stop" here because systemd has lots of alias
# units that prevent a stop from actually calling
# "swapoff".
if ($action ne "dry-activate") {
print STDERR "would stop swap device: $device\n";
} else {
print STDERR "stopping swap device: $device\n";
system("@utillinux@/sbin/swapoff", $device);
}
}
# FIXME: update swap options (i.e. its priority).
}
# Should we have systemd re-exec itself?
my $cur_pid1_path = abs_path("/proc/1/exe") // "/unknown";
my $cur_systemd_system_config = abs_path("/etc/systemd/system.conf") // "/unknown";
my $new_pid1_path = abs_path("$new_systemd/lib/systemd/systemd") or die;
my $new_systemd_system_config = abs_path("$out/etc/systemd/system.conf") // "/unknown";
my $restart_systemd = $cur_pid1_path ne $new_pid1_path;
if ($cur_systemd_system_config ne $new_systemd_system_config) {
$restart_systemd = 1;
}
# Takes an array of unit names and returns an array with the same elements,
# except all units that are also in the global variable `unitsToFilter`.
sub filter_units {
my ($units) = @_;
my @res;
foreach my $unit (sort(keys(%{$units}))) {
if (!defined($units_to_filter{$unit})) {
push(@res, $unit);
}
}
return @res;
}
my @units_to_stop_filtered = filter_units(\%units_to_stop);
# Show dry-run actions.
if ($action eq "dry-activate") {
if (scalar(@units_to_stop_filtered) > 0) {
print STDERR "would stop the following units: ", join(", ", @units_to_stop_filtered), "\n";
}
if (scalar(keys(%units_to_skip)) > 0) {
print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys(%units_to_skip))), "\n";
}
print STDERR "would activate the configuration...\n";
system("$out/dry-activate", "$out");
# Handle the activation script requesting the restart or reload of a unit.
foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) {
my $unit = $_;
my $base_unit = $unit;
my $new_unit_file = "$out/etc/systemd/system/$base_unit";
# Detect template instances.
if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$base_unit = "$1\@.$2";
$new_unit_file = "$out/etc/systemd/system/$base_unit";
}
my $base_name = $base_unit;
$base_name =~ s/\.[[:lower:]]*$//msx;
# Start units if they were not active previously
if (not defined($active_cur->{$unit})) {
$units_to_start{$unit} = 1;
next;
}
handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
}
unlink($dry_restart_by_activation_file);
foreach (split(/\n/msx, read_file($dry_reload_by_activation_file, err_mode => "quiet") // "")) {
my $unit = $_;
if (defined($active_cur->{$unit}) and not $units_to_restart{$unit} and not $units_to_stop{$unit}) {
$units_to_reload{$unit} = 1;
record_unit($reload_list_file, $unit);
}
}
unlink($dry_reload_by_activation_file);
if ($restart_systemd) {
print STDERR "would restart systemd\n";
}
if (scalar(keys(%units_to_reload)) > 0) {
print STDERR "would reload the following units: ", join(", ", sort(keys(%units_to_reload))), "\n";
}
if (scalar(keys(%units_to_restart)) > 0) {
print STDERR "would restart the following units: ", join(", ", sort(keys(%units_to_restart))), "\n";
}
my @units_to_start_filtered = filter_units(\%units_to_start);
if (scalar(@units_to_start_filtered)) {
print STDERR "would start the following units: ", join(", ", @units_to_start_filtered), "\n";
}
exit 0;
}
syslog(LOG_NOTICE, "switching to system configuration $out");
if (scalar(keys(%units_to_stop)) > 0) {
if (scalar(@units_to_stop_filtered)) {
print STDERR "stopping the following units: ", join(", ", @units_to_stop_filtered), "\n";
}
# Use current version of systemctl binary before daemon is reexeced.
system("$cur_systemd/systemctl", "stop", "--", sort(keys(%units_to_stop)));
}
if (scalar(keys(%units_to_skip)) > 0) {
print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys(%units_to_skip))), "\n";
}
# Activate the new configuration (i.e., update /etc, make accounts,
# and so on).
my $res = 0;
print STDERR "activating the configuration...\n";
system("$out/activate", "$out") == 0 or $res = 2;
# Handle the activation script requesting the restart or reload of a unit.
foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) {
my $unit = $_;
my $base_unit = $unit;
my $new_unit_file = "$out/etc/systemd/system/$base_unit";
# Detect template instances.
if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$base_unit = "$1\@.$2";
$new_unit_file = "$out/etc/systemd/system/$base_unit";
}
my $base_name = $base_unit;
$base_name =~ s/\.[[:lower:]]*$//msx;
# Start units if they were not active previously
if (not defined($active_cur->{$unit})) {
$units_to_start{$unit} = 1;
record_unit($start_list_file, $unit);
next;
}
handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
}
# We can remove the file now because it has been propagated to the other restart/reload files
unlink($restart_by_activation_file);
foreach (split(/\n/msx, read_file($reload_by_activation_file, err_mode => "quiet") // "")) {
my $unit = $_;
if (defined($active_cur->{$unit}) and not $units_to_restart{$unit} and not $units_to_stop{$unit}) {
$units_to_reload{$unit} = 1;
record_unit($reload_list_file, $unit);
}
}
# We can remove the file now because it has been propagated to the other reload file
unlink($reload_by_activation_file);
# Restart systemd if necessary. Note that this is done using the
# current version of systemd, just in case the new one has trouble
# communicating with the running pid 1.
if ($restart_systemd) {
print STDERR "restarting systemd...\n";
system("$cur_systemd/systemctl", "daemon-reexec") == 0 or $res = 2;
}
# Forget about previously failed services.
system("$new_systemd/bin/systemctl", "reset-failed");
# Make systemd reload its units.
system("$new_systemd/bin/systemctl", "daemon-reload") == 0 or $res = 3;
# Reload user units
open(my $list_active_users, "-|", "$new_systemd/bin/loginctl", "list-users", "--no-legend") || die("Unable to call loginctl");
while (my $f = <$list_active_users>) {
if ($f !~ /^\s*(?<uid>\d+)\s+(?<user>\S+)/msx) {
next;
}
my ($uid, $name) = ($+{uid}, $+{user});
print STDERR "reloading user units for $name...\n";
system("@su@", "-s", "@shell@", "-l", $name, "-c",
"export XDG_RUNTIME_DIR=/run/user/$uid; " .
"$cur_systemd/systemctl --user daemon-reexec; " .
"$new_systemd/bin/systemctl --user start nixos-activation.service");
}
close($list_active_users) || die("Unable to close the file handle to loginctl");
# Set the new tmpfiles
print STDERR "setting up tmpfiles\n";
system("$new_systemd/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3;
# Before reloading we need to ensure that the units are still active. They may have been
# deactivated because one of their requirements got stopped. If they are inactive
# but should have been reloaded, the user probably expects them to be started.
if (scalar(keys(%units_to_reload)) > 0) {
for my $unit (keys(%units_to_reload)) {
if (!unit_is_active($unit)) {
# Figure out if we need to start the unit
my %unit_info = parse_unit("$out/etc/systemd/system/$unit");
if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) {
$units_to_start{$unit} = 1;
record_unit($start_list_file, $unit);
}
# Don't reload the unit, reloading would fail
delete %units_to_reload{$unit};
unrecord_unit($reload_list_file, $unit);
}
}
}
# Reload units that need it. This includes remounting changed mount
# units.
if (scalar(keys(%units_to_reload)) > 0) {
print STDERR "reloading the following units: ", join(", ", sort(keys(%units_to_reload))), "\n";
system("$new_systemd/bin/systemctl", "reload", "--", sort(keys(%units_to_reload))) == 0 or $res = 4;
unlink($reload_list_file);
}
# Restart changed services (those that have to be restarted rather
# than stopped and started).
if (scalar(keys(%units_to_restart)) > 0) {
print STDERR "restarting the following units: ", join(", ", sort(keys(%units_to_restart))), "\n";
system("$new_systemd/bin/systemctl", "restart", "--", sort(keys(%units_to_restart))) == 0 or $res = 4;
unlink($restart_list_file);
}
# Start all active targets, as well as changed units we stopped above.
# The latter is necessary because some may not be dependencies of the
# targets (i.e., they were manually started). FIXME: detect units
# that are symlinks to other units. We shouldn't start both at the
# same time because we'll get a "Failed to add path to set" error from
# systemd.
my @units_to_start_filtered = filter_units(\%units_to_start);
if (scalar(@units_to_start_filtered)) {
print STDERR "starting the following units: ", join(", ", @units_to_start_filtered), "\n"
}
system("$new_systemd/bin/systemctl", "start", "--", sort(keys(%units_to_start))) == 0 or $res = 4;
unlink($start_list_file);
# Print failed and new units.
my (@failed, @new);
my $active_new = get_active_units();
while (my ($unit, $state) = each(%{$active_new})) {
if ($state->{state} eq "failed") {
push(@failed, $unit);
next;
}
if ($state->{substate} eq "auto-restart") {
# A unit in auto-restart substate is a failure *if* it previously failed to start
open(my $main_status_fd, "-|", "$new_systemd/bin/systemctl", "show", "--value", "--property=ExecMainStatus", $unit) || die("Unable to call 'systemctl show'");
my $main_status = do { local $/ = undef; <$main_status_fd> };
close($main_status_fd) || die("Unable to close 'systemctl show' fd");
chomp($main_status);
if ($main_status ne "0") {
push(@failed, $unit);
next;
}
}
# Ignore scopes since they are not managed by this script but rather
# created and managed by third-party services via the systemd dbus API.
# This only lists units that are not failed (including ones that are in auto-restart but have not failed previously)
if ($state->{state} ne "failed" && !defined($active_cur->{$unit}) && $unit !~ /\.scope$/msx) {
push(@new, $unit);
}
}
if (scalar(@new) > 0) {
print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n"
}
if (scalar(@failed) > 0) {
my @failed_sorted = sort(@failed);
print STDERR "warning: the following units failed: ", join(", ", @failed_sorted), "\n\n";
system("$new_systemd/bin/systemctl status --no-pager --full '" . join("' '", @failed_sorted) . "' >&2");
$res = 4;
}
if ($res == 0) {
syslog(LOG_NOTICE, "finished switching to system configuration $out");
} else {
syslog(LOG_ERR, "switching to system configuration $out failed (status $res)");
}
exit($res);

View file

@ -0,0 +1,362 @@
{ config, lib, pkgs, extendModules, noUserModules, ... }:
with lib;
let
# 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;
systemBuilder =
let
kernelPath = "${config.boot.kernelPackages.kernel}/" +
"${config.system.boot.loader.kernelFile}";
initrdPath = "${config.system.build.initialRamdisk}/" +
"${config.system.boot.loader.initrdFile}";
in ''
mkdir $out
# Containers don't have their own kernel or initrd. They boot
# directly into stage 2.
${optionalString (!config.boot.isContainer) ''
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
ln -s ${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets $out
ln -s ${config.hardware.firmware}/lib/firmware $out/firmware
''}
echo "$activationScript" > $out/activate
echo "$dryActivationScript" > $out/dry-activate
substituteInPlace $out/activate --subst-var out
substituteInPlace $out/dry-activate --subst-var out
chmod u+x $out/activate $out/dry-activate
unset activationScript dryActivationScript
${if config.boot.initrd.systemd.enable then ''
cp ${config.system.build.bootStage2} $out/prepare-root
substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
# This must not be a symlink or the abs_path of the grub builder for the tests
# will resolve the symlink and we end up with a path that doesn't point to a
# system closure.
cp "$systemd/lib/systemd/systemd" $out/init
'' 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
ln -s ${config.system.path} $out/sw
ln -s "$systemd" $out/systemd
echo -n "$configurationName" > $out/configuration-name
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
mkdir $out/specialisation
${concatStringsSep "\n"
(mapAttrsToList (name: path: "ln -s ${path} $out/specialisation/${name}") children)}
mkdir $out/bin
export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
chmod +x $out/bin/switch-to-configuration
${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
if ! output=$($perl/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
echo "switch-to-configuration syntax is not valid:"
echo "$output"
exit 1
fi
''}
echo -n "${toString config.system.extraDependencies}" > $out/extra-dependencies
${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.
baseSystem = pkgs.stdenvNoCC.mkDerivation {
name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
preferLocalBuild = true;
allowSubstitutes = false;
buildCommand = systemBuilder;
inherit (pkgs) coreutils;
systemd = config.systemd.package;
shell = "${pkgs.bash}/bin/sh";
su = "${pkgs.shadow.su}/bin/su";
utillinux = pkgs.util-linux;
kernelParams = config.boot.kernelParams;
installBootLoader = config.system.build.installBootLoader;
activationScript = config.system.activationScripts.script;
dryActivationScript = config.system.dryActivationScript;
nixosLabel = config.system.nixos.label;
configurationName = config.boot.loader.grub.configurationName;
# Needed by switch-to-configuration.
perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp NetDBus ]);
};
# Handle assertions and warnings
failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);
baseSystemAssertWarn = if failedAssertions != []
then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
else showWarnings config.warnings baseSystem;
# Replace runtime dependencies
system = foldr ({ oldDependency, newDependency }: drv:
pkgs.replaceDependency { inherit oldDependency newDependency drv; }
) baseSystemAssertWarn config.system.replaceRuntimeDependencies;
/* Workaround until https://github.com/NixOS/nixpkgs/pull/156533
Call can be replaced by argument when that's merged.
*/
tmpFixupSubmoduleBoundary = subopts:
lib.mkOption {
type = lib.types.submoduleWith {
modules = [ { options = subopts; } ];
};
};
in
{
imports = [
../build.nix
(mkRemovedOptionModule [ "nesting" "clone" ] "Use `specialisation.«name» = { inheritParentConfig = true; configuration = { ... }; }` instead.")
(mkRemovedOptionModule [ "nesting" "children" ] "Use `specialisation.«name».configuration = { ... }` instead.")
];
options = {
specialisation = mkOption {
default = {};
example = lib.literalExpression "{ fewJobsManyCores.configuration = { nix.settings = { core = 0; max-jobs = 1; }; }; }";
description = ''
Additional configurations to build. If
<literal>inheritParentConfig</literal> is true, the system
will be based on the overall system configuration.
To switch to a specialised configuration
(e.g. <literal>fewJobsManyCores</literal>) at runtime, run:
<screen>
<prompt># </prompt>sudo /run/current-system/specialisation/fewJobsManyCores/bin/switch-to-configuration test
</screen>
'';
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;
};
})
);
};
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 = tmpFixupSubmoduleBoundary {
installBootLoader = mkOption {
internal = true;
# "; true" => make the `$out` argument from switch-to-configuration.pl
# go to `true` instead of `echo`, hiding the useless path
# from the log.
default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
description = ''
A program that writes a bootloader installation script to the path passed in the first command line argument.
See <literal>nixos/modules/system/activation/switch-to-configuration.pl</literal>.
'';
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);
};
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 <filename>/etc/nixos/configuration.nix</filename>)
and links it from the resulting system
(getting to <filename>/run/current-system/configuration.nix</filename>).
Note that only this single file is copied, even if it imports others.
'';
};
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.package;
default = [];
description = ''
A list of packages that should be included in the system
closure but not otherwise made available to users. This is
primarily used by the installation tests.
'';
};
system.replaceRuntimeDependencies = mkOption {
default = [];
example = lib.literalExpression "[ ({ original = pkgs.openssl; replacement = pkgs.callPackage /path/to/openssl { }; }) ]";
type = types.listOf (types.submodule (
{ ... }: {
options.original = mkOption {
type = types.package;
description = "The original package to override.";
};
options.replacement = mkOption {
type = types.package;
description = "The replacement package.";
};
})
);
apply = map ({ original, replacement, ... }: {
oldDependency = original;
newDependency = replacement;
});
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.
'';
};
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</option> derivation.
</para><para>
That derivation has the following name:
<literal>"nixos-system-''${config.system.name}-''${config.system.nixos.label}"</literal>
'';
};
};
config = {
system.extraSystemBuilderCmds =
optionalString
config.system.copySystemConfiguration
''ln -s '${import ../../../lib/from-env.nix "NIXOS_CONFIG" <nixos-config>}' \
"$out/configuration.nix"
'';
system.build.toplevel = system;
};
# uses extendModules to generate a type
meta.buildDocsInSandbox = false;
}

View file

@ -0,0 +1,325 @@
{ config, lib, pkgs, ... }:
let
inherit (lib) mkOption types optionalString stringAfter;
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}";
activationSnippet = name: { interpreter, wrapInterpreterInShell, ... }: if wrapInterpreterInShell then ''
rm -f /run/binfmt/${name}
cat > /run/binfmt/${name} << 'EOF'
#!${pkgs.bash}/bin/sh
exec -- ${interpreter} "$@"
EOF
chmod +x /run/binfmt/${name}
'' else ''
rm -f /run/binfmt/${name}
ln -s ${interpreter} /run/binfmt/${name}
'';
getEmulator = system: (lib.systems.elaborate { inherit system; }).emulator pkgs;
getQemuArch = system: (lib.systems.elaborate { inherit system; }).qemuArch;
# 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'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
};
mipsel-linux = {
magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00'';
mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
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\xff\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\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
};
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'';
};
wasm32-wasi = {
magicOrExtension = ''\x00asm'';
mask = ''\xff\xff\xff\xff'';
};
wasm64-wasi = {
magicOrExtension = ''\x00asm'';
mask = ''\xff\xff\xff\xff'';
};
x86_64-windows = {
magicOrExtension = ".exe";
recognitionType = "extension";
};
i686-windows = {
magicOrExtension = ".exe";
recognitionType = "extension";
};
};
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.str;
};
};
};
config = {
boot.binfmt.registrations = builtins.listToAttrs (map (system: {
name = system;
value = let
interpreter = getEmulator system;
qemuArch = getQemuArch system;
preserveArgvZero = "qemu-${qemuArch}" == baseNameOf interpreter;
interpreterReg = let
wrapperName = "qemu-${qemuArch}-binfmt-P";
wrapper = pkgs.wrapQemuBinfmtP wrapperName interpreter;
in
if preserveArgvZero then "${wrapper}/bin/${wrapperName}"
else interpreter;
in {
inherit preserveArgvZero;
interpreter = interpreterReg;
wrapInterpreterInShell = !preserveArgvZero;
interpreterSandboxPath = dirOf (dirOf interpreterReg);
} // (magics.${system} or (throw "Cannot create binfmt registration for system ${system}"));
}) cfg.emulatedSystems);
nix.settings = lib.mkIf (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));
system.activationScripts.binfmt = stringAfter [ "specialfs" ] ''
mkdir -p -m 0755 /run/binfmt
${lib.concatStringsSep "\n" (lib.mapAttrsToList activationSnippet config.boot.binfmt.registrations)}
'';
systemd.additionalUpstreamSystemUnits = lib.mkIf (config.boot.binfmt.registrations != {}) [
"proc-sys-fs-binfmt_misc.automount"
"proc-sys-fs-binfmt_misc.mount"
"systemd-binfmt.service"
];
};
}

View file

@ -0,0 +1,37 @@
{ config, lib, ... }:
with lib;
{
###### interface
options = {
systemd.enableEmergencyMode = mkOption {
default = true;
type = types.bool;
description = ''
Whether to enable emergency mode, which is an
<command>sulogin</command> 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.
'';
};
};
###### implementation
config = {
systemd.additionalUpstreamSystemUnits = optionals
config.systemd.enableEmergencyMode [
"emergency.target" "emergency.service"
];
};
}

View file

@ -0,0 +1,53 @@
# 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 "grow the root partition on boot";
};
config = mkIf config.boot.growPartition {
boot.initrd.extraUtilsCommands = ''
copy_bin_and_libs ${pkgs.gawk}/bin/gawk
copy_bin_and_libs ${pkgs.gnused}/bin/sed
copy_bin_and_libs ${pkgs.util-linux}/sbin/sfdisk
copy_bin_and_libs ${pkgs.util-linux}/sbin/lsblk
substitute "${pkgs.cloud-utils.guest}/bin/.growpart-wrapped" "$out/bin/growpart" \
--replace "${pkgs.bash}/bin/sh" "/bin/sh" \
--replace "awk" "gawk" \
--replace "sed" "gnused"
ln -s sed $out/bin/gnused
'';
boot.initrd.postDeviceCommands = ''
rootDevice="${config.fileSystems."/".device}"
if waitDevice "$rootDevice"; then
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
TMPDIR=/run sh $(type -P growpart) "$parentDevice" "$partNum"
udevadm settle
fi
'';
};
}

View file

@ -0,0 +1,148 @@
{ 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 = config.networking.useDHCP || dhcpInterfaces != [];
dhcpIfShellExpr = if config.networking.useDHCP
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 <literal>ip</literal> kernel parameter,
as described in <link
xlink:href="https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt">the
kernel documentation</link>. Otherwise, if
<option>networking.useDHCP</option> is enabled, an IP address
is acquired using DHCP.
You should add the module(s) required for your network card to
boot.initrd.availableKernelModules.
<literal>lspci -v | grep -iA8 'network\|ethernet'</literal>
will tell you which.
'';
};
boot.initrd.network.flushBeforeStage2 = mkOption {
type = types.bool;
default = true;
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.
'';
};
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</option> and <option>networking.useDHCP</option>
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 = ''
copy_bin_and_libs ${pkgs.klibc}/lib/klibc/bin.static/ipconfig
'';
boot.initrd.preLVMCommands = 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 "$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 ''
for iface in $ifaces; do
ip address flush "$iface"
ip link set "$iface" down
done
'';
};
}

View file

@ -0,0 +1,81 @@
{ 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</option> 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>
<para>
Unless your bootloader supports initrd secrets, this configuration
is stored insecurely in the global Nix store.
</para>
</warning>
'';
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 = ''
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.secrets = {
"/etc/initrd.ovpn" = cfg.configuration;
};
# openvpn --version would exit with 1 instead of 0
boot.initrd.extraUtilsCommandsTest = ''
$out/bin/openvpn --show-gateway
'';
# Add `iproute /bin/ip` to the config, to ensure that openvpn
# is able to set the routes
boot.initrd.network.postCommands = ''
(cat /etc/initrd.ovpn; echo -e '\niproute /bin/ip') | \
openvpn /dev/stdin &
'';
};
}

View file

@ -0,0 +1,215 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.boot.initrd.network.ssh;
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</option>.
'';
};
port = mkOption {
type = types.int;
default = 22;
description = ''
Port on which SSH initrd service should listen.
'';
};
shell = mkOption {
type = types.str;
default = "/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
<citerefentry><refentrytitle>ssh-keygen</refentrytitle><manvolnum>1</manvolnum></citerefentry>:
<screen>
<prompt># </prompt>ssh-keygen -t rsa -N "" -f /etc/secrets/initrd/ssh_host_rsa_key
<prompt># </prompt>ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key
</screen>
<warning>
<para>
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!
</para>
<para>
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.
</para>
</warning>
'';
};
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.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Verbatim contents of <filename>sshd_config</filename>.";
};
};
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 = ''
Port ${toString cfg.port}
PasswordAuthentication no
ChallengeResponseAuthentication no
${flip concatMapStrings cfg.hostKeys (path: ''
HostKey ${initrdKeyPath path}
'')}
KexAlgorithms ${concatStringsSep "," sshdCfg.kexAlgorithms}
Ciphers ${concatStringsSep "," sshdCfg.ciphers}
MACs ${concatStringsSep "," sshdCfg.macs}
LogLevel ${sshdCfg.logLevel}
${if sshdCfg.useDns then ''
UseDNS yes
'' else ''
UseDNS no
''}
${cfg.extraConfig}
'';
in mkIf (config.boot.initrd.network.enable && cfg.enable) {
assertions = [
{
assertion = cfg.authorizedKeys != [];
message = "You should specify at least one authorized key for initrd SSH";
}
{
assertion = cfg.hostKeys != [];
message = ''
You must now pre-generate the host keys for initrd SSH.
See the boot.initrd.network.ssh.hostKeys documentation
for instructions.
'';
}
];
boot.initrd.extraUtilsCommands = ''
copy_bin_and_libs ${pkgs.openssh}/bin/sshd
cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
'';
boot.initrd.extraUtilsCommandsTest = ''
# 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 = ''
echo '${cfg.shell}' > /etc/shells
echo 'root:x:0:0:root:/root:${cfg.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)}
${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 = ''
# 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);
};
}

View file

@ -0,0 +1,347 @@
{ config, lib, pkgs, ... }:
with lib;
let
inherit (config.boot) kernelPatches;
inherit (config.boot.kernel) features randstructSeed;
inherit (config.boot.kernelPackages) kernel;
kernelModulesConf = pkgs.writeText "nixos.conf"
''
${concatStringsSep "\n" config.boot.kernelModules}
'';
in
{
###### interface
options = {
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 <varname>kernel</varname>.
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
<varname>nvidia_x11</varname>.
'';
};
boot.kernelPatches = mkOption {
type = types.listOf types.attrs;
default = [];
example = literalExpression "[ pkgs.kernelPatches.ubuntu_fan_4_4 ]";
description = "A list of additional patches to apply to the kernel.";
};
boot.kernel.randstructSeed = mkOption {
type = types.str;
default = "";
example = "my secret seed";
description = ''
Provides a custom seed for the <varname>RANDSTRUCT</varname> security
option of the Linux kernel. Note that <varname>RANDSTRUCT</varname> 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 <literal>loglevel</literal>. 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 <literal>[ "vga=0x317" "nomodeset" ]</literal> in the
<option>boot.kernelParams</option> 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 = types.listOf types.str;
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</option> or
<option>boot.initrd.kernelModules</option>.
'';
};
boot.initrd.availableKernelModules = mkOption {
type = types.listOf types.str;
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</option>.
'';
};
boot.initrd.kernelModules = mkOption {
type = types.listOf types.str;
default = [];
description = "List of modules that are always loaded by the initrd.";
};
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</option> and
<option>boot.initrd.kernelModules</option>.
'';
};
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 = pkgs.aggregateModules;
};
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"
# 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"
] ++ optionals pkgs.stdenv.hostPlatform.isx86 [
# Misc. x86 keyboard stuff.
"pcips2" "atkbd" "i8042"
# x86 RTC needed by the stage 2 init script.
"rtc_cmos"
]);
boot.initrd.kernelModules =
optionals config.boot.initrd.includeDefaultModules [
# For LVM.
"dm_mod"
];
})
(mkIf (!config.boot.isContainer) {
system.build = { inherit kernel; };
system.modulesTree = [ kernel ] ++ config.boot.extraModulePackages;
# 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 "MODULES")
(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,117 @@
{ 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
if val == null
then ""
else 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,32 @@
{ pkgs, lib, ... }:
{
config = lib.mkIf (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.kexec-tools) {
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,20 @@
{ lib, ... }:
with lib;
{
options.boot.loader.efi = {
canTouchEfiVariables = mkOption {
default = false;
type = types.bool;
description = "Whether the installation process is allowed to modify EFI boot variables.";
};
efiSysMountPoint = mkOption {
default = "/boot";
type = types.str;
description = "Where the EFI System Partition is mounted.";
};
};
}

View file

@ -0,0 +1,106 @@
#! @bash@/bin/sh -e
shopt -s nullglob
export PATH=/empty
for i in @path@; do PATH=$PATH:$i/bin; done
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,62 @@
{ config, lib, pkgs, ... }:
with lib;
let
generationsDirBuilder = pkgs.substituteAll {
src = ./generations-dir-builder.sh;
isExecutable = true;
inherit (pkgs) bash;
path = [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
<literal>/boot</literal>. When enabled,
<literal>/boot/default/kernel</literal>,
<literal>/boot/default/initrd</literal>, 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 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,82 @@
{ 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 pkgs; };
# The builder exposed in populateCmd, which runs on the build architecture
populateBuilder = import ./extlinux-conf-builder.nix { 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 <literal>/boot/extlinux.conf</literal>. For instance,
U-Boot's generic distro boot support uses this file format.
See <link xlink:href="http://git.denx.de/?p=u-boot.git;a=blob;f=doc/README.distro;hb=refs/heads/master">U-boot's documentation</link>
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.
'';
};
populateCmd = mkOption {
type = types.str;
readOnly = true;
description = ''
Contains the builder command used to populate an image,
honoring all options except the <literal>-c &lt;path-to-default-configuration&gt;</literal>
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";
in
mkIf cfg.enable {
system.build.installBootLoader = "${builder} ${builderArgs} -c";
system.boot.loader.id = "generic-extlinux-compatible";
boot.loader.generic-extlinux-compatible.populateCmd = "${populateBuilder} ${builderArgs}";
};
}

View file

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

View file

@ -0,0 +1,157 @@
#! @bash@/bin/sh -e
shopt -s nullglob
export PATH=/empty
for i in @path@; do PATH=$PATH:$i/bin; done
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
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 and negative as instant boot
if [ "$OPTARG" -lt 0 ]; then
timeout=0
elif [ "$OPTARG" = 0 ]; then
timeout=-10
else
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
MENU TITLE ------------------------------------------------------------
TIMEOUT $timeout
EOF
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
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,874 @@
{ config, lib, pkgs, ... }:
with lib;
let
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.version == 1 then grubPkgs.grub
else if cfg.zfsSupport then grubPkgs.grub2.override { zfsSupport = true; }
else if cfg.trustedBoot.enable
then if cfg.trustedBoot.isHPLaptop
then grubPkgs.trustedGrub-for-HP
else grubPkgs.trustedGrub
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 =
# EFI version of Grub v2
if cfg.efiSupport && (cfg.version == 2)
then realGrub.override { efiSupport = cfg.efiSupport; }
else null;
f = x: if x == null then "" else "" + x;
grubConfig = args:
let
efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint;
efiSysMountPoint' = replaceChars [ "/" ] [ "-" ] 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;
grub = f grub;
grubTarget = f (grub.grubTarget or "");
shell = "${pkgs.runtimeShell}";
fullName = lib.getName realGrub;
fullVersion = lib.getVersion realGrub;
grubEfi = f grubEfi;
grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else "";
bootPath = args.path;
storePath = config.boot.loader.grub.storePath;
bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
users = if cfg.users == {} || cfg.version != 1 then cfg.users else throw "GRUB version 1 does not support user accounts.";
theme = f cfg.theme;
inherit efiSysMountPoint;
inherit (args) devices;
inherit (efi) canTouchEfiVariables;
inherit (cfg)
version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber
extraGrubInstallArgs
extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels
default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios;
path = with pkgs; makeBinPath (
[ coreutils gnused gnugrep findutils diffutils btrfs-progs util-linux mdadm ]
++ optional (cfg.efiSupport && (cfg.version == 2)) efibootmgr
++ optionals cfg.useOSProber [ busybox os-prober ]);
font = if cfg.font == null then ""
else (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 {
default = 2;
example = 1;
type = types.int;
description = ''
The version of GRUB to use: <literal>1</literal> for GRUB
Legacy (versions 0.9x), or <literal>2</literal> (the
default) for GRUB 2.
'';
};
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 <literal>nodev</literal> means that a GRUB
boot menu will be generated, but GRUB itself will not
actually be installed. To install GRUB on multiple devices,
use <literal>boot.loader.grub.devices</literal>.
'';
};
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 <literal>device</literal> 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 = with types; attrsOf (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 <literal>boot.loader.efi.canTouchEfiVariables</literal> 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 <literal>grub-install</literal>.
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 <literal>grub rescue</literal> 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
<literal>nativedisk</literal> 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
<link xlink:href="http://git.savannah.gnu.org/cgit/grub.git/tree/grub-core/commands/nativedisk.c?h=grub-2.04#n326">
GRUB source code
</link>
for which disk modules are available.
The list elements are passed directly as <literal>argv</literal>
arguments to the <literal>grub-install</literal> 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 1 example (not GRUB 2 compatible)
title Windows
chainloader (hd0,1)+1
# 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 <filename>/boot</filename>.
Each attribute name denotes the destination file name in
<filename>/boot</filename>, 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 <literal>null</literal> to run GRUB in text mode.
<note><para>
For grub 1:
It must be a 640x480,
14-colour image in XPM format, optionally compressed with
<command>gzip</command> or <command>bzip2</command>.
</para></note>
<note><para>
For grub 2:
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.
</para></note>
'';
};
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.
<note><para>
This options has no effect for GRUB 1.
</para></note>
'';
};
entryOptions = mkOption {
default = "--class nixos --unrestricted";
type = types.nullOr types.str;
description = ''
Options applied to the primary NixOS menu entry.
<note><para>
This options has no effect for GRUB 1.
</para></note>
'';
};
subEntryOptions = mkOption {
default = "--class nixos";
type = types.nullOr types.str;
description = ''
Options applied to the secondary NixOS submenu entry.
<note><para>
This options has no effect for GRUB 1.
</para></note>
'';
};
theme = mkOption {
type = types.nullOr types.path;
example = literalExpression "pkgs.nixos-grub2-theme";
default = null;
description = ''
Grub theme to be used.
<note><para>
This options has no effect for GRUB 1.
</para></note>
'';
};
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.
<note><para>
This options has no effect for GRUB 1.
</para></note>
'';
};
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 <literal>font</literal>
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</command> or
<command>mount</command>. 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.
ZFS support is only available for GRUB v2.
This option is ignored for GRUB v1.
'';
};
efiSupport = mkOption {
default = false;
type = types.bool;
description = ''
Whether GRUB should be built with EFI support.
EFI support is only available for GRUB v2.
This option is ignored for GRUB v1.
'';
};
efiInstallAsRemovable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to invoke <literal>grub-install</literal> with
<literal>--removable</literal>.</para>
<para>Unless you turn this on, GRUB will install itself somewhere in
<literal>boot.loader.efi.efiSysMountPoint</literal> (exactly where
depends on other config variables). If you've set
<literal>boot.loader.efi.canTouchEfiVariables</literal> *AND* you
are currently booted in UEFI mode, then GRUB will use
<literal>efibootmgr</literal> 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 ("<literal>efibootmgr: EFI variables
are not supported on this system.</literal>").</para>
<para>If you turn this feature on, GRUB will install itself in a
special location within <literal>efiSysMountPoint</literal> (namely
<literal>EFI/boot/boot$arch.efi</literal>) which the firmwares
are hardcoded to try first, regardless of NVRAM EFI variables.</para>
<para>To summarize, turn this on if:
<itemizedlist>
<listitem><para>You are installing NixOS and want it to boot in UEFI mode,
but you are currently booted in legacy mode</para></listitem>
<listitem><para>You want to make a drive that will boot regardless of
the NVRAM state of the computer (like a USB "removable" drive)</para></listitem>
<listitem><para>You simply dislike the idea of depending on NVRAM
state to make your drive bootable</para></listitem>
</itemizedlist>
'';
};
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.
'';
};
trustedBoot = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Enable trusted boot. GRUB will measure all critical components during
the boot process to offer TCG (TPM) support.
'';
};
systemHasTPM = mkOption {
default = "";
example = "YES_TPM_is_activated";
type = types.str;
description = ''
Assertion that the target system has an activated TPM. It is a safety
check before allowing the activation of 'trustedBoot.enable'. TrustedBoot
WILL FAIL TO BOOT YOUR SYSTEM if no TPM is available.
'';
};
isHPLaptop = mkOption {
default = false;
type = types.bool;
description = ''
Use a special version of TrustedGRUB that is needed by some HP laptops
and works only for the HP laptops.
'';
};
};
};
};
###### implementation
config = mkMerge [
{ boot.loader.grub.splashImage = mkDefault (
if cfg.version == 1 then pkgs.fetchurl {
url = "http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz";
sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59";
}
# GRUB 1.97 doesn't support gzipped XPMs.
else defaultSplash);
}
(mkIf (cfg.splashImage == defaultSplash) {
boot.loader.grub.backgroundColor = mkDefault "#2F302F";
boot.loader.grub.splashMode = mkDefault "normal";
})
(mkIf cfg.enable {
boot.loader.grub.devices = optional (cfg.device != "") cfg.device;
boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [
{ path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; }
];
boot.loader.supportsInitrdSecrets = true;
system.build.installBootLoader =
let
install-grub-pl = pkgs.substituteAll {
src = ./install-grub.pl;
utillinux = pkgs.util-linux;
btrfsprogs = pkgs.btrfs-progs;
};
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 = optional (grub != null) grub;
boot.loader.grub.extraPrepareConfig =
concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/cp -pf "${v}" "@bootPath@/${n}"
'') config.boot.loader.grub.extraFiles);
assertions = [
{
assertion = !cfg.zfsSupport || cfg.version == 2;
message = "Only GRUB version 2 provides ZFS support";
}
{
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.trustedBoot.enable || cfg.version == 2;
message = "Trusted GRUB is only available for GRUB 2";
}
{
assertion = !cfg.efiSupport || !cfg.trustedBoot.enable;
message = "Trusted GRUB does not have EFI support";
}
{
assertion = !cfg.zfsSupport || !cfg.trustedBoot.enable;
message = "Trusted GRUB does not have ZFS support";
}
{
assertion = !cfg.trustedBoot.enable || cfg.trustedBoot.systemHasTPM == "YES_TPM_is_activated";
message = "Trusted GRUB can break the system! Confirm that the system has an activated TPM by setting 'systemHasTPM'.";
}
{
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";
}
] ++ 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}";
}));
})
];
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" "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,782 @@
use strict;
use warnings;
use Class::Struct;
use XML::LibXML;
use File::Basename;
use File::Path;
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) = @_; local $/ = undef;
open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE;
local $/ = "\n"; chomp $s; return $s;
}
sub writeFile {
my ($fn, $s) = @_;
open FILE, ">$fn" or die "cannot create $fn: $!\n";
print FILE $s or die;
close FILE or die;
}
sub runCommand {
my ($cmd) = @_;
open FILE, "$cmd 2>/dev/null |" or die "Failed to execute: $cmd\n";
my @ret = <FILE>;
close FILE;
return ($?, @ret);
}
my $grub = get("grub");
my $grubVersion = int(get("version"));
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 $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");
die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2;
print STDERR "updating GRUB $grubVersion menu...\n";
mkpath("$bootPath/grub", 0, 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];
next unless -d $mountPoint;
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)) {
$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 = "";
if ($grubVersion > 1) {
# 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 referrenced 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";
if ($grubVersion == 1) {
# $defaultEntry might be "saved", indicating that we want to use the last selected configuration as default.
# Incidentally this is already the correct value for the grub 1 config to achieve this behaviour.
$conf .= "
default $defaultEntry
timeout $timeout
";
if ($splashImage) {
copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath: $!\n";
$conf .= "splashimage " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background.xpm.gz\n";
}
}
else {
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
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";
$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;
mkpath("$bootPath/kernels", 0, 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) = @_;
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") {
my $initrdName = basename($initrd);
my $initrdSecretsPath = "$bootPath/kernels/$initrdName-secrets";
mkpath(dirname($initrdSecretsPath), 0, 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");
system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets: $!\n";
# 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/$initrdName-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") : "";
if ($grubVersion == 1) {
$conf .= "title $name\n";
$conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig;
$conf .= " kernel $xen $xenParams\n" if $xen;
$conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
$conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n";
if ($saveDefault) {
$conf .= " savedefault\n";
}
$conf .= "\n";
} else {
$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";
}
}
# Add default entries.
$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
addEntry("NixOS - Default", $defaultConfig, $entryOptions);
$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
# Find all the children of the current default configuration
# Do not search for grand children
my @links = sort (glob "$defaultConfig/specialisation/*");
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("NixOS - $entryName", $link);
}
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" if $grubVersion == 2;
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]);
addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link, $subEntryOptions);
}
$conf .= "}\n" if $grubVersion == 2;
}
addProfile "/nix/var/nix/profiles/system", "NixOS - All configurations";
if ($grubVersion == 2) {
for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") {
my $name = basename($profile);
next unless $name =~ /^\w+$/;
addProfile $profile, "NixOS - 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 = $grubVersion == 1 ? "$bootPath/grub/menu.lst" : "$bootPath/grub/grub.cfg";
my $tmpFile = $confFile . ".tmp";
writeFile($tmpFile, $conf);
# check whether to install GRUB EFI or not
sub getEfiTarget {
if ($grubVersion == 1) {
return "no"
} elsif (($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 taget 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 FILE, "<$bootPath/grub/state" or return $defaultGrubState;
local $/ = "\n";
my $name = <FILE>;
chomp($name);
my $version = <FILE>;
chomp($version);
my $efi = <FILE>;
chomp($efi);
my $devices = <FILE>;
chomp($devices);
my $efiMountPoint = <FILE>;
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 = <FILE>;
# 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 FILE;
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 $grubVersion 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 $grubVersion EFI 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";
}
if ($canTouchEfiVariables eq "true") {
push @command, "--bootloader-id=$bootloaderId";
} else {
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 FILE, ">$stateFileTmp" or die "cannot create $stateFileTmp: $!\n";
print FILE get("fullName"), "\n" or die;
print FILE get("fullVersion"), "\n" or die;
print FILE $efiTarget, "\n" or die;
print FILE join( ",", @deviceTargets ), "\n" or die;
print FILE $efiSysMountPoint, "\n" or die;
my %jsonState = (
extraGrubInstallArgs => \@extraGrubInstallArgs
);
my $jsonStateLine = encode_json(\%jsonState);
print FILE $jsonStateLine, "\n" or die;
close FILE 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,64 @@
# 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 =
if config.boot.loader.grub.version == 2 then
toString (map grubEntry scripts)
else
throw "iPXE is not supported with GRUB 1.";
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,116 @@
# This module adds Memtest86+/Memtest86 to the GRUB boot menu.
{ config, lib, pkgs, ... }:
with lib;
let
memtest86 = pkgs.memtest86plus;
efiSupport = config.boot.loader.grub.efiSupport;
cfg = config.boot.loader.grub.memtest86;
in
{
options = {
boot.loader.grub.memtest86 = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Make Memtest86+ (or MemTest86 if EFI support is enabled),
a memory testing program, available from the
GRUB boot menu. MemTest86 is an unfree program, so
this requires <literal>allowUnfree</literal> to be set to
<literal>true</literal>.
'';
};
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:
<itemizedlist>
<listitem>
<para><literal>console=...</literal>, set up a serial console.
Examples:
<literal>console=ttyS0</literal>,
<literal>console=ttyS0,9600</literal> or
<literal>console=ttyS0,115200n8</literal>.</para>
</listitem>
<listitem>
<para><literal>btrace</literal>, enable boot trace.</para>
</listitem>
<listitem>
<para><literal>maxcpus=N</literal>, limit number of CPUs.</para>
</listitem>
<listitem>
<para><literal>onepass</literal>, run one pass and exit if there
are no errors.</para>
</listitem>
<listitem>
<para><literal>tstlist=...</literal>, list of tests to run.
Example: <literal>0,1,2</literal>.</para>
</listitem>
<listitem>
<para><literal>cpumask=...</literal>, set a CPU mask, to select CPUs
to use for testing.</para>
</listitem>
</itemizedlist>
This list of command line options was obtained by reading the
Memtest86+ source code.
'';
};
};
};
config = mkMerge [
(mkIf (cfg.enable && efiSupport) {
assertions = [
{
assertion = cfg.params == [];
message = "Parameters are not available for MemTest86";
}
];
boot.loader.grub.extraFiles = {
"memtest86.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi";
};
boot.loader.grub.extraEntries = ''
menuentry "Memtest86" {
chainloader /memtest86.efi
}
'';
})
(mkIf (cfg.enable && !efiSupport) {
boot.loader.grub.extraEntries =
if config.boot.loader.grub.version == 2 then
''
menuentry "Memtest86+" {
linux16 @bootRoot@/memtest.bin ${toString cfg.params}
}
''
else
throw "Memtest86+ is not supported with GRUB 1.";
boot.loader.grub.extraFiles."memtest.bin" = "${memtest86}/memtest.bin";
})
];
}

View file

@ -0,0 +1,92 @@
#! @bash@/bin/sh -e
shopt -s nullglob
export PATH=/empty
for i in @path@; do PATH=$PATH:$i/bin; done
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
numAlienEntries=`cat <<EOF | egrep '^[[:space:]]*title' | wc -l
@extraEntries@
EOF`
# 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 "NixOS - 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 "NixOS - 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 "NixOS - Configuration $generation $suffix" $link "$generation ($date)"
done
mv $tmpOther $targetOther
mv $tmp $target

View file

@ -0,0 +1,51 @@
{ config, lib, pkgs, ... }:
with lib;
let
initScriptBuilder = pkgs.substituteAll {
src = ./init-script-builder.sh;
isExecutable = true;
inherit (pkgs) bash;
path = [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,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,9 @@
{ pkgs, configTxt, firmware ? pkgs.raspberrypifw }:
pkgs.substituteAll {
src = ./raspberrypi-builder.sh;
isExecutable = true;
inherit (pkgs) bash;
path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
inherit firmware configTxt;
}

View file

@ -0,0 +1,143 @@
#! @bash@/bin/sh
# This can end up being called disregarding the shebang.
set -e
shopt -s nullglob
export PATH=/empty
for i in @path@; do PATH=$PATH:$i/bin; done
usage() {
echo "usage: $0 -c <path-to-default-configuration> [-d <boot-dir>]" >&2
exit 1
}
default= # Default configuration
target=/boot # Target directory
while getopts "c:d:" opt; do
case "$opt" in
c) default="$OPTARG" ;;
d) target="$OPTARG" ;;
\?) usage ;;
esac
done
echo "updating the boot generations directory..."
mkdir -p $target/old
# 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/kernels.
declare -A filesCopied
copyToKernelsDir() {
local src="$1"
local dst="$target/old/$(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
}
copyForced() {
local src="$1"
local dst="$2"
cp $src $dst.tmp
mv $dst.tmp $dst
}
outdir=$target/old
mkdir -p $outdir || true
# Copy its kernel and initrd to $target/old.
addEntry() {
local path="$1"
local generation="$2"
if ! test -e $path/kernel -a -e $path/initrd; then
return
fi
local kernel=$(readlink -f $path/kernel)
local initrd=$(readlink -f $path/initrd)
local dtb_path=$(readlink -f $path/dtbs)
if test -n "@copyKernels@"; then
copyToKernelsDir $kernel; kernel=$result
copyToKernelsDir $initrd; initrd=$result
fi
echo $(readlink -f $path) > $outdir/$generation-system
echo $(readlink -f $path/init) > $outdir/$generation-init
cp $path/kernel-params $outdir/$generation-cmdline.txt
echo $initrd > $outdir/$generation-initrd
echo $kernel > $outdir/$generation-kernel
if test "$generation" = "default"; then
copyForced $kernel $target/kernel.img
copyForced $initrd $target/initrd
for dtb in $dtb_path/{broadcom,}/bcm*.dtb; do
dst="$target/$(basename $dtb)"
copyForced $dtb "$dst"
filesCopied[$dst]=1
done
cp "$(readlink -f "$path/init")" $target/nixos-init
echo "`cat $path/kernel-params` init=$path/init" >$target/cmdline.txt
fi
}
addEntry $default default
# 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
# Add the firmware files
fwdir=@firmware@/share/raspberrypi/boot/
copyForced $fwdir/bootcode.bin $target/bootcode.bin
copyForced $fwdir/fixup.dat $target/fixup.dat
copyForced $fwdir/fixup4.dat $target/fixup4.dat
copyForced $fwdir/fixup4cd.dat $target/fixup4cd.dat
copyForced $fwdir/fixup4db.dat $target/fixup4db.dat
copyForced $fwdir/fixup4x.dat $target/fixup4x.dat
copyForced $fwdir/fixup_cd.dat $target/fixup_cd.dat
copyForced $fwdir/fixup_db.dat $target/fixup_db.dat
copyForced $fwdir/fixup_x.dat $target/fixup_x.dat
copyForced $fwdir/start.elf $target/start.elf
copyForced $fwdir/start4.elf $target/start4.elf
copyForced $fwdir/start4cd.elf $target/start4cd.elf
copyForced $fwdir/start4db.elf $target/start4db.elf
copyForced $fwdir/start4x.elf $target/start4x.elf
copyForced $fwdir/start_cd.elf $target/start_cd.elf
copyForced $fwdir/start_db.elf $target/start_db.elf
copyForced $fwdir/start_x.elf $target/start_x.elf
# Add the config.txt
copyForced @configTxt@ $target/config.txt
# Remove obsolete files from $target and $target/old.
for fn in $target/old/*linux* $target/old/*initrd-initrd* $target/bcm*.dtb; do
if ! test "${filesCopied[$fn]}" = 1; then
rm -vf -- "$fn"
fi
done

View file

@ -0,0 +1,105 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.boot.loader.raspberryPi;
builderUboot = import ./uboot-builder.nix { inherit pkgs configTxt; inherit (cfg) version; };
builderGeneric = import ./raspberrypi-builder.nix { inherit pkgs configTxt; };
builder =
if cfg.uboot.enable then
"${builderUboot} -g ${toString cfg.uboot.configurationLimit} -t ${timeoutStr} -c"
else
"${builderGeneric} -c";
blCfg = config.boot.loader;
timeoutStr = if blCfg.timeout == null then "-1" else toString blCfg.timeout;
isAarch64 = pkgs.stdenv.hostPlatform.isAarch64;
optional = pkgs.lib.optionalString;
configTxt =
pkgs.writeText "config.txt" (''
# U-Boot used to need this to work, regardless of whether UART is actually used or not.
# TODO: check when/if this can be removed.
enable_uart=1
# Prevent the firmware from smashing the framebuffer setup done by the mainline kernel
# when attempting to show low-voltage or overtemperature warnings.
avoid_warnings=1
'' + optional isAarch64 ''
# Boot in 64-bit mode.
arm_64bit=1
'' + (if cfg.uboot.enable then ''
kernel=u-boot-rpi.bin
'' else ''
kernel=kernel.img
initramfs initrd followkernel
'') + optional (cfg.firmwareConfig != null) cfg.firmwareConfig);
in
{
options = {
boot.loader.raspberryPi = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to create files with the system generations in
<literal>/boot</literal>.
<literal>/boot/old</literal> will hold files from old generations.
'';
};
version = mkOption {
default = 2;
type = types.enum [ 0 1 2 3 4 ];
description = "";
};
uboot = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Enable using uboot as bootmanager for the raspberry pi.
'';
};
configurationLimit = mkOption {
default = 20;
example = 10;
type = types.int;
description = ''
Maximum number of configurations in the boot menu.
'';
};
};
firmwareConfig = mkOption {
default = null;
type = types.nullOr types.lines;
description = ''
Extra options that will be appended to <literal>/boot/config.txt</literal> file.
For possible values, see: https://www.raspberrypi.org/documentation/configuration/config-txt/
'';
};
};
};
config = mkIf cfg.enable {
assertions = singleton {
assertion = !pkgs.stdenv.hostPlatform.isAarch64 || cfg.version >= 3;
message = "Only Raspberry Pi >= 3 supports aarch64.";
};
system.build.installBootLoader = builder;
system.boot.loader.id = "raspberrypi";
system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
};
}

View file

@ -0,0 +1,37 @@
{ pkgs, version, configTxt }:
let
isAarch64 = pkgs.stdenv.hostPlatform.isAarch64;
uboot =
if version == 0 then
pkgs.ubootRaspberryPiZero
else if version == 1 then
pkgs.ubootRaspberryPi
else if version == 2 then
pkgs.ubootRaspberryPi2
else if version == 3 then
if isAarch64 then
pkgs.ubootRaspberryPi3_64bit
else
pkgs.ubootRaspberryPi3_32bit
else
throw "U-Boot is not yet supported on the raspberry pi 4.";
extlinuxConfBuilder =
import ../generic-extlinux-compatible/extlinux-conf-builder.nix {
pkgs = pkgs.buildPackages;
};
in
pkgs.substituteAll {
src = ./uboot-builder.sh;
isExecutable = true;
inherit (pkgs) bash;
path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
firmware = pkgs.raspberrypifw;
inherit uboot;
inherit configTxt;
inherit extlinuxConfBuilder;
inherit version;
}

View file

@ -0,0 +1,38 @@
#! @bash@/bin/sh -e
target=/boot # Target directory
while getopts "t:c:d:g:" opt; do
case "$opt" in
d) target="$OPTARG" ;;
*) ;;
esac
done
copyForced() {
local src="$1"
local dst="$2"
cp $src $dst.tmp
mv $dst.tmp $dst
}
# Call the extlinux builder
"@extlinuxConfBuilder@" "$@"
# Add the firmware files
fwdir=@firmware@/share/raspberrypi/boot/
copyForced $fwdir/bootcode.bin $target/bootcode.bin
copyForced $fwdir/fixup.dat $target/fixup.dat
copyForced $fwdir/fixup_cd.dat $target/fixup_cd.dat
copyForced $fwdir/fixup_db.dat $target/fixup_db.dat
copyForced $fwdir/fixup_x.dat $target/fixup_x.dat
copyForced $fwdir/start.elf $target/start.elf
copyForced $fwdir/start_cd.elf $target/start_cd.elf
copyForced $fwdir/start_db.elf $target/start_db.elf
copyForced $fwdir/start_x.elf $target/start_x.elf
# Add the uboot file
copyForced @uboot@/u-boot.bin $target/u-boot-rpi.bin
# Add the config.txt
copyForced @configTxt@ $target/config.txt

View file

@ -0,0 +1,317 @@
#! @python3@/bin/python3 -B
import argparse
import shutil
import os
import sys
import errno
import subprocess
import glob
import tempfile
import errno
import warnings
import ctypes
libc = ctypes.CDLL("libc.so.6")
import re
import datetime
import glob
import os.path
from typing import NamedTuple, List, Optional
class SystemIdentifier(NamedTuple):
profile: Optional[str]
generation: int
specialisation: Optional[str]
def copy_if_not_exists(source: str, dest: str) -> None:
if not os.path.exists(dest):
shutil.copyfile(source, dest)
def generation_dir(profile: Optional[str], generation: int) -> str:
if profile:
return "/nix/var/nix/profiles/system-profiles/%s-%d-link" % (profile, generation)
else:
return "/nix/var/nix/profiles/system-%d-link" % (generation)
def system_dir(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str:
d = generation_dir(profile, generation)
if specialisation:
return os.path.join(d, "specialisation", specialisation)
else:
return d
BOOT_ENTRY = """title NixOS{profile}{specialisation}
version Generation {generation} {description}
linux {kernel}
initrd {initrd}
options {kernel_params}
"""
def generation_conf_filename(profile: Optional[str], generation: int, specialisation: Optional[str]) -> 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: Optional[str], generation: int, specialisation: Optional[str]) -> None:
with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f:
if "@timeout@" != "":
f.write("timeout @timeout@\n")
f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation))
if not @editor@:
f.write("editor 0\n");
f.write("console-mode @consoleMode@\n");
os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf")
def profile_path(profile: Optional[str], generation: int, specialisation: Optional[str], name: str) -> str:
return os.path.realpath("%s/%s" % (system_dir(profile, generation, specialisation), name))
def copy_from_profile(profile: Optional[str], generation: int, specialisation: Optional[str], name: str, dry_run: bool = False) -> str:
store_file_path = profile_path(profile, generation, specialisation, name)
suffix = os.path.basename(store_file_path)
store_dir = os.path.basename(os.path.dirname(store_file_path))
efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix)
if not dry_run:
copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path))
return efi_file_path
def describe_generation(generation_dir: str) -> str:
try:
with open("%s/nixos-version" % generation_dir) as f:
nixos_version = f.read()
except IOError:
nixos_version = "Unknown"
kernel_dir = os.path.dirname(os.path.realpath("%s/kernel" % generation_dir))
module_dir = glob.glob("%s/lib/modules/*" % kernel_dir)[0]
kernel_version = os.path.basename(module_dir)
build_time = int(os.path.getctime(generation_dir))
build_date = datetime.datetime.fromtimestamp(build_time).strftime('%F')
description = "NixOS {}, Linux Kernel {}, Built on {}".format(
nixos_version, kernel_version, build_date
)
return description
def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str], machine_id: str) -> None:
kernel = copy_from_profile(profile, generation, specialisation, "kernel")
initrd = copy_from_profile(profile, generation, specialisation, "initrd")
try:
append_initrd_secrets = profile_path(profile, generation, specialisation, "append-initrd-secrets")
subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)])
except FileNotFoundError:
pass
entry_file = "@efiSysMountPoint@/loader/entries/%s" % (
generation_conf_filename(profile, generation, specialisation))
generation_dir = os.readlink(system_dir(profile, generation, specialisation))
tmp_path = "%s.tmp" % (entry_file)
kernel_params = "init=%s/init " % generation_dir
with open("%s/kernel-params" % (generation_dir)) as params_file:
kernel_params = kernel_params + params_file.read()
with open(tmp_path, 'w') as f:
f.write(BOOT_ENTRY.format(profile=" [" + profile + "]" if profile else "",
specialisation=" (%s)" % specialisation if specialisation else "",
generation=generation,
kernel=kernel,
initrd=initrd,
kernel_params=kernel_params,
description=describe_generation(generation_dir)))
if machine_id is not None:
f.write("machine-id %s\n" % machine_id)
os.rename(tmp_path, entry_file)
def mkdir_p(path: str) -> None:
try:
os.makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST or not os.path.isdir(path):
raise
def get_generations(profile: Optional[str] = None) -> List[SystemIdentifier]:
gen_list = subprocess.check_output([
"@nix@/bin/nix-env",
"--list-generations",
"-p",
"/nix/var/nix/profiles/%s" % ("system-profiles/" + profile if profile else "system"),
"--option", "build-users-group", ""],
universal_newlines=True)
gen_lines = gen_list.split('\n')
gen_lines.pop()
configurationLimit = @configurationLimit@
configurations = [
SystemIdentifier(
profile=profile,
generation=int(line.split()[0]),
specialisation=None
)
for line in gen_lines
]
return configurations[-configurationLimit:]
def get_specialisations(profile: Optional[str], generation: int, _: Optional[str]) -> List[SystemIdentifier]:
specialisations_dir = os.path.join(
system_dir(profile, generation, None), "specialisation")
if not os.path.exists(specialisations_dir):
return []
return [SystemIdentifier(profile, generation, spec) for spec in os.listdir(specialisations_dir)]
def remove_old_entries(gens: List[SystemIdentifier]) -> None:
rex_profile = re.compile("^@efiSysMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$")
rex_generation = re.compile("^@efiSysMountPoint@/loader/entries/nixos.*-generation-(.*)\.conf$")
known_paths = []
for gen in gens:
known_paths.append(copy_from_profile(*gen, "kernel", True))
known_paths.append(copy_from_profile(*gen, "initrd", True))
for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos*-generation-[1-9]*.conf"):
try:
if rex_profile.match(path):
prof = rex_profile.sub(r"\1", path)
else:
prof = "system"
gen_number = int(rex_generation.sub(r"\1", path))
if not (prof, gen_number) in gens:
os.unlink(path)
except ValueError:
pass
for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"):
if not path in known_paths and not os.path.isdir(path):
os.unlink(path)
def get_profiles() -> List[str]:
if os.path.isdir("/nix/var/nix/profiles/system-profiles/"):
return [x
for x in os.listdir("/nix/var/nix/profiles/system-profiles/")
if not x.endswith("-link")]
else:
return []
def main() -> None:
parser = argparse.ArgumentParser(description='Update NixOS-related systemd-boot files')
parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot')
args = parser.parse_args()
try:
with open("/etc/machine-id") as machine_file:
machine_id = machine_file.readlines()[0]
except IOError as e:
if e.errno != errno.ENOENT:
raise
# Since systemd version 232 a machine ID is required and it might not
# be there on newly installed systems, so let's generate one so that
# bootctl can find it and we can also pass it to write_entry() later.
cmd = ["@systemd@/bin/systemd-machine-id-setup", "--print"]
machine_id = subprocess.run(
cmd, text=True, check=True, stdout=subprocess.PIPE
).stdout.rstrip()
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"
if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
# bootctl uses fopen() with modes "wxe" and fails if the file exists.
if os.path.exists("@efiSysMountPoint@/loader/loader.conf"):
os.unlink("@efiSysMountPoint@/loader/loader.conf")
flags = []
if "@canTouchEfiVariables@" != "1":
flags.append("--no-variables")
if "@graceful@" == "1":
flags.append("--graceful")
subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@"] + flags + ["install"])
else:
# Update bootloader to latest if needed
available_out = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
installed_out = subprocess.check_output(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True)
# See status_binaries() in systemd bootctl.c for code which generates this
installed_match = re.search(r"^\W+File:.*/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 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))
subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "update"])
else:
print("leaving systemd-boot %s in place (%s is not newer)" % (installed_version, available_version))
mkdir_p("@efiSysMountPoint@/efi/nixos")
mkdir_p("@efiSysMountPoint@/loader/entries")
gens = get_generations()
for profile in get_profiles():
gens += get_generations(profile)
remove_old_entries(gens)
for gen in gens:
try:
write_entry(*gen, machine_id)
for specialisation in get_specialisations(*gen):
write_entry(*specialisation, machine_id)
if os.readlink(system_dir(*gen)) == args.default_config:
write_loader_conf(*gen)
except OSError as e:
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)
for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False):
relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/")
actual_root = os.path.join("@efiSysMountPoint@", relative_root)
for file in files:
actual_file = os.path.join(actual_root, file)
if os.path.exists(actual_file):
os.unlink(actual_file)
os.unlink(os.path.join(root, file))
if not len(os.listdir(actual_root)):
os.rmdir(actual_root)
os.rmdir(root)
mkdir_p("@efiSysMountPoint@/efi/nixos/.extra-files")
subprocess.check_call("@copyExtraFiles@")
# 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("@efiSysMountPoint@", os.O_RDONLY))
if rc != 0:
print("could not sync @efiSysMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,303 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.boot.loader.systemd-boot;
efi = config.boot.loader.efi;
systemdBootBuilder = pkgs.substituteAll {
src = ./systemd-boot-builder.py;
isExecutable = true;
inherit (pkgs) python3;
systemd = config.systemd.package;
nix = config.nix.package.out;
timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else "";
editor = if cfg.editor then "True" else "False";
configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit;
inherit (cfg) consoleMode graceful;
inherit (efi) efiSysMountPoint canTouchEfiVariables;
memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else "";
netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
empty_file=$(${pkgs.coreutils}/bin/mktemp)
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
'') cfg.extraFiles)}
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n}
${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
'') cfg.extraEntries)}
'';
};
checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" {
nativeBuildInputs = [ pkgs.mypy ];
} ''
install -m755 ${systemdBootBuilder} $out
mypy \
--no-implicit-optional \
--disallow-untyped-calls \
--disallow-untyped-defs \
$out
'';
in {
imports =
[ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ])
];
options.boot.loader.systemd-boot = {
enable = mkOption {
default = false;
type = types.bool;
description = "Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager";
};
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.
'';
};
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.
<literal>null</literal> means no limit i.e. all generations
that were not garbage collected yet.
'';
};
consoleMode = mkOption {
default = "keep";
type = types.enum [ "0" "1" "2" "auto" "max" "keep" ];
description = ''
The resolution of the console. The following values are valid:
<itemizedlist>
<listitem><para>
<literal>"0"</literal>: Standard UEFI 80x25 mode
</para></listitem>
<listitem><para>
<literal>"1"</literal>: 80x50 mode, not supported by all devices
</para></listitem>
<listitem><para>
<literal>"2"</literal>: The first non-standard mode provided by the device firmware, if any
</para></listitem>
<listitem><para>
<literal>"auto"</literal>: Pick a suitable mode automatically using heuristics
</para></listitem>
<listitem><para>
<literal>"max"</literal>: Pick the highest-numbered available mode
</para></listitem>
<listitem><para>
<literal>"keep"</literal>: Keep the mode selected by firmware (the default)
</para></listitem>
</itemizedlist>
'';
};
memtest86 = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Make MemTest86 available from the systemd-boot menu. MemTest86 is a
program for testing memory. MemTest86 is an unfree program, so
this requires <literal>allowUnfree</literal> to be set to
<literal>true</literal>.
'';
};
entryFilename = mkOption {
default = "memtest86.conf";
type = types.str;
description = ''
<literal>systemd-boot</literal> orders the menu entries by the config file names,
so if you want something to appear after all the NixOS entries,
it should start with <filename>o</filename> or onwards.
'';
};
};
netbootxyz = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Make <literal>netboot.xyz</literal> available from the
<literal>systemd-boot</literal> menu. <literal>netboot.xyz</literal>
is a menu system that allows you to boot OS installers and
utilities over the network.
'';
};
entryFilename = mkOption {
default = "o_netbootxyz.conf";
type = types.str;
description = ''
<literal>systemd-boot</literal> orders the menu entries by the config file names,
so if you want something to appear after all the NixOS entries,
it should start with <filename>o</filename> or onwards.
'';
};
};
extraEntries = mkOption {
type = types.attrsOf types.lines;
default = {};
example = literalExpression ''
{ "memtest86.conf" = '''
title MemTest86
efi /efi/memtest86/memtest86.efi
'''; }
'';
description = ''
Any additional entries you want added to the <literal>systemd-boot</literal> menu.
These entries will be copied to <filename>/boot/loader/entries</filename>.
Each attribute name denotes the destination file name,
and the corresponding attribute value is the contents of the entry.
<literal>systemd-boot</literal> orders the menu entries by the config file names,
so if you want something to appear after all the NixOS entries,
it should start with <filename>o</filename> or onwards.
'';
};
extraFiles = mkOption {
type = types.attrsOf types.path;
default = {};
example = literalExpression ''
{ "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; }
'';
description = ''
A set of files to be copied to <filename>/boot</filename>.
Each attribute name denotes the destination file name in
<filename>/boot</filename>, while the corresponding
attribute value specifies the source file.
'';
};
graceful = mkOption {
default = false;
type = types.bool;
description = ''
Invoke <literal>bootctl install</literal> with the <literal>--graceful</literal> 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 <literal>systemd-boot</literal> otherwise fails to install, as the
scope or implication of the <literal>--graceful</literal> option may change in the future.
'';
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
message = "This kernel does not support the EFI boot stub";
}
] ++ 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);
boot.loader.grub.enable = mkDefault false;
boot.loader.supportsInitrdSecrets = true;
boot.loader.systemd-boot.extraFiles = mkMerge [
# TODO: This is hard-coded to use the 64-bit EFI app, but it could probably
# be updated to use the 32-bit EFI app on 32-bit systems. The 32-bit EFI
# app filename is BOOTIA32.efi.
(mkIf cfg.memtest86.enable {
"efi/memtest86/BOOTX64.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi";
})
(mkIf cfg.netbootxyz.enable {
"efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
})
];
boot.loader.systemd-boot.extraEntries = mkMerge [
(mkIf cfg.memtest86.enable {
"${cfg.memtest86.entryFilename}" = ''
title MemTest86
efi /efi/memtest86/BOOTX64.efi
'';
})
(mkIf cfg.netbootxyz.enable {
"${cfg.netbootxyz.entryFilename}" = ''
title netboot.xyz
efi /efi/netbootxyz/netboot.xyz.efi
'';
})
];
system = {
build.installBootLoader = checkedSystemdBootBuilder;
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,70 @@
{ config, lib, pkgs, ... }:
with lib;
{
###### interface
options = {
boot.blacklistedKernelModules = mkOption {
type = types.listOf types.str;
default = [];
example = [ "cirrusfb" "i2c_piix4" ];
description = ''
List of names of kernel modules that should not be loaded
automatically by the hardware probing code.
'';
};
boot.extraModprobeConfig = mkOption {
default = "";
example =
''
options parport_pc io=0x378 irq=7 dma=1
'';
description = ''
Any additional configuration to be appended to the generated
<filename>modprobe.conf</filename>. This is typically used to
specify module options. See
<citerefentry><refentrytitle>modprobe.d</refentrytitle>
<manvolnum>5</manvolnum></citerefentry> for details.
'';
type = types.lines;
};
};
###### implementation
config = mkIf (!config.boot.isContainer) {
environment.etc."modprobe.d/ubuntu.conf".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 @@
#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,321 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
inherit (pkgs) nixos-icons;
plymouth = pkgs.plymouth.override {
systemd = config.boot.initrd.systemd.package;
};
cfg = config.boot.plymouth;
opt = options.boot.plymouth;
nixosBreezePlymouth = pkgs.plasma5Packages.breeze-plymouth.override {
logoFile = cfg.logo;
logoName = "nixos";
osName = "NixOS";
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
'';
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}
'';
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 = literalDocBook ''
A NixOS branded variant of the breeze theme when
<literal>config.${opt.theme} == "breeze"</literal>, otherwise
<literal>[ ]</literal>.
'';
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 = "${nixos-icons}/share/icons/hicolor/48x48/apps/nix-snowflake-white.png";
defaultText = literalExpression ''pkgs.fetchurl {
url = "https://nixos.org/logo/nixos-hires.png";
sha256 = "1ivzgd7iz0i06y36p8m5w48fd8pjqwxhdaavc0pxs7w1g7mcy5si";
}'';
description = ''
Logo which is displayed on the splash screen.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Literal string to append to <literal>configFile</literal>
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.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 = [ "multi-user.target" ];
systemd.paths.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
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
contents = {
# Files
"/etc/plymouth/plymouthd.conf".source = configFile;
"/etc/plymouth/plymouthd.defaults".source = "${plymouth}/share/plymouth/plymouthd.defaults";
"/etc/plymouth/logo.png".source = cfg.logo;
# Directories
"/etc/plymouth/plugins".source = pkgs.runCommand "plymouth-initrd-plugins" {} ''
# 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"
exit 1
fi
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/{text,details,label,$moduleName}.so $out
cp ${plymouth}/lib/plymouth/renderers/{drm,frame-buffer}.so $out/renderers
'';
"/etc/plymouth/themes".source = pkgs.runCommand "plymouth-initrd-themes" {} ''
# 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"
exit 1
fi
mkdir $out
cp -r ${themesEnv}/share/plymouth/themes/${cfg.theme} $out
# Copy more themes if the theme depends on others
for theme in $(grep -hRo '/etc/plymouth/themes/.*$' ${themesEnv} | xargs -n1 basename); do
if [[ -d "${themesEnv}/theme" ]]; then
cp -r "${themesEnv}/theme" $out
fi
done
'';
# Fonts
"/etc/plymouth/fonts".source = pkgs.runCommand "plymouth-initrd-fonts" {} ''
mkdir -p $out
cp ${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" ];
};
};
# 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
# 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"
exit 1
fi
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/{text,details,label,$moduleName}.so $out/lib/plymouth
cp ${plymouth}/lib/plymouth/renderers/{drm,frame-buffer}.so $out/lib/plymouth/renderers
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,/nix/.*/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
# 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 ''
mkdir -p /etc/plymouth
mkdir -p /run/plymouth
ln -s ${configFile} /etc/plymouth/plymouthd.conf
ln -s $extraUtils/share/plymouth/plymouthd.defaults /etc/plymouth/plymouthd.defaults
ln -s $extraUtils/share/plymouth/logo.png /etc/plymouth/logo.png
ln -s $extraUtils/share/plymouth/themes /etc/plymouth/themes
ln -s $extraUtils/lib/plymouth /etc/plymouth/plugins
ln -s $extraUtils/etc/fonts /etc/fonts
plymouthd --mode=boot --pid-file=/run/plymouth/pid --attach-to-session
plymouth show-splash
'');
boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
plymouth update-root-fs --new-root-dir="$targetRoot"
'';
# `mkBefore` to ensure that any custom prompts would be visible.
boot.initrd.preFailCommands = mkIf (!config.boot.initrd.systemd.enable) (mkBefore ''
plymouth quit --wait
'');
};
}

View file

@ -0,0 +1,183 @@
{ config, lib, ... }:
with lib;
let
cfg = config.services.resolved;
dnsmasqResolve = config.services.dnsmasq.enable &&
config.services.dnsmasq.resolveLocalQueries;
in
{
options = {
services.resolved.enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to enable the systemd DNS resolver daemon.
'';
};
services.resolved.fallbackDns = mkOption {
default = [ ];
example = [ "8.8.8.8" "2001:4860:4860::8844" ];
type = types.listOf types.str;
description = ''
A list of IPv4 and IPv6 addresses to use as the fallback DNS servers.
If this option is empty, a compiled-in list of DNS servers is used instead.
'';
};
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
<filename>/etc/resolv.conf</filename> 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
<variablelist>
<varlistentry>
<term><literal>"true"</literal></term>
<listitem><para>
Enables full LLMNR responder and resolver support.
</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>"false"</literal></term>
<listitem><para>
Disables both.
</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>"resolve"</literal></term>
<listitem><para>
Only resolution support is enabled, but responding is disabled.
</para></listitem>
</varlistentry>
</variablelist>
'';
};
services.resolved.dnssec = mkOption {
default = "allow-downgrade";
example = "true";
type = types.enum [ "true" "allow-downgrade" "false" ];
description = ''
If set to
<variablelist>
<varlistentry>
<term><literal>"true"</literal></term>
<listitem><para>
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.
</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>"allow-downgrade"</literal></term>
<listitem><para>
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.
</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>"false"</literal></term>
<listitem><para>
DNS lookups are not DNSSEC validated.
</para></listitem>
</varlistentry>
</variablelist>
'';
};
services.resolved.extraConfig = mkOption {
default = "";
type = types.lines;
description = ''
Extra config to append to resolved.conf.
'';
};
};
config = 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 = [ "multi-user.target" ];
aliases = [ "dbus-org.freedesktop.resolve1.service" ];
restartTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
};
environment.etc = {
"systemd/resolved.conf".text = ''
[Resolve]
${optionalString (config.networking.nameservers != [])
"DNS=${concatStringsSep " " config.networking.nameservers}"}
${optionalString (cfg.fallbackDns != [])
"FallbackDNS=${concatStringsSep " " cfg.fallbackDns}"}
${optionalString (cfg.domains != [])
"Domains=${concatStringsSep " " cfg.domains}"}
LLMNR=${cfg.llmnr}
DNSSEC=${cfg.dnssec}
${config.services.resolved.extraConfig}
'';
# 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";
};
}

View file

@ -0,0 +1,27 @@
{ config, lib, pkgs, ... }:
with lib;
{
# 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";
};
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,641 @@
#! @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 "<<< NixOS 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 as bcachefs
# uses this for multi-device filesystems, i.e. /dev/sda1:/dev/sda2:/dev/sda3
local IFS=':'
# 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
}
# 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.
mkdir -p /lib
ln -s @modulesClosure@/lib/modules /lib/modules
ln -s @modulesClosure@/lib/firmware /lib/firmware
# see comment in stage-1.nix for explanation
echo @extraUtils@/bin/modprobe-kernel > /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
# 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
}
# 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"
# Optionally resize the filesystem.
case $options in
*x-nixos.autoresize*)
if [ "$fsType" = ext2 -o "$fsType" = ext3 -o "$fsType" = ext4 ]; then
modprobe "$fsType"
echo "resizing $device..."
e2fsck -fp "$device"
resize2fs "$device"
elif [ "$fsType" = f2fs ]; then
echo "resizing $device..."
fsck.f2fs -fp "$device"
resize.f2fs "$device"
fi
;;
esac
# 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..."
mkdir -p "/mnt-root$mountPoint"
# 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
[ "$mountPoint" == "/" ] &&
[ -f "/mnt-root/etc/NIXOS_LUSTRATE" ] &&
lustrateRoot "/mnt-root"
true
}
lustrateRoot () {
local root="$1"
echo
echo -e "\e[1;33m<<< NixOS 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 invokations 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
# 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
cp -r /tmp-iso/* /mnt-root/iso/
umount /tmp-iso
rmdir /tmp-iso
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" "$mountPoint" "$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,758 @@
# 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, lib, utils, pkgs, ... }:
with lib;
let
udev = config.systemd.package;
kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
modulesTree = config.system.modulesTree.override { name = kernel-name + "-modules"; };
firmware = config.hardware.firmware;
# 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 = modulesTree;
firmware = firmware;
allowMissing = false;
};
# 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.runCommandCC "extra-utils"
{ nativeBuildInputs = [pkgs.buildPackages.nukeReferences];
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 ${pkgs.util-linux}/bin/mount
copy_bin_and_libs ${pkgs.zfs}/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
# Add RAID mdadm tool.
copy_bin_and_libs ${pkgs.mdadm}/sbin/mdadm
copy_bin_and_libs ${pkgs.mdadm}/sbin/mdmon
# Copy udev.
copy_bin_and_libs ${udev}/bin/udevadm
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
# Dirty hack to make sure the kernel properly loads modules
# such as ext4 on demand (e.g. on a `mount(2)` syscall). This is necessary
# because `kmod` isn't linked against `libpthread.so.0` anymore (since
# it was merged into `libc.so.6` since version `2.34`), but still needs
# to access it for some reason. This is not an issue in stage-1 itself
# because of the `LD_LIBRARY_PATH`-variable and anytime later because the rpath of
# kmod/modprobe points to glibc's `$out/lib` where `libpthread.so.6` exists.
# However, this is a problem when the kernel calls `modprobe` inside
# the initial ramdisk because it doesn't know about the
# `LD_LIBRARY_PATH` and the rpath was nuked.
#
# Also, we can't use `makeWrapper` here because `kmod` only does
# `modprobe` functionality if `argv[0] == "modprobe"`.
cat >$out/bin/modprobe-kernel <<EOF
#!$out/bin/ash
export LD_LIBRARY_PATH=$out/lib
exec $out/bin/modprobe "\$@"
EOF
chmod +x $out/bin/modprobe-kernel
# Copy resize2fs if any ext* filesystems are to be resized
${optionalString (any (fs: fs.autoResize && (lib.hasPrefix "ext" fs.fsType)) fileSystems) ''
# We need mke2fs in the initrd.
copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs
''}
# 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
find $out/bin $out/lib -type f | 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" "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"
$out/bin/mdadm --version
${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
# http://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.substituteAll {
src = ./stage-1-init.sh;
shell = "${extraUtils}/bin/ash";
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
'';
inherit linkUnits udevRules extraUtils modulesClosure;
inherit (config.boot) resumeDevice;
inherit (config.system.build) earlyMountScript;
inherit (config.boot.initrd) checkJournalingFS verbose
preLVMCommands preDeviceCommands postDeviceCommands 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.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 = pkgs.writeText "mdadm.conf" config.boot.initrd.services.swraid.mdadmConf;
symlink = "/etc/mdadm.conf";
}
{ object = pkgs.runCommand "initrd-kmod-blacklist-ubuntu" {
src = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
preferLocalBuild = true;
} ''
target=$out
${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out
'';
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.libarchive}/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)
}
(cd "$tmp" && find . -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
${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 <varname>boot.kernelParams</varname> to specify
<literal><replaceable>resume_offset</replaceable></literal>.
'';
};
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.checkJournalingFS = mkOption {
default = true;
type = types.bool;
description = ''
Whether to run <command>fsck</command> 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
<filename>/dev</filename>.
'';
};
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 = literalDocBook "<literal>zstd</literal> if the kernel supports it (5.9+), <literal>gzip</literal> if not";
type = types.either types.str (types.functionTo types.str);
description = ''
The compressor to use on the initrd image. May be any of:
<itemizedlist>
<listitem><para>The name of one of the predefined compressors, see <filename>pkgs/build-support/kernel/initrd-compressor-meta.nix</filename> for the definitions.</para></listitem>
<listitem><para>A function which, given the nixpkgs package set, returns the path to a compressor tool, e.g. <literal>pkgs: "''${pkgs.pigz}/bin/pigz"</literal></para></listitem>
<listitem><para>(not recommended, because it does not work when cross-compiling) the full path to a compressor tool, e.g. <literal>"''${pkgs.pigz}/bin/pigz"</literal></para></listitem>
</itemizedlist>
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).
'';
example = literalExpression
''
{ "/etc/dropbear/dropbear_rsa_host_key" =
./secret-dropbear-key;
}
'';
};
boot.initrd.supportedFilesystems = mkOption {
default = [ ];
example = [ "btrfs" ];
type = types.listOf types.str;
description = "Names of supported filesystem types in the initial ramdisk.";
};
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:
<itemizedlist>
<listitem><para><literal>boot.consoleLogLevel = 0;</literal></para></listitem>
<listitem><para><literal>boot.kernelParams = [ "quiet" "udev.log_level=3" ];</literal></para></listitem>
</itemizedlist>
'';
};
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: "<filename>${i}</filename>")
)}.
'';
};
});
};
};
config = mkIf config.boot.initrd.enable {
assertions = [
{ assertion = 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.loader.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; }
# 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" "initrd" "services" "swraid" "mdadmConf" ])
];
}

View file

@ -0,0 +1,144 @@
#! @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<<< NixOS 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 ]; then
echo "booting system configuration ${systemConfig}"
else
echo "booting system configuration $systemConfig" > /dev/kmsg
fi
# Make /nix/store a read-only bind mount to enforce immutability of
# the Nix 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
if [ -n "@readOnlyStore@" ]; then
if ! [[ "$(findmnt --noheadings --output OPTIONS /nix/store)" =~ ro(,|$) ]]; then
if [ -z "$container" ]; then
mount --bind /nix/store /nix/store
else
mount --rbind /nix/store /nix/store
fi
mount -o remount,ro,bind /nix/store
fi
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 /etc/nixos
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@
# Ensure systemd doesn't try to populate /etc, by forcing its first-boot
# heuristic off. It doesn't matter what's in /etc/machine-id for this purpose,
# and systemd will immediately fill in the file when it starts, so just
# creating it is enough. This `: >>` pattern avoids forking and avoids changing
# the mtime if the file already exists.
: >> /etc/machine-id
# 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,71 @@
{ config, lib, pkgs, ... }:
with lib;
let
useHostResolvConf = config.networking.resolvconf.enable && config.networking.useHostResolvConf;
bootStage2 = pkgs.substituteAll {
src = ./stage-2-init.sh;
shellDebug = "${pkgs.bashInteractive}/bin/bash";
shell = "${pkgs.bash}/bin/bash";
inherit (config.boot) systemdExecutable extraSystemdUnitPaths;
isExecutable = true;
inherit (config.nix) readOnlyStore;
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
{
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.
'';
};
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,631 @@
{ config, lib, pkgs, utils, ... }:
with utils;
with systemdUtils.unitOptions;
with lib;
let
cfg = config.systemd;
systemd = cfg.package;
inherit (systemdUtils.lib)
generateUnits
targetToUnit
serviceToUnit
socketToUnit
timerToUnit
pathToUnit
mountToUnit
automountToUnit
sliceToUnit;
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"
] ++ 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"
# 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"
# Consoles.
"getty.target"
"getty-pre.target"
"getty@.service"
"serial-getty@.service"
"console-getty.service"
"container-getty@.service"
"systemd-vconsole-setup.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-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"
# Maintaining state across reboots.
"systemd-random-seed.service"
"systemd-backlight@.service"
"systemd-rfkill.service"
"systemd-rfkill.socket"
# Hibernate / suspend.
"hibernate.target"
"suspend.target"
"suspend-then-hibernate.target"
"sleep.target"
"hybrid-sleep.target"
"systemd-hibernate.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"
"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"
# 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"
] ++ 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-exit.service"
"systemd-update-done.service"
] ++ 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 = mkOption {
default = pkgs.systemd;
defaultText = literalExpression "pkgs.systemd";
type = types.package;
description = "The systemd package.";
};
systemd.units = mkOption {
description = "Definition of systemd units.";
default = {};
type = systemdUtils.types.units;
};
systemd.packages = mkOption {
default = [];
type = types.listOf types.package;
example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
description = "Packages providing systemd units and hooks.";
};
systemd.targets = mkOption {
default = {};
type = systemdUtils.types.targets;
description = "Definition of systemd target units.";
};
systemd.services = mkOption {
default = {};
type = systemdUtils.types.services;
description = "Definition of systemd service units.";
};
systemd.sockets = mkOption {
default = {};
type = systemdUtils.types.sockets;
description = "Definition of systemd socket units.";
};
systemd.timers = mkOption {
default = {};
type = systemdUtils.types.timers;
description = "Definition of systemd timer units.";
};
systemd.paths = mkOption {
default = {};
type = systemdUtils.types.paths;
description = "Definition of systemd path units.";
};
systemd.mounts = mkOption {
default = [];
type = systemdUtils.types.mounts;
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.
'';
};
systemd.automounts = mkOption {
default = [];
type = systemdUtils.types.automounts;
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.
'';
};
systemd.slices = mkOption {
default = {};
type = systemdUtils.types.slices;
description = "Definition of slice configurations.";
};
systemd.generators = mkOption {
type = types.attrsOf types.path;
default = {};
example = { systemd-gpt-auto-generator = "/dev/null"; };
description = ''
Definition of systemd generators.
For each <literal>NAME = VALUE</literal> pair of the attrSet, a link is generated from
<literal>/etc/systemd/system-generators/NAME</literal> to <literal>VALUE</literal>.
'';
};
systemd.shutdown = mkOption {
type = types.attrsOf types.path;
default = {};
description = ''
Definition of systemd shutdown executables.
For each <literal>NAME = VALUE</literal> pair of the attrSet, a link is generated from
<literal>/etc/systemd/system-shutdown/NAME</literal> to <literal>VALUE</literal>.
'';
};
systemd.defaultUnit = mkOption {
default = "multi-user.target";
type = types.str;
description = "Default unit started when the system boots.";
};
systemd.ctrlAltDelUnit = mkOption {
default = "reboot.target";
type = types.str;
example = "poweroff.target";
description = ''
Target that should be started when Ctrl-Alt-Delete is pressed.
'';
};
systemd.globalEnvironment = mkOption {
type = with types; attrsOf (nullOr (oneOf [ str path package ]));
default = {};
example = { TZ = "CET"; };
description = ''
Environment variables passed to <emphasis>all</emphasis> systemd units.
'';
};
systemd.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
<emphasis>not</emphasis> passed to started units.
'';
};
systemd.enableCgroupAccounting = mkOption {
default = true;
type = types.bool;
description = ''
Whether to enable cgroup accounting.
'';
};
systemd.enableUnifiedCgroupHierarchy = mkOption {
default = true;
type = types.bool;
description = ''
Whether to enable the unified cgroup hierarchy (cgroupsv2).
'';
};
systemd.extraConfig = mkOption {
default = "";
type = types.lines;
example = "DefaultLimitCORE=infinity";
description = ''
Extra config options for systemd. See man systemd-system.conf for
available options.
'';
};
systemd.sleep.extraConfig = mkOption {
default = "";
type = types.lines;
example = "HibernateDelaySec=1h";
description = ''
Extra config options for systemd sleep state logic.
See sleep.conf.d(5) man page for available options.
'';
};
systemd.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.
'';
};
systemd.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</option>, and
<option>systemd.additionalUpstreamSystemUnits</option>. 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.
'';
};
systemd.watchdog.device = mkOption {
type = types.nullOr types.path;
default = null;
example = "/dev/watchdog";
description = ''
The path to a hardware watchdog device which will be managed by systemd.
If not specified, systemd will default to /dev/watchdog.
'';
};
systemd.watchdog.runtimeTime = mkOption {
type = types.nullOr types.str;
default = null;
example = "30s";
description = ''
The amount of time which can elapse before a watchdog hardware device
will automatically reboot the system. Valid time units include "ms",
"s", "min", "h", "d", and "w".
'';
};
systemd.watchdog.rebootTime = mkOption {
type = types.nullOr types.str;
default = null;
example = "10m";
description = ''
The amount of time which can elapse after a reboot has been triggered
before a watchdog hardware device will automatically reboot the system.
Valid time units include "ms", "s", "min", "h", "d", and "w".
'';
};
systemd.watchdog.kexecTime = mkOption {
type = types.nullOr types.str;
default = null;
example = "10m";
description = ''
The amount of time which can elapse when kexec is being executed before
a watchdog hardware device will automatically reboot the system. This
option should only be enabled if reloadTime is also enabled. Valid
time units include "ms", "s", "min", "h", "d", and "w".
'';
};
};
###### implementation
config = {
warnings = 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
);
system.build.units = cfg.units;
system.nssModules = [ systemd.out ];
system.nssDatabases = {
hosts = (mkMerge [
(mkOrder 400 ["mymachines"]) # 400 to ensure it comes before resolve (which is mkBefore'd)
(mkOrder 999 ["myhostname"]) # after files (which is 998), but before regular nss modules
]);
passwd = (mkMerge [
(mkAfter [ "systemd" ])
]);
group = (mkMerge [
(mkAfter [ "systemd" ])
]);
};
environment.systemPackages = [ systemd ];
environment.etc = let
# generate contents for /etc/systemd/system-${type} from attrset of links and packages
hooks = type: links: pkgs.runCommand "system-${type}" {
preferLocalBuild = true;
packages = cfg.packages;
} ''
set -e
mkdir -p $out
for package in $packages
do
for hook in $package/lib/systemd/system-${type}/*
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 = ''
[Manager]
ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)}
${optionalString config.systemd.enableCgroupAccounting ''
DefaultCPUAccounting=yes
DefaultIOAccounting=yes
DefaultBlockIOAccounting=yes
DefaultIPAccounting=yes
''}
DefaultLimitCORE=infinity
${optionalString (config.systemd.watchdog.device != null) ''
WatchdogDevice=${config.systemd.watchdog.device}
''}
${optionalString (config.systemd.watchdog.runtimeTime != null) ''
RuntimeWatchdogSec=${config.systemd.watchdog.runtimeTime}
''}
${optionalString (config.systemd.watchdog.rebootTime != null) ''
RebootWatchdogSec=${config.systemd.watchdog.rebootTime}
''}
${optionalString (config.systemd.watchdog.kexecTime != null) ''
KExecWatchdogSec=${config.systemd.watchdog.kexecTime}
''}
${config.systemd.extraConfig}
'';
"systemd/sleep.conf".text = ''
[Sleep]
${config.systemd.sleep.extraConfig}
'';
"systemd/system-generators" = { source = hooks "generators" cfg.generators; };
"systemd/system-shutdown" = { source = hooks "shutdown" cfg.shutdown; };
});
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;
};
systemd.units =
mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit n v)) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit n v)) cfg.slices
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers
// listToAttrs (map
(v: let n = escapeSystemdPath v.where;
in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts)
// listToAttrs (map
(v: let n = escapeSystemdPath v.where;
in nameValuePair "${n}.automount" (automountToUnit n v)) 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
PATH = lib.makeBinPath config.system.fsPackages;
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}:";
};
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" "AUTOFS4_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 = [ config.system.path ];
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.targets.network-online.wantedBy = [ "multi-user.target" ];
systemd.services.systemd-importd.environment = proxy_env;
systemd.services.systemd-pstore.wantedBy = [ "sysinit.target" ]; # see #81138
# Don't bother with certain units in containers.
systemd.services.systemd-remount-fs.unitConfig.ConditionVirtualization = "!container";
systemd.services.systemd-random-seed.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.is64bit (lib.mkDefault 4194304);
boot.kernelParams = optional (!cfg.enableUnifiedCgroupHierarchy) "systemd.unified_cgroup_hierarchy=0";
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";
};
};
};
# 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.")
];
}

View file

@ -0,0 +1,57 @@
{ 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</command>. 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 coredump.conf(5) man page
for available options.
'';
};
};
config = {
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 = "${systemd}/example/sysctl.d/50-coredump.conf";
"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 = {};
boot.kernel.sysctl."kernel.core_pattern" = mkIf (!cfg.enable) "core";
};
}

View file

@ -0,0 +1,36 @@
{ 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" ];
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); do
mkdir -p "$(dirname "/$secret")"
cp "/.initrd-secrets/$secret" "/$secret"
done
'';
unitConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
# The script needs this
boot.initrd.systemd.extraBin.find = "${pkgs.findutils}/bin/find";
};
}

View file

@ -0,0 +1,502 @@
{ lib, config, utils, pkgs, ... }:
with lib;
let
inherit (utils) systemdUtils escapeSystemdPath;
inherit (systemdUtils.lib)
generateUnits
pathToUnit
serviceToUnit
sliceToUnit
socketToUnit
targetToUnit
timerToUnit
mountToUnit
automountToUnit;
cfg = config.boot.initrd.systemd;
# Copied from fedora
upstreamUnits = [
"basic.target"
"ctrl-alt-del.target"
"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"
"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"
"systemd-tmpfiles-setup-dev.service"
"systemd-tmpfiles-setup.service"
"timers.target"
"umount.target"
# TODO: Networking
# "network-online.target"
# "network-pre.target"
# "network.target"
# "nss-lookup.target"
# "nss-user-lookup.target"
# "remote-fs-pre.target"
# "remote-fs.target"
] ++ 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;
};
fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
fstab = pkgs.writeText "initrd-fstab" (lib.concatMapStringsSep "\n"
({ fsType, mountPoint, device, options, autoFormat, autoResize, ... }@fs: let
opts = options ++ optional autoFormat "x-systemd.makefs" ++ optional autoResize "x-systemd.growfs";
in "${device} /sysroot${mountPoint} ${fsType} ${lib.concatStringsSep "," opts}") fileSystems);
needMakefs = lib.any (fs: fs.autoFormat) fileSystems;
needGrowfs = lib.any (fs: fs.autoResize) fileSystems;
kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
modulesTree = config.system.modulesTree.override { name = kernel-name + "-modules"; };
firmware = config.hardware.firmware;
# 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 = modulesTree;
firmware = firmware;
allowMissing = false;
};
initrdBinEnv = pkgs.buildEnv {
name = "initrd-bin-env";
paths = map getBin cfg.initrdBin;
pathsToLink = ["/bin" "/sbin"];
postBuild = concatStringsSep "\n" (mapAttrsToList (n: v: "ln -s '${v}' $out/bin/'${n}'") cfg.extraBin);
};
initialRamdisk = pkgs.makeInitrdNG {
name = "initrd-${kernel-name}";
inherit (config.boot.initrd) compressor compressorArgs prepend;
contents = map (path: { object = path; symlink = ""; }) (subtractLists cfg.suppressedStorePaths cfg.storePaths)
++ mapAttrsToList (_: v: { object = v.source; symlink = v.target; }) (filterAttrs (_: v: v.enable) cfg.contents);
};
in {
options.boot.initrd.systemd = {
enable = mkEnableOption ''systemd in initrd.
Note: This is in very early development and is highly
experimental. Most of the features NixOS supports in initrd are
not yet supported by the intrd generated with this option.
'';
package = (mkPackageOption pkgs "systemd" {
default = "systemdStage1";
}) // {
visible = false;
};
contents = mkOption {
description = "Set of files that have to be linked into the initrd";
example = literalExpression ''
{
"/etc/hostname".text = "mymachine";
}
'';
visible = false;
default = {};
type = utils.systemdUtils.types.initrdContents;
};
storePaths = mkOption {
description = ''
Store paths to copy into the initrd as well.
'';
type = with types; listOf (oneOf [ singleLineStr package ]);
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 = [];
};
emergencyAccess = mkOption {
type = with types; oneOf [ bool singleLineStr ];
visible = false;
description = ''
Set to true for unauthenticated emergency access, and false for
no emergency access.
Can also be set to a hashed super user password to allow
authenticated access to the emergency mode.
'';
default = false;
};
initrdBin = mkOption {
type = types.listOf types.package;
default = [];
visible = false;
description = ''
Packages to include in /bin for the stage 1 emergency shell.
'';
};
additionalUpstreamUnits = mkOption {
default = [ ];
type = types.listOf types.str;
visible = false;
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" ];
visible = false;
description = ''
A list of units to skip when generating system systemd configuration directory. This has
priority over upstream units, <option>boot.initrd.systemd.units</option>, and
<option>boot.initrd.systemd.additionalUpstreamUnits</option>. 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 = false;
type = systemdUtils.types.units;
};
packages = mkOption {
default = [];
visible = false;
type = types.listOf types.package;
example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
description = "Packages providing systemd units and hooks.";
};
targets = mkOption {
default = {};
visible = false;
type = systemdUtils.types.initrdTargets;
description = "Definition of systemd target units.";
};
services = mkOption {
default = {};
type = systemdUtils.types.initrdServices;
visible = false;
description = "Definition of systemd service units.";
};
sockets = mkOption {
default = {};
type = systemdUtils.types.initrdSockets;
visible = false;
description = "Definition of systemd socket units.";
};
timers = mkOption {
default = {};
type = systemdUtils.types.initrdTimers;
visible = false;
description = "Definition of systemd timer units.";
};
paths = mkOption {
default = {};
type = systemdUtils.types.initrdPaths;
visible = false;
description = "Definition of systemd path units.";
};
mounts = mkOption {
default = [];
type = systemdUtils.types.initrdMounts;
visible = false;
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 = false;
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 = false;
description = "Definition of slice configurations.";
};
};
config = mkIf (config.boot.initrd.enable && cfg.enable) {
system.build = { inherit initialRamdisk; };
boot.initrd.availableKernelModules = [ "autofs4" ]; # systemd needs this for some features
boot.initrd.systemd = {
initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;
extraBin = {
less = "${pkgs.less}/bin/less";
mount = "${cfg.package.util-linux}/bin/mount";
umount = "${cfg.package.util-linux}/bin/umount";
};
contents = {
"/init".source = "${cfg.package}/lib/systemd/systemd";
"/etc/systemd/system".source = stage1Units;
"/etc/systemd/system.conf".text = ''
[Manager]
DefaultEnvironment=PATH=/bin:/sbin ${optionalString (isBool cfg.emergencyAccess && cfg.emergencyAccess) "SYSTEMD_SULOGIN_FORCE=1"}
'';
"/etc/fstab".source = fstab;
"/lib/modules".source = "${modulesClosure}/lib/modules";
"/lib/firmware".source = "${modulesClosure}/lib/firmware";
"/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules;
"/etc/passwd".source = "${pkgs.fakeNss}/etc/passwd";
"/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then "!" else cfg.emergencyAccess}:::::::";
"/bin".source = "${initrdBinEnv}/bin";
"/sbin".source = "${initrdBinEnv}/sbin";
"/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.runCommand "initrd-kmod-blacklist-ubuntu" { } ''
${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out
'';
"/etc/modprobe.d/debian.conf".source = pkgs.kmod-debian-aliases;
};
storePaths = [
# systemd tooling
"${cfg.package}/lib/systemd/systemd-fsck"
(lib.mkIf needGrowfs "${cfg.package}/lib/systemd/systemd-growfs")
"${cfg.package}/lib/systemd/systemd-hibernate-resume"
"${cfg.package}/lib/systemd/systemd-journald"
(lib.mkIf needMakefs "${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"
# 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"
# so NSS can look up usernames
"${pkgs.glibc}/lib/libnss_files.so.2"
] ++ jobScripts;
targets.initrd.aliases = ["default.target"];
units =
mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit n v)) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit n v)) cfg.slices
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers
// listToAttrs (map
(v: let n = escapeSystemdPath v.where;
in nameValuePair "${n}.mount" (mountToUnit n v)) cfg.mounts)
// listToAttrs (map
(v: let n = escapeSystemdPath v.where;
in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts);
# The unit in /run/systemd/generator shadows the unit in
# /etc/systemd/system, but will still apply drop-ins from
# /etc/systemd/system/foo.service.d/
#
# We need IgnoreOnIsolate, otherwise the Requires dependency of
# a mount unit on its makefs unit causes it to be unmounted when
# we isolate for switch-root. Use a dummy package so that
# generateUnits will generate drop-ins instead of unit files.
packages = [(pkgs.runCommand "dummy" {} ''
mkdir -p $out/etc/systemd/system
touch $out/etc/systemd/system/systemd-{makefs,growfs}@.service
'')];
services."systemd-makefs@" = lib.mkIf needMakefs { unitConfig.IgnoreOnIsolate = true; };
services."systemd-growfs@" = lib.mkIf needGrowfs { unitConfig.IgnoreOnIsolate = true; };
services.initrd-nixos-activation = {
after = [ "initrd-fs.target" ];
requiredBy = [ "initrd.target" ];
unitConfig.AssertPathExists = "/etc/initrd-release";
serviceConfig.Type = "oneshot";
description = "NixOS Activation";
script = /* bash */ ''
set -uo pipefail
export PATH="/bin:${cfg.package.util-linux}/bin"
# Figure out what closure to boot
closure=
for o in $(< /proc/cmdline); do
case $o in
init=*)
IFS== read -r -a initParam <<< "$o"
closure="$(dirname "''${initParam[1]}")"
;;
esac
done
# Sanity check
if [ -z "''${closure:-}" ]; then
echo 'No init= parameter on the kernel command line' >&2
exit 1
fi
# 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$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.
mkdir -p /sysroot/run
mount --bind /run /sysroot/run
# Initialize the system
export IN_NIXOS_SYSTEMD_STAGE1=true
exec chroot /sysroot $closure/prepare-root
'';
};
# This will either call systemctl with the new init as the last parameter (which
# is the case when not booting a NixOS system) or with an empty string, causing
# systemd to bypass its verification code that checks whether the next file is a systemd
# and using its compiled-in value
services.initrd-switch-root.serviceConfig = {
EnvironmentFile = "-/etc/switch-root.conf";
ExecStart = [
""
''systemctl --no-block switch-root /sysroot "''${NEW_INIT}"''
];
};
services.panic-on-fail = {
wantedBy = ["emergency.target"];
unitConfig = {
DefaultDependencies = false;
ConditionKernelCommandLine = [
"|boot.panic_on_fail"
"|stage1panic"
];
};
script = ''
echo c > /proc/sysrq-trigger
'';
serviceConfig.Type = "oneshot";
};
};
boot.kernelParams = lib.mkIf (config.boot.resumeDevice != "") [ "resume=${config.boot.resumeDevice}" ];
};
}

View file

@ -0,0 +1,131 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.journald;
in {
options = {
services.journald.console = mkOption {
default = "";
type = types.str;
description = "If non-empty, write log messages to the specified TTY device.";
};
services.journald.rateLimitInterval = mkOption {
default = "30s";
type = 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</option> for important
considerations when setting this value.
'';
};
services.journald.rateLimitBurst = mkOption {
default = 10000;
type = 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
<link xlink:href="https://www.freedesktop.org/software/systemd/man/journald.conf.html">
journald.conf(5)</link>.
Note that the total amount of logs stored is limited by journald settings
such as <literal>SystemMaxUse</literal>, which defaults to a 4 GB cap.
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.extraConfig = mkOption {
default = "";
type = types.lines;
example = "Storage=volatile";
description = ''
Extra config options for systemd-journald. See man journald.conf
for available options.
'';
};
services.journald.enableHttpGateway = mkOption {
default = false;
type = types.bool;
description = ''
Whether to enable the HTTP gateway to the journal.
'';
};
services.journald.forwardToSyslog = mkOption {
default = config.services.rsyslogd.enable || config.services.syslog-ng.enable;
defaultText = literalExpression "services.rsyslogd.enable || services.syslog-ng.enable";
type = 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"
] ++ (optional (!config.boot.isContainer) "systemd-journald-audit.socket") ++ [
"systemd-journald-dev-log.socket"
"syslog.socket"
] ++ optionals cfg.enableHttpGateway [
"systemd-journal-gatewayd.socket"
"systemd-journal-gatewayd.service"
];
environment.etc = {
"systemd/journald.conf".text = ''
[Journal]
Storage=persistent
RateLimitInterval=${cfg.rateLimitInterval}
RateLimitBurst=${toString cfg.rateLimitBurst}
${optionalString (cfg.console != "") ''
ForwardToConsole=yes
TTYPath=${cfg.console}
''}
${optionalString (cfg.forwardToSyslog) ''
ForwardToSyslog=yes
''}
${cfg.extraConfig}
'';
};
users.groups.systemd-journal.gid = config.ids.gids.systemd-journal;
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.sockets.systemd-journal-gatewayd.wantedBy =
optional cfg.enableHttpGateway "sockets.target";
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,117 @@
{ config, lib, pkgs, utils, ... }:
with lib;
let
cfg = config.services.logind;
logindHandlerType = types.enum [
"ignore" "poweroff" "reboot" "halt" "kexec" "suspend"
"hibernate" "hybrid-sleep" "suspend-then-hibernate" "lock"
];
in
{
options = {
services.logind.extraConfig = mkOption {
default = "";
type = types.lines;
example = "IdleAction=lock";
description = ''
Extra config options for systemd-logind. See
<link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html">
logind.conf(5)</link> for available options.
'';
};
services.logind.killUserProcesses = mkOption {
default = false;
type = 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
<link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.scope.html#">
systemd.scope(5)</link>), and processes are not killed.
</para>
<para>
See <link xlink:href="https://www.freedesktop.org/software/systemd/man/logind.conf.html#KillUserProcesses=">logind.conf(5)</link>
for more details.
'';
};
services.logind.lidSwitch = mkOption {
default = "suspend";
example = "ignore";
type = logindHandlerType;
description = ''
Specifies what to be done when the laptop lid is closed.
'';
};
services.logind.lidSwitchDocked = mkOption {
default = "ignore";
example = "suspend";
type = logindHandlerType;
description = ''
Specifies what to be done when the laptop lid is closed
and another screen is added.
'';
};
services.logind.lidSwitchExternalPower = mkOption {
default = cfg.lidSwitch;
defaultText = literalExpression "services.logind.lidSwitch";
example = "ignore";
type = logindHandlerType;
description = ''
Specifies what to do when the laptop lid is closed and the system is
on external power. By default use the same action as specified in
services.logind.lidSwitch.
'';
};
};
config = {
systemd.additionalUpstreamSystemUnits = [
"systemd-logind.service"
"autovt@.service"
"systemd-user-sessions.service"
] ++ optionals config.systemd.package.withImportd [
"dbus-org.freedesktop.import1.service"
] ++ optionals config.systemd.package.withMachined [
"dbus-org.freedesktop.machine1.service"
] ++ [
"dbus-org.freedesktop.login1.service"
"user@.service"
"user-runtime-dir@.service"
];
environment.etc = {
"systemd/logind.conf".text = ''
[Login]
KillUserProcesses=${if cfg.killUserProcesses then "yes" else "no"}
HandleLidSwitch=${cfg.lidSwitch}
HandleLidSwitchDocked=${cfg.lidSwitchDocked}
HandleLidSwitchExternalPower=${cfg.lidSwitchExternalPower}
${cfg.extraConfig}
'';
};
# 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;
};
}

View file

@ -0,0 +1,133 @@
{ 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" "PrivateUserOwnership"
])
(assertValueOneOf "ReadOnly" boolValues)
(assertValueOneOf "Volatile" (boolValues ++ [ "state" ]))
(assertValueOneOf "PrivateUsersChown" boolValues)
(assertValueOneOf "PrivateUserOwnership" [ "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 = 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
<literal>[Exec]</literal> section of this unit. See
<citerefentry><refentrytitle>systemd.nspawn</refentrytitle>
<manvolnum>5</manvolnum></citerefentry> 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
<literal>[Files]</literal> section of this unit. See
<citerefentry><refentrytitle>systemd.nspawn</refentrytitle>
<manvolnum>5</manvolnum></citerefentry> 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
<literal>[Network]</literal> section of this unit. See
<citerefentry><refentrytitle>systemd.nspawn</refentrytitle>
<manvolnum>5</manvolnum></citerefentry> 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" ];
}
];
}

View file

@ -0,0 +1,58 @@
{ config, lib, utils, pkgs, ... }: let
cfg = config.systemd.shutdownRamfs;
ramfsContents = let
storePaths = map (p: "${p}\n") cfg.storePaths;
contents = lib.mapAttrsToList (_: v: "${v.source}\n${v.target}") (lib.filterAttrs (_: v: v.enable) cfg.contents);
in pkgs.writeText "shutdown-ramfs-contents" (lib.concatStringsSep "\n" (storePaths ++ contents));
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 = lib.types.listOf lib.types.singleLineStr;
default = [];
};
};
config = lib.mkIf cfg.enable {
systemd.shutdownRamfs.contents."/shutdown".source = "${config.systemd.package}/lib/systemd/systemd-shutdown";
systemd.shutdownRamfs.storePaths = [pkgs.runtimeShell "${pkgs.coreutils}/bin"];
systemd.services.generate-shutdown-ramfs = {
description = "Generate shutdown ramfs";
wantedBy = [ "shutdown.target" ];
before = [ "shutdown.target" ];
unitConfig = {
DefaultDependencies = false;
ConditionFileIsExecutable = [
"!/run/initramfs/shutdown"
];
};
path = [pkgs.util-linux pkgs.makeInitrdNGTool];
serviceConfig.Type = "oneshot";
script = ''
mkdir -p /run/initramfs
if ! mountpoint -q /run/initramfs; then
mount -t tmpfs tmpfs /run/initramfs
fi
make-initrd-ng ${ramfsContents} /run/initramfs
'';
};
};
}

View file

@ -0,0 +1,120 @@
{ config, lib, pkgs, utils, ... }:
with lib;
let
cfg = config.systemd.tmpfiles;
systemd = config.systemd.package;
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
<citerefentry><refentrytitle>tmpfiles.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for the exact format.
'';
};
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</command> rules.
All files ending in .conf found in
<filename><replaceable>pkg</replaceable>/lib/tmpfiles.d</filename>
will be included.
If this folder does not exist or does not contain any files an error will be returned instead.
If a <filename>lib</filename> output is available, rules are searched there and only there.
If there is no <filename>lib</filename> output it will fall back to <filename>out</filename>
and if that does not exist either, the default output will be used.
'';
};
};
config = {
systemd.additionalUpstreamSystemUnits = [
"systemd-tmpfiles-clean.service"
"systemd-tmpfiles-clean.timer"
"systemd-tmpfiles-setup.service"
"systemd-tmpfiles-setup-dev.service"
];
systemd.additionalUpstreamUserUnits = [
"systemd-tmpfiles-clean.service"
"systemd-tmpfiles-clean.timer"
"systemd-tmpfiles-setup.service"
];
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;
}) + "/*";
};
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/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}
'';
})
];
systemd.tmpfiles.rules = [
"d /nix/var 0755 root root - -"
"L+ /nix/var/nix/gcroots/booted-system 0755 root root - /run/booted-system"
"d /run/lock 0755 root root - -"
"d /var/db 0755 root root - -"
"L /etc/mtab - - - - ../proc/mounts"
"L /var/lock - - - - ../run/lock"
# Boot-time cleanup
"R! /etc/group.lock - - - - -"
"R! /etc/passwd.lock - - - - -"
"R! /etc/shadow.lock - - - - -"
"R! /etc/mtab* - - - - -"
"R! /nix/var/nix/gcroots/tmp - - - - -"
"R! /nix/var/nix/temproots - - - - -"
];
};
}

View file

@ -0,0 +1,153 @@
{ 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
socketToUnit
timerToUnit
pathToUnit;
upstreamUserUnits = [
"app.slice"
"background.slice"
"basic.target"
"bluetooth.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;
in {
options = {
systemd.user.extraConfig = mkOption {
default = "";
type = types.lines;
example = "DefaultCPUAccounting=yes";
description = ''
Extra config options for systemd user instances. See man systemd-user.conf 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.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 n v)) cfg.paths
// mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services
// mapAttrs' (n: v: nameValuePair "${n}.slice" (sliceToUnit n v)) cfg.slices
// mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.sockets
// mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets
// mapAttrs' (n: v: nameValuePair "${n}.timer" (timerToUnit n v)) cfg.timers;
# Generate timer units for all services that have a startAt value.
systemd.user.timers =
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;
};
# Some overrides to upstream units.
systemd.services."user@".restartIfChanged = false;
systemd.services.systemd-user-sessions.restartIfChanged = false; # Restart kills all active sessions.
};
}

View file

@ -0,0 +1,86 @@
{ config, lib, ... }:
with lib;
{
options = {
services.timesyncd = {
enable = mkOption {
default = !config.boot.isContainer;
defaultText = literalExpression "!config.boot.isContainer";
type = types.bool;
description = ''
Enables the systemd NTP client daemon.
'';
};
servers = mkOption {
default = config.networking.timeServers;
defaultText = literalExpression "config.networking.timeServers";
type = types.listOf types.str;
description = ''
The set of NTP servers from which to synchronise.
'';
};
extraConfig = mkOption {
default = "";
type = types.lines;
example = ''
PollIntervalMaxSec=180
'';
description = ''
Extra config options for systemd-timesyncd. See
<link xlink:href="https://www.freedesktop.org/software/systemd/man/timesyncd.conf.html">
timesyncd.conf(5)</link> for available options.
'';
};
};
};
config = mkIf config.services.timesyncd.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 ];
};
environment.etc."systemd/timesyncd.conf".text = ''
[Time]
NTP=${concatStringsSep " " config.services.timesyncd.servers}
${config.services.timesyncd.extraConfig}
'';
users.users.systemd-timesync = {
uid = config.ids.uids.systemd-timesync;
group = "systemd-timesync";
};
users.groups.systemd-timesync.gid = config.ids.gids.systemd-timesync;
system.activationScripts.systemd-timesyncd-migration =
# 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
mkIf (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
'';
system.activationScripts.systemd-timesyncd-init-clock =
# 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
'';
};
}

View file

@ -0,0 +1,64 @@
{ config, lib, ... }:
with lib;
let
cfg = config.boot;
in
{
###### interface
options = {
boot.cleanTmpDir = mkOption {
type = types.bool;
default = false;
description = ''
Whether to delete all files in <filename>/tmp</filename> during boot.
'';
};
boot.tmpOnTmpfs = mkOption {
type = types.bool;
default = false;
description = ''
Whether to mount a tmpfs on <filename>/tmp</filename> during boot.
'';
};
boot.tmpOnTmpfsSize = mkOption {
type = types.oneOf [ types.str types.types.ints.positive ];
default = "50%";
description = ''
Size of tmpfs in percentage.
Percentage is defined by systemd.
'';
};
};
###### implementation
config = {
# When changing remember to update /tmp mount in virtualisation/qemu-vm.nix
systemd.mounts = mkIf cfg.tmpOnTmpfs [
{
what = "tmpfs";
where = "/tmp";
type = "tmpfs";
mountConfig.Options = concatStringsSep "," [ "mode=1777"
"strictatime"
"rw"
"nosuid"
"nodev"
"size=${toString cfg.tmpOnTmpfsSize}" ];
}
];
systemd.tmpfiles.rules = optional config.boot.cleanTmpDir "D! /tmp 1777 root root";
};
}

View file

@ -0,0 +1,21 @@
{ 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,12 @@
{ config, lib, ... }:
let
inherit (lib) stringAfter;
in {
imports = [ ./etc.nix ];
config = {
system.activationScripts.etc =
stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands;
};
}

View file

@ -0,0 +1,201 @@
# Management of static files in /etc.
{ config, lib, pkgs, ... }:
with lib;
let
etc' = filter (f: f.enable) (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
continue
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"
${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [
"makeEtcEntry"
# Force local source paths to be added to the store
"${etcEntry.source}"
etcEntry.target
etcEntry.mode
etcEntry.user
etcEntry.group
]) etc'}
'';
in
{
imports = [ ../build.nix ];
###### interface
options = {
environment.etc = mkOption {
default = {};
example = 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 <filename>/etc</filename>.
'';
type = with types; attrsOf (submodule (
{ name, config, options, ... }:
{ options = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether this /etc file should be generated. This
option allows specific /etc files to be disabled.
'';
};
target = mkOption {
type = types.str;
description = ''
Name of symlink (relative to
<filename>/etc</filename>). Defaults to the attribute
name.
'';
};
text = mkOption {
default = null;
type = types.nullOr types.lines;
description = "Text of the file.";
};
source = mkOption {
type = types.path;
description = "Path of the source file.";
};
mode = mkOption {
type = types.str;
default = "symlink";
example = "0600";
description = ''
If set to something else than <literal>symlink</literal>,
the file is copied instead of symlinked, with the given
file mode.
'';
};
uid = mkOption {
default = 0;
type = types.int;
description = ''
UID of created file. Only takes effect when the file is
copied (that is, the mode is not 'symlink').
'';
};
gid = mkOption {
default = 0;
type = types.int;
description = ''
GID of created file. Only takes effect when the file is
copied (that is, the mode is not 'symlink').
'';
};
user = mkOption {
default = "+${toString config.uid}";
type = types.str;
description = ''
User name of created file.
Only takes effect when the file is copied (that is, the mode is not 'symlink').
Changing this option takes precedence over <literal>uid</literal>.
'';
};
group = mkOption {
default = "+${toString config.gid}";
type = types.str;
description = ''
Group name of created file.
Only takes effect when the file is copied (that is, the mode is not 'symlink').
Changing this option takes precedence over <literal>gid</literal>.
'';
};
};
config = {
target = mkDefault name;
source = mkIf (config.text != null) (
let name' = "etc-" + baseNameOf name;
in mkDerivedConfig options.text (pkgs.writeText name')
);
};
}));
};
};
###### implementation
config = {
system.build.etc = etc;
system.build.etcActivationCommands =
''
# 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
'';
};
}

View file

@ -0,0 +1,146 @@
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;
rename $tmp, $target or return 0;
return 1;
}
# 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;
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;
} 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;
rename "$target.tmp", $target or warn;
}
push @copied, $fn;
print CLEAN "$fn\n";
} elsif (-l "$_") {
atomicSymlink "$static/$fn", $target or warn;
}
}
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" } @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,70 @@
{ 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
fakechroot fakeroot chroot $PWD/fake-root bash -c 'source $stdenv/setup; eval "$fakeRootCommands"'
'';
}