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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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