diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c9a0d79 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95569c5 --- /dev/null +++ b/.gitignore @@ -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-* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d7da54e --- /dev/null +++ b/.vscode/settings.json @@ -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 + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd276b6 Binary files /dev/null and b/LICENSE differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..056cfaf --- /dev/null +++ b/README.md @@ -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..*`. + +[`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 { }).writeTextDir "flake.nix" '' + { outputs = { ... }: { + prefix = ""; + }; } +'' +``` + + +## 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 { } +:lf . # load CWD's flake's outputs as variables +pkgs = nixosConfigurations.target.pkgs +lib = lib { inherit pkgs; inherit (pkgs) lib; } +``` + + +### TODOs + diff --git a/example/install.sh.md b/example/install.sh.md new file mode 100644 index 0000000..442cb22 --- /dev/null +++ b/example/install.sh.md @@ -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:-} +)} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..40df433 Binary files /dev/null and b/flake.lock differ diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2589f7d --- /dev/null +++ b/flake.nix @@ -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; } +]); } diff --git a/hosts/example.nix.md b/hosts/example.nix.md new file mode 100644 index 0000000..834d901 --- /dev/null +++ b/hosts/example.nix.md @@ -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"}'' ]; + + +}) ]; } diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..6b71480 --- /dev/null +++ b/lib/default.nix @@ -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; } diff --git a/lib/flakes.nix b/lib/flakes.nix new file mode 100644 index 0000000..8ce0d86 --- /dev/null +++ b/lib/flakes.nix @@ -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 »/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"} + ''; + }; + + })); + +} diff --git a/lib/imports.nix b/lib/imports.nix new file mode 100644 index 0000000..182349a --- /dev/null +++ b/lib/imports.nix @@ -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 = ""; imports = [ ]; }«. + 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" = ; "b" = ; "c/d" = ; "c/e" = ; } + 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.« 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 ]; } + ]; }; +} diff --git a/lib/misc.nix b/lib/misc.nix new file mode 100644 index 0000000..fbe4736 --- /dev/null +++ b/lib/misc.nix @@ -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)}"; + +} diff --git a/lib/scripts.nix b/lib/scripts.nix new file mode 100644 index 0000000..176529a --- /dev/null +++ b/lib/scripts.nix @@ -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 ]; + +} diff --git a/lib/setup-scripts/README.md b/lib/setup-scripts/README.md new file mode 100644 index 0000000..aee55f4 --- /dev/null +++ b/lib/setup-scripts/README.md @@ -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 `=` 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. diff --git a/lib/setup-scripts/default.nix b/lib/setup-scripts/default.nix new file mode 100644 index 0000000..d31b76b --- /dev/null +++ b/lib/setup-scripts/default.nix @@ -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) diff --git a/lib/setup-scripts/disk.sh b/lib/setup-scripts/disk.sh new file mode 100644 index 0000000..2eea8fb --- /dev/null +++ b/lib/setup-scripts/disk.sh @@ -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 ))" ; } diff --git a/lib/setup-scripts/install.sh b/lib/setup-scripts/install.sh new file mode 100644 index 0000000..2e7598a --- /dev/null +++ b/lib/setup-scripts/install.sh @@ -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 +)} diff --git a/lib/setup-scripts/maintenance.sh b/lib/setup-scripts/maintenance.sh new file mode 100644 index 0000000..3096cc8 --- /dev/null +++ b/lib/setup-scripts/maintenance.sh @@ -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" +)} diff --git a/lib/setup-scripts/utils.sh b/lib/setup-scripts/utils.sh new file mode 100644 index 0000000..d735fe8 --- /dev/null +++ b/lib/setup-scripts/utils.sh @@ -0,0 +1,17 @@ + +## +# Utilities +## + +## Prepends a command to a trap. Especially useful fo define »finally« commands via »prepend_trap '' 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 diff --git a/lib/vars.nix b/lib/vars.nix new file mode 100644 index 0000000..4995bf7 --- /dev/null +++ b/lib/vars.nix @@ -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)); + +} diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 0000000..68f9c05 --- /dev/null +++ b/modules/README.md @@ -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 `.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 + }) ]); + +} +```` diff --git a/modules/base.nix.md b/modules/base.nix.md new file mode 100644 index 0000000..4af536f --- /dev/null +++ b/modules/base.nix.md @@ -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} )) + ''; + + }) ]); + +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..6bba885 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1 @@ +dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { } diff --git a/modules/installer.nix.md b/modules/installer.nix.md new file mode 100644 index 0000000..82cc9f2 --- /dev/null +++ b/modules/installer.nix.md @@ -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 = ""; }; + }; }; + +} diff --git a/modules/services/default.nix b/modules/services/default.nix new file mode 100644 index 0000000..6bba885 --- /dev/null +++ b/modules/services/default.nix @@ -0,0 +1 @@ +dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { } diff --git a/modules/services/dropbear.nix.md b/modules/services/dropbear.nix.md new file mode 100644 index 0000000..d829202 --- /dev/null +++ b/modules/services/dropbear.nix.md @@ -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} + ''; + + }) ]); + +} diff --git a/overlays/README.md b/overlays/README.md new file mode 100644 index 0000000..c1c08e6 --- /dev/null +++ b/overlays/README.md @@ -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.`) 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..*`](../flake.nix), exports all packages resulting from the overlays. + + +## Template/Examples + +Here is a skeleton structure / collection of examples for writing a new `.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; + }; + }; +} +```` diff --git a/overlays/default.nix b/overlays/default.nix new file mode 100644 index 0000000..0b6f5f9 --- /dev/null +++ b/overlays/default.nix @@ -0,0 +1 @@ +dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importOverlays inputs dirname { } diff --git a/overlays/libubootenv.nix.md b/overlays/libubootenv.nix.md new file mode 100644 index 0000000..e52a3f4 --- /dev/null +++ b/overlays/libubootenv.nix.md @@ -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; + }; + }; +} diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..b483d9f --- /dev/null +++ b/patches/README.md @@ -0,0 +1,27 @@ + +# Some Patches + +... for `nixpkgs` or programs therein. + +A patch `-*.patch` is generally for the open source software `` which is added/modified by the nixpkgs overlay in `../overlays/.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. diff --git a/patches/default.nix b/patches/default.nix new file mode 100644 index 0000000..5a448d5 --- /dev/null +++ b/patches/default.nix @@ -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; +} diff --git a/patches/nixpkgs-fix-systemd-boot-install.patch b/patches/nixpkgs-fix-systemd-boot-install.patch new file mode 100644 index 0000000..cac3b23 --- /dev/null +++ b/patches/nixpkgs-fix-systemd-boot-install.patch @@ -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)} + ''; + }; diff --git a/patches/nixpkgs-test.patch b/patches/nixpkgs-test.patch new file mode 100644 index 0000000..91a39ab --- /dev/null +++ b/patches/nixpkgs-test.patch @@ -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