python312Packages.aiohomeconnect: 0.10.0 -> 0.11.0 (#374011)
[NixPkgs.git] / nixos / modules / system / etc / build-composefs-dump.py
blobfe739a621ec4d124fff847dc1c5b85e71d44e573
1 #!/usr/bin/env python3
3 """Build a composefs dump from a Json config
5 See the man page of composefs-dump for details about the format:
6 https://github.com/containers/composefs/blob/main/man/composefs-dump.md
8 Ensure to check the file with the check script when you make changes to it:
10 ./check-build-composefs-dump.sh ./build-composefs_dump.py
11 """
13 import glob
14 import json
15 import os
16 import sys
17 from enum import Enum
18 from pathlib import Path
19 from typing import Any
21 Attrs = dict[str, Any]
24 class FileType(Enum):
25 """The filetype as defined by the `st_mode` stat field in octal
27 You can check the st_mode stat field of a path in Python with
28 `oct(os.stat("/path/").st_mode)`
29 """
31 directory = "4"
32 file = "10"
33 symlink = "12"
36 class ComposefsPath:
37 path: str
38 size: int
39 filetype: FileType
40 mode: str
41 uid: str
42 gid: str
43 payload: str
44 rdev: str = "0"
45 nlink: int = 1
46 mtime: str = "1.0"
47 content: str = "-"
48 digest: str = "-"
50 def __init__(
51 self,
52 attrs: Attrs,
53 size: int,
54 filetype: FileType,
55 mode: str,
56 payload: str,
57 path: str | None = None,
59 if path is None:
60 path = attrs["target"]
61 self.path = path
62 self.size = size
63 self.filetype = filetype
64 self.mode = mode
65 self.uid = attrs["uid"]
66 self.gid = attrs["gid"]
67 self.payload = payload
69 def write_line(self) -> str:
70 line_list = [
71 str(self.path),
72 str(self.size),
73 f"{self.filetype.value}{self.mode}",
74 str(self.nlink),
75 str(self.uid),
76 str(self.gid),
77 str(self.rdev),
78 str(self.mtime),
79 str(self.payload),
80 str(self.content),
81 str(self.digest),
83 return " ".join(line_list)
86 def eprint(*args: Any, **kwargs: Any) -> None:
87 print(*args, **kwargs, file=sys.stderr)
90 def normalize_path(path: str) -> str:
91 return str("/" + os.path.normpath(path).lstrip("/"))
94 def leading_directories(path: str) -> list[str]:
95 """Return the leading directories of path
97 Given the path "alsa/conf.d/50-pipewire.conf", for example, this function
98 returns `[ "alsa", "alsa/conf.d" ]`.
99 """
100 parents = list(Path(path).parents)
101 parents.reverse()
102 # remove the implicit `.` from the start of a relative path or `/` from an
103 # absolute path
104 del parents[0]
105 return [str(i) for i in parents]
108 def add_leading_directories(
109 target: str, attrs: Attrs, paths: dict[str, ComposefsPath]
110 ) -> None:
111 """Add the leading directories of a target path to the composefs paths
113 mkcomposefs expects that all leading directories are explicitly listed in
114 the dump file. Given the path "alsa/conf.d/50-pipewire.conf", for example,
115 this function adds "alsa" and "alsa/conf.d" to the composefs paths.
117 path_components = leading_directories(target)
118 for component in path_components:
119 composefs_path = ComposefsPath(
120 attrs,
121 path=component,
122 size=4096,
123 filetype=FileType.directory,
124 mode="0755",
125 payload="-",
127 paths[component] = composefs_path
130 def main() -> None:
131 """Build a composefs dump from a Json config
133 This config describes the files that the final composefs image is supposed
134 to contain.
136 config_file = sys.argv[1]
137 if not config_file:
138 eprint("No config file was supplied.")
139 sys.exit(1)
141 with open(config_file, "rb") as f:
142 config = json.load(f)
144 if not config:
145 eprint("Config is empty.")
146 sys.exit(1)
148 eprint("Building composefs dump...")
150 paths: dict[str, ComposefsPath] = {}
151 for attrs in config:
152 # Normalize the target path to work around issues in how targets are
153 # declared in `environment.etc`.
154 attrs["target"] = normalize_path(attrs["target"])
156 target = attrs["target"]
157 source = attrs["source"]
158 mode = attrs["mode"]
160 if "*" in source: # Path with globbing
161 glob_sources = glob.glob(source)
162 for glob_source in glob_sources:
163 basename = os.path.basename(glob_source)
164 glob_target = f"{target}/{basename}"
166 composefs_path = ComposefsPath(
167 attrs,
168 path=glob_target,
169 size=100,
170 filetype=FileType.symlink,
171 mode="0777",
172 payload=glob_source,
175 paths[glob_target] = composefs_path
176 add_leading_directories(glob_target, attrs, paths)
177 else: # Without globbing
178 if mode == "symlink" or mode == "direct-symlink":
179 composefs_path = ComposefsPath(
180 attrs,
181 # A high approximation of the size of a symlink
182 size=100,
183 filetype=FileType.symlink,
184 mode="0777",
185 payload=source,
187 elif os.path.isdir(source):
188 composefs_path = ComposefsPath(
189 attrs,
190 size=4096,
191 filetype=FileType.directory,
192 mode=mode,
193 payload=source,
195 else:
196 composefs_path = ComposefsPath(
197 attrs,
198 size=os.stat(source).st_size,
199 filetype=FileType.file,
200 mode=mode,
201 # payload needs to be relative path in this case
202 payload=target.lstrip("/"),
204 paths[target] = composefs_path
205 add_leading_directories(target, attrs, paths)
207 composefs_dump = ["/ 4096 40755 1 0 0 0 0.0 - - -"] # Root directory
208 for key in sorted(paths):
209 composefs_path = paths[key]
210 eprint(composefs_path.path)
211 composefs_dump.append(composefs_path.write_line())
213 print("\n".join(composefs_dump))
216 if __name__ == "__main__":
217 main()