From 130c31f821250b570517a4176e941bd68df6de53 Mon Sep 17 00:00:00 2001 From: Jarrett Tierney Date: Mon, 21 Sep 2026 21:24:00 +0000 Subject: [PATCH 1/7] feat(buildsys): thread BUILD_ID_TIMESTAMP through kit builds Signed-off-by: Jarrett Tierney --- tools/buildsys/src/args.rs | 5 +++++ tools/buildsys/src/builder.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tools/buildsys/src/args.rs b/tools/buildsys/src/args.rs index dad7cb4e6..e0fe6f1f9 100644 --- a/tools/buildsys/src/args.rs +++ b/tools/buildsys/src/args.rs @@ -149,6 +149,11 @@ pub(crate) struct BuildKitArgs { #[arg(long, env = "BUILDSYS_VERSION_BUILD")] pub(crate) version_build: String, + /// Unix seconds of the latest project commit. Forwarded to the buildkit + /// sandbox as `BUILD_ID_TIMESTAMP` for reproducible artifact timestamps. + #[arg(long, env = "BUILDSYS_VERSION_BUILD_TIMESTAMP")] + pub(crate) version_build_timestamp: String, + /// Version number for the workspace #[arg(long, env = "BUILDSYS_VERSION_IMAGE")] pub(crate) version_image: String, diff --git a/tools/buildsys/src/builder.rs b/tools/buildsys/src/builder.rs index 8c00cfb5d..7e88104cb 100644 --- a/tools/buildsys/src/builder.rs +++ b/tools/buildsys/src/builder.rs @@ -180,6 +180,7 @@ impl KitBuildArgs { args.build_arg("KIT", &self.kit); args.build_arg("PACKAGE_DEPENDENCIES", self.package_dependencies.join(" ")); args.build_arg("BUILD_ID", &self.version_build); + args.build_arg("BUILD_ID_TIMESTAMP", &self.version_build_timestamp); args.build_arg("VERSION_ID", &self.version_id); args.build_arg("EXTERNAL_KIT_METADATA", &self.external_kit_metadata); args.build_arg("VENDOR", &self.vendor); @@ -195,6 +196,9 @@ struct KitBuildArgs { local_kits: Vec, vendor: String, version_build: String, + /// Unix seconds of the latest project commit. Forwarded as + /// `BUILD_ID_TIMESTAMP` for reproducible kit OCI timestamps. + version_build_timestamp: String, version_id: String, } @@ -503,6 +507,7 @@ impl DockerBuild { external_kit_metadata: EXTERNAL_KIT_METADATA.into(), package_dependencies: manifest.package_dependencies().context(error::GraphSnafu)?, version_build: args.version_build, + version_build_timestamp: args.version_build_timestamp, version_id: args.version_image, }), secrets_args: Vec::new(), From 07443348ee9a9cf0d34b941fbd9532c1c1d6277d Mon Sep 17 00:00:00 2001 From: Jarrett Tierney Date: Mon, 21 Sep 2026 21:24:49 +0000 Subject: [PATCH 2/7] feat(dockerfile): plumb BUILD_ID_TIMESTAMP through kit and repo stages Signed-off-by: Jarrett Tierney --- twoliter/embedded/build.Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/twoliter/embedded/build.Dockerfile b/twoliter/embedded/build.Dockerfile index 42555a86b..f53463afa 100644 --- a/twoliter/embedded/build.Dockerfile +++ b/twoliter/embedded/build.Dockerfile @@ -178,6 +178,8 @@ ARG PACKAGE_DEPENDENCIES ARG ARCH ARG NOCACHE ARG BUILD_ID +# Consumed by rpm2kit for the kit's OCI `.created` timestamp. +ARG BUILD_ID_TIMESTAMP ARG VERSION_ID ARG EXTERNAL_KIT_METADATA ARG VENDOR @@ -185,6 +187,7 @@ ARG LOCAL_KIT_DEPENDENCIES ARG BYPASS_SOCKET ARG OUTPUT_SOCKET ARG BUILDER_UID +ENV BUILD_ID_TIMESTAMP=${BUILD_ID_TIMESTAMP} WORKDIR /home/builder USER root @@ -273,6 +276,9 @@ ARG KIT_DEPENDENCIES ARG EXTERNAL_KIT_DEPENDENCIES ARG ARCH ARG NOCACHE +# Forwarded to rpmbuild as SOURCE_DATE_EPOCH for the metadata RPM. +ARG BUILD_ID_TIMESTAMP +ENV BUILD_ID_TIMESTAMP=${BUILD_ID_TIMESTAMP} WORKDIR /home/builder USER builder @@ -526,6 +532,9 @@ ARG NOCACHE ARG VARIANT ARG VARIANT_NAME=${VARIANT} ENV VARIANT=${VARIANT_NAME} VERSION_ID=${VERSION_ID} BUILD_ID=${BUILD_ID} +# Forwarded for consistency; rpm2kmodkit emits no timestamped artifacts today. +ARG BUILD_ID_TIMESTAMP +ENV BUILD_ID_TIMESTAMP=${BUILD_ID_TIMESTAMP} ARG BYPASS_SOCKET ARG OUTPUT_SOCKET ARG BUILDER_UID From e51c4273a2a9bc261316d41230df1ab4e67ce47e Mon Sep 17 00:00:00 2001 From: Jarrett Tierney Date: Mon, 21 Sep 2026 21:25:24 +0000 Subject: [PATCH 3/7] fix(rpm2kit): derive OCI created timestamp from BUILD_ID_TIMESTAMP Signed-off-by: Jarrett Tierney --- twoliter/embedded/rpm2kit | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/twoliter/embedded/rpm2kit b/twoliter/embedded/rpm2kit index d54b25a3a..4071c0342 100755 --- a/twoliter/embedded/rpm2kit +++ b/twoliter/embedded/rpm2kit @@ -50,7 +50,13 @@ cleanup() { } trap 'cleanup' EXIT -TIMESTAMP="$(date +"%FT%T.%NZ")" +# Written to the kit's OCI `.created` field and index +# `org.opencontainers.image.created` annotation. Derived from +# BUILD_ID_TIMESTAMP to keep kit metadata reproducible. +: "${BUILD_ID_TIMESTAMP:?BUILD_ID_TIMESTAMP required for reproducible kit metadata}" +[[ "${BUILD_ID_TIMESTAMP}" =~ ^[0-9]+$ ]] || \ + { echo "BUILD_ID_TIMESTAMP must be numeric, got '${BUILD_ID_TIMESTAMP}'" >&2; exit 1; } +TIMESTAMP="$(date -u -d "@${BUILD_ID_TIMESTAMP}" +%Y-%m-%dT%H:%M:%SZ)" FILENAME_PREFIX="${KIT:?}-v${VERSION_ID:?}-${BUILD_ID:?}-${ARCH:?}" # Translate ARCH into the proper docker arch case "${ARCH}" in From 5ac53422783ab7d79e9800552603fe02dd908f0c Mon Sep 17 00:00:00 2001 From: Jarrett Tierney Date: Mon, 21 Sep 2026 21:25:34 +0000 Subject: [PATCH 4/7] fix(imghelper): derive InstalledTime from BUILD_ID_TIMESTAMP Signed-off-by: Jarrett Tierney --- twoliter/embedded/rpm2eif | 7 ++++++- twoliter/embedded/rpm2img | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/twoliter/embedded/rpm2eif b/twoliter/embedded/rpm2eif index 33aa493d3..b642cc17e 100755 --- a/twoliter/embedded/rpm2eif +++ b/twoliter/embedded/rpm2eif @@ -194,7 +194,12 @@ cleanup() { trap cleanup EXIT echo "=== Installing RPMs ===" -INSTALL_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +# Written to `InstalledTime` in application-inventory.json. Derived from +# BUILD_ID_TIMESTAMP to keep the inventory reproducible. +: "${BUILD_ID_TIMESTAMP:?BUILD_ID_TIMESTAMP required for reproducible inventory}" +[[ "${BUILD_ID_TIMESTAMP}" =~ ^[0-9]+$ ]] || \ + { echo "BUILD_ID_TIMESTAMP must be numeric, got '${BUILD_ID_TIMESTAMP}'" >&2; exit 1; } +INSTALL_TIME="$(date -u -d "@${BUILD_ID_TIMESTAMP}" +%Y-%m-%dT%H:%M:%SZ)" rpm -iv --ignorearch --root "${INSTALL_ROOT}" \ "${PACKAGE_DIR}"/*.rpm diff --git a/twoliter/embedded/rpm2img b/twoliter/embedded/rpm2img index 1aa6c1357..92e60a9be 100755 --- a/twoliter/embedded/rpm2img +++ b/twoliter/embedded/rpm2img @@ -238,7 +238,12 @@ if [[ "${STANDALONE_IMAGE}" == "no" && "${PARTITION_PLAN}" == "split" ]]; then --sort --print "${DATA_IMAGE}" fi -INSTALL_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +# Written to `InstalledTime` in application-inventory.json. Derived from +# BUILD_ID_TIMESTAMP to keep the inventory reproducible. +: "${BUILD_ID_TIMESTAMP:?BUILD_ID_TIMESTAMP required for reproducible inventory}" +[[ "${BUILD_ID_TIMESTAMP}" =~ ^[0-9]+$ ]] || \ + { echo "BUILD_ID_TIMESTAMP must be numeric, got '${BUILD_ID_TIMESTAMP}'" >&2; exit 1; } +INSTALL_TIME="$(date -u -d "@${BUILD_ID_TIMESTAMP}" +%Y-%m-%dT%H:%M:%SZ)" rpm -iv --ignorearch --root "${ROOT_MOUNT}" "${PACKAGE_DIR}"/*.rpm # Embed any declared guest variant images into the host rootfs. Each entry has the form From 9fd5835a469d5fc5d99e18b7b990612c10b7a802 Mon Sep 17 00:00:00 2001 From: Jarrett Tierney Date: Mon, 21 Sep 2026 21:25:51 +0000 Subject: [PATCH 5/7] feat(dockerfile): export SOURCE_DATE_EPOCH for reproducible RPMs Signed-off-by: Jarrett Tierney --- twoliter/embedded/build.Dockerfile | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/twoliter/embedded/build.Dockerfile b/twoliter/embedded/build.Dockerfile index f53463afa..92dd9c18e 100644 --- a/twoliter/embedded/build.Dockerfile +++ b/twoliter/embedded/build.Dockerfile @@ -49,6 +49,10 @@ ARG BUILD_ID ARG BUILD_ID_TIMESTAMP ENV BUILD_ID=${BUILD_ID} ENV BUILD_ID_TIMESTAMP=${BUILD_ID_TIMESTAMP} +# Mirror BUILD_ID_TIMESTAMP into SOURCE_DATE_EPOCH so rpm clamps payload/gzip +# mtimes. `BUILDTIME` is not derived from it here; the rpmbuild calls below +# also pass `--define '_buildtime ...'` to force a reproducible header. +ENV SOURCE_DATE_EPOCH=${BUILD_ID_TIMESTAMP} WORKDIR /home/builder USER builder @@ -133,6 +137,13 @@ RUN --mount=source=.cargo/twoliter_cargo_config.toml,target=/home/builder/rpmbui --mount=type=cache,target=/home/builder/.cache,from=cache,source=/cache \ --mount=source=sources,target=/home/builder/rpmbuild/BUILD/sources \ --mount=target=/host \ + # Require a numeric BUILD_ID_TIMESTAMP; an empty value silently disables + # rpm's mtime clamp and buildtime override. + if [[ -z "${BUILD_ID_TIMESTAMP:-}" ]] || \ + ! [[ "${BUILD_ID_TIMESTAMP}" =~ ^[0-9]+$ ]]; then \ + echo "BUILD_ID_TIMESTAMP must be a Unix seconds value for reproducible RPMs, got '${BUILD_ID_TIMESTAMP:-}'" >&2; \ + exit 1; \ + fi && \ # The dist tag is set as the `Release` field in Bottlerocket RPMs. Define it to be # in the form ..br1 # Remove '-dirty' from the commit sha: '-' is an illegal character for the Release field @@ -141,6 +152,7 @@ RUN --mount=source=.cargo/twoliter_cargo_config.toml,target=/home/builder/rpmbui rpmbuild -bb --clean \ --undefine _auto_set_build_flags \ --define "_target_cpu ${ARCH}" \ + --define "_buildtime ${BUILD_ID_TIMESTAMP}" \ --define "dist .${BUILD_ID_TIMESTAMP}.${BUILD_ID//-dirty/}.br1" \ rpmbuild/SPECS/${PACKAGE}.spec @@ -276,20 +288,29 @@ ARG KIT_DEPENDENCIES ARG EXTERNAL_KIT_DEPENDENCIES ARG ARCH ARG NOCACHE -# Forwarded to rpmbuild as SOURCE_DATE_EPOCH for the metadata RPM. +# Forwarded to rpmbuild as SOURCE_DATE_EPOCH for mtime clamping and as +# `_buildtime` for the RPM BUILDTIME header. ARG BUILD_ID_TIMESTAMP ENV BUILD_ID_TIMESTAMP=${BUILD_ID_TIMESTAMP} +ENV SOURCE_DATE_EPOCH=${BUILD_ID_TIMESTAMP} WORKDIR /home/builder USER builder # Build the metadata RPM for the variant. RUN --mount=target=/host \ - cat "/usr/lib/rpm/platform/${ARCH}-bottlerocket/macros" generated.rpmmacros > .rpmmacros \ + # Require a numeric BUILD_ID_TIMESTAMP so SOURCE_DATE_EPOCH reaches rpm. + if [[ -z "${BUILD_ID_TIMESTAMP:-}" ]] || \ + ! [[ "${BUILD_ID_TIMESTAMP}" =~ ^[0-9]+$ ]]; then \ + echo "BUILD_ID_TIMESTAMP must be a Unix seconds value for reproducible RPMs, got '${BUILD_ID_TIMESTAMP:-}'" >&2; \ + exit 1; \ + fi \ + && cat "/usr/lib/rpm/platform/${ARCH}-bottlerocket/macros" generated.rpmmacros > .rpmmacros \ && cat generated.bconds /host/build/tools/metadata.spec >> rpmbuild/SPECS/metadata.spec \ && rpmbuild -ba --clean \ --undefine _auto_set_build_flags \ --define "_target_cpu ${ARCH}" \ + --define "_buildtime ${BUILD_ID_TIMESTAMP}" \ rpmbuild/SPECS/metadata.spec \ && rpm -qp --provides rpmbuild/RPMS/${ARCH}/bottlerocket-metadata-*.${ARCH}.rpm \ && echo ${NOCACHE} From fd9d759c39f39e4379e1e71afae0b4198e8d73f1 Mon Sep 17 00:00:00 2001 From: Jarrett Tierney Date: Mon, 21 Sep 2026 21:26:55 +0000 Subject: [PATCH 6/7] fix(embedded-bundle): normalize tar headers for reproducible tools tarball Signed-off-by: Jarrett Tierney --- twoliter/src/tool-crates/embedded-bundle/build.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/twoliter/src/tool-crates/embedded-bundle/build.rs b/twoliter/src/tool-crates/embedded-bundle/build.rs index 8ba396159..33ead344b 100644 --- a/twoliter/src/tool-crates/embedded-bundle/build.rs +++ b/twoliter/src/tool-crates/embedded-bundle/build.rs @@ -53,6 +53,9 @@ fn main() { let mut buf_writer = Vec::new().writer(); let enc = ZlibEncoder::new(&mut buf_writer, Compression::default()); let mut tar = tar::Builder::new(enc); + // Zero mtimes/uids/gids and mask modes so the embedded tarball is + // byte-identical across builds of the same source tree. + tar.mode(tar::HeaderMode::Deterministic); tar.append_dir_all("", &paths.prep_dir).unwrap(); // Drop tar object to ensure any finalizing steps are done. From ba161bdae38f1fbca6a6b3dc55e04ac653d652a5 Mon Sep 17 00:00:00 2001 From: Jarrett Tierney Date: Mon, 21 Sep 2026 21:27:16 +0000 Subject: [PATCH 7/7] docs(buildsys): document BUILD_ID_TIMESTAMP consumers Signed-off-by: Jarrett Tierney --- twoliter/embedded/Makefile.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/twoliter/embedded/Makefile.toml b/twoliter/embedded/Makefile.toml index 8b4c4fbec..6696bf95a 100644 --- a/twoliter/embedded/Makefile.toml +++ b/twoliter/embedded/Makefile.toml @@ -25,8 +25,10 @@ BUILDSYS_METADATA_DIR = "${BUILDSYS_BUILD_DIR}/metadata" BUILDSYS_CARGO_METADATA_PATH = "${BUILDSYS_METADATA_DIR}/cargo_metadata.json" BUILDSYS_SBKEYS_PROFILE = { script = ['echo "${BUILDSYS_SBKEYS_PROFILE:-local}"'] } BUILDSYS_VERSION_BUILD = { script = ["git describe --always --dirty --exclude '*' --abbrev=8 || echo 00000000"] } -# The unix timestamp in ms of the latest commit of the project. -# This is an input for setting the Release value of a package. +# Unix seconds of the latest project commit. Threaded through buildsys and the +# Dockerfile as `BUILD_ID_TIMESTAMP`; used as the source of truth for the RPM +# `Release` and `BUILDTIME`, `SOURCE_DATE_EPOCH`, kit OCI `.created`, and +# variant `InstalledTime` fields to keep build artifacts reproducible. BUILDSYS_VERSION_BUILD_TIMESTAMP = { script = ["git show -s --format=%ct HEAD || echo 0000000000"] } # For now, release config path can't be overridden with -e, because it's used # later in this section. You have to edit the path here in Makefile.toml to