mirror of
https://github.com/NiklasGollenstede/nixos-installer.git
synced 2024-11-21 15:33:21 +01:00
add hardware config for Raspberry PIs, start making scripts more robust, improve compatibility with containers
This commit is contained in:
parent
1d93a8acc0
commit
df8c451050
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -1,6 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"jnoortheen.nix-ide",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
],
|
||||
}
|
||||
|
21
.vscode/settings.json
vendored
21
.vscode/settings.json
vendored
@ -10,6 +10,7 @@
|
||||
"attrset", "attrsets", // nix/abbr (attribute set)
|
||||
"autologin", // agetty
|
||||
"binfmt", // abbr "binary format"
|
||||
"blkdiscard", // program
|
||||
"blkid", // program / function
|
||||
"blockdev", // program / function
|
||||
"bootable", // word (~= able to boot)
|
||||
@ -20,6 +21,7 @@
|
||||
"builtins", // nix
|
||||
"cachefile", // zfs
|
||||
"canmount", // zfs
|
||||
"cdetect", // abbr
|
||||
"checksumming", // word
|
||||
"cmake", // program
|
||||
"cmds", // abbr (commands)
|
||||
@ -36,22 +38,29 @@
|
||||
"dedup", // zfs
|
||||
"deps", // abbr dependencies
|
||||
"devs", // abbr (devices)
|
||||
"dir_nlink", // ext4 option
|
||||
"dmask", // mount
|
||||
"dnodesize", // zfs
|
||||
"dontUnpack", // nixos
|
||||
"dosfstools", // package
|
||||
"draid", // zfs
|
||||
"dropbear", // program
|
||||
"dtoverlay", // option
|
||||
"dtparam", // option
|
||||
"e2fsprogs", // package
|
||||
"eeprom", // abbr
|
||||
"elif", // abbr (else if)
|
||||
"encryptionroot", // zfs
|
||||
"extglob", // cli arg
|
||||
"extlinux", // program
|
||||
"extra_isize", // ext4 option
|
||||
"fallocate", // program / function
|
||||
"fdisk", // program
|
||||
"fetchpatch", // nix
|
||||
"fetchurl", // nix function
|
||||
"filesystems", // plural
|
||||
"findutils", // package
|
||||
"firmwareLinuxNonfree", // nixos
|
||||
"fmask", // mount
|
||||
"foldl", // nix (fold left)
|
||||
"foldr", // nix (fold right)
|
||||
@ -66,6 +75,7 @@
|
||||
"gnugrep", // package
|
||||
"gnused", // package
|
||||
"gollenstede", // name
|
||||
"gpio", // abbr (general purpose IO)
|
||||
"gptfdisk", // package
|
||||
"headlessly", // word
|
||||
"hostbus", // cli arg
|
||||
@ -82,8 +92,11 @@
|
||||
"keylocation", // zfs
|
||||
"keystatus", // zfs
|
||||
"kmod", // linux
|
||||
"lazy_itable_init", // ext4 option
|
||||
"lazytime", // f2fs
|
||||
"leds", // plural
|
||||
"libblockdev", // package
|
||||
"libraspberrypi", // program
|
||||
"libubootenv", // package
|
||||
"logbias", // zfs
|
||||
"losetup", // program / function
|
||||
@ -91,9 +104,11 @@
|
||||
"lsusb", // program / function
|
||||
"luks", // linux
|
||||
"macaddr", // cli arg
|
||||
"metadata_csum", // ext4 option
|
||||
"mkdir", // program / function
|
||||
"mkenvimage", // program
|
||||
"mktemp", // program / function
|
||||
"mmap", // abbr "memory map"
|
||||
"modifyvm", // virtual box
|
||||
"mountpoint", // program / function
|
||||
"mtab", // linux
|
||||
@ -114,6 +129,7 @@
|
||||
"noheadings", // cli arg
|
||||
"nohibernate", // kernel param
|
||||
"nosuid", // mount option
|
||||
"ondemand", // concat
|
||||
"oneshot", // systemd
|
||||
"optimise", // B/E
|
||||
"ostype", // virtual box
|
||||
@ -137,8 +153,11 @@
|
||||
"raspi3b", // cli arg
|
||||
"rawdisk", // virtual box
|
||||
"realpath", // program / function
|
||||
"redistributable", // word
|
||||
"refreservation", // zfs
|
||||
"relatime", // mount option
|
||||
"rpiboot", // package
|
||||
"rpicm4", // abbr (Raspberry PI Compute Module 4)
|
||||
"rpool", // zfs
|
||||
"rprivate", // linux
|
||||
"sata", // storage protocol
|
||||
@ -176,7 +195,9 @@
|
||||
"upperdir", // mount overlay option
|
||||
"upstreamed", // word
|
||||
"urandom", // linux
|
||||
"usbhid", // kmod
|
||||
"vboxusers", // virtual box
|
||||
"vcgencmd", // program
|
||||
"vdev", "vdevs", // zfs
|
||||
"vfat", // linux
|
||||
"virt", // abbr (virtualization)
|
||||
|
BIN
flake.lock
BIN
flake.lock
Binary file not shown.
@ -6,6 +6,7 @@
|
||||
|
||||
# To update »./flake.lock«: $ nix flake update
|
||||
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.
|
||||
|
||||
}; outputs = inputs: let patches = {
|
||||
@ -20,7 +21,7 @@
|
||||
|
||||
in [ # Run »nix flake show --allow-import-from-derivation« to see what this merges to:
|
||||
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
|
||||
packages = builtins.removeAttrs (lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays) [ "libblockdev" ];
|
||||
defaultPackage = self.packages.${localSystem}.all-systems;
|
||||
|
@ -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«
|
||||
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
|
||||
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";
|
||||
|
||||
@ -43,7 +43,7 @@ in { imports = [ ({ ## Hardware
|
||||
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.
|
||||
wip.fs.disks.partitions."boot-${hash}" = { type = "ef00"; size = "64M"; index = 1; order = 1500; };
|
||||
|
@ -2,5 +2,5 @@ dirname: inputs@{ self, nixpkgs, ...}: let
|
||||
#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 = (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; }; }
|
||||
|
@ -1,10 +1,11 @@
|
||||
dirname: inputs@{ self, nixpkgs, ...}: let
|
||||
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}/scripts.nix" dirname inputs) substituteImplicit;
|
||||
setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs);
|
||||
prefix = inputs.config.prefix;
|
||||
inherit (import "${dirname}/misc.nix" dirname inputs) trace;
|
||||
in rec {
|
||||
|
||||
# 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;
|
||||
in outputs (builtins.mapAttrs (name: input: if name != "self" && patches?${name} && patches.${name} != [ ] then (let
|
||||
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};
|
||||
};
|
||||
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).
|
||||
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;
|
||||
in if (
|
||||
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«.
|
||||
# See »mkSystemsFlake« for documentation of the arguments.
|
||||
mkNixosConfiguration = args@{ name, entryPath, peers, inputs, overlays, modules, nixosSystem, localSystem ? null, ... }: let
|
||||
preface = (getSystemPreface inputs entryPath ({ inherit lib; } // specialArgs));
|
||||
mkNixosConfiguration = args@{ name, entryPath ? null, config ? null, preface ? null,peers ? { }, inputs ? [ ], overlays ? [ ], modules ? [ ], nixosSystem, localSystem ? null, ... }: let
|
||||
preface = args.preface or (getSystemPreface inputs entryPath (specialArgs // { inherit name; }));
|
||||
targetSystem = "${preface.hardware}-linux"; buildSystem = if localSystem != null then localSystem else targetSystem;
|
||||
specialArgs = { # make these available in the attrSet passed to the modules
|
||||
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!
|
||||
} // (args.specialArgs or { }) // {
|
||||
inherit name; nodes = peers; # NixOPS
|
||||
specialArgs = (args.specialArgs or { }) // {
|
||||
nodes = peers; # NixOPS
|
||||
};
|
||||
in let system = { inherit preface; } // (nixosSystem {
|
||||
system = buildSystem;
|
||||
modules = [ (
|
||||
(importWrapped inputs entryPath).module
|
||||
) {
|
||||
# The system architecture (often referred to as »system«).
|
||||
options.${prefix}.preface.hardware = lib.mkOption { type = lib.types.str; readOnly = true; };
|
||||
} {
|
||||
# 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 ]; };
|
||||
} {
|
||||
|
||||
modules = [ # Anything specific to only this evaluation of the module tree should go here.
|
||||
(args.config or (importWrapped inputs entryPath).module)
|
||||
{ _module.args = { inherit name; }; }
|
||||
{ networking.hostName = 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 {
|
||||
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 = {
|
||||
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; };
|
||||
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; };
|
||||
}; })));
|
||||
apply = lib.filterAttrs (k: v: v != null);
|
||||
}; 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 {
|
||||
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;
|
||||
|
||||
# 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).
|
||||
# All other arguments are as specified by »mkSystemsFlake« and are passed to »mkNixosConfiguration«.
|
||||
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
|
||||
stripped = builtins.match ''^(.*)[.]nix[.]md$'' (builtins.baseNameOf entryPath);
|
||||
name = builtins.unsafeDiscardStringContext (if stripped != null then (builtins.elemAt stripped 0) else (builtins.baseNameOf entryPath));
|
||||
in { inherit name; value = entryPath; }) files));
|
||||
|
||||
configs = mapMerge (name: entryPath: (let
|
||||
configs = mapMergeUnique (name: entryPath: (let
|
||||
preface = (getSystemPreface inputs entryPath { });
|
||||
in (mapMerge (name: {
|
||||
in (mapMergeUnique (name: {
|
||||
"${name}" = mkNixosConfiguration ((
|
||||
builtins.removeAttrs args [ "files" "dir" "exclude" ]
|
||||
) // {
|
||||
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;
|
||||
ids = mapMerge (name: node: { "${toString node.preface.id}" = name; }) withId;
|
||||
@ -156,14 +162,14 @@ in rec {
|
||||
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.
|
||||
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«).
|
||||
overlays ? (builtins.concatLists (map (input: if input?overlay then [ input.overlay ] else if input?overlays then builtins.attrValues input.overlays else [ ]) (builtins.attrValues overlayInputs))),
|
||||
# List of overlays to set as »config.nixpkgs.overlays«. Defaults to ».overlays.default« of all »overlayInputs«/»inputs« (incl. »inputs.self«).
|
||||
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; }«.
|
||||
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)«).
|
||||
modules ? (map (input: input.nixosModule or (if input?nixosModules then { imports = builtins.attrValues input.nixosModules; } else { })) (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«.
|
||||
moduleInputs ? (builtins.removeAttrs inputs [ "nixpkgs" ]),
|
||||
# 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 ? (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. Example: »{ inherit (inputs) self flakeA flakeB; }«.
|
||||
moduleInputs ? inputs,
|
||||
# Additional arguments passed to each module evaluated for the host config (if that module is defined as a function).
|
||||
specialArgs ? { },
|
||||
# The »nixosSystem« function defined in »<nixpkgs>/flake.nix«, or equivalent.
|
||||
@ -196,7 +202,7 @@ in rec {
|
||||
# $ 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):
|
||||
# $ 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 [[ $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«.
|
||||
if [[ ''${1:-} == -x ]] ; then shift ; set -x ; fi
|
||||
if [[ ''${1:-} == -c ]] ; then eval "$2" ; else "$@" ; fi
|
||||
''}"; }) nixosConfigurations;
|
||||
''; program = "${derivation}"; }) nixosConfigurations;
|
||||
|
||||
# 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).
|
||||
|
@ -1,19 +1,29 @@
|
||||
dirname: inputs@{ self, nixpkgs, ...}: let
|
||||
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 {
|
||||
|
||||
# Return a list of the absolute paths of all folders and ».nix« or ».nix.md« files in »dir« whose names are not in »except«.
|
||||
getNixFiles = dir: except: let listing = builtins.readDir dir; in (builtins.filter (e: e != null) (map (name: (
|
||||
if !(builtins.elem name except) && (listing.${name} == "directory" || (builtins.match ''.*[.]nix([.]md)?$'' name) != null) then "${dir}/${name}" else null
|
||||
)) (builtins.attrNames listing)));
|
||||
|
||||
# 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
|
||||
# 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: mapMergeUnique (name: type: if (type == "directory") then (
|
||||
if (builtins.pathExists "${dir}/${name}/default.nix") then { ${name} = "${dir}/${name}/default.nix"; } else { }
|
||||
) else (
|
||||
let
|
||||
match = builtins.match ''^(.*)[.]nix([.]md)?$'' name;
|
||||
in if (match != null) then { ${builtins.head match} = "${dir}/${name}"; } else { }
|
||||
) else { }) (builtins.attrNames listing);
|
||||
in if (match != null) then {
|
||||
${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.
|
||||
# Probably because almost everything could be a module declaration (any attribute set or function returning one is potentially a module).
|
||||
@ -26,10 +36,10 @@ in rec {
|
||||
|
||||
## Decides whether a thing could be a NixPkgs overlay.
|
||||
# Any function with two (usually unnamed) arguments returning an attrset could be an overlay, so that's rather vague.
|
||||
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.
|
||||
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.
|
||||
importWrapped = inputs: path: rec {
|
||||
@ -51,7 +61,7 @@ in rec {
|
||||
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.
|
||||
# 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
|
||||
@ -64,22 +74,22 @@ in rec {
|
||||
# The top level »default.nix« returns:
|
||||
# { "a" = <filtered>; "b" = <filtered>; "c/d" = <filtered>; "c/e" = <filtered>; }
|
||||
importFilteredFlattened = dir: inputs: { except ? [ ], filter ? (thing: true), wrap ? (path: thing: thing), }: let
|
||||
files = getNamedNixFiles dir (except ++ [ "default.nix" ]);
|
||||
in mapMerge (name: path: let
|
||||
files = builtins.removeAttrs (getNixFiles dir) except;
|
||||
in mapMergeUnique (name: path: let
|
||||
thing = import path (if endsWith "/default.nix" path then "${dir}/${name}" else dir) inputs;
|
||||
in if (filter thing) then (
|
||||
{ ${name} = wrap path thing; }
|
||||
) else (if (builtins.isAttrs thing) then (
|
||||
mapMerge (name': thing': if (filter thing') then (
|
||||
mapMergeUnique (name': thing': if (filter thing') then (
|
||||
{ "${name}/${name'}" = thing'; }
|
||||
) else { }) thing
|
||||
) 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.
|
||||
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.
|
||||
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.*«.
|
||||
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.
|
||||
getModifiedPackages = pkgs: overlays: let
|
||||
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.
|
||||
# Every module should, but not all modules do, provide such an option themselves.
|
||||
|
@ -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.
|
||||
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
|
||||
function install-system {( set -eu # 1: diskPaths
|
||||
prepare-installer "$@"
|
||||
@ -19,10 +19,10 @@ function install-system {( set -eu # 1: diskPaths
|
||||
|
||||
# `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`.
|
||||
On a NixOS host, this can be run by root as: `#` `nix run .#"$HOST" -- install-system "$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 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.
|
||||
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).
|
||||
|
||||
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.
|
||||
|
@ -94,7 +94,7 @@ function gen-key-yubikey-challenge {( set -eu # 1: _, 2: serialAndSlotAndChallen
|
||||
serial=$( <<<"$args" cut -d: -f1 ) ; slot=$( <<<"$args" cut -d: -f2 )
|
||||
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
|
||||
read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' twice with '"${message:-challenge »"$challenge":1/2«}"'. Enter to continue, or Ctrl+C to abort:'
|
||||
else
|
||||
|
@ -6,25 +6,26 @@
|
||||
## Prepares the disks of the target system for the copying of files.
|
||||
function do-disk-setup { # 1: diskPaths
|
||||
|
||||
prompt-for-user-passwords &&
|
||||
populate-keystore &&
|
||||
prompt-for-user-passwords || return
|
||||
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" &&
|
||||
create-luks-layers && open-luks-layers && # 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} &&
|
||||
partition-disks "$1" || return
|
||||
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} || return
|
||||
|
||||
format-partitions &&
|
||||
{ [[ $(LC_ALL=C type -t create-zpools) != function ]] || create-zpools $mnt ; } &&
|
||||
run-hook-script 'Post Formatting' @{config.wip.fs.disks.postFormatCommands!writeText.postFormatCommands} &&
|
||||
format-partitions || return
|
||||
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} || return
|
||||
|
||||
prepend_trap "unmount-system $mnt" EXIT && mount-system $mnt &&
|
||||
run-hook-script 'Post Mounting' @{config.wip.fs.disks.postMountCommands!writeText.postMountCommands} &&
|
||||
:
|
||||
fix-grub-install || return
|
||||
|
||||
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.
|
||||
# * (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.
|
||||
@ -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.
|
||||
# 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
|
||||
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
function partition-disks { # 1: diskPaths
|
||||
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; 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
|
||||
local path ; for path in ${1//:/ } ; do
|
||||
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
|
||||
done
|
||||
|
||||
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]}"
|
||||
if [[ ${blockDevs[$name]} != /dev/* ]] ; then
|
||||
local outFile=${blockDevs[$name]} &&
|
||||
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
|
||||
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 (( 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 )) && [[ ! ${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 )) ; then echo "Wasting $(( waste / 1024))K of ${blockDevs[$name]} due to the size declared for $name (should be ${size}b)" ; 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" ; 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" 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)" 1>&2 ; fi
|
||||
blockDevs[$name]=$(realpath "${blockDevs[$name]}")
|
||||
fi
|
||||
done
|
||||
|
||||
} && ( set -eu
|
||||
|
||||
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
|
||||
eval 'declare -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
|
||||
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!" 1>&2 ; return 1 ; fi
|
||||
done
|
||||
|
||||
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
|
||||
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
|
||||
# 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]}]}"
|
||||
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
|
||||
|
||||
# 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.
|
||||
function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize
|
||||
name=$1 ; blockDev=$2
|
||||
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
eval 'declare -A disk='"@{config.wip.fs.disks.devices[$name]}"
|
||||
devSize=${3:-$( @{native.util-linux}/bin/blockdev --getsize64 "$blockDev" )}
|
||||
function partition-disk { # 1: name, 2: blockDev, 3?: devSize
|
||||
local name=$1 ; local blockDev=$2
|
||||
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
|
||||
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
|
||||
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]} )) )
|
||||
@ -102,7 +103,7 @@ function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize
|
||||
sgdisk+=( --disk-guid="${disk[guid]}" )
|
||||
|
||||
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[size]:-} =~ ^[0-9]+%$ ]] ; then
|
||||
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]]
|
||||
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
|
||||
printf "
|
||||
@ -140,23 +141,23 @@ function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize
|
||||
|
||||
${disk[extraFDiskCommands]}
|
||||
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
|
||||
)}
|
||||
}
|
||||
|
||||
## Checks whether a »partition« resides on one of the provided »blockDevs«.
|
||||
function is-partition-on-disks {( set -eu # 1: partition, ...: blockDevs
|
||||
partition=$1 ; shift ; declare -a blockDevs=( "$@" )
|
||||
blockDev=$(realpath "$partition") ; if [[ $blockDev == /dev/sd* ]] ; then
|
||||
function is-partition-on-disks { # 1: partition, ...: blockDevs
|
||||
local partition=$1 ; shift ; local -a blockDevs=( "$@" )
|
||||
local blockDev=$(realpath "$partition") ; if [[ $blockDev == /dev/sd* ]] ; then
|
||||
blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" )
|
||||
else
|
||||
blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" )
|
||||
fi
|
||||
[[ ' '"${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.
|
||||
function format-partitions {( set -eu
|
||||
function format-partitions {( set -u
|
||||
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
for fsDecl in "@{config.fileSystems[@]}" ; do
|
||||
@ -166,7 +167,9 @@ function format-partitions {( set -eu
|
||||
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
|
||||
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
|
||||
done
|
||||
for swapDev in "@{config.swapDevices!catAttrs.device[@]}" ; do
|
||||
@ -175,10 +178,28 @@ function format-partitions {( set -eu
|
||||
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
|
||||
else continue ; fi
|
||||
( set -x ; mkswap "$swapDev" >$beLoud 2>$beSilent )
|
||||
( ${_set_x:-:} ; mkswap "$swapDev" >$beLoud 2>$beSilent ) || exit
|
||||
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 »/«).
|
||||
function mount-system {( set -eu # 1: mnt, 2?: fstabPath
|
||||
# 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,/,}
|
||||
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then continue ; fi
|
||||
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)
|
||||
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
|
||||
) || [[ $options == *,nofail,* ]] ; fi # (actually, nofail already makes mount fail silently)
|
||||
done
|
||||
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
|
||||
) || [[ $options == *,nofail,* ]] || exit ; fi # (actually, nofail already makes mount fail silently)
|
||||
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.
|
||||
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
|
||||
if [[ ! $target || $target == none ]] ; then continue ; fi
|
||||
options=,$options, ; options=${options//,ro,/,}
|
||||
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then : ; else continue ; fi
|
||||
if ! mountpoint -q "$mnt"/"$target" ; then (
|
||||
mkdir -p "$mnt"/"$target"
|
||||
mkdir -p "$mnt"/"$target" || exit
|
||||
if [[ $type == overlay ]] ; then
|
||||
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
|
||||
@ -210,11 +231,11 @@ function mount-system {( set -eu # 1: mnt, 2?: fstabPath
|
||||
options=${options//,lowerdir=$lowerdir,/,lowerdir=$mnt/${lowerdir//:/:$mnt\/},} ; source=overlay
|
||||
else
|
||||
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
|
||||
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
|
||||
) || [[ $options == *,nofail,* ]] ; fi
|
||||
done
|
||||
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
|
||||
) || [[ $options == *,nofail,* ]] || exit ; fi
|
||||
done || exit
|
||||
)}
|
||||
|
||||
## Unmounts all file systems (that would be mounted during boot / by »mount-system«).
|
||||
|
@ -4,33 +4,33 @@
|
||||
##
|
||||
|
||||
## 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
|
||||
prepare-installer "$@"
|
||||
do-disk-setup "${argv[0]}"
|
||||
install-system-to $mnt
|
||||
prepare-installer "$@" || exit
|
||||
do-disk-setup "${argv[0]}" || exit
|
||||
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).
|
||||
function prepare-installer { # ...
|
||||
|
||||
generic-arg-parse "$@"
|
||||
generic-arg-parse "$@" || return
|
||||
|
||||
if [[ ${args[debug]:-} ]] ; then set -x ; fi
|
||||
|
||||
: ${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).
|
||||
|
||||
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)
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
# »$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:-}
|
||||
targetSystem=${args[toplevel]:-@{config.system.build.toplevel}}
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
|
||||
# Link/create files that some tooling expects:
|
||||
mkdir -p -m 755 $mnt/nix/var/nix ; mkdir -p -m 1775 $mnt/nix/store
|
||||
mkdir -p $mnt/etc $mnt/run ; mkdir -p -m 1777 $mnt/tmp
|
||||
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.
|
||||
[[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS # for »switch-to-configuration«
|
||||
[[ -e $mnt/etc/mtab ]] || ln -sfn /proc/mounts $mnt/etc/mtab
|
||||
ln -sT $(realpath $targetSystem) $mnt/run/current-system
|
||||
mkdir -p -m 755 $mnt/nix/var/nix || exit ; mkdir -p -m 1775 $mnt/nix/store || exit
|
||||
mkdir -p $mnt/etc $mnt/run || exit ; mkdir -p -m 1777 $mnt/tmp || exit
|
||||
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 || exit # for »switch-to-configuration«
|
||||
[[ -e $mnt/etc/mtab ]] || ln -sfn /proc/mounts $mnt/etc/mtab || exit
|
||||
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.«
|
||||
|
||||
# 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
|
||||
mkdir -p -- $mnt/@{config.environment.etc.nixos.source} ; cp -at $mnt/@{config.environment.etc.nixos.source} -- $targetSystem/config/*
|
||||
chown -R 0:0 $mnt/@{config.environment.etc.nixos.source} ; chmod -R u+w $mnt/@{config.environment.etc.nixos.source}
|
||||
mkdir -p -- $mnt/@{config.environment.etc.nixos.source} || exit
|
||||
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
|
||||
|
||||
# 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)
|
||||
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«.
|
||||
fi
|
||||
|
||||
# Copy system closure to new nix store:
|
||||
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var ; 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
|
||||
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[@]}" --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
|
||||
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):
|
||||
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)
|
||||
run-hook-script 'Pre Installation' @{config.wip.fs.disks.preInstallCommands!writeText.preInstallCommands}
|
||||
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} || exit
|
||||
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!
|
||||
if [[ ! ${args[no-inspect]:-} ]] ; then
|
||||
if [[ ${args[no-inspect]:-} ]] ; then
|
||||
if (( code != 0 )) ; then exit $code ; fi
|
||||
elif [[ ${args[inspect-cmd]:-} ]] ; then
|
||||
if (( code != 0 )) ; then exit $code ; fi
|
||||
eval "${args[inspect-cmd]}" || exit
|
||||
else
|
||||
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." )
|
||||
( 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." )
|
||||
( 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
|
||||
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} )
|
||||
elif (( code != 0 )) ; then
|
||||
exit $code
|
||||
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
|
||||
)}
|
||||
|
@ -96,7 +96,7 @@ function run-qemu {( set -eu # 1: diskImages
|
||||
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
|
||||
fi
|
||||
|
||||
|
@ -74,29 +74,28 @@ function copy-function { # 1: existingName, 2: newName
|
||||
}
|
||||
|
||||
## 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
|
||||
mkdir -p -- "$(dirname "$1")"/
|
||||
install -o root -g root -m 000 -T /dev/null "$1"
|
||||
function write-secret {( set -u # 1: path, 2?: owner[:[group]], 3?: mode
|
||||
mkdir -p -- "$(dirname "$1")"/ || exit
|
||||
install -o root -g root -m 000 -T /dev/null "$1" || exit
|
||||
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 ...
|
||||
chown "${2:-root:root}" -- "$1"
|
||||
chmod "${3:-400}" -- "$1"
|
||||
chown "${2:-root:root}" -- "$1" || exit
|
||||
chmod "${3:-400}" -- "$1" || exit
|
||||
)}
|
||||
|
||||
## Interactively prompts for a password to be entered and confirmed.
|
||||
function prompt-new-password {( set -eu # 1: usage
|
||||
usage=$1
|
||||
read -s -p "Please enter the new password $usage: " password1 ; echo 1>&2
|
||||
read -s -p "Please enter the same password again: " password2 ; echo 1>&2
|
||||
function prompt-new-password {( set -u # 1: usage
|
||||
read -s -p "Please enter the new password $1: " password1 || exit ; echo 1>&2
|
||||
read -s -p "Please enter the same password again: " password2 || exit ; echo 1>&2
|
||||
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.
|
||||
function run-hook-script {( set -eu # 1: title, 2: scriptPath
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
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)
|
||||
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
|
||||
|
@ -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
|
||||
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 vdevs='"${pool[vdevArgs]}"
|
||||
eval 'declare -A poolProps='"${pool[props]}"
|
||||
eval 'declare -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}"
|
||||
eval 'declare -A dataProps='"${dataset[props]}"
|
||||
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 "${!dataProps[@]}" ; do args+=( -O "${name}=${dataProps[$name]}" ) ; done
|
||||
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
|
||||
fi
|
||||
done
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" )
|
||||
) && {
|
||||
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT
|
||||
} ; done &&
|
||||
|
||||
ensure-datasets $mnt
|
||||
<$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
|
||||
) || return
|
||||
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
|
||||
ensure-datasets $mnt '^'"$poolName"'($|[/])' || return
|
||||
}
|
||||
|
||||
## 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/..."«).
|
||||
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 keystore=/run/keystore-$hash
|
||||
local -n __cryptProps=${2:-props} ; local -n __cryptKey=${3:-cryptKey} ; local -n __cryptRoot=${4:-cryptRoot}
|
||||
|
@ -2,7 +2,7 @@
|
||||
# NixOS Modules
|
||||
|
||||
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.
|
||||
|
||||
Ultimately, the goal of a NixOS configuration is to build an operating system, which is basically a structured collection of program and configuration files.
|
||||
|
@ -9,14 +9,14 @@ Things that really should be (more like) this by default.
|
||||
|
||||
```nix
|
||||
#*/# 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;
|
||||
cfg = config.${prefix}.base;
|
||||
in {
|
||||
|
||||
options.${prefix} = { base = {
|
||||
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 ...
|
||||
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));
|
||||
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;
|
||||
nix.autoOptimiseStore = true; # because why not ...
|
||||
|
||||
|
||||
}) (lib.mkIf cfg.makeNoExec { ## Hardening
|
||||
@ -51,6 +52,7 @@ in {
|
||||
# /run/wrappers needs »exec« »suid«
|
||||
# /run/binfmt needs »exec«
|
||||
# /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:
|
||||
boot.initrd.postMountCommands = ''
|
||||
@ -90,39 +92,41 @@ in {
|
||||
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:
|
||||
system.extraSystemBuilderCmds = ''
|
||||
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):
|
||||
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)
|
||||
''; # (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.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:
|
||||
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
|
||||
|
||||
# »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«)
|
||||
system.extraSystemBuilderCmds = ''
|
||||
ln -sT ${specialArgs.inputs.self.outPath} $out/config # (build input for reference)
|
||||
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 = lib.mkIf (cfg.includeInputs?self) ''
|
||||
ln -sT ${cfg.includeInputs.self} $out/config # (build input for reference)
|
||||
'';
|
||||
|
||||
# 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:
|
||||
|
||||
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 ''
|
||||
# Provide a nice prompt if the terminal supports it.
|
||||
if [ "''${TERM:-}" != "dumb" ] ; then
|
||||
@ -146,6 +150,10 @@ in {
|
||||
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)
|
||||
'');
|
||||
|
||||
}) ]);
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
[`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:
|
||||
* 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/*`).
|
||||
* `boot.initrd.luks.devices.${name}` have a `wip.fs.keystore.keys."luks/${name}/0"` key specified.
|
||||
|
||||
|
@ -32,7 +32,7 @@ in {
|
||||
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"; };
|
||||
|
||||
}) ]);
|
||||
|
||||
|
@ -4,10 +4,10 @@
|
||||
|
||||
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,
|
||||
* 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.
|
||||
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:
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ in let module = {
|
||||
); };
|
||||
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; };
|
||||
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); };
|
||||
};
|
||||
}; };
|
||||
@ -65,7 +65,7 @@ in let module = {
|
||||
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)));
|
||||
|
||||
${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.)
|
||||
|
||||
}) ({
|
||||
|
||||
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
@ -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);
|
||||
keep = if cfg.remote.type == "none" then "local" else "remote"; # preferred place for data that should be kept
|
||||
|
||||
optionsFor = type: desc: {
|
||||
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; };
|
||||
optionsFor = type: desc: lib.wip.mergeAttrsRecursive [ ({
|
||||
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 {
|
||||
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 = {
|
||||
@ -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; };
|
||||
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"; };
|
||||
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 = { }; };
|
||||
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 = { };
|
||||
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.
|
||||
in {
|
||||
@ -167,7 +170,7 @@ in {
|
||||
|
||||
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 ([ ({
|
||||
|
||||
@ -260,33 +263,52 @@ in {
|
||||
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";
|
||||
in {
|
||||
|
||||
# TODO: fsck
|
||||
${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}" = {
|
||||
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 [
|
||||
"-O extra_attr" # required by other options
|
||||
",inode_checksum" # enable inode checksum
|
||||
",sb_checksum" # enable superblock checksum
|
||||
",compression" # allow compression
|
||||
#"-w ?" # "sector size in bytes"
|
||||
# sector ? segments < section < zone
|
||||
]); options = optionsToList (cfg.temp.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.
|
||||
compress_mode = "fs"; # enable compression for all files
|
||||
compress_algorithm = "lz4"; # compress using lz4
|
||||
compress_chksum = true; # verify checksums (when decompressing data blocks?)
|
||||
lazytime = true; # update timestamps asynchronously
|
||||
}); };
|
||||
fileSystems.${cfg.local.bind.source} = {
|
||||
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
|
||||
",sb_checksum" # enable superblock checksum
|
||||
",compression" # allow compression
|
||||
#"-w ?" # "sector size in bytes"
|
||||
# sector ? segments < section < zone
|
||||
]);
|
||||
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.
|
||||
compress_mode = "fs"; # enable compression for all files
|
||||
compress_algorithm = "lz4"; # compress using lz4
|
||||
compress_chksum = true; # verify checksums (when decompressing data blocks?)
|
||||
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."
|
||||
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") {
|
||||
@ -328,7 +350,11 @@ in {
|
||||
mount = if (options.noauto or false) == true then "noauto" else true; inherit uid gid mode;
|
||||
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 // {
|
||||
|
@ -23,13 +23,14 @@ in let module = {
|
||||
description = "ZFS pools created during this host's installation.";
|
||||
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; };
|
||||
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 = { }; };
|
||||
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; };
|
||||
}; config = {
|
||||
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.
|
||||
}; })));
|
||||
default = { };
|
||||
@ -70,6 +71,9 @@ in let module = {
|
||||
"${props.mountpoint}" = { fsType = "zfs"; device = path; options = [ "zfsutil" ] ++ (lib.optionals (mount == "noauto") [ "noauto" ]); };
|
||||
} 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} = {
|
||||
# Set default root dataset properties for every pool:
|
||||
fs.zfs.datasets = lib.mapAttrs (name: { ... }: { props = {
|
||||
@ -98,6 +102,7 @@ in let module = {
|
||||
|
||||
|
||||
}) ({ ## Implement »cfg.extraInitrdPools«:
|
||||
|
||||
boot.initrd.postDeviceCommands = (lib.mkAfter ''
|
||||
${lib.concatStringsSep "\n" (map verbose.initrd-import-zpool cfg.extraInitrdPools)}
|
||||
${verbose.initrd-load-keys}
|
||||
|
1
modules/hardware/default.nix
Normal file
1
modules/hardware/default.nix
Normal file
@ -0,0 +1 @@
|
||||
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }
|
108
modules/hardware/rpi.nix.md
Normal file
108
modules/hardware/rpi.nix.md
Normal 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
|
||||
];
|
||||
|
||||
}) ]);
|
||||
}
|
13
patches/nixpkgs-add-extlinud-ui.patch
Normal file
13
patches/nixpkgs-add-extlinud-ui.patch
Normal 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
|
12
patches/nixpkgs-fix-zfs-import-keys.patch
Normal file
12
patches/nixpkgs-fix-zfs-import-keys.patch
Normal 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 ? "" }:
|
14
patches/nixpkgs-grub-part.patch
Normal file
14
patches/nixpkgs-grub-part.patch
Normal 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") {
|
Loading…
Reference in New Issue
Block a user