add hardware config for Raspberry PIs, start making scripts more robust, improve compatibility with containers

This commit is contained in:
Niklas Gollenstede 2022-11-30 13:41:21 +01:00
parent 1d93a8acc0
commit df8c451050
27 changed files with 504 additions and 243 deletions

View File

@ -1,6 +1,5 @@
{ {
"recommendations": [ "recommendations": [
"jnoortheen.nix-ide",
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
], ],
} }

21
.vscode/settings.json vendored
View File

@ -10,6 +10,7 @@
"attrset", "attrsets", // nix/abbr (attribute set) "attrset", "attrsets", // nix/abbr (attribute set)
"autologin", // agetty "autologin", // agetty
"binfmt", // abbr "binary format" "binfmt", // abbr "binary format"
"blkdiscard", // program
"blkid", // program / function "blkid", // program / function
"blockdev", // program / function "blockdev", // program / function
"bootable", // word (~= able to boot) "bootable", // word (~= able to boot)
@ -20,6 +21,7 @@
"builtins", // nix "builtins", // nix
"cachefile", // zfs "cachefile", // zfs
"canmount", // zfs "canmount", // zfs
"cdetect", // abbr
"checksumming", // word "checksumming", // word
"cmake", // program "cmake", // program
"cmds", // abbr (commands) "cmds", // abbr (commands)
@ -36,22 +38,29 @@
"dedup", // zfs "dedup", // zfs
"deps", // abbr dependencies "deps", // abbr dependencies
"devs", // abbr (devices) "devs", // abbr (devices)
"dir_nlink", // ext4 option
"dmask", // mount "dmask", // mount
"dnodesize", // zfs "dnodesize", // zfs
"dontUnpack", // nixos "dontUnpack", // nixos
"dosfstools", // package "dosfstools", // package
"draid", // zfs "draid", // zfs
"dropbear", // program "dropbear", // program
"dtoverlay", // option
"dtparam", // option
"e2fsprogs", // package "e2fsprogs", // package
"eeprom", // abbr
"elif", // abbr (else if) "elif", // abbr (else if)
"encryptionroot", // zfs "encryptionroot", // zfs
"extglob", // cli arg "extglob", // cli arg
"extlinux", // program
"extra_isize", // ext4 option
"fallocate", // program / function "fallocate", // program / function
"fdisk", // program "fdisk", // program
"fetchpatch", // nix "fetchpatch", // nix
"fetchurl", // nix function "fetchurl", // nix function
"filesystems", // plural "filesystems", // plural
"findutils", // package "findutils", // package
"firmwareLinuxNonfree", // nixos
"fmask", // mount "fmask", // mount
"foldl", // nix (fold left) "foldl", // nix (fold left)
"foldr", // nix (fold right) "foldr", // nix (fold right)
@ -66,6 +75,7 @@
"gnugrep", // package "gnugrep", // package
"gnused", // package "gnused", // package
"gollenstede", // name "gollenstede", // name
"gpio", // abbr (general purpose IO)
"gptfdisk", // package "gptfdisk", // package
"headlessly", // word "headlessly", // word
"hostbus", // cli arg "hostbus", // cli arg
@ -82,8 +92,11 @@
"keylocation", // zfs "keylocation", // zfs
"keystatus", // zfs "keystatus", // zfs
"kmod", // linux "kmod", // linux
"lazy_itable_init", // ext4 option
"lazytime", // f2fs "lazytime", // f2fs
"leds", // plural
"libblockdev", // package "libblockdev", // package
"libraspberrypi", // program
"libubootenv", // package "libubootenv", // package
"logbias", // zfs "logbias", // zfs
"losetup", // program / function "losetup", // program / function
@ -91,9 +104,11 @@
"lsusb", // program / function "lsusb", // program / function
"luks", // linux "luks", // linux
"macaddr", // cli arg "macaddr", // cli arg
"metadata_csum", // ext4 option
"mkdir", // program / function "mkdir", // program / function
"mkenvimage", // program "mkenvimage", // program
"mktemp", // program / function "mktemp", // program / function
"mmap", // abbr "memory map"
"modifyvm", // virtual box "modifyvm", // virtual box
"mountpoint", // program / function "mountpoint", // program / function
"mtab", // linux "mtab", // linux
@ -114,6 +129,7 @@
"noheadings", // cli arg "noheadings", // cli arg
"nohibernate", // kernel param "nohibernate", // kernel param
"nosuid", // mount option "nosuid", // mount option
"ondemand", // concat
"oneshot", // systemd "oneshot", // systemd
"optimise", // B/E "optimise", // B/E
"ostype", // virtual box "ostype", // virtual box
@ -137,8 +153,11 @@
"raspi3b", // cli arg "raspi3b", // cli arg
"rawdisk", // virtual box "rawdisk", // virtual box
"realpath", // program / function "realpath", // program / function
"redistributable", // word
"refreservation", // zfs "refreservation", // zfs
"relatime", // mount option "relatime", // mount option
"rpiboot", // package
"rpicm4", // abbr (Raspberry PI Compute Module 4)
"rpool", // zfs "rpool", // zfs
"rprivate", // linux "rprivate", // linux
"sata", // storage protocol "sata", // storage protocol
@ -176,7 +195,9 @@
"upperdir", // mount overlay option "upperdir", // mount overlay option
"upstreamed", // word "upstreamed", // word
"urandom", // linux "urandom", // linux
"usbhid", // kmod
"vboxusers", // virtual box "vboxusers", // virtual box
"vcgencmd", // program
"vdev", "vdevs", // zfs "vdev", "vdevs", // zfs
"vfat", // linux "vfat", // linux
"virt", // abbr (virtualization) "virt", // abbr (virtualization)

Binary file not shown.

View File

@ -6,6 +6,7 @@
# To update »./flake.lock«: $ nix flake update # To update »./flake.lock«: $ nix flake update
nixpkgs = { url = "github:NixOS/nixpkgs/nixos-22.05"; }; nixpkgs = { url = "github:NixOS/nixpkgs/nixos-22.05"; };
nixos-hardware = { url = "github:NixOS/nixos-hardware/master"; };
config = { type = "github"; owner = "NiklasGollenstede"; repo = "nix-wiplib"; dir = "example/defaultConfig"; rev = "5e9cc7ce3440be9ce6aeeaedcc70db9c80489c5f"; }; # Use some previous commit's »./example/defaultConfig/flake.nix« as the default config for this flake. config = { type = "github"; owner = "NiklasGollenstede"; repo = "nix-wiplib"; dir = "example/defaultConfig"; rev = "5e9cc7ce3440be9ce6aeeaedcc70db9c80489c5f"; }; # Use some previous commit's »./example/defaultConfig/flake.nix« as the default config for this flake.
}; outputs = inputs: let patches = { }; outputs = inputs: let patches = {
@ -20,7 +21,7 @@
in [ # Run »nix flake show --allow-import-from-derivation« to see what this merges to: in [ # Run »nix flake show --allow-import-from-derivation« to see what this merges to:
repo # lib.* nixosModules.* overlays.* repo # lib.* nixosModules.* overlays.*
(lib.wip.mkSystemsFlake { inherit inputs; }) # nixosConfigurations.* apps.*-linux.* devShells.*-linux.* packages.*-linux.all-systems (lib.wip.mkSystemsFlake { inherit inputs; moduleInputs = builtins.removeAttrs inputs [ "nixpkgs" "nixos-hardware" ]; }) # nixosConfigurations.* apps.*-linux.* devShells.*-linux.* packages.*-linux.all-systems
(lib.wip.forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: { # packages.*-linux.* defaultPackage.*-linux (lib.wip.forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: { # packages.*-linux.* defaultPackage.*-linux
packages = builtins.removeAttrs (lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays) [ "libblockdev" ]; packages = builtins.removeAttrs (lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays) [ "libblockdev" ];
defaultPackage = self.packages.${localSystem}.all-systems; defaultPackage = self.packages.${localSystem}.all-systems;

View File

@ -31,7 +31,7 @@ dirname: inputs: { config, pkgs, lib, name, ... }: let inherit (inputs.self) lib
#suffix = builtins.head (builtins.match ''example-(.*)'' name); # make differences in config based on this when using »wip.preface.instances« #suffix = builtins.head (builtins.match ''example-(.*)'' name); # make differences in config based on this when using »wip.preface.instances«
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName); hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
in { imports = [ ({ ## Hardware in { imports = [ ({ ## Hardware
wip.preface.instances = [ "example" "example-minimal" "example-raidz" ]; wip.preface.instances = [ "example-explicit" "example" "example-minimal" "example-raidz" ];
wip.preface.hardware = "x86_64"; system.stateVersion = "22.05"; wip.preface.hardware = "x86_64"; system.stateVersion = "22.05";
@ -43,7 +43,7 @@ in { imports = [ ({ ## Hardware
wip.setup.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1000; }; wip.setup.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1000; };
}) (lib.mkIf false { ## Minimal explicit FS setup }) (lib.mkIf (name == "example-explicit") { ## Minimal explicit FS setup
# Declare a boot and system partition. Though not required for EFI, make the boot part visible to boot loaders supporting only MBR. # Declare a boot and system partition. Though not required for EFI, make the boot part visible to boot loaders supporting only MBR.
wip.fs.disks.partitions."boot-${hash}" = { type = "ef00"; size = "64M"; index = 1; order = 1500; }; wip.fs.disks.partitions."boot-${hash}" = { type = "ef00"; size = "64M"; index = 1; order = 1500; };

View File

@ -2,5 +2,5 @@ dirname: inputs@{ self, nixpkgs, ...}: let
#fix = f: let x = f x; in x; #fix = f: let x = f x; in x;
#categories = fix (wip: (import "${dirname}/imports.nix" dirname inputs).importAll (inputs // { self = inputs.self // { lib = nixpkgs.lib // { inherit wip; }; }; })) dirname; #categories = fix (wip: (import "${dirname}/imports.nix" dirname inputs).importAll (inputs // { self = inputs.self // { lib = nixpkgs.lib // { inherit wip; }; }; })) dirname;
categories = (import "${dirname}/imports.nix" dirname inputs).importAll inputs dirname; categories = (import "${dirname}/imports.nix" dirname inputs).importAll inputs dirname;
wip = (builtins.foldl' (a: b: a // b) { } (builtins.attrValues (builtins.removeAttrs categories [ "setup-scripts" ]))) // categories; wip = (builtins.foldl' (a: b: a // (if builtins.isAttrs b then b else { })) { } (builtins.attrValues (builtins.removeAttrs categories [ "setup-scripts" ]))) // categories;
in nixpkgs.lib // { wip = wip // { prefix = inputs.config.prefix; }; } in nixpkgs.lib // { wip = wip // { prefix = inputs.config.prefix; }; }

View File

@ -1,10 +1,11 @@
dirname: inputs@{ self, nixpkgs, ...}: let dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) mapMerge mergeAttrsUnique flipNames; inherit (import "${dirname}/vars.nix" dirname inputs) mapMerge mapMergeUnique mergeAttrsUnique flipNames;
inherit (import "${dirname}/imports.nix" dirname inputs) getModifiedPackages getNixFiles importWrapped; inherit (import "${dirname}/imports.nix" dirname inputs) getModifiedPackages getNixFiles importWrapped;
inherit (import "${dirname}/scripts.nix" dirname inputs) substituteImplicit; inherit (import "${dirname}/scripts.nix" dirname inputs) substituteImplicit;
setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs); setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs);
prefix = inputs.config.prefix; prefix = inputs.config.prefix;
inherit (import "${dirname}/misc.nix" dirname inputs) trace;
in rec { in rec {
# Simplified implementation of »flake-utils.lib.eachSystem«. # Simplified implementation of »flake-utils.lib.eachSystem«.
@ -15,7 +16,7 @@ in rec {
inherit ((import inputs.nixpkgs { overlays = [ ]; config = { }; system = "x86_64-linux"; }).pkgs) applyPatches fetchpatch; inherit ((import inputs.nixpkgs { overlays = [ ]; config = { }; system = "x86_64-linux"; }).pkgs) applyPatches fetchpatch;
in outputs (builtins.mapAttrs (name: input: if name != "self" && patches?${name} && patches.${name} != [ ] then (let in outputs (builtins.mapAttrs (name: input: if name != "self" && patches?${name} && patches.${name} != [ ] then (let
patched = applyPatches { patched = applyPatches {
name = "${name}-patched"; src = input; name = "${name}-patched"; src = "${input}";
patches = map (patch: if patch ? url then fetchpatch patch else patch) patches.${name}; patches = map (patch: if patch ? url then fetchpatch patch else patch) patches.${name};
}; };
sourceInfo = (input.sourceInfo or input) // patched; sourceInfo = (input.sourceInfo or input) // patched;
@ -64,7 +65,7 @@ in rec {
# Given a path to a host config file, returns some properties defined in its first inline module (to be used where accessing them via »nodes.${name}.config...« isn't possible). # Given a path to a host config file, returns some properties defined in its first inline module (to be used where accessing them via »nodes.${name}.config...« isn't possible).
getSystemPreface = inputs: entryPath: args: let getSystemPreface = inputs: entryPath: args: let
imported = (importWrapped inputs entryPath).required ({ config = null; pkgs = null; lib = null; name = null; nodes = null; } // args); imported = (importWrapped inputs entryPath).required ({ config = null; pkgs = null; lib = null; name = null; nodes = null; extraModules = null; } // args);
module = builtins.elemAt imported.imports 0; props = module.${prefix}.preface; module = builtins.elemAt imported.imports 0; props = module.${prefix}.preface;
in if ( in if (
imported?imports && (builtins.isList imported.imports) && (imported.imports != [ ]) && module?${prefix} && module.${prefix}?preface && props?hardware imported?imports && (builtins.isList imported.imports) && (imported.imports != [ ]) && module?${prefix} && module.${prefix}?preface && props?hardware
@ -72,75 +73,80 @@ in rec {
# Builds the System Configuration for a single host. Since each host depends on the context of all other host (in the same "network"), this is essentially only callable through »mkNixosConfigurations«. # Builds the System Configuration for a single host. Since each host depends on the context of all other host (in the same "network"), this is essentially only callable through »mkNixosConfigurations«.
# See »mkSystemsFlake« for documentation of the arguments. # See »mkSystemsFlake« for documentation of the arguments.
mkNixosConfiguration = args@{ name, entryPath, peers, inputs, overlays, modules, nixosSystem, localSystem ? null, ... }: let mkNixosConfiguration = args@{ name, entryPath ? null, config ? null, preface ? null,peers ? { }, inputs ? [ ], overlays ? [ ], modules ? [ ], nixosSystem, localSystem ? null, ... }: let
preface = (getSystemPreface inputs entryPath ({ inherit lib; } // specialArgs)); preface = args.preface or (getSystemPreface inputs entryPath (specialArgs // { inherit name; }));
targetSystem = "${preface.hardware}-linux"; buildSystem = if localSystem != null then localSystem else targetSystem; targetSystem = "${preface.hardware}-linux"; buildSystem = if localSystem != null then localSystem else targetSystem;
specialArgs = { # make these available in the attrSet passed to the modules specialArgs = (args.specialArgs or { }) // {
inherit inputs; # These are global and passed by the caller of this function (or not), so avoid using these (in favor of the own flakes inputs) where possible! nodes = peers; # NixOPS
} // (args.specialArgs or { }) // {
inherit name; nodes = peers; # NixOPS
}; };
in let system = { inherit preface; } // (nixosSystem { in let system = { inherit preface; } // (nixosSystem {
system = buildSystem; system = buildSystem;
modules = [ (
(importWrapped inputs entryPath).module modules = [ # Anything specific to only this evaluation of the module tree should go here.
) { (args.config or (importWrapped inputs entryPath).module)
# The system architecture (often referred to as »system«). { _module.args = { inherit name; }; }
options.${prefix}.preface.hardware = lib.mkOption { type = lib.types.str; readOnly = true; }; { networking.hostName = name; }
} { ];
# List of host names to instantiate this host config for, instead of just for the file name.
options.${prefix}.preface.instances = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ name ]; }; extraModules = modules ++ [ ({ # These are passed as »extraModules« module argument and can thus conveniently be reused when defining containers and such (Therefore define as much stuff as possible here).
} {
}) ({
nixpkgs = { inherit overlays; }
// (if buildSystem != targetSystem then { localSystem.system = buildSystem; crossSystem.system = targetSystem; } else { system = targetSystem; });
_module.args = builtins.removeAttrs specialArgs [ "name" ]; # (pass the args here, so that they also apply to any other evaluation using »extraModules«)
${prefix}.base.includeInputs = lib.mkDefault inputs;
system.nixos.revision = lib.mkIf (inputs?nixpkgs && inputs.nixpkgs?rev) inputs.nixpkgs.rev; # (evaluating the default value fails under some circumstances)
}) ({
options.${prefix}.preface.hardware = lib.mkOption { description = "The name of the system's CPU instruction set (the first part of what is often referred to as »system«)."; type = lib.types.str; readOnly = true; };
}) ({
options.${prefix}.preface.instances = lib.mkOption { description = "List of host names to instantiate this host config for, instead of just for the file name."; type = lib.types.listOf lib.types.str; readOnly = true; };
config.${prefix} = if preface?instances then { } else { preface.instances = [ name ]; };
}) ({
options.${prefix}.setup.scripts = lib.mkOption { options.${prefix}.setup.scripts = lib.mkOption {
description = ''Attrset of bash scripts defining functions that do installation and maintenance operations. See »./setup-scripts/README.md« below for more information.''; description = ''Attrset of bash scripts defining functions that do installation and maintenance operations. See »./setup-scripts/README.md« below for more information.'';
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, config, ... }: { options = { type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, config, ... }: { options = {
name = lib.mkOption { description = "Name that this device is being referred to as in other places."; type = lib.types.str; default = name; readOnly = true; }; name = lib.mkOption { description = "Symbolic name of the script."; type = lib.types.str; default = name; readOnly = true; };
path = lib.mkOption { description = "Path of file for ».text« to be loaded from."; type = lib.types.nullOr lib.types.path; default = null; }; path = lib.mkOption { description = "Path of file for ».text« to be loaded from."; type = lib.types.nullOr lib.types.path; default = null; };
text = lib.mkOption { description = "Script text to process."; type = lib.types.str; default = builtins.readFile config.path; }; text = lib.mkOption { description = "Script text to process."; type = lib.types.str; default = builtins.readFile config.path; };
order = lib.mkOption { description = "Parsing order of the scripts. Higher orders will be parsed later, and can thus overwrite earlier definitions."; type = lib.types.int; default = 1000; }; order = lib.mkOption { description = "Parsing order of the scripts. Higher orders will be parsed later, and can thus overwrite earlier definitions."; type = lib.types.int; default = 1000; };
}; }))); }; })));
apply = lib.filterAttrs (k: v: v != null); apply = lib.filterAttrs (k: v: v != null);
}; config.${prefix}.setup.scripts = lib.mapAttrs (name: path: lib.mkOptionDefault { inherit path; }) (setup-scripts); }; config.${prefix}.setup.scripts = lib.mapAttrs (name: path: lib.mkOptionDefault { inherit path; }) (setup-scripts);
} ({ config, options, pkgs, inputs ? { }, ... }: { }) ({ config, options, pkgs, ... }: {
options.${prefix}.setup.appliedScripts = lib.mkOption { options.${prefix}.setup.appliedScripts = lib.mkOption {
type = lib.types.functionTo lib.types.str; readOnly = true; type = lib.types.functionTo lib.types.str; readOnly = true;
default = context: substituteImplicit { inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues config.${prefix}.setup.scripts); context = { inherit config options pkgs inputs preface; } // context; }; # inherit (builtins) trace; default = context: substituteImplicit { inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues config.${prefix}.setup.scripts); context = { inherit config options pkgs inputs; } // context; }; # inherit (builtins) trace;
}; };
}) ({ config, ... }: {
imports = modules; nixpkgs = { inherit overlays; }
// (if buildSystem != targetSystem then { localSystem.system = buildSystem; crossSystem.system = targetSystem; } else { system = targetSystem; });
networking.hostName = name;
system.extraSystemBuilderCmds = (if !config.boot.initrd.enable then "" else ''
ln -sT ${builtins.unsafeDiscardStringContext config.system.build.bootStage1} $out/boot-stage-1.sh # (this is super annoying to locate otherwise)
'');
}) ]; }) ];
specialArgs = specialArgs; # explicitly passing »pkgs« here breaks »config.nixpkgs.overlays«!
#specialArgs = specialArgs; # (This is already set during module import, while »_module.args« only becomes available during module evaluation (before that, using it causes infinite recursion). Since it can't be ensured that this is set in every circumstance where »extraModules« are being used, it should not be set at all.)
}); in system; }); in system;
# Given either a list (or attr set) of »files« (paths to ».nix« or ».nix.md« files for dirs with »default.nix« files in them) or a »dir« path (and optionally a list of file names to »exclude« from it), this builds the NixOS configuration for each host (per file) in the context of all configs provided. # Given either a list (or attr set) of »files« (paths to ».nix« or ».nix.md« files for dirs with »default.nix« files in them) or a »dir« path (and optionally a list of base names to »exclude« from it), this builds the NixOS configuration for each host (per file) in the context of all configs provided.
# If »files« is an attr set, exactly one host with the attribute's name as hostname is built for each attribute. Otherwise the default is to build for one host per configuration file, named as the file name without extension or the sub-directory name. Setting »${prefix}.preface.instances« can override this to build the same configuration for those multiple names instead (the specific »name« is passed as additional »specialArgs« to the modules and can thus be used to adjust the config per instance). # If »files« is an attr set, exactly one host with the attribute's name as hostname is built for each attribute. Otherwise the default is to build for one host per configuration file, named as the file name without extension or the sub-directory name. Setting »${prefix}.preface.instances« can override this to build the same configuration for those multiple names instead (the specific »name« is passed as additional »specialArgs« to the modules and can thus be used to adjust the config per instance).
# All other arguments are as specified by »mkSystemsFlake« and are passed to »mkNixosConfiguration«. # All other arguments are as specified by »mkSystemsFlake« and are passed to »mkNixosConfiguration«.
mkNixosConfigurations = args: let # { files, dir, exclude, ... } mkNixosConfigurations = args: let # { files, dir, exclude, ... }
files = args.files or (getNixFiles args.dir (args.exclude or [ ])); files = args.files or (builtins.removeAttrs (getNixFiles args.dir) (args.exclude or [ ]));
files' = if builtins.isAttrs files then files else (builtins.listToAttrs (map (entryPath: let files' = if builtins.isAttrs files then files else (builtins.listToAttrs (map (entryPath: let
stripped = builtins.match ''^(.*)[.]nix[.]md$'' (builtins.baseNameOf entryPath); stripped = builtins.match ''^(.*)[.]nix[.]md$'' (builtins.baseNameOf entryPath);
name = builtins.unsafeDiscardStringContext (if stripped != null then (builtins.elemAt stripped 0) else (builtins.baseNameOf entryPath)); name = builtins.unsafeDiscardStringContext (if stripped != null then (builtins.elemAt stripped 0) else (builtins.baseNameOf entryPath));
in { inherit name; value = entryPath; }) files)); in { inherit name; value = entryPath; }) files));
configs = mapMerge (name: entryPath: (let configs = mapMergeUnique (name: entryPath: (let
preface = (getSystemPreface inputs entryPath { }); preface = (getSystemPreface inputs entryPath { });
in (mapMerge (name: { in (mapMergeUnique (name: {
"${name}" = mkNixosConfiguration (( "${name}" = mkNixosConfiguration ((
builtins.removeAttrs args [ "files" "dir" "exclude" ] builtins.removeAttrs args [ "files" "dir" "exclude" ]
) // { ) // {
inherit name entryPath; peers = configs; inherit name entryPath; peers = configs;
}); });
}) (if !(builtins.isAttrs files) && preface?instances then preface.instances else [ name ])))) (files'); }) (if !(args?files && builtins.isAttrs files) && preface?instances then preface.instances else [ name ])))) (files');
withId = lib.filterAttrs (name: node: node.preface?id) configs; withId = lib.filterAttrs (name: node: node.preface?id) configs;
ids = mapMerge (name: node: { "${toString node.preface.id}" = name; }) withId; ids = mapMerge (name: node: { "${toString node.preface.id}" = name; }) withId;
@ -156,14 +162,14 @@ in rec {
inputs ? { }, inputs ? { },
# Arguments »{ files, dir, exclude, }« to »mkNixosConfigurations«, see there for details. May also be a list of those attrsets, in which case those multiple sets of hosts will be built separately by »mkNixosConfigurations«, allowing for separate sets of »peers« passed to »mkNixosConfiguration«. Each call will receive all other arguments, and the resulting sets of hosts will be merged. # Arguments »{ files, dir, exclude, }« to »mkNixosConfigurations«, see there for details. May also be a list of those attrsets, in which case those multiple sets of hosts will be built separately by »mkNixosConfigurations«, allowing for separate sets of »peers« passed to »mkNixosConfiguration«. Each call will receive all other arguments, and the resulting sets of hosts will be merged.
systems ? ({ dir = "${inputs.self}/hosts"; exclude = [ ]; }), systems ? ({ dir = "${inputs.self}/hosts"; exclude = [ ]; }),
# List of overlays to set as »config.nixpkgs.overlays«. Defaults to the ».overlay(s)« of all »overlayInputs«/»inputs« (incl. »inputs.self«). # List of overlays to set as »config.nixpkgs.overlays«. Defaults to ».overlays.default« of all »overlayInputs«/»inputs« (incl. »inputs.self«).
overlays ? (builtins.concatLists (map (input: if input?overlay then [ input.overlay ] else if input?overlays then builtins.attrValues input.overlays else [ ]) (builtins.attrValues overlayInputs))), overlays ? (lib.remove null (map (input: if input?overlays && input.overlays?default then input.overlays.default else if input?overlay then input.overlay else null) (builtins.attrValues overlayInputs))),
# (Subset of) »inputs« that »overlays« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«. # (Subset of) »inputs« that »overlays« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
overlayInputs ? inputs, overlayInputs ? inputs,
# List of Modules to import for all hosts, in addition to the default ones in »nixpkgs«. The host-individual module should selectively enable these. Defaults to all »ModuleInputs«/»inputs«' ».nixosModule(s)« (including »inputs.self.nixosModule(s)«). # List of Modules to import for all hosts, in addition to the default ones in »nixpkgs«. The host-individual module should selectively enable these. Defaults to ».nixosModules.default« of all »moduleInputs«/»inputs« (including »inputs.self«).
modules ? (map (input: input.nixosModule or (if input?nixosModules then { imports = builtins.attrValues input.nixosModules; } else { })) (builtins.attrValues moduleInputs)), modules ? (lib.remove null (map (input: if input?nixosModules && input.nixosModules?default then input.nixosModules.default else if input?nixosModule then input.nixosModule else null) (builtins.attrValues moduleInputs))),
# (Subset of) »inputs« that »modules« will be used from. (The standard) »nixpkgs« does not export any (useful) modules, since the actual modules are included by default by »nixosSystem«. # (Subset of) »inputs« that »modules« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
moduleInputs ? (builtins.removeAttrs inputs [ "nixpkgs" ]), moduleInputs ? inputs,
# Additional arguments passed to each module evaluated for the host config (if that module is defined as a function). # Additional arguments passed to each module evaluated for the host config (if that module is defined as a function).
specialArgs ? { }, specialArgs ? { },
# The »nixosSystem« function defined in »<nixpkgs>/flake.nix«, or equivalent. # The »nixosSystem« function defined in »<nixpkgs>/flake.nix«, or equivalent.
@ -196,7 +202,7 @@ in rec {
# $ nix run /etc/nixos/#$(hostname) # $ nix run /etc/nixos/#$(hostname)
# Run an root session in the context of a different host (useful if Nix is not installed for root on the current host): # Run an root session in the context of a different host (useful if Nix is not installed for root on the current host):
# $ nix run /etc/nixos/#other-host -- sudo # $ nix run /etc/nixos/#other-host -- sudo
apps = lib.mapAttrs (name: system: { type = "app"; program = "${pkgs.writeShellScript "scripts-${name}" '' apps = lib.mapAttrs (name: system: rec { type = "app"; derivation = pkgs.writeShellScript "scripts-${name}" ''
# if first arg is »sudo«, re-execute this script with sudo (as root) # if first arg is »sudo«, re-execute this script with sudo (as root)
if [[ $1 == sudo ]] ; then shift ; exec sudo --preserve-env=SSH_AUTH_SOCK -- "$0" "$@" ; fi if [[ $1 == sudo ]] ; then shift ; exec sudo --preserve-env=SSH_AUTH_SOCK -- "$0" "$@" ; fi
@ -223,7 +229,7 @@ in rec {
# either call »$1« with the remaining parameters as arguments, or if »$1« is »-c« eval »$2«. # either call »$1« with the remaining parameters as arguments, or if »$1« is »-c« eval »$2«.
if [[ ''${1:-} == -x ]] ; then shift ; set -x ; fi if [[ ''${1:-} == -x ]] ; then shift ; set -x ; fi
if [[ ''${1:-} == -c ]] ; then eval "$2" ; else "$@" ; fi if [[ ''${1:-} == -c ]] ; then eval "$2" ; else "$@" ; fi
''}"; }) nixosConfigurations; ''; program = "${derivation}"; }) nixosConfigurations;
# E.g.: $ nix develop /etc/nixos/#$(hostname) # E.g.: $ nix develop /etc/nixos/#$(hostname)
# ... and then call any of the functions in ./utils/setup-scripts/ (in the context of »$(hostname)«, where applicable). # ... and then call any of the functions in ./utils/setup-scripts/ (in the context of »$(hostname)«, where applicable).

View File

@ -1,19 +1,29 @@
dirname: inputs@{ self, nixpkgs, ...}: let dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) mapMerge mergeAttrsRecursive endsWith; inherit (import "${dirname}/vars.nix" dirname inputs) mapMergeUnique mergeAttrsRecursive endsWith;
inherit (import "${dirname}/misc.nix" dirname inputs) trace;
in rec { in rec {
# Return a list of the absolute paths of all folders and ».nix« or ».nix.md« files in »dir« whose names are not in »except«. # Builds an attrset that, for each folder that contains a »default.nix«, and for each ».nix« or ».nix.md« file in »dir«, maps the the name of that folder, or the name of the file without extension(s), to its full path.
getNixFiles = dir: except: let listing = builtins.readDir dir; in (builtins.filter (e: e != null) (map (name: ( getNixFiles = dir: mapMergeUnique (name: type: if (type == "directory") then (
if !(builtins.elem name except) && (listing.${name} == "directory" || (builtins.match ''.*[.]nix([.]md)?$'' name) != null) then "${dir}/${name}" else null if (builtins.pathExists "${dir}/${name}/default.nix") then { ${name} = "${dir}/${name}/default.nix"; } else { }
)) (builtins.attrNames listing))); ) else (
let
# Builds an attrset that, for each folder that contains a »default.nix«, and for each ».nix« or ».nix.md« file in »dir« (other than those whose names are in »except«), maps the the name of that folder, or the name of the file without extension(s), to its full path.
getNamedNixFiles = dir: except: let listing = builtins.readDir dir; in mapMerge (name: if !(builtins.elem name except) then (
if (listing.${name} == "directory" && builtins.pathExists "${dir}/${name}/default.nix") then { ${name} = "${dir}/${name}/default.nix"; } else let
match = builtins.match ''^(.*)[.]nix([.]md)?$'' name; match = builtins.match ''^(.*)[.]nix([.]md)?$'' name;
in if (match != null) then { ${builtins.head match} = "${dir}/${name}"; } else { } in if (match != null) then {
) else { }) (builtins.attrNames listing); ${builtins.head match} = "${dir}/${name}";
} else { }
)) (builtins.readDir dir);
getNixFilesRecursive = dir: let
list = prefix: dir: mapMergeUnique (name: type: if (type == "directory") then (
list "${prefix}${name}/" "${dir}/${name}"
) else (let
match = builtins.match ''^(.*)[.]nix([.]md)?$'' name;
in if (match != null) then {
"${prefix}${builtins.head match}" = "${dir}/${name}";
} else { })) (builtins.readDir dir);
in list "" dir;
## Decides whether a thing is probably a NixOS configuration module or not. ## Decides whether a thing is probably a NixOS configuration module or not.
# Probably because almost everything could be a module declaration (any attribute set or function returning one is potentially a module). # Probably because almost everything could be a module declaration (any attribute set or function returning one is potentially a module).
@ -29,7 +39,7 @@ in rec {
couldBeOverlay = thing: let result1 = thing (builtins.functionArgs thing); result2 = result1 (builtins.functionArgs result1); in builtins.isFunction thing && builtins.isFunction result1 && builtins.isAttrs result2; couldBeOverlay = thing: let result1 = thing (builtins.functionArgs thing); result2 = result1 (builtins.functionArgs result1); in builtins.isFunction thing && builtins.isFunction result1 && builtins.isAttrs result2;
# Builds an attrset that, for each folder (containing a »default.nix«) or ».nix« or ».nix.md« file (other than »./default.nix«) in this folder, as the name of that folder or the name of the file without extension(s), exports the result of importing that file/folder. # Builds an attrset that, for each folder (containing a »default.nix«) or ».nix« or ».nix.md« file (other than »./default.nix«) in this folder, as the name of that folder or the name of the file without extension(s), exports the result of importing that file/folder.
importAll = inputs: dir: builtins.mapAttrs (name: path: import path (if endsWith "/default.nix" path then "${dir}/${name}" else dir) inputs) (getNamedNixFiles dir [ "default.nix" ]); importAll = inputs: dir: builtins.mapAttrs (name: path: import path (if endsWith "/default.nix" path then "${dir}/${name}" else dir) inputs) (builtins.removeAttrs (getNixFiles dir) [ "default" ]);
# Import a Nix file that expects the standard `dirname: inputs: ` arguments, providing some additional information and error handling. # Import a Nix file that expects the standard `dirname: inputs: ` arguments, providing some additional information and error handling.
importWrapped = inputs: path: rec { importWrapped = inputs: path: rec {
@ -51,7 +61,7 @@ in rec {
module = { _file = fullPath; imports = [ required ]; }; module = { _file = fullPath; imports = [ required ]; };
}; };
## Returns an attrset that, for each file in »dir« (except »default.nix« and as filtered and named by »getNamedNixFiles dir except«), imports that file and exposes only if the result passes »filter«. If provided, the imported value is »wrapped« after filtering. ## Returns an attrset that, for each file in »dir« (except ...), imports that file and exposes only if the result passes »filter«. If provided, the imported value is »wrapped« after filtering.
# If a file/folder' import that is rejected by »filter« is an attrset (for example because it results from a call to this function), then all attributes whose values pass »filter« are prefixed with the file/folders name plus a slash and merged into the overall attrset. # If a file/folder' import that is rejected by »filter« is an attrset (for example because it results from a call to this function), then all attributes whose values pass »filter« are prefixed with the file/folders name plus a slash and merged into the overall attrset.
# Example: Given a file tree like this, where each »default.nix« contains only a call to this function with the containing directory as »dir«, and every other file contains a definition of something accepted by the »filter«: # Example: Given a file tree like this, where each »default.nix« contains only a call to this function with the containing directory as »dir«, and every other file contains a definition of something accepted by the »filter«:
# ├── default.nix # ├── default.nix
@ -64,22 +74,22 @@ in rec {
# The top level »default.nix« returns: # The top level »default.nix« returns:
# { "a" = <filtered>; "b" = <filtered>; "c/d" = <filtered>; "c/e" = <filtered>; } # { "a" = <filtered>; "b" = <filtered>; "c/d" = <filtered>; "c/e" = <filtered>; }
importFilteredFlattened = dir: inputs: { except ? [ ], filter ? (thing: true), wrap ? (path: thing: thing), }: let importFilteredFlattened = dir: inputs: { except ? [ ], filter ? (thing: true), wrap ? (path: thing: thing), }: let
files = getNamedNixFiles dir (except ++ [ "default.nix" ]); files = builtins.removeAttrs (getNixFiles dir) except;
in mapMerge (name: path: let in mapMergeUnique (name: path: let
thing = import path (if endsWith "/default.nix" path then "${dir}/${name}" else dir) inputs; thing = import path (if endsWith "/default.nix" path then "${dir}/${name}" else dir) inputs;
in if (filter thing) then ( in if (filter thing) then (
{ ${name} = wrap path thing; } { ${name} = wrap path thing; }
) else (if (builtins.isAttrs thing) then ( ) else (if (builtins.isAttrs thing) then (
mapMerge (name': thing': if (filter thing') then ( mapMergeUnique (name': thing': if (filter thing') then (
{ "${name}/${name'}" = thing'; } { "${name}/${name'}" = thing'; }
) else { }) thing ) else { }) thing
) else { })) files; ) else { })) files;
# Used in a »default.nix« and called with the »dir« it is in, imports all modules in that directory as attribute set. See »importFilteredFlattened« and »isProbablyModule« for details. # Used in a »default.nix« and called with the »dir« it is in, imports all modules in that directory as attribute set. See »importFilteredFlattened« and »isProbablyModule« for details.
importModules = inputs: dir: opts: importFilteredFlattened dir inputs (opts // { filter = isProbablyModule; wrap = path: module: { _file = path; imports = [ module ]; }; }); importModules = inputs: dir: opts: importFilteredFlattened dir inputs ({ except = [ "default" ]; } // opts // { filter = isProbablyModule; wrap = path: module: { _file = path; imports = [ module ]; }; });
# Used in a »default.nix« and called with the »dir« it is in, imports all overlays in that directory as attribute set. See »importFilteredFlattened« and »couldBeOverlay« for details. # Used in a »default.nix« and called with the »dir« it is in, imports all overlays in that directory as attribute set. See »importFilteredFlattened« and »couldBeOverlay« for details.
importOverlays = inputs: dir: opts: importFilteredFlattened dir inputs (opts // { filter = couldBeOverlay; }); importOverlays = inputs: dir: opts: importFilteredFlattened dir inputs ({ except = [ "default" ]; } // opts // { filter = couldBeOverlay; });
# Imports »inputs.nixpkgs« and instantiates it with all ».overlay(s)« provided by »inputs.*«. # Imports »inputs.nixpkgs« and instantiates it with all ».overlay(s)« provided by »inputs.*«.
importPkgs = inputs: args: import inputs.nixpkgs ({ importPkgs = inputs: args: import inputs.nixpkgs ({
@ -89,7 +99,7 @@ in rec {
# Given a list of »overlays« and »pkgs« with them applied, returns the subset of »pkgs« that was directly modified by the overlays. # Given a list of »overlays« and »pkgs« with them applied, returns the subset of »pkgs« that was directly modified by the overlays.
getModifiedPackages = pkgs: overlays: let getModifiedPackages = pkgs: overlays: let
names = builtins.concatLists (map (overlay: builtins.attrNames (overlay { } { })) (builtins.attrValues overlays)); names = builtins.concatLists (map (overlay: builtins.attrNames (overlay { } { })) (builtins.attrValues overlays));
in mapMerge (name: if lib.isDerivation pkgs.${name} then { ${name} = pkgs.${name}; } else { }) names; in mapMergeUnique (name: if lib.isDerivation pkgs.${name} then { ${name} = pkgs.${name}; } else { }) names;
## Given a path to a module in »nixpkgs/nixos/modules/«, when placed in another module's »imports«, this adds an option »disableModule.${modulePath}« that defaults to being false, but when explicitly set to »true«, disables all »config« values set by the module. ## Given a path to a module in »nixpkgs/nixos/modules/«, when placed in another module's »imports«, this adds an option »disableModule.${modulePath}« that defaults to being false, but when explicitly set to »true«, disables all »config« values set by the module.
# Every module should, but not all modules do, provide such an option themselves. # Every module should, but not all modules do, provide such an option themselves.

View File

@ -7,7 +7,7 @@ The (paths to these) scripts are meant to be (and by default are) set as `config
Host-specific nix variables are available to the bash functions as `@{...}` through [`substituteImplicit`](../scripts.nix#substituteImplicit) with the respective host as root context. Host-specific nix variables are available to the bash functions as `@{...}` through [`substituteImplicit`](../scripts.nix#substituteImplicit) with the respective host as root context.
Any script passed later in `scripts` can overwrite the functions of these (earlier) default scripts. Any script passed later in `scripts` can overwrite the functions of these (earlier) default scripts.
With the functions from here, [a simple three-liner](../install.sh) is enough to do a completely automated NixOS installation: With the functions from here, [a simple three-liner](./install.sh) is enough to do a completely automated NixOS installation:
```bash ```bash
function install-system {( set -eu # 1: diskPaths function install-system {( set -eu # 1: diskPaths
prepare-installer "$@" prepare-installer "$@"
@ -19,10 +19,10 @@ function install-system {( set -eu # 1: diskPaths
# `install-system` Documentation # `install-system` Documentation
The above function performs the mostly automated installation of any `$HOST` from [`../../hosts/`](../../hosts/) to the local disk(s) (or image file(s)) `$DISK`. For repositories that use the `lib.wip.mkSystemsFlake` Nix function in their `flake.nix`, the above bash function performs the automated installation of any `nixosConfigurations.$HOST`s (where the host's configurations would usually be placed in the `/hosts/` directory of the repository) to the local disk(s) (or image file(s)) `$DISK`.
On a NixOS host, this can be run by root as: `#` `nix run .#"$HOST" -- install-system "$DISK"`. On a NixOS host or with a Nix multi-user installation, this can be run by root as: `#` `nix run .#"$HOST" -- install-system "$DISK"`.
Doing an installation on non-NixOS (but Linux), where nix isn't installed for root, is a bit of a hack, but works as well. Doing an installation on non-NixOS (but Linux), where nix isn't installed for root, the process is a bit of a hack, but works as well.
In this case, all `nix` commands will be run as `$SUDO_USER`, but this script and some other user-owned (or user-generated) code will (need to) be run as root. In this case, all `nix` commands will be run as `$SUDO_USER`, but this script and some other user-owned (or user-generated) code will (need to) be run as root.
If that is acceptable, run with `sudo` as first argument: `$` `nix run .#"$HOST" -- sudo install-system "$DISK"` (And then maybe `sudo bash -c 'chown $SUDO_USER: '"$DISK"` afterwards.) If that is acceptable, run with `sudo` as first argument: `$` `nix run .#"$HOST" -- sudo install-system "$DISK"` (And then maybe `sudo bash -c 'chown $SUDO_USER: '"$DISK"` afterwards.)
@ -30,6 +30,6 @@ If `$DISK` points to something in `/dev/`, then it is directly formatted and wri
For hosts that install to multiple disks, pass a `:`-separated list of `<disk-name>=<path>` pairs (the name may be omitted only for the "`default`" disk). For hosts that install to multiple disks, pass a `:`-separated list of `<disk-name>=<path>` pairs (the name may be omitted only for the "`default`" disk).
Once done, the disk can be transferred -- or the image be copied -- to the final system, and should boot there. Once done, the disk can be transferred -- or the image be copied -- to the final system, and should boot there.
If the host's hardware target allows, a resulting image can also be passed to [`register-vbox`](../maintenance.sh#register-vbox) to create a bootable VirtualBox instance for the current user, or to [`run-qemu`](../maintenance.sh#run-qemu) to start it in a qemu VM. If the host's hardware target allows, a resulting image can also be passed to [`register-vbox`](./maintenance.sh#register-vbox) to create a bootable VirtualBox instance for the current user, or to [`run-qemu`](./maintenance.sh#run-qemu) to start it in a qemu VM.
The "Installation" section of each host's documentation should contain host specific details, if any. The "Installation" section of each host's documentation should contain host specific details, if any.

View File

@ -94,7 +94,7 @@ function gen-key-yubikey-challenge {( set -eu # 1: _, 2: serialAndSlotAndChallen
serial=$( <<<"$args" cut -d: -f1 ) ; slot=$( <<<"$args" cut -d: -f2 ) serial=$( <<<"$args" cut -d: -f1 ) ; slot=$( <<<"$args" cut -d: -f2 )
challenge=${args/$serial:$slot:/} challenge=${args/$serial:$slot:/}
if [[ "$serial" != "$( @{native.yubikey-personalization}/bin/ykinfo -sq )" ]] ; then printf 'Please insert / change to YubiKey with serial %s!\n' "$serial" 1>&2 ; fi if [[ "$serial" != "$( @{native.yubikey-personalization}/bin/ykinfo -sq 2>/dev/null )" ]] ; then printf 'Please insert / change to YubiKey with serial %s!\n' "$serial" 1>&2 ; fi
if [[ ! "${3:-}" ]] ; then if [[ ! "${3:-}" ]] ; then
read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' twice with '"${message:-challenge »"$challenge":1/2«}"'. Enter to continue, or Ctrl+C to abort:' read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' twice with '"${message:-challenge »"$challenge":1/2«}"'. Enter to continue, or Ctrl+C to abort:'
else else

View File

@ -6,25 +6,26 @@
## Prepares the disks of the target system for the copying of files. ## Prepares the disks of the target system for the copying of files.
function do-disk-setup { # 1: diskPaths function do-disk-setup { # 1: diskPaths
prompt-for-user-passwords && prompt-for-user-passwords || return
populate-keystore && populate-keystore || return
mnt=/tmp/nixos-install-@{config.networking.hostName} && mkdir -p "$mnt" && prepend_trap "rmdir $mnt" EXIT && # »mnt=/run/user/0/...« would be more appropriate, but »nixos-install« does not like the »700« permissions on »/run/user/0« mnt=/tmp/nixos-install-@{config.networking.hostName} && mkdir -p "$mnt" && prepend_trap "rmdir $mnt" EXIT || return # »mnt=/run/user/0/...« would be more appropriate, but »nixos-install« does not like the »700« permissions on »/run/user/0«
partition-disks "$1" && partition-disks "$1" || return
create-luks-layers && open-luks-layers && # other block layers would go here too (but figuring out their dependencies would be difficult) create-luks-layers && open-luks-layers || return # other block layers would go here too (but figuring out their dependencies would be difficult)
run-hook-script 'Post Partitioning' @{config.wip.fs.disks.postPartitionCommands!writeText.postPartitionCommands} && run-hook-script 'Post Partitioning' @{config.wip.fs.disks.postPartitionCommands!writeText.postPartitionCommands} || return
format-partitions && format-partitions || return
{ [[ $(LC_ALL=C type -t create-zpools) != function ]] || create-zpools $mnt ; } && if [[ $(LC_ALL=C type -t create-zpools) == function ]] ; then create-zpools $mnt || return ; fi
run-hook-script 'Post Formatting' @{config.wip.fs.disks.postFormatCommands!writeText.postFormatCommands} && run-hook-script 'Post Formatting' @{config.wip.fs.disks.postFormatCommands!writeText.postFormatCommands} || return
prepend_trap "unmount-system $mnt" EXIT && mount-system $mnt && fix-grub-install || return
run-hook-script 'Post Mounting' @{config.wip.fs.disks.postMountCommands!writeText.postMountCommands} &&
: prepend_trap "unmount-system $mnt" EXIT && mount-system $mnt || return
run-hook-script 'Post Mounting' @{config.wip.fs.disks.postMountCommands!writeText.postMountCommands} || return
} }
# Notes segmentation and alignment: # Notes on segmentation and alignment:
# * Both fdisk and gdisk report start and end in 0-indexed sectors from the start of the block device. # * Both fdisk and gdisk report start and end in 0-indexed sectors from the start of the block device.
# * (fdisk and gdisk have slightly different interfaces, but seem to otherwise be mostly equivalent, (fdisk used to not understand GPT).) # * (fdisk and gdisk have slightly different interfaces, but seem to otherwise be mostly equivalent, (fdisk used to not understand GPT).)
# * The MBR sits only in the first sector, a GPT additionally requires next 33 (34 total) and the (absolute) last 33 sectors. At least fdisk won't put partitions in the first 2048 sectors on MBRs. # * The MBR sits only in the first sector, a GPT additionally requires next 33 (34 total) and the (absolute) last 33 sectors. At least fdisk won't put partitions in the first 2048 sectors on MBRs.
@ -35,66 +36,66 @@ function do-disk-setup { # 1: diskPaths
## Partitions the »diskPaths« instances of all »config.wip.fs.disks.devices« to ensure that all specified »config.wip.fs.disks.partitions« exist. ## Partitions the »diskPaths« instances of all »config.wip.fs.disks.devices« to ensure that all specified »config.wip.fs.disks.partitions« exist.
# Parses »diskPaths«, creates and loop-mounts images for non-/dev/ paths, and tries to abort if any partition already exists on the host. # Parses »diskPaths«, creates and loop-mounts images for non-/dev/ paths, and tries to abort if any partition already exists on the host.
function partition-disks { { # 1: diskPaths function partition-disks { # 1: diskPaths
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
declare -g -A blockDevs=( ) # this ends up in the caller's scope declare -g -A blockDevs=( ) # this ends up in the caller's scope
local path ; for path in ${1//:/ } ; do local path ; for path in ${1//:/ } ; do
local name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi local name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi
if [[ ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name specified more than once. Duplicate definition: $path" ; exit 1 ; fi if [[ ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name specified more than once. Duplicate definition: $path" 1>&2 ; return 1 ; fi
blockDevs[$name]=$path blockDevs[$name]=$path
done done
local name ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do local name ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do
if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" ; exit 1 ; fi if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" 1>&2 ; return 1 ; fi
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}" eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
if [[ ${blockDevs[$name]} != /dev/* ]] ; then if [[ ${blockDevs[$name]} != /dev/* ]] ; then
local outFile=${blockDevs[$name]} && local outFile=${blockDevs[$name]} &&
install -o root -g root -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" && install -o root -g root -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" &&
blockDevs[$name]=$(losetup --show -f "$outFile") && prepend_trap "losetup -d '${blockDevs[$name]}'" EXIT # NOTE: this must not be inside a sub-shell! blockDevs[$name]=$( losetup --show -f "$outFile" ) && prepend_trap "losetup -d '${blockDevs[$name]}'" EXIT # NOTE: this must not be inside a sub-shell!
else else
local size=$( blockdev --getsize64 "${blockDevs[$name]}" || : ) ; local waste=$(( size - ${disk[size]} )) local size=$( blockdev --getsize64 "${blockDevs[$name]}" || : ) ; local waste=$(( size - ${disk[size]} ))
if [[ ! $size ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" ; exit 1 ; fi if [[ ! $size ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" 1>&2 ; return 1 ; fi
if (( waste < 0 )) ; then echo "Block device ${blockDevs[$name]}'s size $size is smaller than the size ${disk[size]} declared for $name" ; exit 1 ; fi if (( waste < 0 )) ; then echo "Block device ${blockDevs[$name]}'s size $size is smaller than the size ${disk[size]} declared for $name" ; return 1 ; fi
if (( waste > 0 )) && [[ ! ${disk[allowLarger]:-} ]] ; then echo "Block device ${blockDevs[$name]}'s size $size is bigger than the size ${disk[size]} declared for $name" ; exit 1 ; fi if (( waste > 0 )) && [[ ! ${disk[allowLarger]:-} ]] ; then echo "Block device ${blockDevs[$name]}'s size $size is bigger than the size ${disk[size]} declared for $name" 1>&2 ; return 1 ; fi
if (( waste > 0 )) ; then echo "Wasting $(( waste / 1024))K of ${blockDevs[$name]} due to the size declared for $name (should be ${size}b)" ; fi if (( waste > 0 )) ; then echo "Wasting $(( waste / 1024))K of ${blockDevs[$name]} due to the size declared for $name (should be ${size}b)" 1>&2 ; fi
blockDevs[$name]=$(realpath "${blockDevs[$name]}") blockDevs[$name]=$(realpath "${blockDevs[$name]}")
fi fi
done done
} && ( set -eu
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
eval 'declare -A part='"$partDecl" eval 'local -A part='"$partDecl"
if [[ -e /dev/disk/by-partlabel/"${part[name]}" ]] && ! is-partition-on-disks /dev/disk/by-partlabel/"${part[name]}" "${blockDevs[@]}" ; then echo "Partition /dev/disk/by-partlabel/${part[name]} already exists on this host and does not reside on one of the target disks ${blockDevs[@]}. Refusing to create another partition with the same partlabel!" ; exit 1 ; fi if [[ -e /dev/disk/by-partlabel/"${part[name]}" ]] && ! is-partition-on-disks /dev/disk/by-partlabel/"${part[name]}" "${blockDevs[@]}" ; then echo "Partition /dev/disk/by-partlabel/${part[name]} already exists on this host and does not reside on one of the target disks ${blockDevs[@]}. Refusing to create another partition with the same partlabel!" 1>&2 ; return 1 ; fi
done done
for name in "@{!config.wip.fs.disks.devices[@]}" ; do for name in "@{!config.wip.fs.disks.devices[@]}" ; do
eval 'declare -A disk='"@{config.wip.fs.disks.devices[$name]}" eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
if [[ ${disk[serial]:-} ]] ; then if [[ ${disk[serial]:-} ]] ; then
actual=$( udevadm info --query=property --name="$blockDev" | grep -oP 'ID_SERIAL_SHORT=\K.*' || echo '<none>' ) actual=$( udevadm info --query=property --name="$blockDev" | grep -oP 'ID_SERIAL_SHORT=\K.*' || echo '<none>' )
if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device $blockDev's serial ($actual) does not match the serial (${disk[serial]}) declared for ${disk[name]}" ; exit 1 ; fi if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device $blockDev's serial ($actual) does not match the serial (${disk[serial]}) declared for ${disk[name]}" 1>&2 ; return 1 ; fi
fi fi
# can (and probably should) restore the backup: # can (and probably should) restore the backup:
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk --zap-all --load-backup=@{config.wip.fs.disks.partitioning}/"${disk[name]}".backup ${disk[allowLarger]:+--move-second-header} "${blockDevs[${disk[name]}]}" >$beLoud 2>$beSilent ) ( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk --zap-all --load-backup=@{config.wip.fs.disks.partitioning}/"${disk[name]}".backup ${disk[allowLarger]:+--move-second-header} "${blockDevs[${disk[name]}]}" >$beLoud 2>$beSilent || exit ) || return
#partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}" #partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}"
done done
@{native.parted}/bin/partprobe "${blockDevs[@]}" &>$beLoud @{native.parted}/bin/partprobe "${blockDevs[@]}" &>$beLoud || return
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet @{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
# ensure that filesystem creation does not complain about the devices already being occupied by a previous filesystem # ensure that filesystem creation does not complain about the devices already being occupied by a previous filesystem
wipefs --all "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >$beLoud 2>$beSilent wipefs --all "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >$beLoud 2>$beSilent || return
)} #</dev/zero head -c 4096 | tee "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >/dev/null
#for part in "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do @{native.util-linux}/bin/blkdiscard -f "$part" || return ; done
}
## Given a declared disk device's »name« and a path to an actual »blockDev« (or image) file, partitions the device as declared in the config. ## Given a declared disk device's »name« and a path to an actual »blockDev« (or image) file, partitions the device as declared in the config.
function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize function partition-disk { # 1: name, 2: blockDev, 3?: devSize
name=$1 ; blockDev=$2 local name=$1 ; local blockDev=$2
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
eval 'declare -A disk='"@{config.wip.fs.disks.devices[$name]}" eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
devSize=${3:-$( @{native.util-linux}/bin/blockdev --getsize64 "$blockDev" )} local devSize=${3:-$( @{native.util-linux}/bin/blockdev --getsize64 "$blockDev" )}
declare -a sgdisk=( --zap-all ) # delete existing part tables local -a sgdisk=( --zap-all ) # delete existing part tables
if [[ ${disk[gptOffset]} != 0 ]] ; then if [[ ${disk[gptOffset]} != 0 ]] ; then
sgdisk+=( --move-main-table=$(( 2 + ${disk[gptOffset]} )) ) # this is incorrectly documented as --adjust-main-table in the man pages (at least versions 1.05 to 1.09 incl) sgdisk+=( --move-main-table=$(( 2 + ${disk[gptOffset]} )) ) # this is incorrectly documented as --adjust-main-table in the man pages (at least versions 1.05 to 1.09 incl)
sgdisk+=( --move-backup-table=$(( devSize/${disk[sectorSize]} - 1 - 32 - ${disk[gptOffset]} )) ) sgdisk+=( --move-backup-table=$(( devSize/${disk[sectorSize]} - 1 - 32 - ${disk[gptOffset]} )) )
@ -102,7 +103,7 @@ function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize
sgdisk+=( --disk-guid="${disk[guid]}" ) sgdisk+=( --disk-guid="${disk[guid]}" )
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
eval 'declare -A part='"$partDecl" eval 'local -A part='"$partDecl"
if [[ ${part[disk]} != "${disk[name]}" ]] ; then continue ; fi if [[ ${part[disk]} != "${disk[name]}" ]] ; then continue ; fi
if [[ ${part[size]:-} =~ ^[0-9]+%$ ]] ; then if [[ ${part[size]:-} =~ ^[0-9]+%$ ]] ; then
part[size]=$(( $devSize / 1024 * ${part[size]:0:(-1)} / 100 ))K part[size]=$(( $devSize / 1024 * ${part[size]:0:(-1)} / 100 ))K
@ -120,7 +121,7 @@ function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize
sgdisk+=( --hybrid "${disk[mbrParts]}" ) # --hybrid: create MBR in addition to GPT; ${disk[mbrParts]}: make these GPT part 1 MBR parts 2[3[4]] sgdisk+=( --hybrid "${disk[mbrParts]}" ) # --hybrid: create MBR in addition to GPT; ${disk[mbrParts]}: make these GPT part 1 MBR parts 2[3[4]]
fi fi
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk "${sgdisk[@]}" "$blockDev" >$beLoud ) # running all at once is much faster ( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk "${sgdisk[@]}" "$blockDev" >$ || exit ) || return # running all at once is much faster
if [[ ${disk[mbrParts]:-} ]] ; then if [[ ${disk[mbrParts]:-} ]] ; then
printf " printf "
@ -140,23 +141,23 @@ function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize
${disk[extraFDiskCommands]} ${disk[extraFDiskCommands]}
p;w;q # print ; write ; quit p;w;q # print ; write ; quit
" | @{native.gnused}/bin/sed -E 's/^ *| *(#.*)?$//g' | @{native.gnused}/bin/sed -E 's/\n\n+| *; */\n/g' | tee >((echo -n '++ ' ; tr $'\n' '|' ; echo) 1>&2) | ( PATH=@{native.util-linux}/bin ; ${_set_x:-:} ; fdisk "$blockDev" &>$beLoud ) " | @{native.gnused}/bin/sed -E 's/^ *| *(#.*)?$//g' | @{native.gnused}/bin/sed -E 's/\n\n+| *; */\n/g' | tee >((echo -n '++ ' ; tr $'\n' '|' ; echo) 1>&2) | ( PATH=@{native.util-linux}/bin ; ${_set_x:-:} ; fdisk "$blockDev" &>$beLoud || exit ) || return
fi fi
)} }
## Checks whether a »partition« resides on one of the provided »blockDevs«. ## Checks whether a »partition« resides on one of the provided »blockDevs«.
function is-partition-on-disks {( set -eu # 1: partition, ...: blockDevs function is-partition-on-disks { # 1: partition, ...: blockDevs
partition=$1 ; shift ; declare -a blockDevs=( "$@" ) local partition=$1 ; shift ; local -a blockDevs=( "$@" )
blockDev=$(realpath "$partition") ; if [[ $blockDev == /dev/sd* ]] ; then local blockDev=$(realpath "$partition") ; if [[ $blockDev == /dev/sd* ]] ; then
blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" ) blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" )
else else
blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" ) blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" )
fi fi
[[ ' '"${blockDevs[@]}"' ' == *' '"$blockDev"' '* ]] [[ ' '"${blockDevs[@]}"' ' == *' '"$blockDev"' '* ]]
)} }
## For each filesystem in »config.fileSystems« whose ».device« is in »/dev/disk/by-partlabel/«, this creates the specified file system on that partition. ## For each filesystem in »config.fileSystems« whose ».device« is in »/dev/disk/by-partlabel/«, this creates the specified file system on that partition.
function format-partitions {( set -eu function format-partitions {( set -u
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
for fsDecl in "@{config.fileSystems[@]}" ; do for fsDecl in "@{config.fileSystems[@]}" ; do
@ -166,7 +167,9 @@ function format-partitions {( set -eu
elif [[ ${fs[device]} == /dev/mapper/* ]] ; then elif [[ ${fs[device]} == /dev/mapper/* ]] ; then
if [[ ! @{config.boot.initrd.luks.devices!catAttrSets.device[${fs[device]/'/dev/mapper/'/}]:-} ]] ; then echo "LUKS device ${fs[device]} used by mount ${fs[mountPoint]} does not point at one of the device mappings ${!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; exit 1 ; fi if [[ ! @{config.boot.initrd.luks.devices!catAttrSets.device[${fs[device]/'/dev/mapper/'/}]:-} ]] ; then echo "LUKS device ${fs[device]} used by mount ${fs[mountPoint]} does not point at one of the device mappings ${!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; exit 1 ; fi
else continue ; fi else continue ; fi
( PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH ; ${_set_x:-:} ; mkfs.${fs[fsType]} ${fs[formatOptions]} "${fs[device]}" >$beLoud 2>$beSilent ) #if [[ ${fs[fsType]} == ext4 && ' '${fs[formatOptions]}' ' != *' -F '* ]] ; then fs[formatOptions]+=' -F' ; fi
#if [[ ${fs[fsType]} == f2fs && ' '${fs[formatOptions]}' ' != *' -f '* ]] ; then fs[formatOptions]+=' -f' ; fi
( PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH ; ${_set_x:-:} ; mkfs.${fs[fsType]} ${fs[formatOptions]} "${fs[device]}" >$beLoud 2>$beSilent ) || exit
@{native.parted}/bin/partprobe "${fs[device]}" || true @{native.parted}/bin/partprobe "${fs[device]}" || true
done done
for swapDev in "@{config.swapDevices!catAttrs.device[@]}" ; do for swapDev in "@{config.swapDevices!catAttrs.device[@]}" ; do
@ -175,10 +178,28 @@ function format-partitions {( set -eu
elif [[ $swapDev == /dev/mapper/* ]] ; then elif [[ $swapDev == /dev/mapper/* ]] ; then
if [[ ! @{config.boot.initrd.luks.devices!catAttrSets.device[${swapDev/'/dev/mapper/'/}]:-} ]] ; then echo "LUKS device $swapDev used for SWAP does not point at one of the device mappings @{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; exit 1 ; fi if [[ ! @{config.boot.initrd.luks.devices!catAttrSets.device[${swapDev/'/dev/mapper/'/}]:-} ]] ; then echo "LUKS device $swapDev used for SWAP does not point at one of the device mappings @{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; exit 1 ; fi
else continue ; fi else continue ; fi
( set -x ; mkswap "$swapDev" >$beLoud 2>$beSilent ) ( ${_set_x:-:} ; mkswap "$swapDev" >$beLoud 2>$beSilent ) || exit
done done
)} )}
## This makes the installation of grub to loop devices shut up, but booting still does not work (no partitions are found). I'm done with GRUB; EXTLINUX works.
# (This needs to happen before mounting.)
function fix-grub-install {
if [[ @{config.boot.loader.grub.enable:-} ]] ; then
if [[ @{config.boot.loader.grub.devices!length:-} != 1 || @{config.boot.loader.grub.mirroredBoots!length:-} != 0 ]] ; then echo "Installation of grub as mirrors or to more than 1 device may not work" 1>&2 ; fi
for mount in '/boot' '/boot/grub' ; do
if [[ ! @{config.fileSystems[$mount]:-} ]] ; then continue ; fi
device=$( eval 'declare -A fs='"@{config.fileSystems[$mount]}" ; echo "${fs[device]}" )
label=${device/\/dev\/disk\/by-partlabel\//}
if [[ $label == "$device" || $label == *' '* || ' '@{config.wip.fs.disks.partitions!attrNames[@]}' ' != *' '$label' '* ]] ; then echo "" 1>&2 ; return 1 ; fi
bootLoop=$( losetup --show -f /dev/disk/by-partlabel/$label ) || return ; prepend_trap "losetup -d $bootLoop" EXIT
ln -sfT ${bootLoop/\/dev/..\/..} /dev/disk/by-partlabel/$label || return
done
#umount $mnt/boot/grub || true ; umount $mnt/boot || true ; mount $mnt/boot || true ; mount $mnt/boot/grub || true
fi
}
## Mounts all file systems as it would happen during boot, but at path prefix »$mnt« (instead of »/«). ## Mounts all file systems as it would happen during boot, but at path prefix »$mnt« (instead of »/«).
function mount-system {( set -eu # 1: mnt, 2?: fstabPath function mount-system {( set -eu # 1: mnt, 2?: fstabPath
# TODO: »config.system.build.fileSystems« is a dependency-sorted list. Could use that ... # TODO: »config.system.build.fileSystems« is a dependency-sorted list. Could use that ...
@ -190,18 +211,18 @@ function mount-system {( set -eu # 1: mnt, 2?: fstabPath
options=,$options, ; options=${options//,ro,/,} options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then continue ; fi if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then ( if ! mountpoint -q "$mnt"/"$target" ; then (
mkdir -p "$mnt"/"$target" mkdir -p "$mnt"/"$target" || exit
[[ $type == tmpfs || $type == */* ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes) [[ $type == tmpfs || $type == */* ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes)
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
) || [[ $options == *,nofail,* ]] ; fi # (actually, nofail already makes mount fail silently) ) || [[ $options == *,nofail,* ]] || exit ; fi # (actually, nofail already makes mount fail silently)
done done || exit
# Since bind mounts may depend on other mounts not only for the target (which the sort takes care of) but also for the source, do all bind mounts last. This would break if there was a different bind mountpoint within a bind-mounted target. # Since bind mounts may depend on other mounts not only for the target (which the sort takes care of) but also for the source, do all bind mounts last. This would break if there was a different bind mountpoint within a bind-mounted target.
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do <$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
if [[ ! $target || $target == none ]] ; then continue ; fi if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,} options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then : ; else continue ; fi if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then : ; else continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then ( if ! mountpoint -q "$mnt"/"$target" ; then (
mkdir -p "$mnt"/"$target" mkdir -p "$mnt"/"$target" || exit
if [[ $type == overlay ]] ; then if [[ $type == overlay ]] ; then
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # Work and upper dirs must be in target. options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # Work and upper dirs must be in target.
workdir=$(<<<"$options" grep -o -P ',workdir=\K[^,]+' || true) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi workdir=$(<<<"$options" grep -o -P ',workdir=\K[^,]+' || true) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi
@ -210,11 +231,11 @@ function mount-system {( set -eu # 1: mnt, 2?: fstabPath
options=${options//,lowerdir=$lowerdir,/,lowerdir=$mnt/${lowerdir//:/:$mnt\/},} ; source=overlay options=${options//,lowerdir=$lowerdir,/,lowerdir=$mnt/${lowerdir//:/:$mnt\/},} ; source=overlay
else else
if [[ $source == /nix/store/* ]] ; then options=,ro$options ; fi if [[ $source == /nix/store/* ]] ; then options=,ro$options ; fi
source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" ; fi source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" || exit ; fi
fi fi
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
) || [[ $options == *,nofail,* ]] ; fi ) || [[ $options == *,nofail,* ]] || exit ; fi
done done || exit
)} )}
## Unmounts all file systems (that would be mounted during boot / by »mount-system«). ## Unmounts all file systems (that would be mounted during boot / by »mount-system«).

View File

@ -4,33 +4,33 @@
## ##
## Entry point to the installation, see »./README.md«. ## Entry point to the installation, see »./README.md«.
function install-system {( set -eu # 1: blockDev function install-system {( set -u # 1: blockDev
trap - EXIT # start with empty traps for sub-shell trap - EXIT # start with empty traps for sub-shell
prepare-installer "$@" prepare-installer "$@" || exit
do-disk-setup "${argv[0]}" do-disk-setup "${argv[0]}" || exit
install-system-to $mnt install-system-to $mnt || exit
)} )}
## Does very simple argument paring and validation, performs some sanity checks, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested). ## Does very simple argument paring and validation, performs some sanity checks, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested).
function prepare-installer { # ... function prepare-installer { # ...
generic-arg-parse "$@" generic-arg-parse "$@" || return
if [[ ${args[debug]:-} ]] ; then set -x ; fi if [[ ${args[debug]:-} ]] ; then set -x ; fi
: ${argv[0]:?"Required: Target disk or image paths."} : ${argv[0]:?"Required: Target disk or image paths."}
if [[ "$(id -u)" != '0' ]] ; then echo 'Script must be run as root.' ; exit 1 ; fi if [[ "$(id -u)" != '0' ]] ; then echo 'Script must be run as root.' 1>&2 ; return 1 ; fi
umask 0022 # Ensure consistent umask (default permissions for new files). umask 0022 # Ensure consistent umask (default permissions for new files).
if [[ -e "/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}" ]] ; then echo "Keystore »/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}/« is already open. Close it and remove the mountpoint before running the installer." ; exit 1 ; fi if [[ -e "/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}" ]] ; then echo "Keystore »/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}/« is already open. Close it and remove the mountpoint before running the installer." 1>&2 ; return 1 ; fi
# (partitions are checked in »partition-disks« once the target devices are known) # (partitions are checked in »partition-disks« once the target devices are known)
local luksName ; for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do local luksName ; for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do
if [[ -e "/dev/mapper/$luksName" ]] ; then echo "LUKS device mapping »$luksName« is already open. Close it before running the installer." ; exit 1 ; fi if [[ -e "/dev/mapper/$luksName" ]] ; then echo "LUKS device mapping »$luksName« is already open. Close it before running the installer." 1>&2 ; return 1 ; fi
done done
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
if @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." ; exit 1 ; fi if @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." 1>&2 ; return 1 ; fi
done done
if [[ ${SUDO_USER:-} ]] ; then # use Nix as the user who called this script, as Nix may not be set up for root if [[ ${SUDO_USER:-} ]] ; then # use Nix as the user who called this script, as Nix may not be set up for root
@ -50,64 +50,71 @@ function prepare-installer { # ...
## The default command that will activate the system and install the bootloader. In a separate function to make it easy to replace. ## The default command that will activate the system and install the bootloader. In a separate function to make it easy to replace.
function nixos-install-cmd {( set -eu # 1: mnt, 2: topLevel function nixos-install-cmd {( set -eu # 1: mnt, 2: topLevel
# »nixos-install« by default does some stateful things (see the »--no« options below), builds and copies the system config (but that's already done), and then calls »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- $topLevel/bin/switch-to-configuration boot«, which is essentially the same as »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- @{config.system.build.installBootLoader} $targetSystem«, i.e. the side effects of »nixos-enter« and then calling the bootloader-installer. # »nixos-install« by default does some stateful things (see the »--no« options below), builds and copies the system config (but that's already done), and then calls »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- $topLevel/bin/switch-to-configuration boot«, which is essentially the same as »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- @{config.system.build.installBootLoader} $targetSystem«, i.e. the side effects of »nixos-enter« and then calling the bootloader-installer.
PATH=@{config.systemd.package}/bin:@{native.nix}/bin:$PATH TMPDIR=/tmp LC_ALL=C @{native.nixos-install-tools}/bin/nixos-install --system "$2" --no-root-passwd --no-channel-copy --root "$1" #--debug PATH=@{config.systemd.package}/bin:@{native.nix}/bin:$PATH TMPDIR=/tmp LC_ALL=C @{native.nixos-install-tools}/bin/nixos-install --system "$2" --no-root-passwd --no-channel-copy --root "$1" || exit #--debug
)} )}
## Copies the system's dependencies to the disks mounted at »$mnt« and installs the bootloader. If »$inspect« is set, a root shell will be opened in »$mnt« afterwards. ## Copies the system's dependencies to the disks mounted at »$mnt« and installs the bootloader. If »$inspect« is set, a root shell will be opened in »$mnt« afterwards.
# »$topLevel« may point to an alternative top-level dependency to install. # »$topLevel« may point to an alternative top-level dependency to install.
function install-system-to {( set -eu # 1: mnt function install-system-to {( set -u # 1: mnt
mnt=$1 ; topLevel=${2:-} mnt=$1 ; topLevel=${2:-}
targetSystem=${args[toplevel]:-@{config.system.build.toplevel}} targetSystem=${args[toplevel]:-@{config.system.build.toplevel}}
trap - EXIT # start with empty traps for sub-shell trap - EXIT # start with empty traps for sub-shell
# Link/create files that some tooling expects: # Link/create files that some tooling expects:
mkdir -p -m 755 $mnt/nix/var/nix ; mkdir -p -m 1775 $mnt/nix/store mkdir -p -m 755 $mnt/nix/var/nix || exit ; mkdir -p -m 1775 $mnt/nix/store || exit
mkdir -p $mnt/etc $mnt/run ; mkdir -p -m 1777 $mnt/tmp mkdir -p $mnt/etc $mnt/run || exit ; mkdir -p -m 1777 $mnt/tmp || exit
mount tmpfs -t tmpfs $mnt/run ; prepend_trap "umount -l $mnt/run" EXIT # If there isn't anything mounted here, »activate« will mount a tmpfs (inside »nixos-enter«'s private mount namespace). That would hide the additions below. mount tmpfs -t tmpfs $mnt/run || exit ; prepend_trap "umount -l $mnt/run" EXIT || exit # If there isn't anything mounted here, »activate« will mount a tmpfs (inside »nixos-enter«'s private mount namespace). That would hide the additions below.
[[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS # for »switch-to-configuration« [[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS || exit # for »switch-to-configuration«
[[ -e $mnt/etc/mtab ]] || ln -sfn /proc/mounts $mnt/etc/mtab [[ -e $mnt/etc/mtab ]] || ln -sfn /proc/mounts $mnt/etc/mtab || exit
ln -sT $(realpath $targetSystem) $mnt/run/current-system ln -sT $(realpath $targetSystem) $mnt/run/current-system || exit
#mkdir -p /nix/var/nix/db # »nixos-containers« requires this but nothing creates it before nix is used. BUT »nixos-enter« screams: »/nix/var/nix/db exists and is not a regular file.« #mkdir -p /nix/var/nix/db # »nixos-containers« requires this but nothing creates it before nix is used. BUT »nixos-enter« screams: »/nix/var/nix/db exists and is not a regular file.«
# If the system configuration is supposed to be somewhere on the system, might as well initialize that: # If the system configuration is supposed to be somewhere on the system, might as well initialize that:
if [[ @{config.environment.etc.nixos.source:-} && @{config.environment.etc.nixos.source} != /nix/store/* && @{config.environment.etc.nixos.source} != /run/current-system/config && ! -e $mnt/@{config.environment.etc.nixos.source} && -e $targetSystem/config ]] ; then if [[ @{config.environment.etc.nixos.source:-} && @{config.environment.etc.nixos.source} != /nix/store/* && @{config.environment.etc.nixos.source} != /run/current-system/config && ! -e $mnt/@{config.environment.etc.nixos.source} && -e $targetSystem/config ]] ; then
mkdir -p -- $mnt/@{config.environment.etc.nixos.source} ; cp -at $mnt/@{config.environment.etc.nixos.source} -- $targetSystem/config/* mkdir -p -- $mnt/@{config.environment.etc.nixos.source} || exit
chown -R 0:0 $mnt/@{config.environment.etc.nixos.source} ; chmod -R u+w $mnt/@{config.environment.etc.nixos.source} cp -at $mnt/@{config.environment.etc.nixos.source} -- $targetSystem/config/* || exit
chown -R 0:0 $mnt/@{config.environment.etc.nixos.source} || exit
chmod -R u+w $mnt/@{config.environment.etc.nixos.source} || exit
fi fi
# Set this as the initial system generation: # Set this as the initial system generation:
mkdir -p -m 755 $mnt/nix/var/nix/profiles ; ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link ; ln -sT system-1-link $mnt/nix/var/nix/profiles/system mkdir -p -m 755 $mnt/nix/var/nix/profiles || exit
ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link || exit
ln -sT system-1-link $mnt/nix/var/nix/profiles/system || exit
# Support cross architecture installation (not sure if this is actually required) # Support cross architecture installation (not sure if this is actually required)
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.wip.preface.hardware}"-linux ]] ; then if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.wip.preface.hardware}"-linux ]] ; then
mkdir -p $mnt/run/binfmt ; [[ ! -e /run/binfmt/"@{config.wip.preface.hardware}"-linux ]] || cp -a {,$mnt}/run/binfmt/"@{config.wip.preface.hardware}"-linux mkdir -p $mnt/run/binfmt || exit ; [[ ! -e /run/binfmt/"@{config.wip.preface.hardware}"-linux ]] || cp -a {,$mnt}/run/binfmt/"@{config.wip.preface.hardware}"-linux || exit
# Ubuntu (by default) expects the "interpreter" at »/usr/bin/qemu-@{config.wip.preface.hardware}-static«. # Ubuntu (by default) expects the "interpreter" at »/usr/bin/qemu-@{config.wip.preface.hardware}-static«.
fi fi
# Copy system closure to new nix store: # Copy system closure to new nix store:
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var ; fi if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var || exit ; fi
( cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} ) ; if [[ ${args[quiet]:-} ]] ; then "${cmd[@]}" ; else set -x ; time "${cmd[@]}" ; fi ) ; rm -rf $mnt/nix/var/nix/gcroots ( cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} ) ; if [[ ${args[quiet]:-} ]] ; then "${cmd[@]}" --quiet &>/dev/null || exit ; else set -x ; time "${cmd[@]}" || exit ; fi ) || exit ; rm -rf $mnt/nix/var/nix/gcroots || exit
# TODO: if the target has @{config.nix.autoOptimiseStore} and the host doesn't (there is no .links dir?), optimize now # TODO: if the target has @{config.nix.autoOptimiseStore} and the host doesn't (there is no .links dir?), optimize now
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix $mnt/nix/var ; chown :30000 $mnt/nix/store ; fi if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix $mnt/nix/var || exit ; chown :30000 $mnt/nix/store || exit ; fi
# Run the main install command (primarily for the bootloader): # Run the main install command (primarily for the bootloader):
mount -o bind,ro /nix/store $mnt/nix/store ; prepend_trap '! mountpoint -q $mnt/nix/store || umount -l $mnt/nix/store' EXIT # all the things required to _run_ the system are copied, but (may) need some more things to initially install it and/or enter the chroot (like qemu, see above) mount -o bind,ro /nix/store $mnt/nix/store || exit ; prepend_trap '! mountpoint -q $mnt/nix/store || umount -l $mnt/nix/store' EXIT || exit # all the things required to _run_ the system are copied, but (may) need some more things to initially install it and/or enter the chroot (like qemu, see above)
run-hook-script 'Pre Installation' @{config.wip.fs.disks.preInstallCommands!writeText.preInstallCommands} run-hook-script 'Pre Installation' @{config.wip.fs.disks.preInstallCommands!writeText.preInstallCommands} || exit
code=0 ; nixos-install-cmd $mnt "${topLevel:-$targetSystem}" || code=$? code=0 ; nixos-install-cmd $mnt "${topLevel:-$targetSystem}" || code=$?
run-hook-script 'post Installation' @{config.wip.fs.disks.postInstallCommands!writeText.postInstallCommands} run-hook-script 'Post Installation' @{config.wip.fs.disks.postInstallCommands!writeText.postInstallCommands} || exit
# Done! # Done!
if [[ ! ${args[no-inspect]:-} ]] ; then if [[ ${args[no-inspect]:-} ]] ; then
if (( code != 0 )) ; then if (( code != 0 )) ; then exit $code ; fi
( set +x ; echo "Something went wrong in the last step of the installation. Inspect the output above and the mounted system in this chroot shell to decide whether it is critical. Exit the shell with 0 to proceed, or non-zero to abort." ) elif [[ ${args[inspect-cmd]:-} ]] ; then
if (( code != 0 )) ; then exit $code ; fi
eval "${args[inspect-cmd]}" || exit
else else
( set +x ; echo "Installation done! This shell is in a chroot in the mounted system for inspection. Exiting the shell will unmount the system." ) if (( code != 0 )) ; then
( set +x ; echo "Something went wrong in the last step of the installation. Inspect the output above and the mounted system in this chroot shell to decide whether it is critical. Exit the shell with 0 to proceed, or non-zero to abort." 1>&2 )
else
( set +x ; echo "Installation done! This shell is in a chroot in the mounted system for inspection. Exiting the shell will unmount the system." 1>&2 )
fi fi
PATH=@{config.systemd.package}/bin:$PATH @{native.nixos-install-tools}/bin/nixos-enter --root $mnt # TODO: construct path as it would be at login PATH=@{config.systemd.package}/bin:$PATH @{native.nixos-install-tools}/bin/nixos-enter --root $mnt || exit # TODO: construct path as it would be at login
#( cd $mnt ; mnt=$mnt @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} ) #( cd $mnt ; mnt=$mnt @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} )
elif (( code != 0 )) ; then
exit $code
fi fi
( mkdir -p $mnt/var/lib/systemd/timesync ; touch $mnt/var/lib/systemd/timesync/clock ) || true # save current time mkdir -p $mnt/var/lib/systemd/timesync && touch $mnt/var/lib/systemd/timesync/clock || true # save current time
)} )}

View File

@ -96,7 +96,7 @@ function run-qemu {( set -eu # 1: diskImages
fi fi
fi fi
if [[ ! ${args[no-nat]:-} ]] ; then # e.g. --nat-fw=8000-:8000,8001-:8001 if [[ ! ${args[no-nat]:-} ]] ; then # e.g. --nat-fw=8000-:8000,8001-:8001,2022-:22
qemu+=( -nic user,model=virtio-net-pci${args[nat-fw]:+,hostfwd=tcp::${args[nat-fw]//,/,hostfwd=tcp::}} ) # NATed, IPs: 10.0.2.15+/32, gateway: 10.0.2.2 qemu+=( -nic user,model=virtio-net-pci${args[nat-fw]:+,hostfwd=tcp::${args[nat-fw]//,/,hostfwd=tcp::}} ) # NATed, IPs: 10.0.2.15+/32, gateway: 10.0.2.2
fi fi

View File

@ -74,29 +74,28 @@ function copy-function { # 1: existingName, 2: newName
} }
## Writes a »$name«d secret from stdin to »$targetDir«, ensuring proper file permissions. ## Writes a »$name«d secret from stdin to »$targetDir«, ensuring proper file permissions.
function write-secret {( set -eu # 1: path, 2?: owner[:[group]], 3?: mode function write-secret {( set -u # 1: path, 2?: owner[:[group]], 3?: mode
mkdir -p -- "$(dirname "$1")"/ mkdir -p -- "$(dirname "$1")"/ || exit
install -o root -g root -m 000 -T /dev/null "$1" install -o root -g root -m 000 -T /dev/null "$1" || exit
secret=$(tee "$1") # copy stdin to path without removing or adding anything secret=$(tee "$1") # copy stdin to path without removing or adding anything
if [[ "${#secret}" == 0 ]] ; then echo "write-secret to $1 was empty!" 1>&2 ; exit 1 ; fi # could also stat the file ... if [[ "${#secret}" == 0 ]] ; then echo "write-secret to $1 was empty!" 1>&2 ; exit 1 ; fi # could also stat the file ...
chown "${2:-root:root}" -- "$1" chown "${2:-root:root}" -- "$1" || exit
chmod "${3:-400}" -- "$1" chmod "${3:-400}" -- "$1" || exit
)} )}
## Interactively prompts for a password to be entered and confirmed. ## Interactively prompts for a password to be entered and confirmed.
function prompt-new-password {( set -eu # 1: usage function prompt-new-password {( set -u # 1: usage
usage=$1 read -s -p "Please enter the new password $1: " password1 || exit ; echo 1>&2
read -s -p "Please enter the new password $usage: " password1 ; echo 1>&2 read -s -p "Please enter the same password again: " password2 || exit ; echo 1>&2
read -s -p "Please enter the same password again: " password2 ; echo 1>&2
if (( ${#password1} == 0 )) || [[ "$password1" != "$password2" ]] ; then printf 'Passwords empty or mismatch, aborting.\n' 1>&2 ; exit 1 ; fi if (( ${#password1} == 0 )) || [[ "$password1" != "$password2" ]] ; then printf 'Passwords empty or mismatch, aborting.\n' 1>&2 ; exit 1 ; fi
printf %s "$password1" printf %s "$password1" || exit
)} )}
## Runs an installer hook script, optionally stepping through the script. ## Runs an installer hook script, optionally stepping through the script.
function run-hook-script {( set -eu # 1: title, 2: scriptPath function run-hook-script {( set -eu # 1: title, 2: scriptPath
trap - EXIT # start with empty traps for sub-shell trap - EXIT # start with empty traps for sub-shell
if [[ ${args[inspectScripts]:-} && "$(cat "$2")" != $'' ]] ; then if [[ ${args[inspectScripts]:-} && "$(cat "$2")" != $'' ]] ; then
echo "Running $1 commands. For each command printed, press Enter to continue or Ctrl+C to abort the installation:" echo "Running $1 commands. For each command printed, press Enter to continue or Ctrl+C to abort the installation:" 1>&2
# (this does not help against intentionally malicious scripts, it's quite easy to trick this) # (this does not help against intentionally malicious scripts, it's quite easy to trick this)
BASH_PREV_COMMAND= ; set -o functrace ; trap 'if [[ $BASH_COMMAND != "$BASH_PREV_COMMAND" ]] ; then echo -n "> $BASH_COMMAND" >&2 ; read ; fi ; BASH_PREV_COMMAND=$BASH_COMMAND' debug BASH_PREV_COMMAND= ; set -o functrace ; trap 'if [[ $BASH_COMMAND != "$BASH_PREV_COMMAND" ]] ; then echo -n "> $BASH_COMMAND" >&2 ; read ; fi ; BASH_PREV_COMMAND=$BASH_COMMAND' debug
fi fi

View File

@ -1,14 +1,25 @@
## Creates the system's ZFS pools and their datasets. ## Creates all of the system's ZFS pools that are »createDuringInstallation«, plus their datasets.
function create-zpools { # 1: mnt function create-zpools { # 1: mnt
local mnt=$1 ; local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do ( set -eu local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
if [[ ! @{config.wip.fs.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
create-zpool "$1" "$poolName"
done
}
## Creates a single of the system's ZFS pools and its datasets.
function create-zpool { # 1: mnt, 2: poolName
local mnt=$1 ; local poolName=$2 ; ( set -u
eval 'declare -A pool='"@{config.wip.fs.zfs.pools[$poolName]}" eval 'declare -A pool='"@{config.wip.fs.zfs.pools[$poolName]}"
eval 'declare -a vdevs='"${pool[vdevArgs]}" eval 'declare -a vdevs='"${pool[vdevArgs]}"
eval 'declare -A poolProps='"${pool[props]}" eval 'declare -A poolProps='"${pool[props]}"
eval 'declare -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}" eval 'declare -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}"
eval 'declare -A dataProps='"${dataset[props]}" eval 'declare -A dataProps='"${dataset[props]}"
get-zfs-crypt-props "${dataset[name]}" dataProps get-zfs-crypt-props "${dataset[name]}" dataProps
declare -a args=( ) declare -a args=( ) ; keySrc=/dev/null
if [[ ${dataProps[keyformat]:-} == ephemeral ]] ; then
dataProps[encryption]=aes-256-gcm ; dataProps[keyformat]=hex ; dataProps[keylocation]=file:///dev/stdin ; keySrc=/dev/urandom
fi
for name in "${!poolProps[@]}" ; do args+=( -o "${name}=${poolProps[$name]}" ) ; done for name in "${!poolProps[@]}" ; do args+=( -o "${name}=${poolProps[$name]}" ) ; done
for name in "${!dataProps[@]}" ; do args+=( -O "${name}=${dataProps[$name]}" ) ; done for name in "${!dataProps[@]}" ; do args+=( -O "${name}=${dataProps[$name]}" ) ; done
for index in "${!vdevs[@]}" ; do for index in "${!vdevs[@]}" ; do
@ -20,12 +31,11 @@ function create-zpools { # 1: mnt
if ! is-partition-on-disks "$part" "${blockDevs[@]}" ; then echo "Partition alias $part used by zpool ${pool[name]} does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi if ! is-partition-on-disks "$part" "${blockDevs[@]}" ; then echo "Partition alias $part used by zpool ${pool[name]} does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
fi fi
done done
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" ) <$keySrc tr -dc 0-9a-f | head -c 64 | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" || exit ) || exit
) && { @{native.zfs}/bin/zfs unload-key "$poolName" &>/dev/null || true
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT ) || return
} ; done && prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
ensure-datasets $mnt '^'"$poolName"'($|[/])' || return
ensure-datasets $mnt
} }
## Ensures that the system's datasets exist and have the defined properties (but not that they don't have properties that aren't defined). ## Ensures that the system's datasets exist and have the defined properties (but not that they don't have properties that aren't defined).
@ -108,7 +118,7 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
)} )}
## Given the name (»datasetPath«) of a ZFS dataset, this deducts crypto-related options from the declared keys (»config.wip.fs.keystore.keys."zfs/..."«). ## Given the name (»datasetPath«) of a ZFS dataset, this deducts crypto-related options from the declared keys (»config.wip.fs.keystore.keys."zfs/..."«).
function get-zfs-crypt-props { # 1: datasetPath, 2: name_cryptProps, 3: name_cryptKey, 4: name_cryptRoot function get-zfs-crypt-props { # 1: datasetPath, 2?: name_cryptProps, 3?: name_cryptKey, 4?: name_cryptRoot
local hash=@{config.networking.hostName!hashString.sha256:0:8} local hash=@{config.networking.hostName!hashString.sha256:0:8}
local keystore=/run/keystore-$hash local keystore=/run/keystore-$hash
local -n __cryptProps=${2:-props} ; local -n __cryptKey=${3:-cryptKey} ; local -n __cryptRoot=${4:-cryptRoot} local -n __cryptProps=${2:-props} ; local -n __cryptKey=${3:-cryptKey} ; local -n __cryptRoot=${4:-cryptRoot}

View File

@ -2,7 +2,7 @@
# NixOS Modules # NixOS Modules
A NixOS module is a collection of any number of NixOS option definitions and value assignments to those or other options. A NixOS module is a collection of any number of NixOS option definitions and value assignments to those or other options.
While the set of imported modules, and thereby that of the defined options, is static (in this case starting with the modules passed to `mkNixosSystem` in `../flake.nix`), the value assignments can generally be contingent on other values (as long as there are no logical loops), making for a highly flexible system construction. While the set of imported modules, and thereby that of the defined options, is static (in this case starting with the modules passed to `mkNixosSystem` in `../flake.nix`), the value assignments can generally be contingent on other values (as long as there are no logical loops), making for highly flexible system constructions.
Since modules can't be imported (or excluded) dynamically, most modules have an `enable` option, which, if false, effectively disables whatever that module does. Since modules can't be imported (or excluded) dynamically, most modules have an `enable` option, which, if false, effectively disables whatever that module does.
Ultimately, the goal of a NixOS configuration is to build an operating system, which is basically a structured collection of program and configuration files. Ultimately, the goal of a NixOS configuration is to build an operating system, which is basically a structured collection of program and configuration files.

View File

@ -9,14 +9,14 @@ Things that really should be (more like) this by default.
```nix ```nix
#*/# end of MarkDown, beginning of NixOS module: #*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let dirname: inputs: specialArgs@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix; prefix = inputs.config.prefix;
cfg = config.${prefix}.base; cfg = config.${prefix}.base;
in { in {
options.${prefix} = { base = { options.${prefix} = { base = {
enable = lib.mkEnableOption "saner defaults"; enable = lib.mkEnableOption "saner defaults";
includeInputs = lib.mkOption { description = "Whether to include all build inputs to the configuration in the final system, such that they are available for self-rebuilds, in the flake registry, and on the »NIX_PATH« entry (e.g. as »pkgs« on the CLI)."; type = lib.types.bool; default = specialArgs?inputs && specialArgs.inputs?self && specialArgs.inputs?nixpkgs; }; includeInputs = lib.mkOption { description = "The system's build inputs, to be included in the flake registry, and on the »NIX_PATH« entry, such that they are available for self-rebuilds and e.g. as »pkgs« on the CLI."; type = lib.types.attrsOf lib.types.anything; apply = lib.filterAttrs (k: v: v != null); default = { }; };
panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; }; # It's stupidly hard to remove items from lists ... panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; }; # It's stupidly hard to remove items from lists ...
makeNoExec = lib.mkEnableOption "(almost) all filesystems being mounted as »noexec« (and »nosuid« and »nodev«)" // { default = false; }; makeNoExec = lib.mkEnableOption "(almost) all filesystems being mounted as »noexec« (and »nosuid« and »nodev«)" // { default = false; };
}; }; }; };
@ -36,6 +36,7 @@ in {
networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName)); networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName));
environment.etc."machine-id".text = lib.mkDefault (builtins.substring 0 32 (builtins.hashString "sha256" "${config.networking.hostName}:machine-id")); # this works, but it "should be considered "confidential", and must not be exposed in untrusted environments" (not sure _why_ though) environment.etc."machine-id".text = lib.mkDefault (builtins.substring 0 32 (builtins.hashString "sha256" "${config.networking.hostName}:machine-id")); # this works, but it "should be considered "confidential", and must not be exposed in untrusted environments" (not sure _why_ though)
documentation.man.enable = lib.mkDefault config.documentation.enable; documentation.man.enable = lib.mkDefault config.documentation.enable;
nix.autoOptimiseStore = true; # because why not ...
}) (lib.mkIf cfg.makeNoExec { ## Hardening }) (lib.mkIf cfg.makeNoExec { ## Hardening
@ -51,6 +52,7 @@ in {
# /run/wrappers needs »exec« »suid« # /run/wrappers needs »exec« »suid«
# /run/binfmt needs »exec« # /run/binfmt needs »exec«
# /run /run/user/* may need »exec« (TODO: test) # /run /run/user/* may need »exec« (TODO: test)
# The Nix build dir (default: /tmp) needs »exec« (TODO)
# Ensure that the /nix/store is not »noexec«, even if the FS it is on is: # Ensure that the /nix/store is not »noexec«, even if the FS it is on is:
boot.initrd.postMountCommands = '' boot.initrd.postMountCommands = ''
@ -90,39 +92,41 @@ in {
systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot. systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot.
}) (lib.mkIf cfg.includeInputs { # non-flake }) (let
name = config.networking.hostName;
in lib.mkIf (cfg.includeInputs?self && cfg.includeInputs.self?nixosConfigurations && cfg.includeInputs.self.nixosConfigurations?${name}) { # non-flake
# Importing »<nixpkgs>« as non-flake returns a lambda returning the evaluated Nix Package Collection (»pkgs«). The most accurate representation of what that should be on the target host is the »pkgs« constructed when building it: # Importing »<nixpkgs>« as non-flake returns a lambda returning the evaluated Nix Package Collection (»pkgs«). The most accurate representation of what that should be on the target host is the »pkgs« constructed when building it:
system.extraSystemBuilderCmds = '' system.extraSystemBuilderCmds = ''
ln -sT ${pkgs.writeText "pkgs.nix" '' ln -sT ${pkgs.writeText "pkgs.nix" ''
# Provide the exact same version of (nix)pkgs on the CLI as in the NixOS-configuration (but note that this ignores the args passed to it; and it'll be a bit slower, as it partially evaluates the host's configuration): # Provide the exact same version of (nix)pkgs on the CLI as in the NixOS-configuration (but note that this ignores the args passed to it; and it'll be a bit slower, as it partially evaluates the host's configuration):
args: (builtins.getFlake ${builtins.toJSON specialArgs.inputs.self.outPath}).nixosConfigurations.${name}.pkgs args: (builtins.getFlake ${builtins.toJSON cfg.includeInputs.self}).nixosConfigurations.${name}.pkgs
''} $out/pkgs # (nixpkgs with overlays) ''} $out/pkgs # (nixpkgs with overlays)
''; # (use this indirection so that all open shells update automatically) ''; # (use this indirection so that all open shells update automatically)
nix.nixPath = [ "nixpkgs=/run/current-system/pkgs" ]; # this intentionally replaces the defaults: nixpkgs is here, /etc/nixos/flake.nix is implicit, channels are impure nix.nixPath = [ "nixpkgs=/run/current-system/pkgs" ]; # this intentionally replaces the defaults: nixpkgs is here, /etc/nixos/flake.nix is implicit, channels are impure
nix.autoOptimiseStore = true; # because why not ...
environment.shellAliases = { "with" = ''nix-shell --run "bash --login" -p''; }; # »with« doesn't seem to be a common linux command yet, and it makes sense here: with $package => do stuff in shell
}) (lib.mkIf cfg.includeInputs { # flake things }) (lib.mkIf (cfg.includeInputs != { }) { # flake things
# "input" to the system build is definitely also a nix version that works with flakes: # "input" to the system build is definitely also a nix version that works with flakes:
nix.extraOptions = "experimental-features = nix-command flakes"; # apparently, even nix 2.8 (in nixos-22.05) needs this nix.extraOptions = "experimental-features = nix-command flakes"; # apparently, even nix 2.8 (in nixos-22.05) needs this
environment.systemPackages = [ pkgs.git ]; # necessary as external dependency when working with flakes environment.systemPackages = [ pkgs.git ]; # necessary as external dependency when working with flakes
# »inputs.self« does not have a name (that is known here), so just register it as »/etc/nixos/« system config: # »inputs.self« does not have a name (that is known here), so just register it as »/etc/nixos/« system config:
environment.etc.nixos.source = lib.mkDefault "/run/current-system/config"; # (use this indirection to prevent every change in the config to necessarily also change »/etc«) environment.etc.nixos.source = lib.mkIf (cfg.includeInputs?self) (lib.mkDefault "/run/current-system/config"); # (use this indirection to prevent every change in the config to necessarily also change »/etc«)
system.extraSystemBuilderCmds = '' system.extraSystemBuilderCmds = lib.mkIf (cfg.includeInputs?self) ''
ln -sT ${specialArgs.inputs.self.outPath} $out/config # (build input for reference) ln -sT ${cfg.includeInputs.self} $out/config # (build input for reference)
''; '';
# Add all inputs to the flake registry: # Add all inputs to the flake registry:
nix.registry = lib.mapAttrs (name: input: lib.mkDefault { flake = input; }) (builtins.removeAttrs specialArgs.inputs [ "self" ]); nix.registry = lib.mapAttrs (name: input: lib.mkDefault { flake = input; }) (builtins.removeAttrs cfg.includeInputs [ "self" ]);
}) ({ }) ({
# Free convenience: # Free convenience:
environment.shellAliases = { "with" = ''nix-shell --run "bash --login" -p''; }; # »with« doesn't seem to be a common linux command yet, and it makes sense here: with $package => do stuff in shell
programs.bash.promptInit = lib.mkDefault '' programs.bash.promptInit = lib.mkDefault ''
# Provide a nice prompt if the terminal supports it. # Provide a nice prompt if the terminal supports it.
if [ "''${TERM:-}" != "dumb" ] ; then if [ "''${TERM:-}" != "dumb" ] ; then
@ -146,6 +150,10 @@ in {
fi fi
''; '';
system.extraSystemBuilderCmds = (if !config.boot.initrd.enable then "" else ''
ln -sT ${builtins.unsafeDiscardStringContext config.system.build.bootStage1} $out/boot-stage-1.sh # (this is super annoying to locate otherwise)
'');
}) ]); }) ]);
} }

View File

@ -5,10 +5,10 @@ NixOS usually gets set up by booting a live system (e.g. from USB), manually set
This runs completely contrary to how NixOS usually does (and should) work (the system state should be derived from the config, not the config from the system state), and it does not at all work for hermetic flakes (where all config needs to be committed in the flake repo) or for automated installations. This runs completely contrary to how NixOS usually does (and should) work (the system state should be derived from the config, not the config from the system state), and it does not at all work for hermetic flakes (where all config needs to be committed in the flake repo) or for automated installations.
[`disks`](./disks.nix.md), [`keystore`](./keystore.nix.md), and [`zfs`](./zfs.nix.md), allow to not only specify how file systems are to be mounted, but also which devices, partitions, and LUKS layers they live on, and for ZFS the complete pool and dataset layout. [`disks`](./disks.nix.md), [`keystore`](./keystore.nix.md), and [`zfs`](./zfs.nix.md) allow to not only specify how file systems are to be mounted, but also which devices, partitions, and LUKS layers they live on, and for ZFS the complete pool and dataset layout.
The bash functions in [`lib/setup-scripts`](../../lib/setup-scripts/README.md) take these definitions and perform arbitrarily complex partitioning and file systems creation, as long as: The bash functions in [`lib/setup-scripts`](../../lib/setup-scripts/README.md) take these definitions and perform arbitrarily complex partitioning and file systems creation, as long as:
* Actual device instances or image files (by path) are passed to the installer for each device specified in the configuration. * Actual device instances or image files (by path) are passed to the installer for each device specified in the configuration.
* Disks are may be partitioned with GPT, but may optionally have an MBR for boot partitions if the loader requires that. * Disks are partitioned with GPT, but may optionally have an MBR for boot partitions if the loader requires that.
* `fileSystems.*.device`/`boot.initrd.luks.devices.*.device`/`wip.fs.zfs.pools.*.vdevArgs` refer to partitions by GPT partition label (`/dev/disk/by-partlabel/*`) or (LUKS) mapped name (`dev/mapper/*`). * `fileSystems.*.device`/`boot.initrd.luks.devices.*.device`/`wip.fs.zfs.pools.*.vdevArgs` refer to partitions by GPT partition label (`/dev/disk/by-partlabel/*`) or (LUKS) mapped name (`dev/mapper/*`).
* `boot.initrd.luks.devices.${name}` have a `wip.fs.keystore.keys."luks/${name}/0"` key specified. * `boot.initrd.luks.devices.${name}` have a `wip.fs.keystore.keys."luks/${name}/0"` key specified.

View File

@ -32,7 +32,7 @@ in {
a;1 # active/boot ; part1 a;1 # active/boot ; part1
''; }; }; ''; }; };
}; };
fileSystems.${cfg.mountpoint} = { fsType = "vfat"; device = "/dev/disk/by-partlabel/boot-${hash}"; neededForBoot = true; options = [ "nosuid" "nodev" "noexec" "noatime" "umask=0022" ]; formatOptions = "-F 32"; }; fileSystems.${cfg.mountpoint} = { fsType = "vfat"; device = "/dev/disk/by-partlabel/boot-${hash}"; neededForBoot = true; options = [ "nosuid" "nodev" "noexec" "noatime" "umask=0027" ]; formatOptions = "-F 32"; };
}) ]); }) ]);

View File

@ -4,10 +4,10 @@
This module does two related things: This module does two related things:
* it provides the specification for encryption keys to be generated during system installation, which are then (automatically) used by the [setup scripts](../../lib/setup-scripts/README.md) for various pieces of file system encryption, * it provides the specification for encryption keys to be generated during system installation, which are then (automatically) used by the [setup scripts](../../lib/setup-scripts/README.md) for various pieces of file system encryption,
* and it configures a `keystore` LUKS device to be opened (according to the keys specified for it) in the initramfs boot stage to use those keys to unlock other encrypted file systems. * and it configures a `keystore` LUKS device to be opened (according to the `keys` specified for it) in the initramfs boot stage, to use those keys to unlock other encrypted file systems.
Keys can always be specified, and the installer may decide to use the setup script functions populating the keystore or not. Keys can always be specified, and the installer may decide to use the setup script functions populating the keystore or not.
The default functions in [`lib/setup-scripts`](../../lib/setup-scripts/README.md) do populating the keystore, and then use the keys according to the description below. The default functions in [`lib/setup-scripts`](../../lib/setup-scripts/README.md) do populate the keystore, and then use the keys according to the description below.
What keys are used for is derived from the attribute name in the `.keys` specification, which (plus a `.key` suffix) also becomes their storage path in the keystore: What keys are used for is derived from the attribute name in the `.keys` specification, which (plus a `.key` suffix) also becomes their storage path in the keystore:
* Keys in `luks/` are used for LUKS devices, where the second path label is both the target device name and source device GPT partition label, and the third and final label is the LUKS key slot (`0` is required to be specified, `1` to `7` are optional). * Keys in `luks/` are used for LUKS devices, where the second path label is both the target device name and source device GPT partition label, and the third and final label is the LUKS key slot (`0` is required to be specified, `1` to `7` are optional).
@ -19,7 +19,7 @@ The format of the key specification is `method[=args]`, where `method` is the su
Most key generation methods only make sense in some key usage contexts. A `random` key is impossible to provide to unlock the keystore (which it is stored in), but is well suited to unlock other devices (if the keystore has backups); conversely a USB-partition can be used to headlessly unlock the keystore, but would be redundant for any further devices, as it would also be copied into the keystore. Most key generation methods only make sense in some key usage contexts. A `random` key is impossible to provide to unlock the keystore (which it is stored in), but is well suited to unlock other devices (if the keystore has backups); conversely a USB-partition can be used to headlessly unlock the keystore, but would be redundant for any further devices, as it would also be copied into the keystore.
If the module is `enable`d, a partition and LUKS device `keystore-...` gets configured and the contents of the installation time keystore is copied to it (in its entirety, including intermediate or derived keys and those unlocking the keystore itself (TODO: this could be optimized)). If the module is `enable`d, a partition and LUKS device `keystore-...` gets configured and the contents of the installation time keystore is copied to it (in its entirety, including intermediate or derived keys and those unlocking the keystore itself (TODO: this could be optimized)).
This LUKS device is then configured to be unlocked (using any ot the key methods specified for it) before anything else during boot, and closed before leaving the initramfs phase. This LUKS device is then configured to be unlocked (using any of the key methods specified for it -- by default, key slot 0 is set to the `hostname`) before anything else during boot, and closed before leaving the initramfs phase.
Any number of other devices may thus specify paths in the keystore as keylocation to be unlocked during boot without needing to prompt for further secrets, and without exposing the keys to the running system. Any number of other devices may thus specify paths in the keystore as keylocation to be unlocked during boot without needing to prompt for further secrets, and without exposing the keys to the running system.
@ -43,7 +43,7 @@ in let module = {
); }; ); };
unlockMethods = { unlockMethods = {
trivialHostname = lib.mkOption { description = "For headless auto boot, use »hostname« (in a file w/o newline) as trivial password/key for the keystore."; type = lib.types.bool; default = lib.elem "hostname" keystoreKeys; }; trivialHostname = lib.mkOption { description = "For headless auto boot, use »hostname« (in a file w/o newline) as trivial password/key for the keystore."; type = lib.types.bool; default = lib.elem "hostname" keystoreKeys; };
usbPartition = lib.mkOption { type = lib.types.bool; default = (lib.elem "usb-part" keystoreKeys); }; usbPartition = lib.mkOption { description = "Use (the random key stored on) a specifically named (tiny) GPT partition (usually on a USB-stick) to automatically unlock the keystore. Use »nix run .#$hostName -- add-bootkey-to-keydev $devPath« (see »${inputs.self}/lib/setup-scripts/maintenance.sh«) to cerate such a partition."; type = lib.types.bool; default = (lib.elem "usb-part" keystoreKeys); };
pinThroughYubikey = lib.mkOption { type = lib.types.bool; default = (lib.any (type: lib.wip.matches "^yubikey-pin=.*$" type) keystoreKeys); }; pinThroughYubikey = lib.mkOption { type = lib.types.bool; default = (lib.any (type: lib.wip.matches "^yubikey-pin=.*$" type) keystoreKeys); };
}; };
}; }; }; };
@ -65,7 +65,7 @@ in let module = {
allowDiscards = lib.mkDefault true; # If attackers can observe trimmed blocks, then they can probably do much worse ... allowDiscards = lib.mkDefault true; # If attackers can observe trimmed blocks, then they can probably do much worse ...
}; }) (lib.wip.filterMatching ''^luks/.*/0$'' (lib.attrNames cfg.keys))); }; }) (lib.wip.filterMatching ''^luks/.*/0$'' (lib.attrNames cfg.keys)));
${prefix}.fs.keystore.keys."luks/keystore-${hash}/0" = lib.mkOptionDefault "hostname"; ${prefix}.fs.keystore.keys."luks/keystore-${hash}/0" = lib.mkOptionDefault "hostname"; # (This is the only key that the setup scripts unconditionally require to be declared.)
}) ({ }) ({

View File

@ -31,7 +31,7 @@ As backed for `local`, which primarily holds the nix store, it benefits from tra
With `fsync` disabled, and the ability to roll back snapshots, it also works to create very large storage areas for `temp` data. With `fsync` disabled, and the ability to roll back snapshots, it also works to create very large storage areas for `temp` data.
ZFS though struggles on lower-end systems. BtrFS could probably be configured to serve the roles with similar capability. ZFS though struggles on lower-end systems. BtrFS could probably be configured to serve the roles with similar capability.
F2FS also supports checksumming and compression, though it currently does not automatically reclaim space gained by the latter (but TODO: Nix could be tuned to do this explicitly). F2FS also supports checksumming and compression, though it currently does not automatically reclaim space gained by the latter (and "manually" enabling it per file prevents `mmap`ing those files).
This and its design optimized for flash storage should make it an optimal backend for the `local` data, esp. on lower-end hardware. This and its design optimized for flash storage should make it an optimal backend for the `local` data, esp. on lower-end hardware.
EXT4 supports checksumming only for metadata, and does not support compression. Block device layers could in principle be used for this. EXT4 supports checksumming only for metadata, and does not support compression. Block device layers could in principle be used for this.
Using a sime filesystem with external backup tools is possible yet suboptimal for `remote` data, unless the system doesn't actually have any/much of it. Using a sime filesystem with external backup tools is possible yet suboptimal for `remote` data, unless the system doesn't actually have any/much of it.
@ -101,10 +101,13 @@ dirname: inputs: specialArgs@{ config, pkgs, lib, utils, ... }: let inherit (inp
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName); hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
keep = if cfg.remote.type == "none" then "local" else "remote"; # preferred place for data that should be kept keep = if cfg.remote.type == "none" then "local" else "remote"; # preferred place for data that should be kept
optionsFor = type: desc: { optionsFor = type: desc: lib.wip.mergeAttrsRecursive [ ({
bind.source = lib.mkOption { description = "Prefix for bind-mount targets."; type = lib.types.str; default = "/.${type}"; };
bind.base = lib.mkOption { description = "Filesystem to automatically create as the ».source« for the bind mounts."; type = lib.types.enum ([ null ] ++ (lib.optionals (type == "local") [ "f2fs" "f2fs-encrypted" ])); default = null; };
zfs.dataset = lib.mkOption { description = "Dataset path under which to create the ${desc} »${type}« datasets."; type = lib.types.str; default = "rpool-${hash}/${type}"; }; zfs.dataset = lib.mkOption { description = "Dataset path under which to create the ${desc} »${type}« datasets."; type = lib.types.str; default = "rpool-${hash}/${type}"; };
}) (lib.optionalAttrs (type != "temp") {
bind.source = lib.mkOption { description = "Prefix for bind-mount targets."; type = lib.types.str; default = "/.${type}"; };
}) (lib.optionalAttrs (type == "local") {
bind.base = lib.mkOption { description = "Filesystem to automatically create as the ».source« for the bind mounts."; type = lib.types.enum [ null "f2fs" "f2fs-encrypted" "ext4" "ext4-encrypted" ]; default = null; };
}) ({
mounts = lib.mkOption { mounts = lib.mkOption {
description = "Locations (for »temp« in addition to »/«) where a ${desc} filesystem should be mounted. Some are declared by default but may be removed by setting them to »null«."; description = "Locations (for »temp« in addition to »/«) where a ${desc} filesystem should be mounted. Some are declared by default but may be removed by setting them to »null«.";
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = { type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = {
@ -113,15 +116,15 @@ dirname: inputs: specialArgs@{ config, pkgs, lib, utils, ... }: let inherit (inp
uid = lib.mkOption { description = "UID owning the mounted target."; type = lib.types.ints.unsigned; default = 0; }; uid = lib.mkOption { description = "UID owning the mounted target."; type = lib.types.ints.unsigned; default = 0; };
gid = lib.mkOption { description = "GID owning the mounted target."; type = lib.types.ints.unsigned; default = 0; }; gid = lib.mkOption { description = "GID owning the mounted target."; type = lib.types.ints.unsigned; default = 0; };
mode = lib.mkOption { description = "Permission mode of the mounted target."; type = lib.types.str; default = "750"; }; mode = lib.mkOption { description = "Permission mode of the mounted target."; type = lib.types.str; default = "750"; };
options = lib.mkOption { description = "Additional mount options to set. Note that not all mount types support all options, they may be silently ignored or cause errors. »bind« supports setting »nosuid«, »nodev«, »noexec«, »noatime«, »nodiratime«, and »relatime«. »zfs« will explicitly heed »noauto«, the other options are applied but may conflict with the ones implied by the ».zfsProps«."; type = lib.types.attrsOf (lib.types.either lib.types.bool lib.types.str); default = { }; }; options = lib.mkOption { description = "Additional mount options to set. Note that not all mount types support all options, they may be silently ignored or cause errors. »bind« supports setting »nosuid«, »nodev«, »noexec«, »noatime«, »nodiratime«, and »relatime«. »zfs« will explicitly heed »noauto«, the other options are applied but may conflict with the ones implied by the ».zfsProps«."; type = lib.types.attrsOf (lib.types.oneOf [ lib.types.bool lib.types.str lib.types.str lib.types.int ]); default = { }; };
extraFsConfig = lib.mkOption { description = "Extra config options to set on the generated »fileSystems.*« entry (unless this mount is forced to »null«)."; type = lib.types.attrsOf lib.types.anything; default = { }; }; extraFsConfig = lib.mkOption { description = "Extra config options to set on the generated »fileSystems.*« entry (unless this mount is forced to »null«)."; type = lib.types.attrsOf lib.types.anything; default = { }; };
zfsProps = lib.mkOption { description = "ZFS properties to set on the dataset, if mode type is »zfs«."; type = lib.types.attrsOf (lib.types.nullOr lib.types.str); default = { }; }; zfsProps = lib.mkOption { description = "ZFS properties to set on the dataset, if mode type is »zfs«. Note that ZFS mounts made in the initramfs don't have the correct mount options from ZFS properties, so properties that affect mount options should (also) be set as ».options«."; type = lib.types.attrsOf (lib.types.nullOr lib.types.str); default = { }; };
}; }))); }; })));
default = { }; default = { };
apply = lib.filterAttrs (k: v: v != null); apply = lib.filterAttrs (k: v: v != null);
}; };
mountOptions = lib.mkOption { description = "Mount options that will be merged under ».mounts.*.options«."; type = lib.types.attrsOf (lib.types.either lib.types.bool lib.types.str); default = { nosuid = true; nodev = true; noatime = true; }; }; mountOptions = lib.mkOption { description = "Mount options that will be merged under ».mounts.*.options«."; type = lib.types.attrsOf (lib.types.oneOf [ lib.types.bool lib.types.str lib.types.str lib.types.int ]); default = { nosuid = true; nodev = true; noatime = true; }; };
}; }) ];
zfsNoSyncProps = { sync = "disabled"; logbias = "throughput"; }; # According to the documentation, »logbias« should be irrelevant without sync (i.e. no logging), but some claim setting it to »throughput« still improves performance. zfsNoSyncProps = { sync = "disabled"; logbias = "throughput"; }; # According to the documentation, »logbias« should be irrelevant without sync (i.e. no logging), but some claim setting it to »throughput« still improves performance.
in { in {
@ -167,7 +170,7 @@ in {
config = let config = let
optionsToList = attrs: lib.mapAttrsToList (k: v: if v == true then k else "${k}=${v}") (lib.filterAttrs (k: v: v != false) attrs); optionsToList = attrs: lib.mapAttrsToList (k: v: if v == true then k else "${k}=${toString v}") (lib.filterAttrs (k: v: v != false) attrs);
in lib.mkIf cfg.enable (lib.mkMerge ([ ({ in lib.mkIf cfg.enable (lib.mkMerge ([ ({
@ -260,33 +263,52 @@ in {
boot.cleanTmpDir = true; # Clear »/tmp« on reboot. boot.cleanTmpDir = true; # Clear »/tmp« on reboot.
}) (lib.mkIf (cfg.local.type == "bind" && (cfg.local.bind.base == "f2fs" || cfg.local.bind.base == "f2fs-encrypted")) (let # Convenience option to create a local F2FS optimized to host the nix store: }) (lib.mkIf (cfg.local.type == "bind" && (cfg.local.bind.base != null)) (let # Convenience option to create a local F2FS/EXT4 optimized to host the nix store:
type = if cfg.local.bind.base == "f2fs" || cfg.local.bind.base == "f2fs-encrypted" then "f2fs" else "ext4";
encrypted = cfg.local.bind.base == "f2fs-encrypted" || cfg.local.bind.base == "ext4-encrypted";
useLuks = config.${prefix}.fs.keystore.keys?"luks/local-${hash}/0"; useLuks = config.${prefix}.fs.keystore.keys?"luks/local-${hash}/0";
in { in {
# TODO: fsck # TODO: fsck
${prefix} = { ${prefix} = {
fs.keystore.keys."luks/local-${hash}/0" = lib.mkIf (cfg.local.bind.base == "f2fs-encrypted") (lib.mkOptionDefault "random"); fs.keystore.keys."luks/local-${hash}/0" = lib.mkIf encrypted (lib.mkOptionDefault "random");
fs.disks.partitions."local-${hash}" = { fs.disks.partitions."local-${hash}" = {
type = "8300"; order = lib.mkDefault 1000; disk = lib.mkDefault "primary"; size = lib.mkDefault (if cfg.remote.type == "none" then null else "50%"); type = "8300"; order = lib.mkDefault 1000; disk = lib.mkDefault "primary"; size = lib.mkDefault (if cfg.remote.type == "none" then null else "50%");
}; };
}; };
fileSystems.${cfg.local.bind.source} = { fsType = "f2fs"; device = "/dev/${if useLuks then "mapper" else "disk/by-partlabel"}/local-${hash}"; formatOptions = (lib.concatStrings [ fileSystems.${cfg.local.bind.source} = {
"-O extra_attr" # required by other options fsType = type; device = "/dev/${if useLuks then "mapper" else "disk/by-partlabel"}/local-${hash}";
} // (if type == "f2fs" then {
formatOptions = (lib.concatStrings [
" -O extra_attr" # required by other options
",inode_checksum" # enable inode checksum ",inode_checksum" # enable inode checksum
",sb_checksum" # enable superblock checksum ",sb_checksum" # enable superblock checksum
",compression" # allow compression ",compression" # allow compression
#"-w ?" # "sector size in bytes" #"-w ?" # "sector size in bytes"
# sector ? segments < section < zone # sector ? segments < section < zone
]); options = optionsToList (cfg.temp.mountOptions // { ]);
options = optionsToList (cfg.local.mountOptions // {
# F2FS compresses only for performance and wear. The whole uncompressed space is still reserved (in case the file content needs to get replaced by incompressible data in-place). To free the gained space, »ioctl(fd, F2FS_IOC_RELEASE_COMPRESS_BLOCKS)« needs to be called per file, making the file immutable. Nix could do that when moving stuff into the store. # F2FS compresses only for performance and wear. The whole uncompressed space is still reserved (in case the file content needs to get replaced by incompressible data in-place). To free the gained space, »ioctl(fd, F2FS_IOC_RELEASE_COMPRESS_BLOCKS)« needs to be called per file, making the file immutable. Nix could do that when moving stuff into the store.
compress_mode = "fs"; # enable compression for all files compress_mode = "fs"; # enable compression for all files
compress_algorithm = "lz4"; # compress using lz4 compress_algorithm = "lz4"; # compress using lz4
compress_chksum = true; # verify checksums (when decompressing data blocks?) compress_chksum = true; # verify checksums (when decompressing data blocks?)
lazytime = true; # update timestamps asynchronously lazytime = true; # update timestamps asynchronously
}); }; });
} else {
formatOptions = (lib.concatStrings [
" -O inline_data" # embed data of small files in the top-level inode
",has_journal,extent,huge_file,flex_bg,metadata_csum,64bit,dir_nlink,extra_isize" # (ext4 default options)
#",lazy_journal_init,lazy_itable_init" # speed up creation (but: Invalid filesystem option set)
" -I 256" # inode size (ext default, allows for timestamps past 2038)
" -i 16384" # create one inode per 16k bytes of disk (ext default)
" -b 4096" # block size (ext default)
" -E nodiscard" # do not trim the whole blockdev upon formatting
" -e panic" # when (critical?) FS errors are detected, reset the system
]); options = optionsToList (cfg.local.mountOptions // {
});
});
# TODO: "F2FS and its tools support various parameters not only for configuring on-disk layout, but also for selecting allocation and cleaning algorithms." # TODO: "F2FS and its tools support various parameters not only for configuring on-disk layout, but also for selecting allocation and cleaning algorithms."
boot.initrd.kernelModules = [ "f2fs" ]; # This is not generally, but sometimes, required to boot. Strange. (Kernel message: »request_module fs-f2fs succeeded, but still no fs?«) boot.initrd.kernelModules = [ type ]; # This is not generally, but sometimes, required to boot. Strange. (Kernel message: »request_module fs-f2fs succeeded, but still no fs?«)
})) ] ++ (map (type: (lib.mkIf (cfg.${type}.type == "bind") { })) ] ++ (map (type: (lib.mkIf (cfg.${type}.type == "bind") {
@ -328,7 +350,11 @@ in {
mount = if (options.noauto or false) == true then "noauto" else true; inherit uid gid mode; mount = if (options.noauto or false) == true then "noauto" else true; inherit uid gid mode;
props = { canmount = "noauto"; mountpoint = target; } // zfsProps; props = { canmount = "noauto"; mountpoint = target; } // zfsProps;
}; };
}) cfg.${type}.mounts); } // (
lib.wip.mapMerge (prefix: if (lib.any (_:_.source == prefix) (lib.attrValues cfg.${type}.mounts)) then { } else {
"${dataset}/${prefix}" = lib.mkDefault { props.canmount = "off"; };
}) (lib.wip.parentPaths source)
)) cfg.${type}.mounts);
}; };
fileSystems = lib.mapAttrs (target: args@{ extraFsConfig, ... }: extraFsConfig // { fileSystems = lib.mapAttrs (target: args@{ extraFsConfig, ... }: extraFsConfig // {

View File

@ -23,13 +23,14 @@ in let module = {
description = "ZFS pools created during this host's installation."; description = "ZFS pools created during this host's installation.";
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = { type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = {
name = lib.mkOption { description = "Attribute name as name of the pool."; type = lib.types.str; default = name; readOnly = true; }; name = lib.mkOption { description = "Attribute name as name of the pool."; type = lib.types.str; default = name; readOnly = true; };
vdevArgs = lib.mkOption { description = "List of arguments that specify the virtual devices (vdevs) used when initially creating the pool. Can consist of the device type keywords and partition labels. The latter are prefixed with »/dev/mapper/« if a mapping with that name is configured or »/dev/disk/by-partlabel/« otherwise, and then the resulting argument sequence is is used verbatim in »zpool create«."; type = lib.types.listOf lib.types.str; default = [ name ]; example = [ "raidz1 data1-..." "data2-..." "data3-..." "cache" "cache-..." ]; }; vdevArgs = lib.mkOption { description = "List of arguments that specify the virtual devices (vdevs) used when initially creating the pool. Can consist of the device type keywords and partition labels. The latter are prefixed with »/dev/mapper/« if a mapping with that name is configured or »/dev/disk/by-partlabel/« otherwise, and then the resulting argument sequence is is used verbatim in »zpool create«."; type = lib.types.listOf lib.types.str; default = [ name ]; example = [ "raidz1" "data1-..." "data2-..." "data3-..." "cache" "cache-..." ]; };
props = lib.mkOption { description = "Zpool properties to pass when creating the pool. May also set »feature@...« and »compatibility«."; type = lib.types.attrsOf (lib.types.nullOr lib.types.str); default = { }; }; props = lib.mkOption { description = "Zpool properties to pass when creating the pool. May also set »feature@...« and »compatibility«."; type = lib.types.attrsOf (lib.types.nullOr lib.types.str); default = { }; };
autoApplyDuringBoot = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets in the initramfs phase during boot for this pool> This can be useful since the keystore is open but no datasets are mounted at that time") // { default = true; }; createDuringInstallation = (lib.mkEnableOption "creation of this pool during system installation. If disabled, the pool needs to exist already or be created manually and the pools disk devices are expected to be present from the first boot onwards") // { default = true; };
autoApplyDuringBoot = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets in the initramfs phase during boot for this pool. This can be useful since the keystore is open but no datasets are mounted at that time") // { default = true; };
autoApplyOnActivation = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets on system activation for this pool. This may fail for some changes since datasets may be mounted and the keystore is usually closed at this time. Enable ».autoApplyDuringBoot« and reboot to address this") // { default = true; }; autoApplyOnActivation = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets on system activation for this pool. This may fail for some changes since datasets may be mounted and the keystore is usually closed at this time. Enable ».autoApplyDuringBoot« and reboot to address this") // { default = true; };
}; config = { }; config = {
props.ashift = lib.mkOptionDefault "12"; # be explicit props.ashift = lib.mkOptionDefault "12"; # be explicit
props.comment = lib.mkOptionDefault "hostname=${config.networking.hostName};"; # This is just nice to know without needing to inspect the datasets. props.comment = lib.mkOptionDefault "hostname=${config.networking.hostName};"; # This is just nice to know without needing to inspect the datasets (»zpool import« shows the comment).
props.cachefile = lib.mkOptionDefault "none"; # If it works on first boot without (stateful) cachefile, then it will also do so later. props.cachefile = lib.mkOptionDefault "none"; # If it works on first boot without (stateful) cachefile, then it will also do so later.
}; }))); }; })));
default = { }; default = { };
@ -70,6 +71,9 @@ in let module = {
"${props.mountpoint}" = { fsType = "zfs"; device = path; options = [ "zfsutil" ] ++ (lib.optionals (mount == "noauto") [ "noauto" ]); }; "${props.mountpoint}" = { fsType = "zfs"; device = path; options = [ "zfsutil" ] ++ (lib.optionals (mount == "noauto") [ "noauto" ]); };
} else { }) cfg.datasets; } else { }) cfg.datasets;
## Load keys (only) for (all) datasets that are declared as encryption roots and aren't disabled:
boot.zfs.requestEncryptionCredentials = lib.attrNames (lib.filterAttrs (name: { props, ... }: if props?keylocation then props.keylocation != "file:///dev/null" else config.${prefix}.fs.keystore.keys?"zfs/${name}") cfg.datasets);
${prefix} = { ${prefix} = {
# Set default root dataset properties for every pool: # Set default root dataset properties for every pool:
fs.zfs.datasets = lib.mapAttrs (name: { ... }: { props = { fs.zfs.datasets = lib.mapAttrs (name: { ... }: { props = {
@ -98,6 +102,7 @@ in let module = {
}) ({ ## Implement »cfg.extraInitrdPools«: }) ({ ## Implement »cfg.extraInitrdPools«:
boot.initrd.postDeviceCommands = (lib.mkAfter '' boot.initrd.postDeviceCommands = (lib.mkAfter ''
${lib.concatStringsSep "\n" (map verbose.initrd-import-zpool cfg.extraInitrdPools)} ${lib.concatStringsSep "\n" (map verbose.initrd-import-zpool cfg.extraInitrdPools)}
${verbose.initrd-load-keys} ${verbose.initrd-load-keys}

View File

@ -0,0 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

108
modules/hardware/rpi.nix.md Normal file
View File

@ -0,0 +1,108 @@
/*
# Raspberry PI 3 / 4 / Zero 2
Device specific config for any 64bit capable Raspberry PI (esp. rPI4 and CM4, but also 3B(+) and Zero 2, maybe others).
## Notes
* All the `boot.loader.raspberryPi.*` stuff (and maybe also `boot.kernelParams`) seems to be effectively disabled if `boot.loader.generic-extlinux-compatible.enable == true`.
The odd thing is that various online sources, including `nixos-hardware`, enable extlinux (this may simply be because `nixos-generate-config` sets this by default, purely based on the presence of `/boot/extlinux` in the (derived from more generic) default images).
Without extlinux, u-boot is also disabled, which means that (on an rPI4) there is no way to get a generation selection menu, and generations would need to be restored by moving files in the `/boot` partition manually.
* Installing to the eMMC of a CM4: set the "eMMC Boot" jumper/switch to off/disabled; run `nix-shell -p rpiboot --run 'sudo rpiboot'` on a different host; and connect the CM4 carrier to it via the USB-OTG (microUSB / USB-C) port. Then install to the new block device (`/dev/sdX`) that should pop up on the host.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.hardware.raspberry-pi;
in {
options.${prefix} = { hardware.raspberry-pi = {
enable = lib.mkEnableOption "base configuration for Raspberry Pi 64bit hardware";
i2c = lib.mkEnableOption "the ARM i²c /dev/i2c-1 on pins 3+5 / GPIO2+3 (/ SDA+SCL)";
lightless = lib.mkEnableOption "operation without any activity lights";
}; };
## Import the rPI4 config from nixos-hardware, but have it disabled by default.
# This provides some options for additional onboard hardware components as »hardware.raspberry-pi."4".*«, see: https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/
imports = let
path = "${inputs.nixos-hardware}/raspberry-pi/4/default.nix"; module = import path args;
in [ { _file = path; imports = [ {
config = lib.mkIf cfg.enable (builtins.removeAttrs module [ "imports" ]);
} ]; } ] ++ module.imports;
config = lib.mkIf cfg.enable (lib.mkMerge [ ({ ## General rPI Stuff (firmware/boot/drivers)
boot.loader.raspberryPi.enable = true;
boot.loader.raspberryPi.version = lib.mkDefault 4; # (For now only relevant with »loader.raspberryPi.uboot.enable«? Which doesn't work with RPI4 yet.)
#boot.loader.raspberryPi.native.copyOldKernels = false; # TODO: Should tell »raspberrypi-builder.sh#copyToKernelsDir« to not to copy old kernels (but instead let the user get them from the main FS to restore them). This is implemented in the script, but there is no option (or code path) to change that parameter.
# use »boot.loader.raspberryPi.version = 3;« for Raspberry PI 3(B+)
boot.loader.grub.enable = false;
boot.loader.generic-extlinux-compatible.enable = lib.mkForce false; # See "Notes" above
# GPU support: https://nixos.wiki/wiki/NixOS_on_ARM/Raspberry_Pi_4#With_GPU
boot.kernelPackages = pkgs.linuxPackagesFor pkgs."linux_rpi${toString config.boot.loader.raspberryPi.version}";
boot.initrd.kernelModules = [ ];
boot.initrd.availableKernelModules = [ "usbhid" "usb_storage" "vc4" ]; # (»vc4« ~= VideoCore driver)
## Serial console:
# Different generations of rPIs have different hardware capabilities in terms of UARTs (driver chips and pins they are connected to), and different device trees (and options for them) and boot stages (firmware/bootloader/OS) can initialize them differently.
# There are three components to a UART port: the driver chip in the CPU, which GPIOs (and through the PCB thereby physical pins) or other interface (bluetooth) they are connected to, and how they are exposed by the running kernel (»/dev/?«).
# All rPIs so far have at least one (faster, data transfer capable) "fully featured PL011" uart0 chip, and a slower, console only "mini" uart1 chip.
# In Linux, uart0 usually maps to »/dev/ttyAMA0«, and uart1 to »/dev/ttyS0«. »/dev/serial0«/»1« are symlinks in Raspbian.
# What interfaces (GPIO/bluetooth) the chips connect to is configured in the device tree. The following should be true for the official/default device trees:
# When bluetooth is enabled (hardware present and not somehow disabled) then uart0 "physically" connects to that, and uart1 connects to pins 08+10 / GPIO 14+15, otherwise the former connects to those pins and the latter is unused. The uart at GPIO 14+15 is referred to as "primary" in the rPI documentation.
# If uart1 is primary, then UART is disabled by default, because uart1 only works at a fixed (250MHz) GPU/bus speed. At some performance or energy cost, the speed can be fixed, enabling the UART, by setting »enable_uart=1« (or »core_freq=250« or »force_turbo=1« (i.e. 400?)) in the firmware config.
# Bottom line, to use UART on GPIO 14+15, one needs to either disable bluetooth / not have it / disconnect uart0 from it and can (only then) use »/dev/ttyAMA0«, or fix the GPU speed and use uart1 (»/dev/ttyS0«).
# On a rPI4, one could use the additional (fast) uart2/3/4/5. Those need to be enabled via device tree( overlay)s, and will be exposed as »/dev/ttyAMAx«.
# For example: boot.loader.raspberryPi.firmwareConfig = "enable_uart=1\n"; boot.kernelParams = [ "console=ttyS0,115200" ];
# TODO: NixOS sets »enable_uart=1« by default. That is probably a bad idea.
boot.kernelParams = [
#"8250.nr_uarts=1" # 8250 seems to be the name of a serial port driver kernel module, which has a compiled limit of ports it manages. Setting this can override that. Not sure whether the 8250 module has any relevance for the rPI.
# (someones claim:) Some gui programs need this
#"cma=128M" # (Continuous Memory Area?)
];
hardware.enableRedistributableFirmware = true; # for WiFi
#hardware.firmware = [ pkgs.firmwareLinuxNonfree pkgs.raspberrypiWirelessFirmware ]; # TODO: try with this instead
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand"; # (not sure whether this is the best choice ...)
hardware.raspberry-pi."4".dwc2.enable = true; # this is required for (the builtin) USBs on the CM4 to work (with NixOS' device trees, setting »dtoverlay=dwc2« in config.txt or eeprom has no effect)
#hardware.raspberry-pi."4".dwc2.dr_mode = lib.mkDefault "otg"; # or "peripheral" or "host" ("otg" seems to work just fine also as host)
hardware.deviceTree.enable = true;
hardware.deviceTree.filter = lib.mkForce "bcm271[01]-rpi-*.dtb"; # rPI(cm)2/3/4(B(+))/Zero2(W) models
environment.systemPackages = with pkgs; [
libraspberrypi # »vcgencmd measure_temp« etc.
raspberrypi-eeprom # rpi-eeprom-update
];
}) (lib.mkIf cfg.i2c { ## i2c
hardware.i2c.enable = true; # includes »boot.kernelModules = [ "i2c-dev" ]« and some »services.udev.extraRules«
environment.systemPackages = [ pkgs.i2c-tools ]; # e.g. »i2cdetect«
boot.loader.raspberryPi.firmwareConfig = "dtparam=i2c_arm=on\n"; # with the default dtb, this enables the ARM i²c /dev/i2c-1 on pins 3+5 / GPIO2+3 (/ SDA+SCL) of all tested rPI models (this has mostly the same effect as setting »hardware.raspberry-pi."4".i2c1.enable«)
# "dtparam=i2c_vc=on" enables the VideoCore i²c on pins 27+28 / GPIO0+1, but https://raspberrypi.stackexchange.com/questions/116726/enabling-of-i2c-0-via-dtparam-i2c-vc-on-on-pi-3b-causes-i2c-10-i2c-11-t
# (there is also »hardware.raspberry-pi."4".i2c{0,1}.enable« as an alternative way to enable i2c_arm and i2c_vc, but that option seems bcm2711(/rPI4) specific)
}) (lib.mkIf cfg.lightless {
boot.loader.raspberryPi.firmwareConfig = ''
# turn off ethernet LEDs
dtparam=eth_led0=4
dtparam=eth_led1=4
'';
systemd.tmpfiles.rules = [
"w /sys/class/leds/led0/brightness - - - - 0" # yellow (activity) LED
"w /sys/class/leds/led1/brightness - - - - 0" # red (power) LED
];
}) ]);
}

View File

@ -0,0 +1,13 @@
diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
index 1a0da005029..7a82bbeeb19 100644
--- a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
+++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
@@ -128,6 +128,8 @@ DEFAULT nixos-default
MENU TITLE ------------------------------------------------------------
TIMEOUT $timeout
+# TODO: Check whether this is an issue with uboot
+UI menu.c32
EOF
addEntry $default default >> $tmpFile

View File

@ -0,0 +1,12 @@
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 05174e03754..b4620ee1715 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -100,6 +100,7 @@ let
getKeyLocations = pool:
if isBool cfgZfs.requestEncryptionCredentials
then "${cfgZfs.package}/sbin/zfs list -rHo name,keylocation,keystatus ${pool}"
+ else if (filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials) == [ ] then ":"
else "${cfgZfs.package}/sbin/zfs list -Ho name,keylocation,keystatus ${toString (filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials)}";
createImportService = { pool, systemd, force, prefix ? "" }:

View File

@ -0,0 +1,14 @@
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index 0c93b288..d26d630a 100644
# For systems with BIOS boot, allow specifying the grub device by partition (e.g. by partlabel).
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -725,6 +725,8 @@ symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!";
if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
foreach my $dev (@deviceTargets) {
next if $dev eq "nodev";
+ my $realpath = qx{realpath $dev};
+ if ($realpath =~ m;^/dev/(sd[a-z](?=\d+\s$)|\w+\d+(?=p\d+\s$));) { $dev = "/dev/$1"; }
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") {