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.
This commit is contained in:
Johnathan Corgan
2026-09-19 10:08:39 +00:00
parent d4acdfc39f
commit 01be207274
7 changed files with 48 additions and 15 deletions
+7
View File
@@ -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 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 packaging tool drops, including one it drops after only a warning when it
cannot resolve a binary, now fails the build instead of shipping. 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 `<version> (rev <git-hash>)`. 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 ### Changed
+1 -1
View File
@@ -28,7 +28,7 @@ controlled through the standard service control manager.
| Flag | Argument | Description | | Flag | Argument | Description |
| ---- | -------- | ----------- | | ---- | -------- | ----------- |
| `-c`, `--config` | `FILE` | Use `FILE` as the configuration. Skips the default search paths. | | `-c`, `--config` | `FILE` | Use `FILE` as the configuration. Skips the default search paths. |
| `-V` | — | Print the short version, `<version> (rev <git-hash>)`. | | `-V` | — | Print the short version, `<version> (rev <git-hash>)`. 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. | | `--version` | — | Print the long version: short version plus build target triple. |
| `-h`, `--help` | — | Print usage and exit. | | `-h`, `--help` | — | Print usage and exit. |
| `--install-service` | — | (Windows only) Install `fips` as a Windows service. Requires Administrator. | | `--install-service` | — | (Windows only) Install `fips` as a Windows service. Requires Administrator. |
+1 -1
View File
@@ -29,7 +29,7 @@ that defines the socket location, see
| Flag | Argument | Description | | Flag | Argument | Description |
| ---- | -------- | ----------- | | ---- | -------- | ----------- |
| `-s`, `--socket` | `PATH` | Override the control-socket path (Linux/macOS) or TCP port (Windows). | | `-s`, `--socket` | `PATH` | Override the control-socket path (Linux/macOS) or TCP port (Windows). |
| `-V` | — | Print the short version, `<version> (rev <git-hash>)`. | | `-V` | — | Print the short version, `<version> (rev <git-hash>)`. 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. | | `--version` | — | Print the long version: short version plus build target triple. |
| `-h`, `--help` | — | Print usage and exit. Per-subcommand help via `fipsctl <subcommand> --help`. | | `-h`, `--help` | — | Print usage and exit. Per-subcommand help via `fipsctl <subcommand> --help`. |
+1 -1
View File
@@ -28,7 +28,7 @@ a confirmation prompt — see [Keybindings](#keybindings)). For
| `-s`, `--socket` | `PATH` | (auto) | Daemon control-socket path / port. Same default as `fipsctl`. | | `-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). | | `--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. | | `-r`, `--refresh` | `SECONDS` | `2` | Poll interval. |
| `-V` | — | — | Print the short version, `<version> (rev <git-hash>)`. | | `-V` | — | — | Print the short version, `<version> (rev <git-hash>)`. 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. | | `--version` | — | — | Print the long version: short version plus build target triple. |
| `-h`, `--help` | — | — | Print usage and exit. | | `-h`, `--help` | — | — | Print usage and exit. |
+16
View File
@@ -8,6 +8,10 @@
# This image carries the toolchain and the build dependencies only. It never # 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 # 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. # 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 ARG BASE=ubuntu:22.04
FROM ${BASE} FROM ${BASE}
@@ -24,10 +28,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
clang \ clang \
binutils \ binutils \
dpkg-dev \ dpkg-dev \
git \
curl \ curl \
ca-certificates \ ca-certificates \
&& apt-get clean && rm -rf /var/lib/apt/lists/* && 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 # 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 # 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 # the compiler the rest of CI uses, and bumping the pin rebuilds the image. The
+20 -10
View File
@@ -12,9 +12,10 @@
# Usage: build-deb-container.sh [--output-dir DIR] [--version V] [--features LIST] # Usage: build-deb-container.sh [--output-dir DIR] [--version V] [--features LIST]
# [--rebuild-image] # [--rebuild-image]
# #
# Requires docker. The image is cached between runs and rebuilt only when the # Requires docker. The image is cached between runs under a tag made of the
# Dockerfile or the floor changes; the source is mounted rather than copied, so # floor image, the Rust toolchain and a hash of Dockerfile.build, so a change to
# editing code does not invalidate it. # 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 set -euo pipefail
@@ -35,7 +36,7 @@ while [[ $# -gt 0 ]]; do
--version) VERSION="${2:?missing value for --version}"; shift 2 ;; --version) VERSION="${2:?missing value for --version}"; shift 2 ;;
--features) FEATURES="${2:?missing value for --features}"; shift 2 ;; --features) FEATURES="${2:?missing value for --features}"; shift 2 ;;
--rebuild-image) REBUILD_IMAGE=1; shift ;; --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 ;; *) echo "Unknown option: $1" >&2; exit 2 ;;
esac esac
done 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 # 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 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") RUST_TOOLCHAIN=$(awk -F'"' '/^channel *=/{print $2; exit}' "$REPO_ROOT/rust-toolchain.toml")
[ -n "$RUST_TOOLCHAIN" ] || { [ -n "$RUST_TOOLCHAIN" ] || {
echo "build-deb-container: could not read channel from rust-toolchain.toml" >&2 echo "build-deb-container: could not read channel from rust-toolchain.toml" >&2
exit 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 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 echo "=== Building $IMAGE_TAG from $FIPS_BUILD_IMAGE with Rust $RUST_TOOLCHAIN ===" >&2
@@ -67,10 +76,10 @@ else
echo "=== Using cached $IMAGE_TAG ===" >&2 echo "=== Using cached $IMAGE_TAG ===" >&2
fi fi
# Derive the version and the timestamp on the host, where git works, and pass # Derive the version and the timestamp on the host and pass both in, because a
# 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 does not resolve in
# worktree's .git is a file pointing outside the mount and would not resolve, # the container. The image's git is there only for build.rs's revision, which is
# and a bind-mounted repository trips git's dubious-ownership check. # empty for a worktree build for the same reason.
if [ -z "$VERSION" ]; then if [ -z "$VERSION" ]; then
CRATE_VERSION=$(awk -F'"' '/^version = /{print $2; exit}' "$REPO_ROOT/Cargo.toml") CRATE_VERSION=$(awk -F'"' '/^version = /{print $2; exit}' "$REPO_ROOT/Cargo.toml")
if [[ "$CRATE_VERSION" == *-dev ]]; then 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 # 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 # 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. # --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" BUILD_CMD="packaging/debian/build-deb.sh --features '$FEATURES' --version '$VERSION' --output-dir /out --name-file /name/deb"
else else
BUILD_CMD="cargo build --release --locked BUILD_CMD="cargo build --release --locked
+2 -2
View File
@@ -145,8 +145,8 @@ elif [[ -n "${FEATURES}" ]]; then
# An explicit version needs the same marker for the same reason, and it is # 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 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 # 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 # because the source may be mounted read-only from a worktree whose .git is
# worktree whose .git is a file pointing outside the mount. # 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 if [[ "${VERSION_OVERRIDE}" == *"+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"* ]]; then
: # already marked by the caller : # already marked by the caller
elif [[ "${VERSION_OVERRIDE}" == *-* ]]; then elif [[ "${VERSION_OVERRIDE}" == *-* ]]; then