lol

nixos/image: add repart builder

nikstur ec8d30cc a662dc8b

+317
+113
nixos/modules/image/amend-repart-definitions.py
··· 1 + #!/usr/bin/env python 2 + 3 + """Amend systemd-repart definiton files. 4 + 5 + In order to avoid Import-From-Derivation (IFD) when building images with 6 + systemd-repart, the definition files created by Nix need to be amended with the 7 + store paths from the closure. 8 + 9 + This is achieved by adding CopyFiles= instructions to the definition files. 10 + 11 + The arbitrary files configured via `contents` are also added to the definition 12 + files using the same mechanism. 13 + """ 14 + 15 + import json 16 + import sys 17 + import shutil 18 + import os 19 + import tempfile 20 + from pathlib import Path 21 + 22 + 23 + def add_contents_to_definition( 24 + definition: Path, contents: dict[str, dict[str, str]] | None 25 + ) -> None: 26 + """Add CopyFiles= instructions to a definition for all files in contents.""" 27 + if not contents: 28 + return 29 + 30 + copy_files_lines: list[str] = [] 31 + for target, options in contents.items(): 32 + source = options["source"] 33 + 34 + copy_files_lines.append(f"CopyFiles={source}:{target}\n") 35 + 36 + with open(definition, "a") as f: 37 + f.writelines(copy_files_lines) 38 + 39 + 40 + def add_closure_to_definition( 41 + definition: Path, closure: Path | None, strip_nix_store_prefix: bool | None 42 + ) -> None: 43 + """Add CopyFiles= instructions to a definition for all paths in the closure. 44 + 45 + If strip_nix_store_prefix is True, `/nix/store` is stripped from the target path. 46 + """ 47 + if not closure: 48 + return 49 + 50 + copy_files_lines: list[str] = [] 51 + with open(closure, "r") as f: 52 + for line in f: 53 + if not isinstance(line, str): 54 + continue 55 + 56 + source = Path(line.strip()) 57 + target = str(source.relative_to("/nix/store/")) 58 + target = f":{target}" if strip_nix_store_prefix else "" 59 + 60 + copy_files_lines.append(f"CopyFiles={source}{target}\n") 61 + 62 + with open(definition, "a") as f: 63 + f.writelines(copy_files_lines) 64 + 65 + 66 + def main() -> None: 67 + """Amend the provided repart definitions by adding CopyFiles= instructions. 68 + 69 + For each file specified in the `contents` field of a partition in the 70 + partiton config file, a `CopyFiles=` instruction is added to the 71 + corresponding definition file. 72 + 73 + The same is done for every store path of the `closure` field. 74 + 75 + Print the path to a directory that contains the amended repart 76 + definitions to stdout. 77 + """ 78 + partition_config_file = sys.argv[1] 79 + if not partition_config_file: 80 + print("No partition config file was supplied.") 81 + sys.exit(1) 82 + 83 + repart_definitions = sys.argv[2] 84 + if not repart_definitions: 85 + print("No repart definitions were supplied.") 86 + sys.exit(1) 87 + 88 + with open(partition_config_file, "rb") as f: 89 + partition_config = json.load(f) 90 + 91 + if not partition_config: 92 + print("Partition config is empty.") 93 + sys.exit(1) 94 + 95 + temp = tempfile.mkdtemp() 96 + shutil.copytree(repart_definitions, temp, dirs_exist_ok=True) 97 + 98 + for name, config in partition_config.items(): 99 + definition = Path(f"{temp}/{name}.conf") 100 + os.chmod(definition, 0o644) 101 + 102 + contents = config.get("contents") 103 + add_contents_to_definition(definition, contents) 104 + 105 + closure = config.get("closure") 106 + strip_nix_store_prefix = config.get("stripStorePaths") 107 + add_closure_to_definition(definition, closure, strip_nix_store_prefix) 108 + 109 + print(temp) 110 + 111 + 112 + if __name__ == "__main__": 113 + main()
+204
nixos/modules/image/repart.nix
··· 1 + # This module exposes options to build a disk image with a GUID Partition Table 2 + # (GPT). It uses systemd-repart to build the image. 3 + 4 + { config, pkgs, lib, utils, ... }: 5 + 6 + let 7 + cfg = config.image.repart; 8 + 9 + partitionOptions = { 10 + options = { 11 + storePaths = lib.mkOption { 12 + type = with lib.types; listOf path; 13 + default = [ ]; 14 + description = lib.mdDoc "The store paths to include in the partition."; 15 + }; 16 + 17 + stripNixStorePrefix = lib.mkOption { 18 + type = lib.types.bool; 19 + default = false; 20 + description = lib.mdDoc '' 21 + Whether to strip `/nix/store/` from the store paths. This is useful 22 + when you want to build a partition that only contains store paths and 23 + is mounted under `/nix/store`. 24 + ''; 25 + }; 26 + 27 + contents = lib.mkOption { 28 + type = with lib.types; attrsOf (submodule { 29 + options = { 30 + source = lib.mkOption { 31 + type = types.path; 32 + description = lib.mdDoc "Path of the source file."; 33 + }; 34 + }; 35 + }); 36 + default = { }; 37 + example = lib.literalExpression '' { 38 + "/EFI/BOOT/BOOTX64.EFI".source = 39 + "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi"; 40 + 41 + "/loader/entries/nixos.conf".source = systemdBootEntry; 42 + } 43 + ''; 44 + description = lib.mdDoc "The contents to end up in the filesystem image."; 45 + }; 46 + 47 + repartConfig = lib.mkOption { 48 + type = with lib.types; attrsOf (oneOf [ str int bool ]); 49 + example = { 50 + Type = "home"; 51 + SizeMinBytes = "512M"; 52 + SizeMaxBytes = "2G"; 53 + }; 54 + description = lib.mdDoc '' 55 + Specify the repart options for a partiton as a structural setting. 56 + See <https://www.freedesktop.org/software/systemd/man/repart.d.html> 57 + for all available options. 58 + ''; 59 + }; 60 + }; 61 + }; 62 + in 63 + { 64 + options.image.repart = { 65 + 66 + name = lib.mkOption { 67 + type = lib.types.str; 68 + description = lib.mdDoc "The name of the image."; 69 + }; 70 + 71 + seed = lib.mkOption { 72 + type = with lib.types; nullOr str; 73 + # Generated with `uuidgen`. Random but fixed to improve reproducibility. 74 + default = "0867da16-f251-457d-a9e8-c31f9a3c220b"; 75 + description = lib.mdDoc '' 76 + A UUID to use as a seed. You can set this to `null` to explicitly 77 + randomize the partition UUIDs. 78 + ''; 79 + }; 80 + 81 + split = lib.mkOption { 82 + type = lib.types.bool; 83 + default = false; 84 + description = lib.mdDoc '' 85 + Enables generation of split artifacts from partitions. If enabled, for 86 + each partition with SplitName= set, a separate output file containing 87 + just the contents of that partition is generated. 88 + ''; 89 + }; 90 + 91 + partitions = lib.mkOption { 92 + type = with lib.types; attrsOf (submodule partitionOptions); 93 + default = { }; 94 + example = lib.literalExpression '' { 95 + "10-esp" = { 96 + contents = { 97 + "/EFI/BOOT/BOOTX64.EFI".source = 98 + "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi"; 99 + } 100 + repartConfig = { 101 + Type = "esp"; 102 + Format = "fat"; 103 + }; 104 + }; 105 + "20-root" = { 106 + storePaths = [ config.system.build.toplevel ]; 107 + repartConfig = { 108 + Type = "root"; 109 + Format = "ext4"; 110 + Minimize = "guess"; 111 + }; 112 + }; 113 + }; 114 + ''; 115 + description = lib.mdDoc '' 116 + Specify partitions as a set of the names of the partitions with their 117 + configuration as the key. 118 + ''; 119 + }; 120 + 121 + }; 122 + 123 + config = { 124 + 125 + system.build.image = 126 + let 127 + fileSystemToolMapping = with pkgs; { 128 + "vfat" = [ dosfstools mtools ]; 129 + "ext4" = [ e2fsprogs.bin ]; 130 + "squashfs" = [ squashfsTools ]; 131 + "erofs" = [ erofs-utils ]; 132 + "btrfs" = [ btrfs-progs ]; 133 + "xfs" = [ xfsprogs ]; 134 + }; 135 + 136 + fileSystems = lib.filter 137 + (f: f != null) 138 + (lib.mapAttrsToList (_n: v: v.repartConfig.Format or null) cfg.partitions); 139 + 140 + fileSystemTools = builtins.concatMap (f: fileSystemToolMapping."${f}") fileSystems; 141 + 142 + 143 + makeClosure = paths: pkgs.closureInfo { rootPaths = paths; }; 144 + 145 + # Add the closure of the provided Nix store paths to cfg.partitions so 146 + # that amend-repart-definitions.py can read it. 147 + addClosure = _name: partitionConfig: partitionConfig // ( 148 + lib.optionalAttrs 149 + (partitionConfig.storePaths or [ ] != [ ]) 150 + { closure = "${makeClosure partitionConfig.storePaths}/store-paths"; } 151 + ); 152 + 153 + 154 + finalPartitions = lib.mapAttrs addClosure cfg.partitions; 155 + 156 + 157 + amendRepartDefinitions = pkgs.runCommand "amend-repart-definitions.py" 158 + { 159 + nativeBuildInputs = with pkgs; [ black ruff mypy ]; 160 + buildInputs = [ pkgs.python3 ]; 161 + } '' 162 + install ${./amend-repart-definitions.py} $out 163 + patchShebangs --host $out 164 + 165 + black --check --diff $out 166 + ruff --line-length 88 $out 167 + mypy --strict $out 168 + ''; 169 + 170 + format = pkgs.formats.ini { }; 171 + 172 + definitionsDirectory = utils.systemdUtils.lib.definitions 173 + "repart.d" 174 + format 175 + (lib.mapAttrs (_n: v: { Partition = v.repartConfig; }) finalPartitions); 176 + 177 + partitions = pkgs.writeText "partitions.json" (builtins.toJSON finalPartitions); 178 + in 179 + pkgs.runCommand cfg.name 180 + { 181 + nativeBuildInputs = with pkgs; [ 182 + fakeroot 183 + systemd 184 + ] ++ fileSystemTools; 185 + } '' 186 + amendedRepartDefinitions=$(${amendRepartDefinitions} ${partitions} ${definitionsDirectory}) 187 + 188 + mkdir -p $out 189 + cd $out 190 + 191 + fakeroot systemd-repart \ 192 + --dry-run=no \ 193 + --empty=create \ 194 + --size=auto \ 195 + --seed="${cfg.seed}" \ 196 + --definitions="$amendedRepartDefinitions" \ 197 + --split="${lib.boolToString cfg.split}" \ 198 + image.raw 199 + ''; 200 + 201 + meta.maintainers = with lib.maintainers; [ nikstur ]; 202 + 203 + }; 204 + }