nixos/etc: optionally mount etc as an overlay
This commit is contained in:
209
nixos/modules/system/etc/build-composefs-dump.py
Normal file
209
nixos/modules/system/etc/build-composefs-dump.py
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Build a composefs dump from a Json config
|
||||
|
||||
See the man page of composefs-dump for details about the format:
|
||||
https://github.com/containers/composefs/blob/main/man/composefs-dump.md
|
||||
|
||||
Ensure to check the file with the check script when you make changes to it:
|
||||
|
||||
./check-build-composefs-dump.sh ./build-composefs_dump.py
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
Attrs = dict[str, Any]
|
||||
|
||||
|
||||
class FileType(Enum):
|
||||
"""The filetype as defined by the `st_mode` stat field in octal
|
||||
|
||||
You can check the st_mode stat field of a path in Python with
|
||||
`oct(os.stat("/path/").st_mode)`
|
||||
"""
|
||||
|
||||
directory = "4"
|
||||
file = "10"
|
||||
symlink = "12"
|
||||
|
||||
|
||||
class ComposefsPath:
|
||||
path: str
|
||||
size: int
|
||||
filetype: FileType
|
||||
mode: str
|
||||
uid: str
|
||||
gid: str
|
||||
payload: str
|
||||
rdev: str = "0"
|
||||
nlink: int = 1
|
||||
mtime: str = "1.0"
|
||||
content: str = "-"
|
||||
digest: str = "-"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attrs: Attrs,
|
||||
size: int,
|
||||
filetype: FileType,
|
||||
mode: str,
|
||||
payload: str,
|
||||
path: str | None = None,
|
||||
):
|
||||
if path is None:
|
||||
path = attrs["target"]
|
||||
self.path = "/" + path
|
||||
self.size = size
|
||||
self.filetype = filetype
|
||||
self.mode = mode
|
||||
self.uid = attrs["uid"]
|
||||
self.gid = attrs["gid"]
|
||||
self.payload = payload
|
||||
|
||||
def write_line(self) -> str:
|
||||
line_list = [
|
||||
str(self.path),
|
||||
str(self.size),
|
||||
f"{self.filetype.value}{self.mode}",
|
||||
str(self.nlink),
|
||||
str(self.uid),
|
||||
str(self.gid),
|
||||
str(self.rdev),
|
||||
str(self.mtime),
|
||||
str(self.payload),
|
||||
str(self.content),
|
||||
str(self.digest),
|
||||
]
|
||||
return " ".join(line_list)
|
||||
|
||||
|
||||
def eprint(*args, **kwargs) -> None:
|
||||
print(args, **kwargs, file=sys.stderr)
|
||||
|
||||
|
||||
def leading_directories(path: str) -> list[str]:
|
||||
"""Return the leading directories of path
|
||||
|
||||
Given the path "alsa/conf.d/50-pipewire.conf", for example, this function
|
||||
returns `[ "alsa", "alsa/conf.d" ]`.
|
||||
"""
|
||||
parents = list(Path(path).parents)
|
||||
parents.reverse()
|
||||
# remove the implicit `.` from the start of a relative path or `/` from an
|
||||
# absolute path
|
||||
del parents[0]
|
||||
return [str(i) for i in parents]
|
||||
|
||||
|
||||
def add_leading_directories(
|
||||
target: str, attrs: Attrs, paths: dict[str, ComposefsPath]
|
||||
) -> None:
|
||||
"""Add the leading directories of a target path to the composefs paths
|
||||
|
||||
mkcomposefs expects that all leading directories are explicitly listed in
|
||||
the dump file. Given the path "alsa/conf.d/50-pipewire.conf", for example,
|
||||
this function adds "alsa" and "alsa/conf.d" to the composefs paths.
|
||||
"""
|
||||
path_components = leading_directories(target)
|
||||
for component in path_components:
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
path=component,
|
||||
size=4096,
|
||||
filetype=FileType.directory,
|
||||
mode="0755",
|
||||
payload="-",
|
||||
)
|
||||
paths[component] = composefs_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Build a composefs dump from a Json config
|
||||
|
||||
This config describes the files that the final composefs image is supposed
|
||||
to contain.
|
||||
"""
|
||||
config_file = sys.argv[1]
|
||||
if not config_file:
|
||||
eprint("No config file was supplied.")
|
||||
sys.exit(1)
|
||||
|
||||
with open(config_file, "rb") as f:
|
||||
config = json.load(f)
|
||||
|
||||
if not config:
|
||||
eprint("Config is empty.")
|
||||
sys.exit(1)
|
||||
|
||||
eprint("Building composefs dump...")
|
||||
|
||||
paths: dict[str, ComposefsPath] = {}
|
||||
for attrs in config:
|
||||
target = attrs["target"]
|
||||
source = attrs["source"]
|
||||
mode = attrs["mode"]
|
||||
|
||||
if "*" in source: # Path with globbing
|
||||
glob_sources = glob.glob(source)
|
||||
for glob_source in glob_sources:
|
||||
basename = os.path.basename(glob_source)
|
||||
glob_target = f"{target}/{basename}"
|
||||
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
path=glob_target,
|
||||
size=100,
|
||||
filetype=FileType.symlink,
|
||||
mode="0777",
|
||||
payload=glob_source,
|
||||
)
|
||||
|
||||
paths[glob_target] = composefs_path
|
||||
add_leading_directories(glob_target, attrs, paths)
|
||||
else: # Without globbing
|
||||
if mode == "symlink":
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
# A high approximation of the size of a symlink
|
||||
size=100,
|
||||
filetype=FileType.symlink,
|
||||
mode="0777",
|
||||
payload=source,
|
||||
)
|
||||
else:
|
||||
if os.path.isdir(source):
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
size=4096,
|
||||
filetype=FileType.directory,
|
||||
mode=mode,
|
||||
payload=source,
|
||||
)
|
||||
else:
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
size=os.stat(source).st_size,
|
||||
filetype=FileType.file,
|
||||
mode=mode,
|
||||
payload=target,
|
||||
)
|
||||
paths[target] = composefs_path
|
||||
add_leading_directories(target, attrs, paths)
|
||||
|
||||
composefs_dump = ["/ 4096 40755 1 0 0 0 0.0 - - -"] # Root directory
|
||||
for key in sorted(paths):
|
||||
composefs_path = paths[key]
|
||||
eprint(composefs_path.path)
|
||||
composefs_dump.append(composefs_path.write_line())
|
||||
|
||||
print("\n".join(composefs_dump))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user