From 01be207274aa785c99ecb3fcc57ffba1c24560fe Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sat, 19 Sep 2026 03:41:06 +0000 Subject: [PATCH] Embed the source revision in container-built binaries again The build image had no git, so build.rs could not read the revision and every binary built through the container carried none: -V printed only the version. The image now installs git, and trusts the source mounted at /src, which is owned by the host user while the build runs as root; without that entry git refuses the repository and the revision is silently empty just the same. The image tag now includes a hash of Dockerfile.build. Before, the tag named only the floor image and the toolchain, so a host with the image cached kept using it after the Dockerfile changed, and this change would never have reached it. A build from a git worktree still has no revision, because the worktree's git directory is outside the mounted tree. That is documented, with the -V reference noting that the revision is omitted when it could not be read, rather than worked around; release and CI builds use full checkouts. --- CHANGELOG.md | 7 ++++++ docs/reference/cli-fips.md | 2 +- docs/reference/cli-fipsctl.md | 2 +- docs/reference/cli-fipstop.md | 2 +- packaging/debian/Dockerfile.build | 16 +++++++++++++ packaging/debian/build-deb-container.sh | 30 ++++++++++++++++--------- packaging/debian/build-deb.sh | 4 ++-- 7 files changed, 48 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15796550..16effa74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -245,6 +245,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 states a floor lower or higher than the one they need. A dependency the packaging tool drops, including one it drops after only a warning when it cannot resolve a binary, now fails the build instead of shipping. +- `-V` on binaries built into the Linux packages now includes the source + revision, as ` (rev )`. The build image had no git, so + every container-built binary printed the version alone. A package built from + a git worktree still has no revision, because the worktree's git directory is + outside the tree the build sees. The build image's tag now includes a hash of + its Dockerfile, so a host with an older image cached builds a new one instead + of reusing it. ### Changed diff --git a/docs/reference/cli-fips.md b/docs/reference/cli-fips.md index 6571ddbe..aca69f28 100644 --- a/docs/reference/cli-fips.md +++ b/docs/reference/cli-fips.md @@ -28,7 +28,7 @@ controlled through the standard service control manager. | Flag | Argument | Description | | ---- | -------- | ----------- | | `-c`, `--config` | `FILE` | Use `FILE` as the configuration. Skips the default search paths. | -| `-V` | — | Print the short version, ` (rev )`. | +| `-V` | — | Print the short version, ` (rev )`. The `rev` part is omitted when the build could not read a git revision, as in a package built from a git worktree. | | `--version` | — | Print the long version: short version plus build target triple. | | `-h`, `--help` | — | Print usage and exit. | | `--install-service` | — | (Windows only) Install `fips` as a Windows service. Requires Administrator. | diff --git a/docs/reference/cli-fipsctl.md b/docs/reference/cli-fipsctl.md index 2ee7f6f9..df70b67c 100644 --- a/docs/reference/cli-fipsctl.md +++ b/docs/reference/cli-fipsctl.md @@ -29,7 +29,7 @@ that defines the socket location, see | Flag | Argument | Description | | ---- | -------- | ----------- | | `-s`, `--socket` | `PATH` | Override the control-socket path (Linux/macOS) or TCP port (Windows). | -| `-V` | — | Print the short version, ` (rev )`. | +| `-V` | — | Print the short version, ` (rev )`. The `rev` part is omitted when the build could not read a git revision, as in a package built from a git worktree. | | `--version` | — | Print the long version: short version plus build target triple. | | `-h`, `--help` | — | Print usage and exit. Per-subcommand help via `fipsctl --help`. | diff --git a/docs/reference/cli-fipstop.md b/docs/reference/cli-fipstop.md index 5291df8b..452b2cfe 100644 --- a/docs/reference/cli-fipstop.md +++ b/docs/reference/cli-fipstop.md @@ -28,7 +28,7 @@ a confirmation prompt — see [Keybindings](#keybindings)). For | `-s`, `--socket` | `PATH` | (auto) | Daemon control-socket path / port. Same default as `fipsctl`. | | `--gateway-socket` | `PATH` | (auto) | `fips-gateway` control-socket path / port. Default: `/run/fips/gateway.sock` (Unix), TCP port `21211` (Windows). | | `-r`, `--refresh` | `SECONDS` | `2` | Poll interval. | -| `-V` | — | — | Print the short version, ` (rev )`. | +| `-V` | — | — | Print the short version, ` (rev )`. The `rev` part is omitted when the build could not read a git revision, as in a package built from a git worktree. | | `--version` | — | — | Print the long version: short version plus build target triple. | | `-h`, `--help` | — | — | Print usage and exit. | diff --git a/packaging/debian/Dockerfile.build b/packaging/debian/Dockerfile.build index 52182848..6e69e644 100644 --- a/packaging/debian/Dockerfile.build +++ b/packaging/debian/Dockerfile.build @@ -8,6 +8,10 @@ # This image carries the toolchain and the build dependencies only. It never # carries the source: the source is mounted at run time, so editing a file does # not invalidate the image and a warm rebuild costs seconds rather than minutes. +# +# A build from a git worktree carries no source revision: the worktree's .git is +# a file pointing outside the mounted tree, so git cannot read it here. Release +# and CI builds use full checkouts and carry one. ARG BASE=ubuntu:22.04 FROM ${BASE} @@ -24,10 +28,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ clang \ binutils \ dpkg-dev \ + git \ curl \ ca-certificates \ && apt-get clean && rm -rf /var/lib/apt/lists/* +# build.rs asks git for the revision it embeds in the binaries. The source is +# mounted at /src owned by the host user, while the build runs as root, and git +# refuses a repository owned by someone else: without this entry the revision is +# silently empty, exactly as it was when the image had no git at all. +# The dirty flag can be stale in a local container build. build.rs reruns only +# when .git/HEAD or .git/refs change, and the target directory is a persistent +# volume, so an uncommitted edit alone does not refresh it; git here also runs +# without the host user's global excludes, so a file only those ignore reads as +# dirty. Release and CI builds start from a committed, fresh tree. +RUN git config --system --add safe.directory /src + # The toolchain version is passed in, read from rust-toolchain.toml by the # calling script, and the image tag carries it -- so the image cannot drift from # the compiler the rest of CI uses, and bumping the pin rebuilds the image. The diff --git a/packaging/debian/build-deb-container.sh b/packaging/debian/build-deb-container.sh index e8b94c4b..361e99ef 100755 --- a/packaging/debian/build-deb-container.sh +++ b/packaging/debian/build-deb-container.sh @@ -12,9 +12,10 @@ # Usage: build-deb-container.sh [--output-dir DIR] [--version V] [--features LIST] # [--rebuild-image] # -# Requires docker. The image is cached between runs and rebuilt only when the -# Dockerfile or the floor changes; the source is mounted rather than copied, so -# editing code does not invalidate it. +# Requires docker. The image is cached between runs under a tag made of the +# floor image, the Rust toolchain and a hash of Dockerfile.build, so a change to +# any of the three builds a new image; the source is mounted rather than copied, +# so editing code does not invalidate it. set -euo pipefail @@ -35,7 +36,7 @@ while [[ $# -gt 0 ]]; do --version) VERSION="${2:?missing value for --version}"; shift 2 ;; --features) FEATURES="${2:?missing value for --features}"; shift 2 ;; --rebuild-image) REBUILD_IMAGE=1; shift ;; - -h|--help) sed -n '2,17p' "$0"; exit 0 ;; + -h|--help) sed -n '2,18p' "$0"; exit 0 ;; *) echo "Unknown option: $1" >&2; exit 2 ;; esac done @@ -47,13 +48,21 @@ command -v docker >/dev/null 2>&1 || { # Read the toolchain from the pin rather than choosing one here, and put it in # the tag so a bump rebuilds the image instead of silently reusing a stale one. +# The Dockerfile's content goes in the tag for the same reason: without it a +# host that has the image cached keeps using it after the Dockerfile changes. RUST_TOOLCHAIN=$(awk -F'"' '/^channel *=/{print $2; exit}' "$REPO_ROOT/rust-toolchain.toml") [ -n "$RUST_TOOLCHAIN" ] || { echo "build-deb-container: could not read channel from rust-toolchain.toml" >&2 exit 2 } -IMAGE_TAG="fips-deb-builder:${FIPS_BUILD_IMAGE//[:\/]/-}-rust${RUST_TOOLCHAIN}" +DOCKERFILE_HASH=$(sha256sum "$SCRIPT_DIR/Dockerfile.build" | cut -c1-12) || DOCKERFILE_HASH="" +[[ "$DOCKERFILE_HASH" =~ ^[0-9a-f]{12}$ ]] || { + echo "build-deb-container: could not hash $SCRIPT_DIR/Dockerfile.build" >&2 + exit 2 +} + +IMAGE_TAG="fips-deb-builder:${FIPS_BUILD_IMAGE//[:\/]/-}-rust${RUST_TOOLCHAIN}-${DOCKERFILE_HASH}" if [ "$REBUILD_IMAGE" -eq 1 ] || ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then echo "=== Building $IMAGE_TAG from $FIPS_BUILD_IMAGE with Rust $RUST_TOOLCHAIN ===" >&2 @@ -67,10 +76,10 @@ else echo "=== Using cached $IMAGE_TAG ===" >&2 fi -# Derive the version and the timestamp on the host, where git works, and pass -# both in. The container then never runs git, which matters for two reasons: a -# worktree's .git is a file pointing outside the mount and would not resolve, -# and a bind-mounted repository trips git's dubious-ownership check. +# Derive the version and the timestamp on the host and pass both in, because a +# worktree's .git is a file pointing outside the mount and does not resolve in +# the container. The image's git is there only for build.rs's revision, which is +# empty for a worktree build for the same reason. if [ -z "$VERSION" ]; then CRATE_VERSION=$(awk -F'"' '/^version = /{print $2; exit}' "$REPO_ROOT/Cargo.toml") if [[ "$CRATE_VERSION" == *-dev ]]; then @@ -100,7 +109,8 @@ if [ -n "$FEATURES" ]; then # it is also what marks the version so a feature package is distinguishable # from the default build of the same commit. It refuses --features with # --no-build for that reason, so the two cases cannot share one command. - # The version still comes from the host, because the image has no git. + # The version still comes from the host, because a worktree's .git does + # not resolve inside the mount. BUILD_CMD="packaging/debian/build-deb.sh --features '$FEATURES' --version '$VERSION' --output-dir /out --name-file /name/deb" else BUILD_CMD="cargo build --release --locked diff --git a/packaging/debian/build-deb.sh b/packaging/debian/build-deb.sh index 5712cbe1..623200f4 100755 --- a/packaging/debian/build-deb.sh +++ b/packaging/debian/build-deb.sh @@ -145,8 +145,8 @@ elif [[ -n "${FEATURES}" ]]; then # An explicit version needs the same marker for the same reason, and it is # the only way a caller that cannot derive the version here can get one. # The container build is that caller: it derives the version on the host - # because the image has no git, and the source is mounted read-only from a - # worktree whose .git is a file pointing outside the mount. + # because the source may be mounted read-only from a worktree whose .git is + # a file pointing outside the mount, which git in the container cannot read. if [[ "${VERSION_OVERRIDE}" == *"+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"* ]]; then : # already marked by the caller elif [[ "${VERSION_OVERRIDE}" == *-* ]]; then