From: Paul Meyer <49727155+katexochen@users.noreply.github.com> Date: Fri, 25 Aug 2023 14:53:08 +0000 (+0200) Subject: normalize mtime X-Git-Tag: v16~28 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bdf37e11d127b31e49272f60085d735dbfe41cca;p=thirdparty%2Fmkosi.git normalize mtime If set, the time stamp from SOURCE_DATE_EPOCH is used to normalize mtime of files. We also need to pass the environment trough when mkosi is invoking itself. Co-authored-by: Malte Poll --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index e64caf925..ece33ea4d 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -934,6 +934,8 @@ def build_initrd(state: MkosiState) -> Path: "--make-initrd", "yes", "--bootable", "no", "--manifest-format", "", + *(["--source-date-epoch", str(state.config.source_date_epoch)] + if state.config.source_date_epoch is not None else []), *(["--locale", state.config.locale] if state.config.locale else []), *(["--locale-messages", state.config.locale_messages] if state.config.locale_messages else []), *(["--keymap", state.config.keymap] if state.config.keymap else []), @@ -1813,10 +1815,13 @@ def build_image(args: MkosiArgs, config: MkosiConfig) -> None: run_selinux_relabel(state) run_finalize_script(state) + normalize_mtime(state.root, state.config.source_date_epoch) partitions = make_image(state, skip=("esp", "xbootldr")) install_unified_kernel(state, partitions) prepare_grub_efi(state) prepare_grub_bios(state, partitions) + normalize_mtime(state.root, state.config.source_date_epoch, directory=Path("boot")) + normalize_mtime(state.root, state.config.source_date_epoch, directory=Path("efi")) partitions = make_image(state) install_grub_bios(state, partitions) make_image(state, split=True) @@ -2255,3 +2260,14 @@ def run_verb(args: MkosiArgs, presets: Sequence[MkosiConfig]) -> None: if args.verb == Verb.serve: run_serve(last) + + +def normalize_mtime(root: Path, mtime: Optional[int], directory: Optional[Path] = None) -> None: + directory = directory or Path("") + if mtime is None: + return + + with complete_step(f"Normalizing modification times of /{directory}"): + os.utime(root / directory, (mtime, mtime), follow_symlinks=False) + for p in (root / directory).rglob("*"): + os.utime(p, (mtime, mtime), follow_symlinks=False) diff --git a/mkosi/config.py b/mkosi/config.py index f60a642e0..d36631254 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -249,6 +249,7 @@ def config_parse_compression(value: Optional[str], old: Optional[Compression]) - except KeyError: return Compression.zst if parse_boolean(value) else Compression.none + def config_parse_seed(value: Optional[str], old: Optional[str]) -> Optional[uuid.UUID]: if not value or value == "random": return None @@ -259,6 +260,18 @@ def config_parse_seed(value: Optional[str], old: Optional[str]) -> Optional[uuid die(f"{value} is not a valid UUID") +def config_parse_source_date_epoch(value: Optional[str], old: Optional[int]) -> Optional[int]: + if value is None: + return None + + try: + timestamp = int(value) + except ValueError: + raise ValueError(f"{value} is not a valid timestamp") + if timestamp < 0: + raise ValueError(f"{value} is negative") + return timestamp + def config_default_compression(namespace: argparse.Namespace) -> Compression: if namespace.output_format == OutputFormat.cpio: @@ -310,6 +323,13 @@ def config_default_mirror(namespace: argparse.Namespace) -> Optional[str]: return None +def config_default_source_date_epoch(namespace: argparse.Namespace) -> Optional[int]: + for env in namespace.environment: + if env.startswith("SOURCE_DATE_EPOCH="): + return config_parse_source_date_epoch(env.removeprefix("SOURCE_DATE_EPOCH="), None) + return config_parse_source_date_epoch(os.environ.get("SOURCE_DATE_EPOCH"), None) + + def make_enum_parser(type: Type[enum.Enum]) -> Callable[[str], enum.Enum]: def parse_enum(value: str) -> enum.Enum: try: @@ -649,6 +669,7 @@ class MkosiConfig: remove_packages: list[str] remove_files: list[str] clean_package_metadata: ConfigFeature + source_date_epoch: Optional[int] prepare_script: Optional[Path] build_script: Optional[Path] @@ -1000,7 +1021,7 @@ class MkosiConfigParser: parse=config_parse_feature, help="Use btrfs subvolumes for faster directory operations where possible", ), - MkosiConfigSetting( + MkosiConfigSetting( dest="seed", metavar="UUID", section="Output", @@ -1093,6 +1114,15 @@ class MkosiConfigParser: parse=config_parse_feature, help="Remove package manager database and other files", ), + MkosiConfigSetting( + dest="source_date_epoch", + metavar="TIMESTAMP", + section="Content", + parse=config_parse_source_date_epoch, + default_factory=config_default_source_date_epoch, + default_factory_depends=("environment",), + help="Set the $SOURCE_DATE_EPOCH timestamp", + ), MkosiConfigSetting( dest="prepare_script", metavar="PATH", @@ -2055,6 +2085,8 @@ def load_environment(args: argparse.Namespace) -> dict[str, str]: env["IMAGE_ID"] = args.image_id if args.image_version is not None: env["IMAGE_VERSION"] = args.image_version + if args.source_date_epoch is not None: + env["SOURCE_DATE_EPOCH"] = str(args.source_date_epoch) if (proxy := os.environ.get("http_proxy")): env["http_proxy"] = proxy if (proxy := os.environ.get("https_proxy")): @@ -2236,6 +2268,7 @@ def summary(args: MkosiArgs, config: MkosiConfig) -> str: Remove Packages: {line_join_list(config.remove_packages)} Remove Files: {line_join_list(config.remove_files)} Clean Package Manager Metadata: {yes_no_auto(config.clean_package_metadata)} + Source Date Epoch: {none_to_none(config.source_date_epoch)} Prepare Script: {none_to_none(config.prepare_script)} Build Script: {none_to_none(config.build_script)} diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 92e318c5e..58caa7587 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -577,6 +577,16 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, builds, where deterministic UUIDs and other partition metadata should be derived on each build. +`SourceDateEpoch=`, `--source-date-epoch=` + +: Takes a timestamp as argument. Resets file modification times of all files to + this timestamp. The variable is also propagated to systemd-repart and + scripts executed by mkosi. If not set explicitly, `SOURCE_DATE_EPOCH` from + `--environment` and from the host environment are tried in that order. + This is useful to make builds reproducible. See + [SOURCE_DATE_EPOCH](https://reproducible-builds.org/specs/source-date-epoch/) + for more information. + ### [Content] Section `Packages=`, `--package=`, `-p` @@ -1276,6 +1286,12 @@ Scripts executed by mkosi receive the following environment variables: The build script should avoid any network communication in case `$WITH_NETWORK` is `0`. +* `$SOURCE_DATE_EPOCH` is defined if requested (`SourceDateEpoch=TIMESTAMP`, + `Environment=SOURCE_DATE_EPOCH=TIMESTAMP` or the host environment variable + `$SOURCE_DATE_EPOCH`). This is useful to make builds reproducible. See + [SOURCE_DATE_EPOCH](https://reproducible-builds.org/specs/source-date-epoch/) + for more information. + Additionally, when a script is executed, a few scripts are made available via `$PATH` to simplify common usecases.