Skip to content

Commit 81f0d5a

Browse files
committed
build-sys: Various improvements
Pass SOURCE_DATE_EPOCH from git commit timestamp through to rpmbuild, enabling bit-for-bit reproducible RPM builds. This is useful for verification and caching. Then fix the idempotency of the default `just build` to ensure we're not incorrectly invalidating caches. Add `just check-buildsys` command that builds packages twice and verifies checksums match, confirming reproducibility. The CI package job now uses this to catch regressions. Assisted-by: OpenCode (Opus 4.5) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 64ad5c5 commit 81f0d5a

File tree

9 files changed

+134
-41
lines changed

9 files changed

+134
-41
lines changed

.dockerignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,5 @@
2121
# Workaround for podman bug with secrets + remote
2222
# https://github.com/containers/podman/issues/25314
2323
!podman-build-secret*
24-
# Pre-built packages for builds that use them
25-
!target/packages/
2624
# And finally of course all the Rust sources
2725
!crates/

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ jobs:
134134
BASE=$(just pullspec-for-os base ${{ matrix.test_os }})
135135
echo "BOOTC_base=${BASE}" >> $GITHUB_ENV
136136
137-
- name: Build packages
138-
run: just package
137+
- name: Build packages (and verify build system)
138+
run: just check-buildsys
139139

140140
- name: Upload package artifacts
141141
uses: actions/upload-artifact@v6

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ COPY . /src
1515
FROM scratch as packaging
1616
COPY contrib/packaging /
1717

18-
# This image captures pre-built packages from the context.
19-
# By COPYing into a stage, we avoid SELinux issues with context bind mounts.
20-
FROM scratch as packages
21-
COPY target/packages/*.rpm /
22-
2318
FROM $base as base
2419
# Mark this as a test image (moved from --label build flag to fix layer caching)
2520
LABEL bootc.testimage="1"
@@ -51,6 +46,9 @@ RUN /src/contrib/packaging/configure-systemdboot download
5146
FROM buildroot as build
5247
# Version for RPM build (optional, computed from git in Justfile)
5348
ARG pkgversion
49+
# For reproducible builds, SOURCE_DATE_EPOCH must be exported as ENV for rpmbuild to see it
50+
ARG SOURCE_DATE_EPOCH
51+
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
5452
# Build RPM directly from source, using cached target directory
5553
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm
5654

@@ -71,27 +69,33 @@ ENV TMPDIR=/var/tmp
7169
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none make install-unit-tests
7270

7371
# This just does syntax checking
74-
FROM build as validate
72+
FROM buildroot as validate
7573
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none make validate
7674

77-
# The final image that derives from the original base and adds the release binaries
78-
FROM base
79-
# See the Justfile for possible variants
75+
# Common base for final images: configures variant, rootfs, and injects extra content
76+
FROM base as final-common
8077
ARG variant
8178
RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging \
8279
--mount=type=bind,from=sdboot-content,target=/run/sdboot-content \
8380
--mount=type=bind,from=sdboot-signed,target=/run/sdboot-signed \
8481
/run/packaging/configure-variant "${variant}"
85-
# Support overriding the rootfs at build time conveniently
86-
ARG rootfs
82+
ARG rootfs=""
8783
RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-rootfs "${variant}" "${rootfs}"
88-
# Inject additional content
8984
COPY --from=packaging /usr-extras/ /usr/
90-
# Install packages from the packages stage
91-
# Using bind from a stage avoids SELinux issues with context bind mounts
85+
86+
# Default target for source builds (just build)
87+
# Installs packages from the internal build stage
88+
FROM final-common as final
89+
RUN --mount=type=bind,from=packaging,target=/run/packaging \
90+
--mount=type=bind,from=build,target=/build-output \
91+
--network=none \
92+
/run/packaging/install-rpm-and-setup /build-output/out
93+
RUN bootc container lint --fatal-warnings
94+
95+
# Alternative target for pre-built packages (CI workflow)
96+
# Use with: podman build --target=final-from-packages -v path/to/packages:/run/packages:ro
97+
FROM final-common as final-from-packages
9298
RUN --mount=type=bind,from=packaging,target=/run/packaging \
93-
--mount=type=bind,from=packages,target=/build-packages \
9499
--network=none \
95-
/run/packaging/install-rpm-and-setup /build-packages
96-
# Finally, testour own linting
100+
/run/packaging/install-rpm-and-setup /run/packages
97101
RUN bootc container lint --fatal-warnings

Justfile

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,39 @@ buildargs := base_buildargs + " --secret=id=secureboot_key,src=target/test-secur
4343
# Args for build-sealed (no base arg, it sets that itself)
4444
sealed_buildargs := "--build-arg=variant=" + variant + " --secret=id=secureboot_key,src=target/test-secureboot/db.key --secret=id=secureboot_cert,src=target/test-secureboot/db.crt"
4545

46+
# Compute SOURCE_DATE_EPOCH and VERSION from git for reproducible builds.
47+
# Outputs shell variable assignments that can be eval'd.
48+
_git-build-vars:
49+
#!/bin/bash
50+
set -euo pipefail
51+
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
52+
# Compute version from git (matching xtask.rs gitrev logic)
53+
if VERSION=$(git describe --tags --exact-match 2>/dev/null); then
54+
VERSION="${VERSION#v}"
55+
VERSION="${VERSION//-/.}"
56+
else
57+
COMMIT=$(git rev-parse HEAD | cut -c1-10)
58+
COMMIT_TS=$(git show -s --format=%ct)
59+
TIMESTAMP=$(date -u -d @${COMMIT_TS} +%Y%m%d%H%M)
60+
VERSION="${TIMESTAMP}.g${COMMIT}"
61+
fi
62+
echo "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}"
63+
echo "VERSION=${VERSION}"
64+
4665
# The default target: build the container image from current sources.
4766
# Note commonly you might want to override the base image via e.g.
4867
# `just build --build-arg=base=quay.io/fedora/fedora-bootc:42`
49-
build: package _keygen
50-
podman build {{base_buildargs}} -t {{base_img}}-bin {{buildargs}} .
68+
#
69+
# The Dockerfile builds RPMs internally in its 'build' stage, so we don't need
70+
# to call 'package' first. This avoids cache invalidation from external files.
71+
build: _keygen
72+
#!/bin/bash
73+
set -xeuo pipefail
74+
eval $(just _git-build-vars)
75+
podman build {{base_buildargs}} --target=final \
76+
--build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} \
77+
--build-arg=pkgversion=${VERSION} \
78+
-t {{base_img}}-bin {{buildargs}} .
5179
./hack/build-sealed {{variant}} {{base_img}}-bin {{base_img}} {{sealed_buildargs}}
5280

5381
# Generate Secure Boot keys (only for our own CI/testing)
@@ -62,18 +90,9 @@ build-sealed:
6290
_packagecontainer:
6391
#!/bin/bash
6492
set -xeuo pipefail
65-
# Compute version from git (matching xtask.rs gitrev logic)
66-
if VERSION=$(git describe --tags --exact-match 2>/dev/null); then
67-
VERSION="${VERSION#v}"
68-
VERSION="${VERSION//-/.}"
69-
else
70-
COMMIT=$(git rev-parse HEAD | cut -c1-10)
71-
COMMIT_TS=$(git show -s --format=%ct)
72-
TIMESTAMP=$(date -u -d @${COMMIT_TS} +%Y%m%d%H%M)
73-
VERSION="${TIMESTAMP}.g${COMMIT}"
74-
fi
93+
eval $(just _git-build-vars)
7594
echo "Building RPM with version: ${VERSION}"
76-
podman build {{base_buildargs}} --build-arg=pkgversion=${VERSION} -t localhost/bootc-pkg --target=build .
95+
podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} -t localhost/bootc-pkg --target=build .
7796

7897
# Build packages (e.g. RPM) into target/packages/
7998
# Any old packages will be removed.
@@ -86,7 +105,8 @@ package: _packagecontainer
86105
podman rmi localhost/bootc-pkg
87106

88107
# Copy pre-existing packages from PATH into target/packages/
89-
# Used to prepare for building with pre-built packages
108+
# Note: This is mainly for CI artifact extraction; build-from-package
109+
# now uses volume mounts directly instead of copying to target/packages/.
90110
copy-packages-from PATH:
91111
#!/bin/bash
92112
set -xeuo pipefail
@@ -101,11 +121,15 @@ copy-packages-from PATH:
101121
chmod a+r target/packages/*.rpm
102122

103123
# Build the container image using pre-existing packages from PATH
104-
# Note: The Dockerfile reads from target/packages/, so copy the packages there first
105-
# if they're in a different location.
124+
# Uses the 'final-from-packages' target with a volume mount to inject packages,
125+
# avoiding Docker context cache invalidation issues.
106126
build-from-package PATH: _keygen
107-
@if [ "{{PATH}}" != "target/packages" ]; then just copy-packages-from {{PATH}}; fi
108-
podman build {{base_buildargs}} -t {{base_img}}-bin {{buildargs}} .
127+
#!/bin/bash
128+
set -xeuo pipefail
129+
# Resolve to absolute path for podman volume mount
130+
# Use :z for SELinux relabeling
131+
pkg_path=$(realpath "{{PATH}}")
132+
podman build {{base_buildargs}} --target=final-from-packages -v "${pkg_path}":/run/packages:ro,z -t {{base_img}}-bin {{buildargs}} .
109133
./hack/build-sealed {{variant}} {{base_img}}-bin {{base_img}} {{sealed_buildargs}}
110134

111135
# Pull images used by hack/lbi
@@ -137,7 +161,10 @@ run-container-external-tests:
137161

138162
# We build the unit tests into a container image
139163
build-units:
140-
podman build {{base_buildargs}} --target units -t localhost/bootc-units .
164+
#!/bin/bash
165+
set -xeuo pipefail
166+
eval $(just _git-build-vars)
167+
podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} --target units -t localhost/bootc-units .
141168

142169
# Perform validation (build, linting) in a container build environment
143170
validate:
@@ -209,3 +236,10 @@ mdbook-serve: build-mdbook
209236
# Use this after adding, removing, or modifying CLI options or schemas.
210237
update-generated:
211238
cargo run -p xtask update-generated
239+
240+
# Verify build system properties (reproducible builds)
241+
#
242+
# This runs `just package` twice and verifies that the resulting RPMs
243+
# are bit-for-bit identical, confirming SOURCE_DATE_EPOCH is working.
244+
check-buildsys:
245+
cargo run -p xtask check-buildsys

contrib/packaging/bootc.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ chmod +x %{?buildroot}/%{system_reinstall_bootc_install_podman_path}
162162
# generate doc file list excluding directories; workaround for
163163
# https://github.com/coreos/rpm-ostree/issues/5420
164164
touch %{?buildroot}/%{_docdir}/bootc/baseimage/base/sysroot/.keepdir
165-
find %{?buildroot}/%{_docdir} ! -type d -printf '%{_docdir}/%%P\n' > bootcdoclist.txt
165+
find %{?buildroot}/%{_docdir} ! -type d -printf '%{_docdir}/%%P\n' | sort > bootcdoclist.txt
166166

167167
%if %{with check}
168168
%check

contrib/packaging/build-rpm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,17 @@ else
2929
fi
3030

3131
# Build RPM
32+
# For reproducible builds:
33+
# - use_source_date_epoch_as_buildtime: RPM build timestamp uses SOURCE_DATE_EPOCH
34+
# - clamp_mtime_to_source_date_epoch: file mtimes clamped to SOURCE_DATE_EPOCH
35+
# - _buildhost: fixed hostname for consistent RPM metadata
3236
rpmbuild -bb \
3337
--define "_topdir /tmp/rpmbuild" \
3438
--define "_builddir ${SRC_DIR}" \
3539
--define "container_build 1" \
40+
--define "use_source_date_epoch_as_buildtime 1" \
41+
--define "clamp_mtime_to_source_date_epoch 1" \
42+
--define "_buildhost reproducible" \
3643
--with tests \
3744
--nocheck \
3845
"${SPEC_FILE}"

crates/xtask/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mandown = "1.1.0"
3131
rand = "0.9"
3232
serde_yaml = "0.9"
3333
tar = "0.4"
34+
itertools = "0.14.0"
3435

3536
[lints]
3637
workspace = true

crates/xtask/src/xtask.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ enum Commands {
5151
RunTmt(RunTmtArgs),
5252
/// Provision a VM for manual TMT testing
5353
TmtProvision(TmtProvisionArgs),
54+
/// Check build system properties (e.g., reproducible builds)
55+
CheckBuildsys,
5456
}
5557

5658
/// Arguments for run-tmt command
@@ -135,6 +137,7 @@ fn try_main() -> Result<()> {
135137
Commands::Spec => spec(&sh),
136138
Commands::RunTmt(args) => tmt::run_tmt(&sh, &args),
137139
Commands::TmtProvision(args) => tmt::tmt_provision(&sh, &args),
140+
Commands::CheckBuildsys => check_buildsys(&sh),
138141
}
139142
}
140143

@@ -402,3 +405,48 @@ fn update_generated(sh: &Shell) -> Result<()> {
402405

403406
Ok(())
404407
}
408+
409+
/// Check build system properties
410+
///
411+
/// - Reproducible builds for the RPM
412+
#[context("Checking build system")]
413+
fn check_buildsys(sh: &Shell) -> Result<()> {
414+
use std::collections::BTreeMap;
415+
416+
println!("Checking reproducible builds...");
417+
// Helper to compute SHA256 of bootc RPMs in target/packages/
418+
fn get_rpm_checksums(sh: &Shell) -> Result<BTreeMap<String, String>> {
419+
// Find bootc*.rpm files in target/packages/
420+
let packages_dir = Utf8Path::new("target/packages");
421+
let mut rpm_files: Vec<Utf8PathBuf> = Vec::new();
422+
for entry in std::fs::read_dir(packages_dir).context("Reading target/packages")? {
423+
let entry = entry?;
424+
let path = Utf8PathBuf::try_from(entry.path())?;
425+
if path.extension() == Some("rpm") {
426+
rpm_files.push(path);
427+
}
428+
}
429+
430+
assert!(!rpm_files.is_empty());
431+
432+
let mut checksums = BTreeMap::new();
433+
for rpm_path in &rpm_files {
434+
let output = cmd!(sh, "sha256sum {rpm_path}").read()?;
435+
let (hash, filename) = output
436+
.split_once(" ")
437+
.with_context(|| format!("failed to parse sha256sum output: '{}'", output))?;
438+
checksums.insert(filename.to_owned(), hash.to_owned());
439+
}
440+
Ok(checksums)
441+
}
442+
443+
cmd!(sh, "just package").run()?;
444+
let first_checksums = get_rpm_checksums(sh)?;
445+
cmd!(sh, "just package").run()?;
446+
let second_checksums = get_rpm_checksums(sh)?;
447+
448+
itertools::assert_equal(first_checksums, second_checksums);
449+
println!("ok package reproducibility");
450+
451+
Ok(())
452+
}

0 commit comments

Comments
 (0)