Skip to content

Commit

Permalink
normalize mtime
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
katexochen and malt3 committed Aug 31, 2023
1 parent ce8396e commit 4ccad9e
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 1 deletion.
16 changes: 16 additions & 0 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
35 changes: 34 additions & 1 deletion mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")):
Expand Down Expand Up @@ -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)}
Expand Down
16 changes: 16 additions & 0 deletions mkosi/resources/mkosi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.

Expand Down

0 comments on commit 4ccad9e

Please sign in to comment.