at 24.11-pre 6.2 kB view raw
1#!/usr/bin/env python3 2 3"""Build a composefs dump from a Json config 4 5See the man page of composefs-dump for details about the format: 6https://github.com/containers/composefs/blob/main/man/composefs-dump.md 7 8Ensure to check the file with the check script when you make changes to it: 9 10./check-build-composefs-dump.sh ./build-composefs_dump.py 11""" 12 13import glob 14import json 15import os 16import sys 17from enum import Enum 18from pathlib import Path 19from typing import Any 20 21Attrs = dict[str, Any] 22 23 24class FileType(Enum): 25 """The filetype as defined by the `st_mode` stat field in octal 26 27 You can check the st_mode stat field of a path in Python with 28 `oct(os.stat("/path/").st_mode)` 29 """ 30 31 directory = "4" 32 file = "10" 33 symlink = "12" 34 35 36class 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 = "-" 49 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, 58 ): 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 68 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), 82 ] 83 return " ".join(line_list) 84 85 86def eprint(*args: Any, **kwargs: Any) -> None: 87 print(*args, **kwargs, file=sys.stderr) 88 89 90def normalize_path(path: str) -> str: 91 return str("/" + os.path.normpath(path).lstrip("/")) 92 93 94def leading_directories(path: str) -> list[str]: 95 """Return the leading directories of path 96 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] 106 107 108def 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 112 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. 116 """ 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="-", 126 ) 127 paths[component] = composefs_path 128 129 130def main() -> None: 131 """Build a composefs dump from a Json config 132 133 This config describes the files that the final composefs image is supposed 134 to contain. 135 """ 136 config_file = sys.argv[1] 137 if not config_file: 138 eprint("No config file was supplied.") 139 sys.exit(1) 140 141 with open(config_file, "rb") as f: 142 config = json.load(f) 143 144 if not config: 145 eprint("Config is empty.") 146 sys.exit(1) 147 148 eprint("Building composefs dump...") 149 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"]) 155 156 target = attrs["target"] 157 source = attrs["source"] 158 mode = attrs["mode"] 159 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}" 165 166 composefs_path = ComposefsPath( 167 attrs, 168 path=glob_target, 169 size=100, 170 filetype=FileType.symlink, 171 mode="0777", 172 payload=glob_source, 173 ) 174 175 paths[glob_target] = composefs_path 176 add_leading_directories(glob_target, attrs, paths) 177 else: # Without globbing 178 if mode == "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, 186 ) 187 else: 188 if os.path.isdir(source): 189 composefs_path = ComposefsPath( 190 attrs, 191 size=4096, 192 filetype=FileType.directory, 193 mode=mode, 194 payload=source, 195 ) 196 else: 197 composefs_path = ComposefsPath( 198 attrs, 199 size=os.stat(source).st_size, 200 filetype=FileType.file, 201 mode=mode, 202 # payload needs to be relative path in this case 203 payload=target.lstrip("/"), 204 ) 205 paths[target] = composefs_path 206 add_leading_directories(target, attrs, paths) 207 208 composefs_dump = ["/ 4096 40755 1 0 0 0 0.0 - - -"] # Root directory 209 for key in sorted(paths): 210 composefs_path = paths[key] 211 eprint(composefs_path.path) 212 composefs_dump.append(composefs_path.write_line()) 213 214 print("\n".join(composefs_dump)) 215 216 217if __name__ == "__main__": 218 main()