about summary refs log tree commit diff
path: root/nixos/modules/image
diff options
context:
space:
mode:
authornikstur <nikstur@outlook.com>2023-07-25 00:19:20 +0200
committernikstur <nikstur@outlook.com>2023-07-26 23:33:14 +0200
commitec8d30cc50e49f3d6a50b27a8f351d1f1bb6a7cc (patch)
treead15684067dfb3b8ff4db99b392c7ef59d12e50a /nixos/modules/image
parenta662dc8b7369605e4b8e977d76797be982897b0c (diff)
nixos/image: add repart builder
Diffstat (limited to 'nixos/modules/image')
-rw-r--r--nixos/modules/image/amend-repart-definitions.py113
-rw-r--r--nixos/modules/image/repart.nix204
2 files changed, 317 insertions, 0 deletions
diff --git a/nixos/modules/image/amend-repart-definitions.py b/nixos/modules/image/amend-repart-definitions.py
new file mode 100644
index 0000000000000..e50ed6fd39a79
--- /dev/null
+++ b/nixos/modules/image/amend-repart-definitions.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+
+"""Amend systemd-repart definiton files.
+
+In order to avoid Import-From-Derivation (IFD) when building images with
+systemd-repart, the definition files created by Nix need to be amended with the
+store paths from the closure.
+
+This is achieved by adding CopyFiles= instructions to the definition files.
+
+The arbitrary files configured via `contents` are also added to the definition
+files using the same mechanism.
+"""
+
+import json
+import sys
+import shutil
+import os
+import tempfile
+from pathlib import Path
+
+
+def add_contents_to_definition(
+    definition: Path, contents: dict[str, dict[str, str]] | None
+) -> None:
+    """Add CopyFiles= instructions to a definition for all files in contents."""
+    if not contents:
+        return
+
+    copy_files_lines: list[str] = []
+    for target, options in contents.items():
+        source = options["source"]
+
+        copy_files_lines.append(f"CopyFiles={source}:{target}\n")
+
+    with open(definition, "a") as f:
+        f.writelines(copy_files_lines)
+
+
+def add_closure_to_definition(
+    definition: Path, closure: Path | None, strip_nix_store_prefix: bool | None
+) -> None:
+    """Add CopyFiles= instructions to a definition for all paths in the closure.
+
+    If strip_nix_store_prefix is True, `/nix/store` is stripped from the target path.
+    """
+    if not closure:
+        return
+
+    copy_files_lines: list[str] = []
+    with open(closure, "r") as f:
+        for line in f:
+            if not isinstance(line, str):
+                continue
+
+            source = Path(line.strip())
+            target = str(source.relative_to("/nix/store/"))
+            target = f":{target}" if strip_nix_store_prefix else ""
+
+            copy_files_lines.append(f"CopyFiles={source}{target}\n")
+
+    with open(definition, "a") as f:
+        f.writelines(copy_files_lines)
+
+
+def main() -> None:
+    """Amend the provided repart definitions by adding CopyFiles= instructions.
+
+    For each file specified in the `contents` field of a partition in the
+    partiton config file, a `CopyFiles=` instruction is added to the
+    corresponding definition file.
+
+    The same is done for every store path of the `closure` field.
+
+    Print the path to a directory that contains the amended repart
+    definitions to stdout.
+    """
+    partition_config_file = sys.argv[1]
+    if not partition_config_file:
+        print("No partition config file was supplied.")
+        sys.exit(1)
+
+    repart_definitions = sys.argv[2]
+    if not repart_definitions:
+        print("No repart definitions were supplied.")
+        sys.exit(1)
+
+    with open(partition_config_file, "rb") as f:
+        partition_config = json.load(f)
+
+    if not partition_config:
+        print("Partition config is empty.")
+        sys.exit(1)
+
+    temp = tempfile.mkdtemp()
+    shutil.copytree(repart_definitions, temp, dirs_exist_ok=True)
+
+    for name, config in partition_config.items():
+        definition = Path(f"{temp}/{name}.conf")
+        os.chmod(definition, 0o644)
+
+        contents = config.get("contents")
+        add_contents_to_definition(definition, contents)
+
+        closure = config.get("closure")
+        strip_nix_store_prefix = config.get("stripStorePaths")
+        add_closure_to_definition(definition, closure, strip_nix_store_prefix)
+
+    print(temp)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/nixos/modules/image/repart.nix b/nixos/modules/image/repart.nix
new file mode 100644
index 0000000000000..c885200a8604a
--- /dev/null
+++ b/nixos/modules/image/repart.nix
@@ -0,0 +1,204 @@
+# This module exposes options to build a disk image with a GUID Partition Table
+# (GPT). It uses systemd-repart to build the image.
+
+{ config, pkgs, lib, utils, ... }:
+
+let
+  cfg = config.image.repart;
+
+  partitionOptions = {
+    options = {
+      storePaths = lib.mkOption {
+        type = with lib.types; listOf path;
+        default = [ ];
+        description = lib.mdDoc "The store paths to include in the partition.";
+      };
+
+      stripNixStorePrefix = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to strip `/nix/store/` from the store paths. This is useful
+          when you want to build a partition that only contains store paths and
+          is mounted under `/nix/store`.
+        '';
+      };
+
+      contents = lib.mkOption {
+        type = with lib.types; attrsOf (submodule {
+          options = {
+            source = lib.mkOption {
+              type = types.path;
+              description = lib.mdDoc "Path of the source file.";
+            };
+          };
+        });
+        default = { };
+        example = lib.literalExpression '' {
+          "/EFI/BOOT/BOOTX64.EFI".source =
+            "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
+
+          "/loader/entries/nixos.conf".source = systemdBootEntry;
+        }
+        '';
+        description = lib.mdDoc "The contents to end up in the filesystem image.";
+      };
+
+      repartConfig = lib.mkOption {
+        type = with lib.types; attrsOf (oneOf [ str int bool ]);
+        example = {
+          Type = "home";
+          SizeMinBytes = "512M";
+          SizeMaxBytes = "2G";
+        };
+        description = lib.mdDoc ''
+          Specify the repart options for a partiton as a structural setting.
+          See <https://www.freedesktop.org/software/systemd/man/repart.d.html>
+          for all available options.
+        '';
+      };
+    };
+  };
+in
+{
+  options.image.repart = {
+
+    name = lib.mkOption {
+      type = lib.types.str;
+      description = lib.mdDoc "The name of the image.";
+    };
+
+    seed = lib.mkOption {
+      type = with lib.types; nullOr str;
+      # Generated with `uuidgen`. Random but fixed to improve reproducibility.
+      default = "0867da16-f251-457d-a9e8-c31f9a3c220b";
+      description = lib.mdDoc ''
+        A UUID to use as a seed. You can set this to `null` to explicitly
+        randomize the partition UUIDs.
+      '';
+    };
+
+    split = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Enables generation of split artifacts from partitions. If enabled, for
+        each partition with SplitName= set, a separate output file containing
+        just the contents of that partition is generated.
+      '';
+    };
+
+    partitions = lib.mkOption {
+      type = with lib.types; attrsOf (submodule partitionOptions);
+      default = { };
+      example = lib.literalExpression '' {
+        "10-esp" = {
+          contents = {
+            "/EFI/BOOT/BOOTX64.EFI".source =
+              "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
+          }
+          repartConfig = {
+            Type = "esp";
+            Format = "fat";
+          };
+        };
+        "20-root" = {
+          storePaths = [ config.system.build.toplevel ];
+          repartConfig = {
+            Type = "root";
+            Format = "ext4";
+            Minimize = "guess";
+          };
+        };
+      };
+      '';
+      description = lib.mdDoc ''
+        Specify partitions as a set of the names of the partitions with their
+        configuration as the key.
+      '';
+    };
+
+  };
+
+  config = {
+
+    system.build.image =
+      let
+        fileSystemToolMapping = with pkgs; {
+          "vfat" = [ dosfstools mtools ];
+          "ext4" = [ e2fsprogs.bin ];
+          "squashfs" = [ squashfsTools ];
+          "erofs" = [ erofs-utils ];
+          "btrfs" = [ btrfs-progs ];
+          "xfs" = [ xfsprogs ];
+        };
+
+        fileSystems = lib.filter
+          (f: f != null)
+          (lib.mapAttrsToList (_n: v: v.repartConfig.Format or null) cfg.partitions);
+
+        fileSystemTools = builtins.concatMap (f: fileSystemToolMapping."${f}") fileSystems;
+
+
+        makeClosure = paths: pkgs.closureInfo { rootPaths = paths; };
+
+        # Add the closure of the provided Nix store paths to cfg.partitions so
+        # that amend-repart-definitions.py can read it.
+        addClosure = _name: partitionConfig: partitionConfig // (
+          lib.optionalAttrs
+            (partitionConfig.storePaths or [ ] != [ ])
+            { closure = "${makeClosure partitionConfig.storePaths}/store-paths"; }
+        );
+
+
+        finalPartitions = lib.mapAttrs addClosure cfg.partitions;
+
+
+        amendRepartDefinitions = pkgs.runCommand "amend-repart-definitions.py"
+          {
+            nativeBuildInputs = with pkgs; [ black ruff mypy ];
+            buildInputs = [ pkgs.python3 ];
+          } ''
+          install ${./amend-repart-definitions.py} $out
+          patchShebangs --host $out
+
+          black --check --diff $out
+          ruff --line-length 88 $out
+          mypy --strict $out
+        '';
+
+        format = pkgs.formats.ini { };
+
+        definitionsDirectory = utils.systemdUtils.lib.definitions
+          "repart.d"
+          format
+          (lib.mapAttrs (_n: v: { Partition = v.repartConfig; }) finalPartitions);
+
+        partitions = pkgs.writeText "partitions.json" (builtins.toJSON finalPartitions);
+      in
+      pkgs.runCommand cfg.name
+        {
+          nativeBuildInputs = with pkgs; [
+            fakeroot
+            systemd
+          ] ++ fileSystemTools;
+        } ''
+        amendedRepartDefinitions=$(${amendRepartDefinitions} ${partitions} ${definitionsDirectory})
+
+        mkdir -p $out
+        cd $out
+
+        fakeroot systemd-repart \
+          --dry-run=no \
+          --empty=create \
+          --size=auto \
+          --seed="${cfg.seed}" \
+          --definitions="$amendedRepartDefinitions" \
+          --split="${lib.boolToString cfg.split}" \
+          image.raw
+      '';
+
+    meta.maintainers = with lib.maintainers; [ nikstur ];
+
+  };
+}