]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
normalize mtime
authorPaul Meyer <49727155+katexochen@users.noreply.github.com>
Fri, 25 Aug 2023 14:53:08 +0000 (16:53 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 31 Aug 2023 08:56:26 +0000 (10:56 +0200)
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 <mp@edgeless.systems>
mkosi/__init__.py
mkosi/config.py
mkosi/resources/mkosi.md

index e64caf925085835e03f7868b5bb7b544f1e398dc..ece33ea4d1a29c34782543bf0885cc4c421abc7e 100644 (file)
@@ -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)
index f60a642e0052984a0964408d467c0b77d4da8b95..d36631254973226646b07ba13973389ae33ea0ee 100644 (file)
@@ -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)}
index 92e318c5e3d3ba30c83a30657026ab4e2bb0b55e..58caa75877c6c7bb586aabdb72dfb71cdff71284 100644 (file)
@@ -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.