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, nix_store_prefix: str | None
40) -> None:
41 """Add CopyFiles= instructions to a definition for all paths in the closure.
42
43 Replace `/nix/store` with the value of nix_store_prefix.
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 option = f"CopyFiles={source}"
56 if nix_store_prefix:
57 target = nix_store_prefix / source.relative_to("/nix/store/")
58 option = f"{option}:{target}"
59
60 copy_files_lines.append(f"{option}\n")
61
62 with open(definition, "a") as f:
63 f.writelines(copy_files_lines)
64
65
66def 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 target_dir = Path("amended-repart.d")
96 target_dir.mkdir()
97 shutil.copytree(repart_definitions, target_dir, dirs_exist_ok=True)
98
99 for name, config in partition_config.items():
100 definition = target_dir.joinpath(f"{name}.conf")
101 definition.chmod(0o644)
102
103 contents = config.get("contents")
104 add_contents_to_definition(definition, contents)
105
106 closure = config.get("closure")
107 nix_store_prefix = config.get("nixStorePrefix")
108 add_closure_to_definition(definition, closure, nix_store_prefix)
109
110 print(target_dir.absolute())
111
112
113if __name__ == "__main__":
114 main()