1#!/usr/bin/env python
2
3"""Amend systemd-repart definiton files.
4
5In order to avoid Import-From-Derivation (IFD) when building images with
6systemd-repart, the definition files created by Nix need to be amended with the
7store paths from the closure.
8
9This is achieved by adding CopyFiles= instructions to the definition files.
10
11The arbitrary files configured via `contents` are also added to the definition
12files using the same mechanism.
13"""
14
15import json
16import sys
17import shutil
18from pathlib import Path
19
20
21def add_contents_to_definition(
22 definition: Path, contents: dict[str, dict[str, str]] | None
23) -> None:
24 """Add CopyFiles= instructions to a definition for all files in contents."""
25 if not contents:
26 return
27
28 copy_files_lines: list[str] = []
29 for target, options in contents.items():
30 source = options["source"]
31
32 copy_files_lines.append(f"CopyFiles={source}:{target}\n")
33
34 with open(definition, "a") as f:
35 f.writelines(copy_files_lines)
36
37
38def add_closure_to_definition(
39 definition: Path, closure: Path | None, strip_nix_store_prefix: bool | None
40) -> None:
41 """Add CopyFiles= instructions to a definition for all paths in the closure.
42
43 If strip_nix_store_prefix is True, `/nix/store` is stripped from the target path.
44 """
45 if not closure:
46 return
47
48 copy_files_lines: list[str] = []
49 with open(closure, "r") as f:
50 for line in f:
51 if not isinstance(line, str):
52 continue
53
54 source = Path(line.strip())
55 target = str(source.relative_to("/nix/store/"))
56 target = f":/{target}" if strip_nix_store_prefix else ""
57
58 copy_files_lines.append(f"CopyFiles={source}{target}\n")
59
60 with open(definition, "a") as f:
61 f.writelines(copy_files_lines)
62
63
64def main() -> None:
65 """Amend the provided repart definitions by adding CopyFiles= instructions.
66
67 For each file specified in the `contents` field of a partition in the
68 partiton config file, a `CopyFiles=` instruction is added to the
69 corresponding definition file.
70
71 The same is done for every store path of the `closure` field.
72
73 Print the path to a directory that contains the amended repart
74 definitions to stdout.
75 """
76 partition_config_file = sys.argv[1]
77 if not partition_config_file:
78 print("No partition config file was supplied.")
79 sys.exit(1)
80
81 repart_definitions = sys.argv[2]
82 if not repart_definitions:
83 print("No repart definitions were supplied.")
84 sys.exit(1)
85
86 with open(partition_config_file, "rb") as f:
87 partition_config = json.load(f)
88
89 if not partition_config:
90 print("Partition config is empty.")
91 sys.exit(1)
92
93 target_dir = Path("amended-repart.d")
94 target_dir.mkdir()
95 shutil.copytree(repart_definitions, target_dir, dirs_exist_ok=True)
96
97 for name, config in partition_config.items():
98 definition = target_dir.joinpath(f"{name}.conf")
99 definition.chmod(0o644)
100
101 contents = config.get("contents")
102 add_contents_to_definition(definition, contents)
103
104 closure = config.get("closure")
105 strip_nix_store_prefix = config.get("stripNixStorePrefix")
106 add_closure_to_definition(definition, closure, strip_nix_store_prefix)
107
108 print(target_dir.absolute())
109
110
111if __name__ == "__main__":
112 main()