This commit is contained in:
Niklas Gollenstede 2022-05-09 13:20:43 +02:00
parent 5e9cc7ce34
commit 1f72d9bf26
34 changed files with 1649 additions and 0 deletions

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
# Treat lock file as binary (collapsed diff, no line count, no EOL treatment):
flake.lock binary
# Dont include license in line count:
LICENSE binary

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# This file is also used in »rsync --exclude-from='.gitignore'«, so keep the format compatible!
# (nixos-rebuild build, etc)
/result
/result-*

97
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,97 @@
{
"cSpell.diagnosticLevel": "Information", // to find spelling mistakes
"cSpell.words": [
"aarch64", // processor architecture
"attrset", "attrsets", // nix/abbr (attribute set)
"autologin", // agetty
"binfmt", // abbr "binary format"
"blockdev", // program / function
"bootable", // word (~= able to boot)
"bootloader", // word
"bridgeadapter", // virtual box
"bridgedifs", // virtual box
"builtins", // nix
"cmake", // program
"cmds", // abbr commands
"concat", // abbr
"controlvm", // virtual box
"convertfromraw", // virtual box
"coreutils", // package
"createrawvmdk", // virtual box
"createvm", // virtual box
"ctxify", // abbr (~= add context)
"CWD", // abbr
"deps", // abbr dependencies
"devs", // abbr (devices)
"dontUnpack", // nixos
"dropbear", // program
"extglob", // cli arg
"fallocate", // program / function
"fdisk", // program
"fetchpatch", // nix
"fetchurl", // nix function
"foldl", // nix (fold left)
"foldr", // nix (fold right)
"FUNCNAME", // bash var
"fw_printenv", // program
"fw_setenv", // program
"gdisk", // program
"getsize64", // cli arg
"getty", // serice
"gids", // abbr/plural (group IDs)
"gollenstede", // name
"hostiocache", // virtual box
"internalcommands", // virtual box
"libubootenv", // package
"losetup", // program / function
"lowerdir", // mount overlay option
"mkdir", // program / function
"mktemp", // program / function
"modifyvm", // virtual box
"mountpoint", // program / function
"namespacing", // word
"netbootxyz", // option
"nixos", // (duh)
"nixpkgs", // nix
"noatime", // mount option
"nodiscard", // cli arg
"ostype", // virtual box
"partlabel", // linux
"partprobe", // program / function
"pkgs", // nix
"pname", // nix/abbr (package name)
"portcount", // virtual box
"poweroff", // program / function
"raspberrypi", // abbr
"raspberrypifw", // package
"rawdisk", // virtual box
"realpath", // program / function
"rpool", // zfs
"sata", // storage protocol
"sbabic", // name
"screenshotpng", // virtual box
"sgdisk", // program
"showvminfo", // virtual box
"sigs", // cli arg
"socat", // program / function
"startvm", // virtual box
"stdenv", // nix
"storageattach", // virtual box
"timesync", // systemd
"TMPDIR", // env var
"tmpfs", // linux
"toplevel", // nix
"uart", "uarts", // serial protocol
"uartmode", // virtual box
"udev", // program
"udevadm", // program
"udptunnel", // program
"uids", // abbr/plural (group IDs)
"upperdir", // mount overlay option
"vboxusers", // virtual box
"vfat", // linux
"vmdk", // file type (virtual disk format)
"wiplib", // name / abbr (WIP library)
"workdir", // mount overlay option
]
}

BIN
LICENSE Normal file

Binary file not shown.

88
README.md Normal file
View File

@ -0,0 +1,88 @@
# Work In Progress Nix(OS) Library
Whenever I have a Nix function, NixOS Module, nixpkgs package/overlay, related bash script, or combination of those that I need in more than one project, I first put it here so that it can be shared between them.
Eventually I may decide to move parts of this into their own flake repository, but as long as they live here, APIs are not necessarily stable.
The more interesting of the components currently in this repository are largely concerned with good structures for Nix flakes, in particular those defining NixOS configurations, and installing hosts from those configurations.
## Repo Layout
This is a nix flake repository, so [`flake.nix`](./flake.nix) is the entry point and export mechanism for almost everything.
[`lib/`](./lib/) adds additional library functions as `.wip` to the default `nixpkgs.lib` and exports the whole thing as `lib`. Other folders in this repo may thus use them as `inputs.self.lib.wip.*`. \
[`lib/setup-scripts/`](./lib/setup-scripts/) contains some bash scripts that integrate with the options defined in [modules/installer.nix.md](./modules/installer.nix.md) and some default options to help installing NixOS hosts.
[`modules/`](./modules/) contains NixOS configuration modules. Added options' names start with `wip.` (or a custom prefix, see [Namespacing](#namespacing-in-nixos)).
The modules are inactive by default, and are, where possible, designed to be independent from each other and the other things in this repo. Some though do have dependencies on added or modified packages, or other modules in the same directory.
[`modules/default.nix`](./modules/default.nix) exports an attr set of the modules defined in the individual files, which is also what is exported as `flake#outputs.nixosModules` and merged as `flake#outputs.nixosModule`.
[`overlays/`](./overlays/) contains nixpkgs overlays. Some modify packages from `nixpkgs`, others add packages not in there (yet).
[`overlays/default.nix`](./overlays/default.nix) exports an attr set of the overlays defined in the individual files, which is also what is exported as `flake#outputs.overlays` and merged as `flake#outputs.overlay`. Additionally, the added or modified packages are exported as `flake#outputs.packages.<arch>.*`.
[`patches/`](./patches/) contains patches which are either applied to the flake's inputs in [`flake.nix`](./flake.nix) or to packages in one of the [`overlays/`](./overlays/).
[`hosts/`](./hosts/) contains the main NixOS config modules for each host. Generally, there is one file for each host, but the [flake](./flake.nix) can be instructed to reuse the config for multiple hosts (in which case the module should probably interpret the `name` argument passed to it).
Any `preface.*` options have to be set in the first sub-module in these files (`## Hardware` section). \
This flake only defines a single [`example`](./hosts/example.nix.md) host meant to demonstrate how other flakes can use the (NixOS) flake library framework.
[`utils/`](./utils/) contains the [installation](./utils/install.sh.md) script for the hosts (which is three lines bash, plus a lot of documentation) and this flake's [default config](./utils/defaultConfig/) (see [Namespacing](#namespacing-in-nixos)).
## Namespacing in NixOS
One of the weak points of NixOS is namespacing. NixOS is traditionally based on the `nixpkgs` monorepo.
The `pkgs` package set is intentionally a global namespace, so that different parts of the system by default use the same instance of each respective package (unless there is a specific reason not to).
The caller to the top-level function constructing a NixOS system can provide `lib` as a set of Nix library functions. This library set is provided as global argument to all imported modules. `nixpkgs` has its default `lib` set, which its modules depend on.
If a flake exports `nixosModules` to be used by another flake to construct systems, then those modules either need to restrict themselves to the default `lib` (in the expectation that that is what will be passed) or instruct the caller to attach some additional functions (exported together with the modules) to `lib`. The former leads to code duplication within the modules, the latter is an additional requirement on the caller, and since `lib` is global, naming conflicts in the `lib` required by different modules are quite possible. The same problem applies to the strategy of supplying additional global arguments to the modules.
Since a nix flake exports instantiated Nix language constructs, not source code, it is possible to define the modules in their source code files wrapped in an outer function, which gets called by the exporting flake before exporting. Consequently, it can supply arguments which are under control of the module author, providing a library set tailored to and exposed exclusively to the local modules, thus completely avoiding naming conflicts.
NixOS modules however define their configuration options in a hierarchical, but global, namespace, and some of those options are necessarily meant to be accessed from modules external to the defining flake.
Usually, for any given module, an importing flake would only have the option to either include a module or not. If two modules define options of conflicting names, then they can't be imported at the same time, even if they could otherwise coexist.
The only workaround (that I could come up with) is to have a flake-level option that allows to change the names of the options defined in the modules exported by that flake, for example by changing their first hierarchical label.
Since flakes are purely functional, the only way to provide configuration to a flake as a whole (as opposed to exporting parts of the flake as functions, which would break the convention on flake exports) is via the flakes `inputs`, and those inputs must be flakes themselves.
The inputs have defaults defined by the flake itself, but can be overridden by the importing flake.
A flake using the modules exported by this flake may thus accept the default that all options are defined under the prefix `wip.`, or it may override its `config` input by a flake of the same shape as [`utils/defaultConfig/`](./utils/defaultConfig/) but with a different `prefix`.
As a local experiment, the result of running this in a `nix repl` is sufficient:
```nix
:b (import <nixpkgs> { }).writeTextDir "flake.nix" ''
{ outputs = { ... }: {
prefix = "<str>";
}; }
''
```
## Other Concepts
### `.xx.md` files
Often, the concept expressed by a source code file is at least as important as the concrete implementation of it.
`nix` unfortunately isn't super readable and also does not have documentation tooling support nearly on par with languages like TypeScript.
Embedding the source code "file" within a MarkDown file emphasizes the importance of textual expressions of the motivation and context of each piece of source code, and should thus incentivize writing sufficient documentation
Technically, Nix (and most other code files) don't need to have any specific file extension. By embedding the MarkDown header in a block comment, the file can still be a valid source code file, while the MarDown header ending in a typed code block ensures proper syntax highlighting of the source code in editors or online repos.
## Notepad
### `nix repl`
```nix
pkgs = import <nixpkgs> { }
:lf . # load CWD's flake's outputs as variables
pkgs = nixosConfigurations.target.pkgs
lib = lib { inherit pkgs; inherit (pkgs) lib; }
```
### TODOs

16
example/install.sh.md Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
: << '```bash'
# System Installer Script
This is a minimal example for a NixOS system installation function using the functions defined in [`../lib/setup-scripts/`](../lib/setup-scripts/). See its [README](../lib/setup-scripts/README.md) for more documentation.
## Implementation
```bash
function install-system {( set -eu # 1: blockDev
prepare-installer "$@"
do-disk-setup "$1"
install-system-to $mnt prompt=true @{config.th.minify.topLevel:-}
)}

BIN
flake.lock Normal file

Binary file not shown.

36
flake.nix Normal file
View File

@ -0,0 +1,36 @@
{ description = (
"Work In Progress: a collection of Nix things that are used in more than one project, but aren't refined enough to be standalone libraries/modules/... (yet)."
/**
* This flake file defines the main inputs (all except for some files/archives fetched by hardcoded hash) and exports almost all usable results.
* It should always pass »nix flake check« and »nix flake show --allow-import-from-derivation«, which means inputs and outputs comply with the flake convention.
*/
); inputs = {
# To update »./flake.lock«: $ nix flake update
nixpkgs = { url = "github:NixOS/nixpkgs/nixos-unstable"; };
config = { type = "github"; owner = "NiklasGollenstede"; repo = "nix-wiplib"; dir = "example/defaultConfig"; };
}; outputs = inputs: let patches = {
nixpkgs = [
./patches/nixpkgs-test.patch # after »nix build«, check »result/inputs/nixpkgs/patched!« to see that these patches were applied
./patches/nixpkgs-fix-systemd-boot-install.patch
];
}; in (import "${./.}/lib/flakes.nix" "${./.}/lib" inputs).patchFlakeInputsAndImportRepo inputs patches ./. (inputs@ { self, nixpkgs, ... }: repo@{ overlays, lib, ... }: let
systemsFlake = lib.wip.mkSystemsFalke (rec {
#systems = { dir = "${./.}/hosts"; exclude = [ ]; };
inherit inputs;
scripts = [ ./example/install.sh.md ] ++ (lib.attrValues lib.wip.setup-scripts);
});
in [ # Run »nix flake show --allow-import-from-derivation« to see what this merges to:
repo
(if true then systemsFlake else { })
(lib.wip.forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: {
packages = lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays;
defaultPackage = systemsFlake.packages.${localSystem}.all-systems;
}))
{ patches = import "${./.}/patches" "${./.}/patches" inputs; }
]); }

80
hosts/example.nix.md Normal file
View File

@ -0,0 +1,80 @@
/*
# Example Host Configuration
Just to provide an example of what a host configuration using this set of libraries can look like.
## Installation
To prepare a virtual machine disk, as `sudo` user with `nix` installed, run in `..`:
```bash
nix run '.#example' -- sudo install-system /home/$(id -un)/vm/disks/example.img && sudo chown $(id -un): /home/$(id -un)/vm/disks/example.img
```
Then as the user that is supposed to run the VM(s):
```bash
nix run '.#example' -- register-vbox /home/$(id -un)/vm/disks/example.img
```
And manage the VM(s) using the UI or the commands printed.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS config flake input:
dirname: inputs: { config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let
#suffix = builtins.head (builtins.match ''example-(.*)'' name); # make differences in config based on this when using »preface.instances«
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
in { imports = [ ({ ## Hardware
#preface.instances = [ "example-a" "example-b" "example-c" ];
preface.hardware = "x86_64"; system.stateVersion = "22.05";
## What follows is a whole bunch of boilerplate-ish stuff, most of which multiple hosts would have in common and which would thus be moved to one or more modules:
boot.loader.systemd-boot.enable = true; boot.loader.grub.enable = false;
# Declare a boot and system partition. Though not required for EFI, make the boot part visible to boot loaders supporting only MBR.
wip.installer.partitions."boot-${hash}" = { type = "ef00"; size = "64M"; index = 1; order = 1500; };
wip.installer.partitions."system-${hash}" = { type = "8300"; size = null; order = 500; };
wip.installer.disks = { primary = { mbrParts = "1"; extraFDiskCommands = ''
t;1;c # type ; part1 ; W95 FAT32 (LBA)
a;1 # active/boot ; part1
''; }; };
# Put everything except for /boot and /nix/store on a tmpfs. This is the absolute minimum, most usable systems require some more paths that are persistent (e.g. all of /nix and /home).
fileSystems."/" = { fsType = "tmpfs"; device = "tmpfs"; neededForBoot = true; options = [ "mode=755" ]; };
fileSystems."/boot" = { fsType = "vfat"; device = "/dev/disk/by-partlabel/boot-${hash}"; neededForBoot = true; options = [ "noatime" ]; formatOptions = "-F 32"; };
fileSystems."/system" = { fsType = "ext4"; device = "/dev/disk/by-partlabel/system-${hash}"; neededForBoot = true; options = [ "noatime" ]; formatOptions = "-O inline_data -E nodiscard -F"; };
fileSystems."/nix/store" = { options = ["bind,ro"]; device = "/system/nix/store"; neededForBoot = true; };
# Some base config:
users.mutableUsers = false; users.allowNoPasswordLogin = true;
networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName));
environment.etc."machine-id".text = (builtins.substring 0 32 (builtins.hashString "sha256" "${config.networking.hostName}:machine-id"));
boot.kernelParams = [ "panic=10" "boot.panic_on_fail" ]; # Reboot on kernel panic, panic if boot fails.
systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot.
# Static config for VBox Adapter 1 set to NAT (the default):
networking.interfaces.enp0s3.ipv4.addresses = [ {
address = "10.0.2.15"; prefixLength = 24;
} ];
networking.defaultGateway = "10.0.2.2";
networking.nameservers = [ "1.1.1.1" ]; # [ "10.0.2.3" ];
}) ({ ## Actual Config
## And here would go the things that actually make the host unique (and do something productive). For now just some debugging things:
environment.systemPackages = [ pkgs.curl pkgs.htop ];
services.getty.autologinUser = "root"; users.users.root.password = "root";
boot.kernelParams = [ "boot.shell_on_fail" ];
wip.services.dropbear.enable = true;
#wip.services.dropbear.rootKeys = [ ''${lib.readFile "${dirname}/....pub"}'' ];
}) ]; }

6
lib/default.nix Normal file
View File

@ -0,0 +1,6 @@
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;
in nixpkgs.lib // { inherit wip; }

225
lib/flakes.nix Normal file
View File

@ -0,0 +1,225 @@
dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) mapMerge mergeAttrsUnique flipNames;
inherit (import "${dirname}/imports.nix" dirname inputs) getModifiedPackages getNixFiles importWrapped;
inherit (import "${dirname}/scripts.nix" dirname inputs) substituteImplicit;
in rec {
# Simplified implementation of »flake-utils.lib.eachSystem«.
forEachSystem = systems: do: flipNames (mapMerge (arch: { ${arch} = do arch; }) systems);
# Sooner or later this should be implemented in nix itself, for now require »inputs.nixpkgs« and a system that can run »x86_64-linux« (native or through qemu).
patchFlakeInputs = inputs: patches: outputs: let
inherit ((import inputs.nixpkgs { 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;
patches = map (patch: if patch ? url then fetchpatch patch else patch) patches.${name};
};
sourceInfo = (input.sourceInfo or input) // patched;
in (
# sourceInfo = { lastModified; narHash; rev; lastModifiedDate; outPath; shortRev; }
# A non-flake has only the attrs of »sourceInfo«.
# A flake has »{ inputs; outputs; sourceInfo; } // outputs // sourceInfo«, where »inputs« is what's passed to the outputs function without »self«, and »outputs« is the result of calling the outputs function. Don't know the merge priority.
if (!input?sourceInfo) then sourceInfo else (let
outputs = (import "${patched.outPath}/flake.nix").outputs ({ self = outputs; } // input.inputs);
in { inherit (input) inputs; inherit outputs; inherit sourceInfo; } // outputs // sourceInfo)
)) else input) inputs);
# Generates implicit flake outputs by importing conventional paths in the local repo.
importRepo = inputs: repoPath: outputs: let list = (outputs inputs ((if builtins.pathExists "${repoPath}/lib/default.nix" then {
lib = import "${repoPath}/lib" "${repoPath}/lib" inputs;
} else { }) // (if builtins.pathExists "${repoPath}/overlays/default.nix" then rec {
overlays = import "${repoPath}/overlays" "${repoPath}/overlays" inputs;
overlay = final: prev: builtins.foldl' (prev: overlay: prev // (overlay final prev)) prev (builtins.attrValues overlays);
} else { }) // (if builtins.pathExists "${repoPath}/modules/default.nix" then rec {
nixosModules = import "${repoPath}/modules" "${repoPath}/modules" inputs;
nixosModule = { imports = builtins.attrValues nixosModules; };
} else { }))); in if (builtins.isList list) then mergeOutputs list else list;
# Combines »patchFlakeInputs« and »importRepo« in a single call.
patchFlakeInputsAndImportRepo = inputs: patches: repoPath: outputs: (
patchFlakeInputs inputs patches (inputs: importRepo inputs repoPath outputs)
);
# Merges a list of flake output attribute sets.
mergeOutputs = outputList: lib.zipAttrsWith (type: values: (
if ((lib.length values) == 1) then (lib.head values)
else if (lib.all lib.isAttrs values) then (lib.zipAttrsWith (system: values: mergeAttrsUnique values) values)
else throw "Outputs.${type} has multiple values which are not all attribute sets, can't merge."
)) outputList;
# 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) ({ config = null; pkgs = null; lib = null; name = null; nodes = null; } // args);
module = builtins.elemAt imported.imports 0; props = module.preface;
in if (
imported?imports && (builtins.isList imported.imports) && (imported.imports != [ ]) && module?preface && props?hardware
) then (props) else throw "File ${entryPath} must fulfill the structure: dirname: inputs: { ... }: { imports = [ { preface = { hardware = str; ... } } ]; }";
# 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 »mkSystemsFalke« for documentation of the arguments.
mkNixosConfiguration = args@{ name, entryPath, peers, inputs, overlays, modules, nixosSystem, localSystem ? null, ... }: let
preface = (getSystemPreface inputs entryPath ({ inherit lib; } // specialArgs));
targetSystem = "${preface.hardware}-linux"; buildSystem = if localSystem != null then localSystem else targetSystem;
specialArgs = (args.specialArgs or { }) // { # make these available in the attrSet passed to the modules
inherit name; nodes = peers; # NixOPS
};
in { inherit preface; } // (nixosSystem {
system = targetSystem;
modules = [ (
{ _file = entryPath; imports = [ (importWrapped inputs entryPath) ]; } # (preserve the location of reported errors)
) {
# The system architecture (often referred to as »system«).
options.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.preface.instances = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ name ]; };
} ({ 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«!
});
# 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.
# 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 »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 »mkSystemsFalke« and are passed to »mkNixosConfiguration«.
mkNixosConfigurations = args: let # { files, dir, exclude, ... }
files = args.files or (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 = if stripped != null then (builtins.elemAt stripped 0) else (builtins.baseNameOf entryPath);
in { inherit name; value = entryPath; }) files));
configs = mapMerge (name: entryPath: (let
preface = (getSystemPreface inputs entryPath { });
in (mapMerge (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');
withId = lib.filterAttrs (name: node: node.preface?id) configs;
ids = mapMerge (name: node: { "${toString node.preface.id}" = name; }) withId;
duplicate = builtins.removeAttrs withId (builtins.attrValues ids);
in if duplicate != { } then (
throw "»my.system.id«s are not unique! The following hosts share their IDs with some other host: ${builtins.concatStringsSep ", " (builtins.attrNames duplicate)}"
) else configs;
# Builds a system of NixOS hosts and exports them plus managing functions as flake outputs.
# All arguments are optional, as long as the default can be derived from the other arguments as passed.
mkSystemsFalke = args@{
# An attrset of imported Nix flakes, for example the argument(s) passed to the flake »outputs« function. All other arguments are optional (and have reasonable defaults) if this is provided and contains »self« and the standard »nixpkgs«. This is also the second argument passed to the individual host's top level config files.
inputs ? { },
# Root path of the NixOS configuration. »./.« in the »flake.nix«
configPath ? inputs.self.outPath,
# 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 = "${configPath}/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))),
# (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" ]),
# Additional arguments passed to each module evaluated for the host config (if that module is defined as a function).
specialArgs ? { },
# List of bash scripts defining functions that do installation and maintenance operations. See »apps« below for more information.
scripts ? [ ],
# The function of that name as defined in »<nixpkgs>/flake.nix«, or equivalent.
nixosSystem ? inputs.nixpkgs.lib.nixosSystem,
# If provided, then cross compilation is enabled for all hosts whose target architecture is different from this. Since cross compilation currently fails for (some stuff in) NixOS, better don't set »localSystem«. Without it, building for other platforms works fine (just slowly) if »boot.binfmt.emulatedSystems« is configured on the building system for the respective target(s).
localSystem ? null,
... }: let
otherArgs = (builtins.removeAttrs args [ "systems" ]) // { inherit systems overlays modules specialArgs scripts inputs configPath nixosSystem localSystem; };
nixosConfigurations = if builtins.isList systems then mergeAttrsUnique (map (systems: mkNixosConfigurations (otherArgs // systems)) systems) else mkNixosConfigurations (otherArgs // systems);
in {
inherit nixosConfigurations;
} // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: let
pkgs = (import inputs.nixpkgs { inherit overlays; system = localSystem; });
nix_wrapped = pkgs.writeShellScriptBin "nix" ''exec ${pkgs.nix}/bin/nix --extra-experimental-features nix-command "$@"'';
in (if scripts == [ ] then { } else {
# E.g.: $ nix run .#$target -- install-system /tmp/system-$target.img
# E.g.: $ nix run /etc/nixos/#$(hostname) -- sudo
# If the first argument (after »--«) is »sudo«, then the program will re-execute itself with sudo as root (minus that »sudo« argument).
# If the first/next argument is »bash«, it will execute an interactive shell with the variables and functions sourced (largely equivalent to »nix develop .#$host«).
apps = lib.mapAttrs (name: system: let
appliedScripts = substituteImplicit { inherit pkgs scripts; context = system; };
in { type = "app"; program = "${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,debug -- "$0" "$@" ; fi
# if the (now) first arg is »bash« or there are no args, re-execute this script as bash »--init-file«, starting an interactive bash in the context of the script
if [[ $1 == bash ]] || [[ $# == 0 && $0 == *-scripts-${name} ]] ; then
set -x
exec ${pkgs.bashInteractive}/bin/bash --init-file <(cat << "EOS"${"\n"+''
# prefix the script to also include the default init files
! [[ -e /etc/profile ]] || . /etc/profile
for file in ~/.bash_profile ~/.bash_login ~/.profile ; do
if [[ -r $file ]] ; then . $file ; break ; fi
done ; unset $file
# add active »hostName« to shell prompt
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
''}EOS
cat $0) -i
fi
# provide installer tools (native to localSystem, not targetSystem)
PATH=${pkgs.nixos-install-tools}/bin:${nix_wrapped}/bin:${pkgs.nix}/bin:$PATH
${appliedScripts}
# 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;
# E.g.: $ nix develop /etc/nixos/#$(hostname)
# ... and then call any of the functions in ./utils/functions.sh (in the context of »$(hostname)«, where applicable).
# To get an equivalent root shell: $ nix run /etc/nixos/#functions-$(hostname) -- sudo bash
devShells = lib.mapAttrs (name: system: pkgs.mkShell (let
appliedScripts = substituteImplicit { inherit pkgs scripts; context = system; };
in {
nativeBuildInputs = [ pkgs.nixos-install-tools nix_wrapped pkgs.nix ];
shellHook = ''
${appliedScripts}
# add active »hostName« to shell prompt
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
'';
})) nixosConfigurations;
}) // {
packages.all-systems = pkgs.stdenv.mkDerivation { # dummy that just pulls in all system builds
name = "all-systems"; src = ./.; installPhase = ''
mkdir -p $out/systems
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: (
"ln -sT ${system.config.system.build.toplevel} $out/systems/${name}"
)) nixosConfigurations)}
${lib.optionalString (inputs != { }) ''
mkdir -p $out/inputs
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: { outPath, ... }: "ln -sT ${outPath} $out/inputs/${name}") inputs)}
''}
${lib.optionalString (configPath != null) "ln -sT ${configPath} $out/config"}
'';
};
}));
}

107
lib/imports.nix Normal file
View File

@ -0,0 +1,107 @@
dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) mapMerge mergeAttrsRecursive endsWith;
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
match = builtins.match ''^(.*)[.]nix([.]md)?$'' name;
in if (match != null) then { ${builtins.head match} = "${dir}/${name}"; } else { }
) else { }) (builtins.attrNames listing);
## 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).
# Per convention, modules (at least those declared stand-alone in a file) are declared as functions taking at least the named arguments »config«, »pkgs«, and »lib«. Once entered into the module system, to remember where they came from, modules get wrapped in an attrset »{ _file = "<path>"; imports = [ <actual_module> ]; }«.
isProbablyModule = thing: let args = builtins.functionArgs thing; in (
(builtins.isFunction thing) && (builtins.isAttrs (thing args)) && (builtins.isBool (args.config or null)) && (builtins.isBool (args.lib or null)) && (builtins.isBool (args.pkgs or null))
) || (
(builtins.isAttrs thing) && ((builtins.attrNames thing) == [ "_file" "imports" ]) && ((builtins.isString thing._file) || (builtins.isPath thing._file)) && (builtins.isList thing.imports)
);
## 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;
# Builds an attrset that, for each folder 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" ]);
# Import a Nix file that expects the standard `dirname: inputs: ` arguments.
importWrapped = inputs: path: import path (if (builtins.match ''^(.*)[.]nix([.]md)?$'' path) != null then builtins.dirOf path else path) inputs;
## 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.
# 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
# ├── a.nix.md
# ├── b.nix
# └── c
# ├── default.nix
# ├── d.nix
# └── e.nix.md
# 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
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 (
{ "${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 ]; }; });
# 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; });
# Imports »inputs.nixpkgs« and instantiates it with all ».overlay(s)« provided by »inputs.*«.
importPkgs = inputs: args: import inputs.nixpkgs ({
overlays = builtins.concatLists (map (input: if input?overlay then [ input.overlay ] else if input?overlays then builtins.attrValues input.overlays else [ ]) (builtins.attrValues inputs));
} // args);
# 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: { ${name} = pkgs.${name}; }) names;
## Given a path to a module in »nixpkgs/nixos/modules/« and placed in another module's »imports«, adds an option »disableModule.<path>« 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.
# This is similar to adding the path to »disabledModules«, but:
# * leaves the module's other definitions (options, imports) untouched, preventing further breakage due to missing options
# * makes the disabling an option, i.e. it can be changed dynamically based on other config values
makeNixpkgsModuleConfigOptional = nixpkgs: specialArgs: modulePath: let
fullPath = "${nixpkgs.outPath}/nixos/modules/${modulePath}";
moduleArgs = { utils = import "${nixpkgs.outPath}/nixos/lib/utils.nix" { inherit (specialArgs) lib config pkgs; }; } // specialArgs;
module = import fullPath moduleArgs;
in { _file = fullPath; imports = [
{ options.disableModule.${modulePath} = lib.mkOption { description = "Disable the nixpkgs module ${modulePath}"; type = lib.types.bool; default = false; }; }
(if module?config then (
module // { config = lib.mkIf (!specialArgs.config.disableModule.${modulePath}) module.config; }
) else (
{ config = lib.mkIf (!specialArgs.config.disableModule.${modulePath}) module; }
))
{ disabledModules = [ modulePath ]; }
]; };
## Given a path to a module and a function taking the instantiation of the original and returning a partial module as override, recursively applies that override to the original module definition.
# This allows for much more fine-grained overriding of the configuration (or even other parts) of a module than »makeNixpkgsModuleConfigOptional«, but the override function needs to be tailored to internal implementation details of the original module.
# Esp. it is important to know that »mkIf« both existing in the original module and in the return from the override results in an attrset »{ _type="if"; condition; content; }«. Accessing content from an existing »mkIf« thus requires adding ».content« to the lookup path, and the »content« of returned »mkIf«s may get merged with any existing attribute of that name.
overrideNixpkgsModule = nixpkgs: specialArgs: modulePath: override: let
fullPath = "${nixpkgs.outPath}/nixos/modules/${modulePath}";
moduleArgs = { utils = import "${nixpkgs.outPath}/nixos/lib/utils.nix" { inherit (specialArgs) lib config pkgs; }; } // specialArgs;
module = import fullPath moduleArgs;
in { _file = fullPath; imports = [
(mergeAttrsRecursive [ { imports = module.imports or [ ]; options = module.options or { }; config = module.config or { }; } (override module) ])
{ disabledModules = [ modulePath ]; }
]; };
}

38
lib/misc.nix Normal file
View File

@ -0,0 +1,38 @@
dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) startsWith;
in rec {
## Logic Flow
notNull = value: value != null;
ifNull = value: default: (if value == null then default else value);
withDefault = default: value: (if value == null then default else value);
passNull = mayNull: expression: (if mayNull == null then null else expression);
## Misc
# Creates a package for `config.systemd.packages` that adds an `override.conf` to the specified `unit` (which is the only way to modify a single service template instance).
mkSystemdOverride = pkgs: unit: text: (pkgs.runCommandNoCC unit { preferLocalBuild = true; allowSubstitutes = false; } ''
mkdir -p $out/${lib.escapeShellArg "/etc/systemd/system/${unit}.d/"}
<<<${lib.escapeShellArg text} cat >$out/${lib.escapeShellArg "/etc/systemd/system/${unit}.d/override.conf"}
'');
# Given »config.ids« (or equivalent) and a user name, returns the users numeric »uid:gid« pair as string.
getOwnership = { gids, uids, ... }: user: "${toString uids.${user}}:${toString gids.${user}}";
# Given »from« and »to« as »config.my.network.spec.hosts.*«,
# picks the first of »to«'s IPs whose required subnet is either empty/any, or a prefix to any of the subnets in »from«:
# ip = preferredRoute self.subNets other.routes;
# ip6 = preferredRoute self.subNets (builtins.filter (r: r.is6) other.routes);
# to.find(({ ip, prefix }) => from.any(_=>_.startsWith(prefix))).ip
preferredRoute = from: to: (lib.findFirst ({ prefix, ip, ... }: prefix == "" || (builtins.any (fromSub: startsWith prefix fromSub) from)) { ip = ""; } to).ip;
# Given a message and any value, traces both the message and the value, and returns the value.
trace = lib: message: value: (builtins.trace (message +": "+ (lib.generators.toPretty { } value)) value);
rpoolOf = hostName: "rpool-${builtins.substring 0 8 (builtins.hashString "sha256" hostName)}";
}

100
lib/scripts.nix Normal file
View File

@ -0,0 +1,100 @@
dirname: { self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
in rec {
# Turns an attr set into a bash dictionary (associative array) declaration, e.g.:
# bashSnippet = "declare -A dict=(\n${asBashDict { } { foo = "42"; bar = "baz"; }}\n)"
asBashDict = { mkName ? (n: v: n), mkValue ? (n: v: v), indent ? " ", ... }: attrs: (
builtins.concatStringsSep "\n" (lib.mapAttrsToList (name: value: (
"${indent}[${lib.escapeShellArg (mkName name value)}]=${lib.escapeShellArg (mkValue name value)}"
)) attrs)
);
# Turns an attrset into a string that can (safely) be bash-»eval«ed to declare the attributes (prefixed with a »_«) as variables into the current scope.
asBashEvalSet = recursive: attrs: builtins.concatStringsSep " ; " (lib.mapAttrsToList (name: value: (
"_${name}=${lib.escapeShellArg (if recursive && builtins.isAttrs value then asBashEvalSet true value else toString value)}"
)) attrs);
# Makes an attrset of attrsets eligible to be passed to »asBashDict«. The bash script can (safely) call »eval« on each first-level attribute value to get the second-level attributes (prefixed with a »_«) into the current variable scope.
# Meant primarily as a helper for »substituteLazy«.
attrsAsBashEvalSets = attrs: builtins.mapAttrs (name: asBashEvalSet true) attrs;
# Makes a list of attrsets eligible to be passed to »asBashDict«. The bash script can (safely) call »eval« on each list item to get the contained attributes (prefixed with a »_«) into the current variable scope.
# Meant primarily as a helper for »substituteLazy«.
listAsBashEvalSets = list: map (asBashEvalSet true) list;
# This function allows using nix values in bash scripts without passing an explicit and manually curated list of values to the script.
# Given a path list of bash script »sources« and an attrset »context«, the function parses the scripts for the literal sequence »@{« followed by a lookup path of period-joined words, resolves that attribute path against »context«, declares a variable with that value and swaps out the »@{« plus path for a »${« use of the declared variable. The returned script sources the variable definitions and all translated »sources« in order.
# The lookup path may end in »!« plus the name of a (single argument) »builtins.*« function,in which case the resolved value will be passed to that function and its return value is used instead (e.g. for »attrNames«, »attrValues«, »toJSON«, »catAttrs«, ...).
# The names of the declared values are the lookup paths, with ».« and »!« replaced by »_« and »__«.
# The symbol immediately following the lookup path (/builtin name) can be »}« or any other symbol that bash variable substitutions allow after the variable name (like »:«, »/«), eliminating the need to assign to a local variable to do things like replacements, fallbacks or substrings.
# If the lookup path does not exist in »context«, then the value will be considered the same as »null«, and a value of »null« will result in a bash variable that is not defined (which can then be handled in the bash script).
# Other scalars (bool, float, int, path) will be passed to »builtins.toString«, Lists will be mapped with »toString« and declared as bash arrays, attribute sets will be declared using »asBashDict« with their values »toString«ed as well.
# Any other value (functions), and things that »builtins.toString« doesn't like, will throw here.
substituteImplicit = args@{ pkgs, scripts, context, helpers ? { }, }: let
scripts = map (source: rec {
text = builtins.readFile source; inherit source;
parsed = builtins.split ''@\{([#!]?)([a-zA-Z][a-zA-Z0-9_.-]*[a-zA-Z0-9](![a-zA-Z][a-zA-Z0-9_.-]*[a-zA-Z0-9])?)([:*@\[#%/^,\}])'' text; # (first part of a bash parameter expansion, with »@« instead of »$«)
}) args.scripts;
decls = lib.unique (map (match: builtins.elemAt match 1) (builtins.filter builtins.isList (builtins.concatMap (script: script.parsed) scripts)));
vars = pkgs.writeText "vars" (lib.concatMapStringsSep "\n" (decl: let
call = let split = builtins.split "!" decl; in if (builtins.length split) == 1 then null else builtins.elemAt split 2;
path = (builtins.filter builtins.isString (builtins.split "[.]" (if call == null then decl else builtins.substring 0 ((builtins.stringLength decl) - (builtins.stringLength call) - 1) decl)));
resolved = lib.attrByPath path null context;
applied = if call == null || resolved == null then resolved else (let
split = builtins.filter builtins.isString (builtins.split "[.]" call); name = builtins.head split; args = builtins.tail split;
func = builtins.foldl' (func: arg: func arg) (helpers.${name} or self.lib.wip.${name} or (pkgs.lib or lib).${name} or pkgs.${name}) args;
in func resolved);
value = if builtins.isString (applied.outPath or null) then applied.outPath else if (
(builtins.isBool applied) || (builtins.isFloat applied) || (builtins.isInt applied) || (builtins.isPath applied)
) then builtins.toString applied else applied;
name = builtins.replaceStrings [ "." "!" ] [ "_" "__" ] decl; #(builtins.trace decl decl);
in (
if (value == null) then ""
else if (builtins.isString value) then "${name}=${lib.escapeShellArg value}"
else if (builtins.isList value) then "${name}=(${lib.escapeShellArgs (map builtins.toString value)})"
else if (builtins.isAttrs value) then "declare -A ${name}=${"(\n${asBashDict { mkValue = name: builtins.toString; } value}\n)"}"
else throw "Can't use value of unsupported type ${builtins.typeOf} as substitution for ${decl}" # builtins.isFunction
)) decls);
in ''
source ${vars}
${lib.concatMapStringsSep "\n" (script: "source ${pkgs.writeScript (builtins.baseNameOf script.source) (
lib.concatMapStringsSep "" (seg: if builtins.isString seg then seg else (
"$"+"{"+(builtins.head seg)+(builtins.replaceStrings [ "." "!" ] [ "_" "__" ] (builtins.elemAt seg 1))+(toString (builtins.elemAt seg 3))
)) script.parsed
)}") scripts}
'';
# Used as a »system.activationScripts« snippet, this performs substitutions on a »text« before writing it to »path«.
# For each name-value pair in »substitutes«, all verbatim occurrences of the attribute name in »text« are replaced by the content of the file with path of the attribute value.
# Since this happens one by one in no defined order, the attribute values should be chosen such that they don't appear in any of the files that are substituted in.
# If a file that is supposed to be substituted in is missing, then »placeholder« is inserted instead, and the activation snipped reports a failure.
# If »enable« is false, then the file at »path« is »rm«ed instead.
writeSubstitutedFile = { enable ? true, path, text, substitutes, placeholder ? "", perms ? "440", owner ? "root", group ? "root", }: let
hash = builtins.hashString "sha256" text;
esc = lib.escapeShellArg;
in { "write ${path}" = if enable then ''
text=$(cat << '#${hash}'
${text}
#${hash}
)
${builtins.concatStringsSep "\n" (lib.mapAttrsToList (name: file: "text=\"\${text//${esc name}/$( if ! cat ${esc file} ; then printf %s ${esc placeholder} ; false ; fi )}\"") substitutes)}
install -m ${esc (toString perms)} -o ${esc (toString owner)} -g ${esc (toString group)} /dev/null ${esc path}
<<<"$text" cat >${esc path}
'' else ''rm ${esc path} || true''; };
# Wraps a (bash) script into a "package", making »deps« available on the script's path.
wrap-script = args@{ pkgs, src, deps, ... }: let
name = args.name or (builtins.baseNameOf (builtins.unsafeDiscardStringContext "${src}"));
in pkgs.runCommandLocal name {
script = src; nativeBuildInputs = [ pkgs.makeWrapper ];
} ''makeWrapper $script $out/bin/${name} --prefix PATH : ${lib.makeBinPath deps}'';
# Simplifies a path (or any other string) such that it can be used as a systemd unit name.
escapeUnitName = name: lib.concatMapStringsSep "" (s: if builtins.isList s then "-" else s) (builtins.split "[^a-zA-Z0-9_.\\-]+" name); # from nixos/modules/services/backup/syncoid.nix
pathToName = path: (builtins.replaceStrings [ "/" ":" ] [ "-" "-" ] path);
# (If »path« ends with »/«, then »path[0:-1]« is the closest "parent".)
parentPaths = path: let parent = builtins.dirOf path; in if parent == "." || parent == "/" then [ ] else (parentPaths parent) ++ [ parent ];
}

View File

@ -0,0 +1,34 @@
# Host Setup Scripts
This is a library of bash functions, mostly for NixOS system installation.
The (paths to these) scripts are meant to me passed in the `scripts` argument to [`mkSystemsFalke`](../flakes.nix#mkSystemsFalke), which makes their functions available in the per-host `devShells`/`apps`.
Host-specific nix variables are available to the bash functions as `@{...}` through [`substituteImplicit`](../scripts.nix#substituteImplicit) with the respective host as root context.
With the functions from here, adding a simple three-liner can be enough to do a completely automated NixOS installation:
```bash
function install-system {( set -eu # 1: blockDev
prepare-installer "$@"
do-disk-setup "$1"
install-system-to $mnt prompt=true @{config.th.minify.topLevel:-}
)}
```
# `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 script can be run by root as: `#` `( cd /etc/nixos/ && 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.
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: `$` `( cd /etc/nixos/ && nix run .#"$HOST" -- sudo install-system "$DISK" )` (And then maybe `sudo bash -c 'chown $SUDO_USER: '"$DISK"` afterwards.)
The `nix run` in the above commands substitutes a number of `@{`-prefixed variables based on the `$HOST` name and its configuration from [`../hosts/`](../hosts/), and then sources this script and calls the `install-system` function.
If `$DISK` points to something in `/dev/`, then it is directly formatted and written to as block device, otherwise `$DISK` is (re-)created as raw image and then used as loop device.
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`](../lib/setup-scripts/maintenance.sh#register-vbox) to create a bootable VirtualBox instance for the current user.
The "Installation" section of each host's documentation should contain host specific details, if any.

View File

@ -0,0 +1,19 @@
dirname: inputs: let
getNamedScriptFiles = dir: builtins.removeAttrs (builtins.listToAttrs (map (name: let
match = builtins.match ''^(.*)[.]sh([.]md)?$'' name;
in if (match != null) then {
name = builtins.head match; value = "${dir}/${name}";
} else { name = ""; value = null; }) (builtins.attrNames (builtins.readDir dir)))) [ "" ];
inherit (inputs.config) prefix;
replacePrefix = if prefix == "wip" then (x: x) else (builtins.mapAttrs (name: path: (
builtins.toFile name (builtins.replaceStrings
[ "@{config.wip." "@{#config.wip." "@{!config.wip." ]
[ "@{config.${prefix}." "@{#config.${prefix}." "@{!config.${prefix}." ]
(builtins.readFile path)
)
)));
in replacePrefix (getNamedScriptFiles dirname)

154
lib/setup-scripts/disk.sh Normal file
View File

@ -0,0 +1,154 @@
##
# Disk Partitioning and Formatting
##
## Prepares the disks of the target system for the copying of files.
function do-disk-setup { # 1: diskPaths
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«
partition-disks "$1"
# ... block layers would go here ...
source @{config.wip.installer.postPartitionCommands!writeText.postPartitionCommands}
format-partitions
source @{config.wip.installer.postFormatCommands!writeText.postFormatCommands}
prepend_trap "unmount-system $mnt" EXIT ; mount-system $mnt
source @{config.wip.installer.postMountCommands!writeText.postMountCommands}
}
## Partitions all »config.installer.disks« to ensure that all (correctly) specified »{config.installer.partitions« exist.
function partition-disks { { # 1: diskPaths
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then beQuiet=/dev/stdout ; fi
declare -g -A blockDevs=( ) # this ends up in the caller's scope
local path ; for path in ${1/:/ } ; do
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
blockDevs[$name]=$path
done
local name ; for name in "@{!config.wip.installer.disks!attrsAsBashEvalSets[@]}" ; do
if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" ; exit 1 ; fi
if [[ ! ${blockDevs[$name]} =~ ^(/dev/.*)$ ]] ; then
local outFile=${blockDevs[$name]} ; ( set -eu
eval "@{config.wip.installer.disks!attrsAsBashEvalSets[$name]}" # _size
install -o root -g root -m 640 -T /dev/null "$outFile" && fallocate -l "$_size" "$outFile"
) && blockDevs[$name]=$(losetup --show -f "$outFile") && prepend_trap "losetup -d ${blockDevs[$name]}" EXIT # NOTE: this must not be inside a sub-shell!
else
if [[ ! "$(blockdev --getsize64 "${blockDevs[$name]}")" ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" ; exit 1 ; fi
blockDevs[$name]=$(realpath "${blockDevs[$name]}")
fi
done
} ; ( set -eu
for name in "@{!config.wip.installer.disks!attrsAsBashEvalSets[@]}" ; do (
eval "@{config.wip.installer.disks!attrsAsBashEvalSets[$name]}" # _name ; _size ; _serial ; _alignment ; _mbrParts ; _extraFDiskCommands
if [[ $_serial ]] ; then
actual=$(udevadm info --query=property --name="${blockDevs[$name]}" | grep -oP 'ID_SERIAL_SHORT=\K.*')
if [[ $_serial != "$actual" ]] ; then echo "Block device ${blockDevs[$name]} does not match the serial declared for $name" ; exit 1 ; fi
fi
sgdisk=( --zap-all ) # delete existing part tables
for partDecl in "@{config.wip.installer.partitionList!listAsBashEvalSets[@]}" ; do
eval "$partDecl" # _name ; _disk ; _type ; _size ; _index
if [[ $_disk != "$name" ]] ; then exit ; fi # i.e. continue
if [[ $_position =~ ^[0-9]+$ ]] ; then alignment=1 ; else alignment=$_alignment ; fi # if position is an absolute number, start precisely there
sgdisk+=( -a "$alignment" -n "${_index:-0}":"$_position":+"$_size" -t 0:"$_type" -c 0:"$_name" )
done
if [[ $_mbrParts ]] ; then
sgdisk+=( --hybrid "$_mbrParts" ) # --hybrid: create MBR in addition to GPT; $_mbrParts: make these GPT part 1 MBR parts 2[3[4]]
fi
sgdisk "${sgdisk[@]}" "${blockDevs[$name]}" >$beQuiet # running all at once is much faster
if [[ $_mbrParts ]] ; then
printf "
M # edit hybrid MBR
d;1 # delete parts 1 (GPT)
# move the selected »mbrParts« to slots 1[2[3]] instead of 2[3[4]] (by re-creating part1 in the last sector, then sorting)
n;p;1 # new ; primary ; part1
$(( $(blockSectorCount "${blockDevs[$name]}") - 1)) # start (size 1sec)
x;f;r # expert mode ; fix order ; return
d;$(( (${#_mbrParts} + 1) / 2 + 1 )) # delete ; part(last)
# create GPT part (spanning primary GPT area) as last part
n;p;4 # new ; primary ; part4
1;33 # start ; end
t;4;ee # type ; part4 ; GPT
${_extraFDiskCommands}
p;w;q # print ; write ; quit
" | perl -pe 's/^ *| *(#.*)?$//g' | perl -pe 's/\n\n+| *; */\n/g' | fdisk "${blockDevs[$name]}" &>$beQuiet
fi
partprobe "${blockDevs[$name]}"
) ; done
sleep 1 # sometimes partitions aren't quite made available yet (TODO: wait "for udev to settle" instead?)
)}
## 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
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then beQuiet=/dev/stdout ; fi
for fsDecl in "@{config.fileSystems!attrsAsBashEvalSets[@]}" ; do (
eval "$fsDecl" # _name ; _device ; _fsType ; _formatOptions ; ...
if [[ $_device != /dev/disk/by-partlabel/* ]] ; then exit ; fi # i.e. continue
blockDev=$(realpath "$_device") ; if [[ $blockDev == /dev/sd* ]] ; then
blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" )
else
blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" )
fi
if [[ ' '"${blockDevs[@]}"' ' != *' '"$blockDev"' '* ]] ; then echo "Partition alias $_device does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
mkfs.${_fsType} ${_formatOptions} "${_device}" >$beQuiet
partprobe "${_device}"
) ; done
)}
## Mounts all file systems as it would happen during boot, but at path prefix »$mnt«.
function mount-system {( set -eu # 1: mnt, 2?: fstabPath
# mount --all --fstab @{config.system.build.toplevel.outPath}/etc/fstab --target-prefix "$1" -o X-mount.mkdir # (»--target-prefix« is not supported on Ubuntu 20.04)
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel.outPath}/etc/fstab"}
<$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 continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then
mkdir -p "$mnt"/"$target"
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
fi
done
# 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"
if [[ $type == overlay ]] ; then
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # work and upper dirs must be in target, lower dirs are probably store paths
workdir=$(<<<"$options" grep -o -P ',workdir=\K[^,]+' || true) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi
upperdir=$(<<<"$options" grep -o -P ',upperdir=\K[^,]+' || true) ; if [[ $upperdir ]] ; then mkdir -p "$upperdir" ; fi
else
source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" ; fi
fi
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
fi
done
)}
## Unmounts all file systems (that would be mounted during boot / by »mount-system«).
function unmount-system {( set -eu # 1: mnt, 2?: fstabPath
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel.outPath}/etc/fstab"}
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 -r | while read source target rest ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
if mountpoint -q "$mnt"/"$target" ; then
umount "$mnt"/"$target"
fi
done
)}
## Given a block device path, returns the number of 512byte sectors it can hold.
function blockSectorCount { printf %s "$(( $(blockdev --getsize64 "$1") / 512 ))" ; }

View File

@ -0,0 +1,57 @@
##
# NixOS Installation
##
## Ensures that the installer gets called by root and with an argument, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested).
function prepare-installer { # ...
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then set -x ; beQuiet=/dev/stdout ; fi
if [[ "$(id -u)" != '0' ]] ; then echo 'Script must be run in a root (e.g. in a »sudo --preserve-env=SSH_AUTH_SOCK -i«) shell.' ; exit ; fi
if [[ ${SUDO_USER:-} ]] ; then function nix {( args=("$@") ; su - "$SUDO_USER" -c "$(declare -p args)"' ; nix "${args[@]}"' )} ; fi
: ${1:?"Required: Target disk or image paths."}
if [[ $debug ]] ; then set +e ; set -E ; trap 'code= ; bash -l || code=$? ; if [[ $code ]] ; then exit $code ; fi' ERR ; fi # On error, instead of exiting straight away, open a shell to allow diagnosing/fixing the issue. Only exit if that shell reports failure (e.g. CtrlC + CtrlD). Unfortunately, the exiting has to be repeated for level of each nested sub-shells.
}
## 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 {( # 1: mnt, 2?: inspect, 3?: topLevel
mnt=$1 ; inspect=${2:-} ; topLevel=${3:-}
targetSystem=@{config.system.build.toplevel}
trap - EXIT # start with empty traps for sub-shell
for dir in dev/ sys/ run/ ; do mkdir -p $mnt/$dir ; mount tmpfs -t tmpfs $mnt/$dir ; prepend_trap "while umount -l $mnt/$dir 2>$beQuiet ; do : ; done" EXIT ; done # proc/ run/
mkdir -p -m 755 $mnt/nix/var ; mkdir -p -m 1775 $mnt/nix/store
if [[ ${SUDO_USER:-} ]] ; then chown $SUDO_USER: $mnt/nix/store $mnt/nix/var ; fi
( set -x ; time nix copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} )
ln -sT $(realpath $targetSystem) $mnt/run/current-system
mkdir -p -m 755 $mnt/nix/var/nix/profiles ; ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system
mkdir -p $mnt/etc/ ; [[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.preface.hardware}"-linux ]] ; then # cross architecture installation
mkdir -p $mnt/run/binfmt ; cp -a {,$mnt}/run/binfmt/"@{config.preface.hardware}"-linux || true
# Ubuntu (by default) expects the "interpreter" at »/usr/bin/qemu-@{config.preface.hardware}-static«.
fi
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix ; chown :30000 $mnt/nix/store ; fi
mount -o bind /nix/store $mnt/nix/store # all the things required to _run_ the system are copied, but (may) need some more things to initially install it
code=0 ; TMPDIR=/tmp LC_ALL=C nixos-install --system ${topLevel:-$targetSystem} --no-root-passwd --no-channel-copy --root $mnt || code=$? #--debug
umount -l $mnt/nix/store
if [[ $inspect ]] ; then
if (( code != 0 )) ; then
( set +x ; echo "Something went wrong in the last step of the installation. Inspect the output above and the system mounted in CWD to decide whether it is critical. Exit the shell with 0 to proceed, or non-zero to abort." )
else
( set +x ; echo "Installation done, but the system is still mounted in CWD for inspection. Exit the shell to unmount it." )
fi
( cd $mnt ; mnt=$mnt bash -l )
fi
( mkdir -p $mnt/var/lib/systemd/timesync ; touch $mnt/var/lib/systemd/timesync/clock ) || true # save current time
)}

View File

@ -0,0 +1,40 @@
##
# NixOS Maintenance
##
## On the host and for the user it is called by, creates/registers a VirtualBox VM meant to run the shells target host. Requires the path to the target host's »diskImage« as the result of running the install script. The image file may not be deleted or moved. If »bridgeTo« is set (to a host interface name, e.g. as »eth0«), it is added as bridged network "Adapter 2" (which some hosts need).
function register-vbox {( set -eu # 1: diskImage, 2?: bridgeTo
diskImage=$1 ; bridgeTo=${2:-}
vmName="nixos-@{config.networking.hostName}"
if [[ ! -e $diskImage.vmdk ]] ; then
VBoxManage internalcommands createrawvmdk -filename $diskImage.vmdk -rawdisk $diskImage # pass-through
fi
VBoxManage createvm --name "$vmName" --register --ostype Linux26_64
VBoxManage modifyvm "$vmName" --memory 2048 --pae off --firmware efi
VBoxManage storagectl "$vmName" --name SATA --add sata --portcount 4 --bootable on --hostiocache on
VBoxManage storageattach "$vmName" --storagectl SATA --port 0 --device 0 --type hdd --medium $diskImage.vmdk
if [[ $bridgeTo ]] ; then # VBoxManage list bridgedifs
VBoxManage modifyvm "$vmName" --nic2 bridged --bridgeadapter2 $bridgeTo
fi
VBoxManage modifyvm "$vmName" --uart1 0x3F8 4 --uartmode1 server /run/user/$(id -u)/$vmName.socket # (guest sets speed)
set +x # avoid double-echoing
echo '# VM info:'
echo " VBoxManage showvminfo $vmName"
echo '# start VM:'
echo " VBoxManage startvm $vmName --type headless"
echo '# kill VM:'
echo " VBoxManage controlvm $vmName poweroff"
echo '# create TTY:'
echo " socat UNIX-CONNECT:/run/user/$(id -u)/$vmName.socket PTY,link=/run/user/$(id -u)/$vmName.pty"
echo '# connect TTY:'
echo " screen /run/user/$(id -u)/$vmName.pty"
echo '# screenshot:'
echo " ssh $(hostname) VBoxManage controlvm $vmName screenshotpng /dev/stdout | display"
)}

View File

@ -0,0 +1,17 @@
##
# Utilities
##
## Prepends a command to a trap. Especially useful fo define »finally« commands via »prepend_trap '<command>' EXIT«.
# NOTE: When calling this in a sub-shell whose parents already has traps installed, make sure to do »trap - trapName« first. On a new shell, this should be a no-op, but without it, the parent shell's traps will be added to the sub-shell as well (due to strange behavior of »trap -p« (in bash ~5.1.8)).
prepend_trap() { # 1: command, ...: trapNames
fatal() { printf "ERROR: $@\n" >&2 ; return 1 ; }
local cmd=$1 ; shift || fatal "${FUNCNAME} usage error"
local name ; for name in "$@" ; do
trap -- "$( set +x
printf '%s\n' "( ${cmd} ) || true ; "
p3() { printf '%s\n' "${3:-}" ; } ; eval "p3 $(trap -p "${name}")"
)" "${name}" || fatal "unable to add to trap ${name}"
done
} ; declare -f -t prepend_trap # required to modify DEBUG or RETURN traps

78
lib/vars.nix Normal file
View File

@ -0,0 +1,78 @@
dirname: { self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
in rec {
## Data Structures
# Given a function and a list, calls the function for each list element, and returns the merge of all attr sets returned by the function
# attrs = mapMerge (value: { "${newKey}" = newValue; }) list
# attrs = mapMerge (key: value: { "${newKey}" = newValue; }) attrs
mapMerge = toAttr: listOrAttrs: mergeAttrs (if builtins.isAttrs listOrAttrs then lib.mapAttrsToList toAttr listOrAttrs else map toAttr listOrAttrs);
# Given a list of attribute sets, returns the merged set of all contained attributes, with those in elements with higher indices taking precedence.
mergeAttrs = attrsList: builtins.foldl' (a: b: a // b) { } attrsList;
# Given a list of attribute sets, returns the merged set of all contained attributes. Throws if the same attribute name occurs more than once.
mergeAttrsUnique = attrsList: let
merged = mergeAttrs attrsList;
names = builtins.concatLists (map builtins.attrNames attrsList);
duplicates = builtins.filter (a: (lib.count (b: a == b) names) >= 2) (builtins.attrNames merged);
in (
if (builtins.length (builtins.attrNames merged)) == (builtins.length names) then merged
else throw "Duplicate key(s) in attribute merge set: ${builtins.concatStringsSep ", " duplicates}"
);
mergeAttrsRecursive = attrsList: let # slightly adjusted from https://stackoverflow.com/a/54505212
merge = attrPath: lib.zipAttrsWith (name: values:
if builtins.length values == 1
then builtins.head values
else if builtins.all builtins.isList values
then lib.unique (builtins.concatLists values)
else if builtins.all builtins.isAttrs values
then merge (attrPath ++ [ name ]) values
else builtins.elemAt values (builtins.length values - 1)
);
in merge [ ] attrsList;
getListAttr = name: attrs: if attrs != null then ((attrs."${name}s" or [ ]) ++ (if attrs?${name} then [ attrs.${name} ] else [ ])) else [ ];
repeat = count: element: builtins.genList (i: element) count;
# Given an attribute set of attribute sets (»{ ${l1name}.${l2name} = value; }«), flips the positions of the first and second name level, producing »{ ${l3name}.${l1name} = value; }«. The set of »l2name«s does not need to be the same for each »l1name«.
flipNames = attrs: let
l1names = builtins.attrNames attrs;
l2names = builtins.concatMap builtins.attrNames (builtins.attrValues attrs);
in mapMerge (l2name: {
${l2name} = mapMerge (l1name: if attrs.${l1name}?${l2name} then { ${l1name} = attrs.${l1name}.${l2name}; } else { }) l1names;
}) l2names;
## String Manipulation
# Given a regular expression with capture groups and a list of strings, returns the flattened list of all the matched capture groups of the strings matched in their entirety by the regular expression.
mapMatching = exp: strings: (builtins.filter (v: v != null) (builtins.concatLists (builtins.filter (v: v != null) (map (string: (builtins.match exp string)) strings))));
# Given a regular expression and a list of strings, returns the list of all the strings matched in their entirety by the regular expression.
filterMatching = exp: strings: (builtins.filter (matches exp) strings);
matches = exp: string: builtins.match exp string != null;
extractChars = exp: string: let match = (builtins.match "^.*(${exp}).*$" string); in if match == null then null else builtins.head match;
# If »exp« (which mustn't match across »\n«) matches (a part of) exactly one line in »text«, return that »line« including tailing »\n«, plus the text part »before« and »after«, and the text »without« the line.
extractLine = exp: text: let split = builtins.split "([^\n]*${exp}[^\n]*\n)" (builtins.unsafeDiscardStringContext text); get = builtins.elemAt split; ctxify = str: lib.addContextFrom text str; in if builtins.length split != 3 then null else rec { before = ctxify (get 0); line = ctxify (builtins.head (get 1)); after = ctxify (get 2); without = ctxify (before + after); }; # (TODO: The string context stuff is actually required, but why? Shouldn't »builtins.split« propagate the context?)
# Given a string, returns its first/last char (or last utf-8(?) byte?).
firstChar = string: builtins.substring (0) 1 string;
lastChar = string: builtins.substring (builtins.stringLength string - 1) 1 string;
startsWith = prefix: string: let length = builtins.stringLength prefix; in (builtins.substring (0) (length) string) == prefix;
endsWith = suffix: string: let length = builtins.stringLength suffix; in (builtins.substring (builtins.stringLength string - length) (length) string) == suffix;
removeTailingNewline = string: if lastChar string == "\n" then builtins.substring 0 (builtins.stringLength string - 1) string else string;
## Math
pow = (let pow = b: e: if e == 1 then b else if e == 0 then 1 else b * pow b (e - 1); in pow); # (how is this not an operator or builtin?)
toBinString = int: builtins.concatStringsSep "" (map builtins.toString (lib.toBaseDigits 2 int));
}

43
modules/README.md Normal file
View File

@ -0,0 +1,43 @@
# 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 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.
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.
To that end, there are a number of pre-defined options (in `nixpkgs`) that collect programs, create and write configuration files (primarily in `/etc`), compose a boot loader, etc.
Other modules use those options to manipulate how the system is built.
## Template
Here is a skeleton structure for writing a new `<module>.nix.md`:
````md
/*
# TODO: title
TODO: documentation
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.${TODO: name};
in {
options.${prefix} = { ${TODO: name} = {
enable = lib.mkEnableOption "TODO: what";
# TODO: more options
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [ ({
# TODO: implementation
}) ]);
}
````

62
modules/base.nix.md Normal file
View File

@ -0,0 +1,62 @@
/*
# System Defaults
Things that really should be (more like) this by default.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { 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";
}; };
config = let
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
implied = true; # some mount points are implied (and forced) to be »neededForBoot« in »specialArgs.utils.pathsNeededForBoot« (this marks those here)
in lib.mkIf cfg.enable (lib.mkMerge [ ({
users.mutableUsers = false; users.allowNoPasswordLogin = true; # Don't babysit. Can roll back or redeploy.
networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName));
environment.etc."machine-id".text = (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)
}) ({
# Robustness/debugging:
boot.kernelParams = [ "panic=10" "boot.panic_on_fail" ]; # Reboot on kernel panic, panic if boot fails.
# might additionally want to do this: https://stackoverflow.com/questions/62083796/automatic-reboot-on-systemd-emergency-mode
systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot.
}) ({
# Free convenience:
# The non-interactive version of bash does not remove »\[« and »\]« from PS1. Seems to work just fine without those. So make the prompt pretty (and informative):
programs.bash.promptInit = ''
# Provide a nice prompt if the terminal supports it.
if [ "''${TERM:-}" != "dumb" ] ; then
if [[ "$UID" == '0' ]] ; then if [[ ! "''${SUDO_USER:-}" ]] ; then # direct root: red username + green hostname
PS1='\e[0m\e[48;5;234m\e[96m$(printf "%-+ 4d" $?)\e[93m[\D{%Y-%m-%d %H:%M:%S}] \e[91m\u\e[97m@\e[92m\h\e[97m:\e[96m\w'"''${TERM_RECURSION_DEPTH:+\e[91m["$TERM_RECURSION_DEPTH"]}"'\e[24;97m\$ \e[0m'
else # sudo root: red username + red hostname
PS1='\e[0m\e[48;5;234m\e[96m$(printf "%-+ 4d" $?)\e[93m[\D{%Y-%m-%d %H:%M:%S}] \e[91m\u\e[97m@\e[91m\h\e[97m:\e[96m\w'"''${TERM_RECURSION_DEPTH:+\e[91m["$TERM_RECURSION_DEPTH"]}"'\e[24;97m\$ \e[0m'
fi ; else # other user: green username + green hostname
PS1='\e[0m\e[48;5;234m\e[96m$(printf "%-+ 4d" $?)\e[93m[\D{%Y-%m-%d %H:%M:%S}] \e[92m\u\e[97m@\e[92m\h\e[97m:\e[96m\w'"''${TERM_RECURSION_DEPTH:+\e[91m["$TERM_RECURSION_DEPTH"]}"'\e[24;97m\$ \e[0m'
fi
if test "$TERM" = "xterm" ; then
PS1="\033]2;\h:\u:\w\007$PS1"
fi
fi
export TERM_RECURSION_DEPTH=$(( 1 + ''${TERM_RECURSION_DEPTH:-0} ))
'';
}) ]);
}

1
modules/default.nix Normal file
View File

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

52
modules/installer.nix.md Normal file
View File

@ -0,0 +1,52 @@
/*
# Installer Declarations
Options to declare Disks and Partitions to be picked up by the installer scripts.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.installer;
in {
options.${prefix} = { installer = {
disks = lib.mkOption {
description = "Set of disks that this host will be installed on.";
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { options = {
name = lib.mkOption { description = "Name that this disk is being referred to as in other places."; type = lib.types.str; default = name; readOnly = true; };
size = lib.mkOption { description = "The size of the image to create, when using an image for this disk, as argument to »fallocate -l«."; type = lib.types.str; default = "8G"; };
serial = lib.mkOption { description = "Serial number of the specific hardware disk to use. If set the disk path passed to the installer must point to the device with this serial. Use »udevadm info --query=property --name=$DISK | grep -oP 'ID_SERIAL_SHORT=\K.*'« to get the serial."; type = lib.types.nullOr lib.types.str; default = null; };
alignment = lib.mkOption { description = "Partition alignment quantifier. Should be at least the optimal physical write size of the disk, but going larger at worst wastes this many times the number of partitions disk sectors."; type = lib.types.int; default = 16384; };
mbrParts = lib.mkOption { description = "Up to three colon-separated (GPT) partition numbers that will be made available in a hybrid MBR."; type = lib.types.nullOr lib.types.str; default = null; };
extraFDiskCommands = lib.mkOption { description = "»fdisk« menu commands to run against the hybrid MBR. ».mbrParts« 1[2[3]] exist as transfers from the GPT table, and part4 is the protective GPT part. Can do things like marking partitions as bootable or changing their type. Spaces and end-of-line »#«-prefixed comments are removed, new lines and »;« also mean return."; type = lib.types.lines; default = null; example = ''
t;1;b # type ; part1 ; W95 FAT32
a;1 # active/boot ; part1
''; };
}; }));
default = { primary = { }; };
};
partitions = lib.mkOption {
description = "Set of disks disk partitions that the system will need/use. Partitions will be created on their respective ».disk«s in ».order« using »sgdisk -n X:+0+$size«.";
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { options = {
name = lib.mkOption { description = "Name/partlabel that this partition can be referred to as once created."; type = lib.types.str; default = name; readOnly = true; };
disk = lib.mkOption { description = "Name of the disk that this partition resides on."; type = lib.types.str; default = "primary"; };
type = lib.mkOption { description = "»gdisk« partition type of this partition."; type = lib.types.str; };
size = lib.mkOption { description = "Partition size, as number suffixed with »K«, »M«, »G«, etc. Or »null« to fill the remaining disk space."; type = lib.types.nullOr lib.types.str; default = null; };
position = lib.mkOption { description = "Position at which to create the partition. The default »+0« means the beginning of the largest free block."; type = lib.types.str; default = "+0"; };
index = lib.mkOption { description = "Optionally explicit partition table index to place this partition in. Use ».order« to make sure that this index hasn't been used yet.."; type = lib.types.nullOr lib.types.int; default = null; };
order = lib.mkOption { description = "Creation order ranking of this partition. Higher orders will be created first, and will thus be placed earlier in the partition table (if ».index« isn't explicitly set) and also further to the front of the disk space."; type = lib.types.int; default = 1000; };
}; }));
default = { };
};
partitionList = lib.mkOption { description = "Partitions as a sorted list"; type = lib.types.listOf (lib.types.attrsOf lib.types.anything); default = lib.sort (before: after: before.order >= after.order) (lib.attrValues cfg.partitions); readOnly = true; internal = true; };
postPartitionCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
postFormatCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
postMountCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
}; };
}

View File

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

View File

@ -0,0 +1,78 @@
/*
# Dropbear SSHd Configuration
OpenSSH adds ~35MB closure size. Let's try `dropbear` instead!
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.services.dropbear;
in {
options.${prefix} = { services.dropbear = {
enable = lib.mkEnableOption "dropbear SSH daemon";
socketActivation = lib.mkEnableOption "socket activation mode for dropbear";
rootKeys = lib.mkOption { default = [ ]; type = lib.types.listOf lib.types.str; description = "Literal lines to write to »/root/.ssh/authorized_keys«"; };
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [ ({
environment.systemPackages = (with pkgs; [ dropbear ]);
networking.firewall.allowedTCPPorts = [ 22 ];
#environment.etc."dropbear/.mkdir".text = "";
environment.etc.dropbear.source = "/run/user/0"; # allow for readonly /etc
}) (lib.mkIf (!cfg.socketActivation) {
systemd.services."dropbear" = {
description = "dropbear SSH server (listening)";
wantedBy = [ "multi-user.target" ]; after = [ "network.target" ];
serviceConfig.ExecStart = lib.concatStringsSep "" [
"${pkgs.dropbear}/bin/dropbear"
" -F -E" # don't fork, use stderr
" -p 22" # handle a single connection on stdio
" -R" # generate host keys on connection
#" -r .../dropbear_rsa_host_key"
];
#serviceConfig.PIDFile = "/var/run/dropbear.pid"; serviceConfig.Type = "forking"; after = [ "network.target" ]; # alternative to »-E -F« (?)
};
}) (lib.mkIf (cfg.socketActivation) {
# This did not work: dropbear errors out with "socket operation on non-socket".
systemd.sockets.dropbear = { # start a »dropbear@.service« on any number of TCP connections on port 22
conflicts = [ "dropbear.service" ];
listenStreams = [ "22" ];
socketConfig.Accept = true;
wantedBy = [ "sockets.target" ]; # (isn't this implicit?)
};
systemd.services."dropbear@" = {
description = "dropbear SSH server (per-connection)";
after = [ "syslog.target" ];
serviceConfig.ExecStart = lib.concatStringsSep "" [
"-" # for the most part ignore exit != 0
"${pkgs.dropbear}/bin/dropbear"
" -i" # handle a single connection on stdio
" -R" # generate host keys on connection
#" -r .../dropbear_rsa_host_key"
];
};
}) (lib.mkIf (cfg.rootKeys != [ ]) {
system.activationScripts.root-authorized_keys = ''
mkdir -pm 700 /root/.ssh/
[ -e /root/.ssh/authorized_keys ] || install -m 600 -T /dev/null /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
${lib.concatMapStringsSep "\n" (key: "printf %s ${lib.escapeShellArg key} >>/root/.ssh/authorized_keys") cfg.rootKeys}
'';
}) ]);
}

96
overlays/README.md Normal file
View File

@ -0,0 +1,96 @@
# NixOS Overlays
Nix(OS) manages its packages in a global attribute set, mostly referred to as `nixpkgs` (as repository/sources) or simply as `pkgs` (when evaluated).
Overlays are a mechanism to add or replace packages in that attribute set, such that wherever else they are referenced (e.g. as `pkg.<package>`) the added/replaced version is used.
Any number of overlays can be applied in sequence when instantiating/evaluating `nixpkgs` into `pkgs`.
Each overlay is a function with two parameters returning an attrset which is merged onto `pkgs`.
The first parameter (called `final`) is the `pkgs` as it will result after applying all overlays. This works because of nix's lazy evaluation, but accessing attributes that are based on the result of the current overlay will logically cause unresolvable recursions.
For that reason, the second parameter `prev` is the version of `pkgs` from before applying the overlay.
As a general guideline, use `final` where possible (to avoid consuming unpatched packages) and `prev` only when necessary to avoid recursions.
`prev` thus gives access to the packages being overridden and allows (the build instructions for) the overriding package to be based off the unmodified package.
Most packages in `nixpkgs` are constructed using something like `callPackage ({ ...args }: mkDerivation { ...attributes }) { ...settings }`, where `callPackage` is usually in `all-packages.nix` and imports the code in the parentheses from a different file.
Passed by `callPackage`, `args` includes `pkgs` plus optionally the `settings` to the package.
The `attributes` are then based on local values and packages and settings from `args`.
Any package built that way then has two functions which allow overlays (or code elsewhere) to define modified versions of that package:
* `.overwrite` is a function taking an attrset that is merged over `args` before re-evaluation the package;
* `.overrideAttrs` is a function from the old `attributes` to ones that are merged over `attributes` before building the derivation.
Using the above mechanisms, each file in this folder adds and/or modifies one or more packages to/in `pkgs`.
[`./default.nix`](./default.nix) exports all overlays as an attribute set; [`flake#outputs.packages.<arch>.*`](../flake.nix), exports all packages resulting from the overlays.
## Template/Examples
Here is a skeleton structure / collection of examples for writing a new `<overlay>.nix.md`:
````md
/*
# TODO: title
TODO: documentation
## Implementation
```nix
#*/# end of MarkDown, beginning of NixPkgs overlay:
dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib;
in {
# e.g.: add a patched version of a package (use the same name to replace)
systemd-patched = prev.systemd.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
../patches/systemd-....patch
];
});
# e.g.: add a prebuilt program as package
qemu-aarch64-static = pkgs.stdenv.mkDerivation {
name = "qemu-aarch64-static";
src = builtins.fetchurl {
url = "https://github.com/multiarch/qemu-user-static/releases/download/v6.1.0-8/qemu-aarch64-static";
sha256 = "075l122p3qfq6nm07qzvwspmsrslvrghar7i5advj945lz1fm6dd";
}; dontUnpack = true;
installPhase = "install -D -m 0755 $src $out/bin/qemu-aarch64-static";
};
# e.g.: update (or pin the version of) a package
raspberrypifw = prev.raspberrypifw.overrideAttrs (old: rec {
version = "1.20220308";
src = pkgs.fetchFromGitHub {
owner = "raspberrypi"; repo = "firmware"; rev = version;
sha256 = "sha256-pwhI9sklAGq5+fJqQSadrmW09Wl6+hOFI/hEewkkLQs=";
};
});
# e.g.: add a program as new package
udptunnel = pkgs.stdenv.mkDerivation rec {
pname = "udptunnel"; version = "1"; # (not versioned)
src = pkgs.fetchFromGitHub {
owner = "rfc1036"; repo = pname; rev = "482ed94388a0dde68561584926c7d5c14f079f7e"; # 2018-11-18
sha256 = "1wkzzxslwjm5mbpyaq30bilfi2mfgi2jqld5l15hm5076mg31vp7";
};
patches = [ ../patches/....patch ];
installPhase = ''
mkdir -p $out/bin $out/share/udptunnel
cp -T udptunnel $out/bin/${pname}
cp COPYING $out/share/udptunnel
'';
meta = {
homepage = "https://github.com/rfc1036/udptunnel";
description = "Tunnel UDP packets in a TCP connection ";
license = lib.licenses.gpl2;
maintainers = with lib.maintainers; [ ];
platforms = with lib.platforms; linux;
};
};
}
````

1
overlays/default.nix Normal file
View File

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

View File

@ -0,0 +1,44 @@
/*
# `libubootenv` - Library to access U-Boot environment
As an `environment.systemPackages` entry this provides the `fw_printenv` / `fw_setenv` commands to work with U-Boot's environment variables.
## Example
Assuming `/dev/disk/by-partlabel/config-${...}` is placed at the same location that U-Boot was configured (via `CONFIG_ENV_OFFSET` and `CONFIG_ENV_SIZE`) to expect/save the environment:
```nix
{
environment.systemPackages = [ pkgs.libubootenv ];
environment.etc."fw_env.config".text = "/dev/disk/by-partlabel/config-${...} 0x0 0x${lib.concatStrings (map toString (lib.toBaseDigits 16 envSize))}";
}
```
## Implementation
```nix
#*/# end of MarkDown, beginning of NixPkgs overlay:
dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib;
in {
libubootenv = pkgs.stdenv.mkDerivation rec {
pname = "libubootenv"; version = "0.3.2";
src = pkgs.fetchFromGitHub {
owner = "sbabic"; repo = pname; rev = "ba7564f5006d09bec51058cf4f5ac90d4dc18b3c"; # 2018-11-18
hash = "sha256-6cHkr3s7/2BVXBTn9bUfPFbYAfv9VYh6C9GAbWILNjs=";
};
nativeBuildInputs = [ pkgs.cmake pkgs.zlib ];
meta = {
homepage = "https://github.com/sbabic/libubootenv";
description = "Generic library and tools to access and modify U-Boot environment from User Space";
license = [ lib.licenses.lgpl21Plus lib.licenses.mit lib.licenses.cc0 ];
maintainers = with lib.maintainers; [ ];
platforms = with lib.platforms; linux;
};
};
}

27
patches/README.md Normal file
View File

@ -0,0 +1,27 @@
# Some Patches
... for `nixpkgs` or programs therein.
A patch `<name>-*.patch` is generally for the open source software `<name>` which is added/modified by the nixpkgs overlay in `../overlays/<name>.nix.md`.
Patches for `nixpkgs` are applied in `../flake.nix`.
To create/"commit" a patch of the current directory vs its latest commit:
```bash
git diff >.../overlays/patches/....patch
```
To test a patch against the repo in CWD, or to "check it out" to edit and then "commit" again:
```bash
git reset --hard HEAD # destructively reset the working tree to the current commit
patch --dry-run -p1 <.../overlays/patches/....patch # test only
patch -p1 <.../overlays/patches/....patch # apply to CWD
```
## License
Patches included in this repository are written by the direct contributors to this repository (unless individually noted otherwise; pre-existing patches should be referenced by URL).
Each individual patch shall be licensed by the most permissive license (up to common domain / CC0) that the software it is for (and derived from) allows.
Usually that would probably be the license of the original software itself, which should be mentioned in the respective overlay and/or the linked source code.

13
patches/default.nix Normal file
View File

@ -0,0 +1,13 @@
# Returns an attrset where the values are the paths to all ».patch« files in this directory, and the names the respective »basename -s .patch«s.
dirname: inputs: let
getNamedPatchFiles = dir: builtins.removeAttrs (builtins.listToAttrs (map (name: let
match = builtins.match ''^(.*)[.]patch$'' name;
in if (match != null) then {
name = builtins.head match; value = "${dir}/${name}";
} else { name = ""; value = null; }) (builtins.attrNames (builtins.readDir dir)))) [ "" ];
in (getNamedPatchFiles dirname) // {
# When referring to the patches by a path derived from »dirname«, then their paths change whenever that changes, which happens when any file in this repo changes. Changing patch paths mean that the derivations the patches are inputs to need to be rebuilt, so using local paths, which put their targets into a new store artifact (i.e. separate input) is much more efficient.
# TODO: automate this, somehow:
nixpkgs-fix-systemd-boot-install = ./nixpkgs-fix-systemd-boot-install.patch;
nixpkgs-test = ./nixpkgs-test.patch;
}

View File

@ -0,0 +1,23 @@
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
index c07567ec..ecd69f04 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -33,16 +33,14 @@ let
netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
- empty_file=$(${pkgs.coreutils}/bin/mktemp)
-
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n}
- ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
+ ${pkgs.coreutils}/bin/install -D /dev/null "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
'') cfg.extraFiles)}
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n}
- ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
+ ${pkgs.coreutils}/bin/install -D /dev/null "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
'') cfg.extraEntries)}
'';
};

View File

@ -0,0 +1,7 @@
diff --git a/patched! b/patched!
new file mode 100644
index 0000000..e69de29
--- a/patched!
+++ b/patched!
@@ -1,0 +1,1 @@
+yay