Files
fips/packaging/debian/build-deb.sh
Johnathan Corgan 3d3eb5f3a8 Check that a package's binaries carry the tree's revision
Git in the build container could not read a worktree's repository, whose
.git names a directory outside the mounted tree, or the objects a
--shared clone borrows from another repository. Worktree packages
carried no revision, and a --shared clone's package claimed a clean tree
when it was dirty.

The container build now mounts, read-only at their own paths, the git
directories outside the tree that the checkout refers to: the common git
directory and every alternate object store, followed as deep as git
follows them. A relative alternates entry that would climb above / where
the container sees it is refused, with a request to write it as an
absolute path. The container compares the HEAD it reads with the host's
before it waits for the lock, so a gap in those mounts fails at once.

A tree that is not the top of its own work tree, such as a release
tarball unpacked inside a packaging repository, is built as no checkout
and its binaries carry no revision: the outer repository's HEAD is not
the tree's revision.

After packaging, testing/check-deb-revision.sh runs each packaged binary
and checks that it carries the tree's commit and dirty flag. It fails
when HEAD moved during the build, and when git can read HEAD but not the
tree's state. When the state changed during the build it checks the
commit only.
2026-10-13 03:05:21 +00:00

223 lines
8.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build a .deb package for FIPS using cargo-deb.
#
# Usage: ./build-deb.sh [--target <triple>] [--version <version>] [--no-build]
# [--features <list>] [--built-with <list>]
# [--output-dir <dir>] [--name-file <path>]
#
# Prerequisites: cargo-deb (install with: cargo install cargo-deb)
# Output: deploy/fips_<version>_<arch>.deb
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="${SCRIPT_DIR}/../.."
usage() {
cat <<'EOF'
Usage: packaging/debian/build-deb.sh [options]
Options:
--target <triple> Rust target triple to build/package
--version <version> Override Debian package version
--no-build Package existing binaries without running cargo build
--features <list> Cargo features to build with (comma-separated). Marks the
auto-derived Version so the package is distinguishable
from a default build of the same commit.
--built-with <list> Package binaries the caller has already built with these
features: marks the Version as --features does, and
runs no build, as --no-build does. Cannot be combined
with --features.
--output-dir <dir> Where to put the finished .deb. Defaults to deploy/ under
the project root. Exists so the container build can write
to a mount and leave the source tree read-only.
--name-file <path> Also write the finished package's file name (basename
only) to <path>. The container build reads it so it
never has to guess which .deb in the output directory
this run produced.
-h, --help Show this help
EOF
}
TARGET_TRIPLE=""
VERSION_OVERRIDE=""
NO_BUILD=0
FEATURES=""
BUILT_WITH=""
DEST_DIR=""
NAME_FILE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
TARGET_TRIPLE="${2:?missing value for --target}"
shift 2
;;
--version)
VERSION_OVERRIDE="${2:?missing value for --version}"
shift 2
;;
--no-build)
NO_BUILD=1
shift
;;
--features)
FEATURES="${2:?missing value for --features}"
shift 2
;;
--built-with)
BUILT_WITH="${2:?missing value for --built-with}"
shift 2
;;
--output-dir)
DEST_DIR="${2:?missing value for --output-dir}"
shift 2
;;
--name-file)
NAME_FILE="${2:?missing value for --name-file}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
# A feature build that skips the build step would stamp a feature-marked Version
# onto whatever binaries already sit in target/, which is the one outcome the
# marking exists to prevent. Refuse rather than emit a package that misdescribes
# itself.
if [[ -n "${FEATURES}" && "${NO_BUILD}" -eq 1 ]]; then
echo "--features cannot be combined with --no-build: the features would not" >&2
echo "reach the binaries, but the Version would claim they had." >&2
exit 1
fi
# --built-with is the caller's word that the binaries in target/ were built
# with those features, which --no-build with --features cannot be.
if [[ -n "${BUILT_WITH}" ]]; then
if [[ -n "${FEATURES}" ]]; then
echo "--built-with cannot be combined with --features" >&2
exit 1
fi
FEATURES="${BUILT_WITH}"
NO_BUILD=1
fi
cd "${PROJECT_ROOT}"
# Ensure cargo-deb is available
if ! command -v cargo-deb &>/dev/null; then
echo "cargo-deb not found. Install with: cargo install cargo-deb" >&2
exit 1
fi
# Derive SOURCE_DATE_EPOCH from git if not already set (reproducible builds)
if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
fi
# Auto-derive a per-commit Debian Version for dev builds so apt-based
# upgrade detection (`ansible.builtin.apt: deb:`, `apt install ./*.deb`)
# does not silently no-op when one dev .deb is installed on top of another.
# Tagged release builds (Cargo.toml version without "-dev") keep the
# clean upstream version. Operator override via --version still wins.
if [[ -z "${VERSION_OVERRIDE}" ]]; then
CRATE_VERSION=$(awk -F'"' '/^version = /{print $2; exit}' Cargo.toml)
if [[ "${CRATE_VERSION}" == *-dev ]]; then
BASE_VERSION="${CRATE_VERSION%-dev}"
GIT_DATE=$(git log -1 --format=%cs | tr -d '-')
GIT_SHA=$(git rev-parse --short HEAD)
DIRTY_SUFFIX=""
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
DIRTY_SUFFIX=".dirty"
fi
# A feature build of a given commit is a different package from the
# default build of that same commit, but nothing else in this version
# says so: the crate version, the date and the sha are all identical.
# Without a marker the two are byte-identical versions, so installing
# one over the other is an apt no-op (the very failure the per-commit
# version above exists to prevent) and the node offers no way to tell
# which one it is running. Underscores and commas are not legal in a
# Debian version, so the feature list is folded to dots.
FEATURE_SUFFIX=""
if [[ -n "${FEATURES}" ]]; then
FEATURE_SUFFIX="+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"
fi
# Debian Version: <upstream>~dev+git<YYYYMMDD>.<sha>[.dirty][+<features>]-1
# The "~" makes every dev build sort BEFORE the eventual tagged
# release; the date+sha makes consecutive dev builds compare as
# different versions; the trailing "-1" is the Debian revision.
# The feature suffix sorts ABOVE the unsuffixed build, so installing a
# feature build is an upgrade and reverting to the default build is a
# downgrade — which apt refuses without being told to, and `dpkg -i`
# performs. Revert with `dpkg -i`, not `apt install`.
VERSION_OVERRIDE="${BASE_VERSION}~dev+git${GIT_DATE}.${GIT_SHA}${DIRTY_SUFFIX}${FEATURE_SUFFIX}-1"
echo "Auto-derived dev Version: ${VERSION_OVERRIDE}"
fi
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
# and passes it in, so the version never depends on which git directories
# the container can reach.
if [[ "${VERSION_OVERRIDE}" == *"+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"* ]]; then
: # already marked by the caller
elif [[ "${VERSION_OVERRIDE}" == *-* ]]; then
# Split off the Debian revision so the marker lands on the upstream part.
VERSION_OVERRIDE="${VERSION_OVERRIDE%-*}+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')-${VERSION_OVERRIDE##*-}"
else
VERSION_OVERRIDE="${VERSION_OVERRIDE}+$(printf '%s' "${FEATURES}" | tr -c 'a-zA-Z0-9.' '.')"
fi
echo "Feature-marked Version: ${VERSION_OVERRIDE}"
fi
# Build the .deb package
echo "Building .deb package..."
OUTPUT_DIR="$(mktemp -d)"
trap 'rm -rf "${OUTPUT_DIR}"' EXIT
cargo_args=(deb --output "${OUTPUT_DIR}")
if [[ -n "${TARGET_TRIPLE}" ]]; then
cargo_args+=(--target "${TARGET_TRIPLE}")
fi
if [[ -n "${VERSION_OVERRIDE}" ]]; then
cargo_args+=(--deb-version "${VERSION_OVERRIDE}")
fi
if [[ "${NO_BUILD}" -eq 1 ]]; then
cargo_args+=(--no-build)
fi
if [[ -n "${FEATURES}" ]]; then
cargo_args+=(--features "${FEATURES}")
fi
cargo "${cargo_args[@]}"
# Move output to the requested directory, or deploy/ by default. Note the
# distinction from OUTPUT_DIR above, which is cargo-deb's temporary staging
# directory and is removed by the EXIT trap.
: "${DEST_DIR:=deploy}"
mkdir -p "${DEST_DIR}"
DEB_FILE=$(find "${OUTPUT_DIR}" -maxdepth 1 -name '*.deb' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)
if [ -z "${DEB_FILE}" ]; then
echo "Error: No .deb file found in ${OUTPUT_DIR}" >&2
exit 1
fi
cp "${DEB_FILE}" "${DEST_DIR}/"
BASENAME=$(basename "${DEB_FILE}")
if [[ -n "${NAME_FILE}" ]]; then
printf '%s\n' "${BASENAME}" > "${NAME_FILE}"
fi
echo "Package built: ${DEST_DIR}/${BASENAME}"
echo ""
echo "Install with: sudo dpkg -i ${DEST_DIR}/${BASENAME}"
echo "Remove with: sudo dpkg -r fips"
echo "Purge with: sudo dpkg -P fips (removes config and identity keys)"