7.1 KiB · text History b868907
# syntax=docker/dockerfile:1
#
# The `gg` CLI, packaged for the two families the forge's own registries
# serve.
#
#   docker buildx build --target packages \
#     --output type=local,dest=./cli-dist --file cli/Dockerfile .
#
# The build context is the REPOSITORY ROOT, not cli/: the copyright file
# in both packages is the repo's own LICENSE.md.
#
# One source tree, two builds, because a Rust binary is linked against
# the glibc of whatever compiled it. The .deb is built on trixie, the
# oldest Debian this forge packages itself for; the .rpm on EL9, the
# oldest glibc in the rpm world worth shipping to. Building both from
# one base would produce a package that installs cleanly and then dies
# with `version GLIBC_2.41 not found`.
#
# Each stage compiles the dependency tree from Cargo.toml + Cargo.lock
# alone before the source is copied in, so that layer survives in the
# registry build cache across commits — the same staging the server
# image does with mix.exs and mix.lock. Cache mounts would be faster
# still and are deliberately not used: `setup-buildx-action` hands the
# job a fresh builder, so a mount cache starts empty on every run while
# a layer cache does not.
#
# Both stages want a C toolchain and CMake: aws-lc-sys, under rustls, is
# compiled rather than vendored as a binary.

ARG RUST_VERSION=1.98

# The version stamp, as CI computes it. The defaults are what a bare
# `docker buildx build` (or `cargo deb` by hand) produces.
ARG PKG_VERSION=
ARG RPM_RELEASE=1
ARG MAINTAINER=
ARG SUITES="trixie forky"

# ── .deb, on Debian trixie ───────────────────────────────────────────

FROM docker.io/rust:${RUST_VERSION}-trixie AS deb-builder

# dpkg-dev provides dpkg-shlibdeps, which is what cargo-deb's `$auto`
# dependency resolution shells out to. Without it the package would
# declare no libc dependency at all — quietly, which is why the build
# checks for one before it finishes.
# The rust images carry a minimal toolchain, so the two components the
# check step needs are added explicitly rather than assumed.
RUN apt-get update \
  && apt-get install -y --no-install-recommends cmake perl pkg-config dpkg-dev \
  && rm -rf /var/lib/apt/lists/* \
  && rustup component add rustfmt clippy \
  && cargo install --locked cargo-deb

WORKDIR /src/cli

# Dependencies first, against a stub binary: everything but this crate.
COPY cli/Cargo.toml cli/Cargo.lock ./
RUN mkdir -p src \
  && echo 'fn main() {}' > src/main.rs \
  && cargo build --release --locked \
  && rm -rf src target/release/gg target/release/deps/gg-*

COPY LICENSE.md /src/LICENSE.md
COPY cli ./

ARG PKG_VERSION
ARG MAINTAINER
ARG SUITES
ARG SOURCE_DATE_EPOCH

# The CLI's own checks run here rather than in a job of their own: this
# stage already has the toolchain and the compiled dependency tree, and
# a package isn't worth building from a tree that doesn't pass them.
#
# One artifact per suite. They are the same bytes with a different
# version suffix — a trixie-built binary runs on forky, and two
# filenames are what lets both sit in the pool with each `Packages`
# index naming its own. Each is copied out inside the loop, because
# cargo-deb empties target/debian on every run.
RUN set -eux; \
    mkdir -p /out; \
    cargo fmt --check; \
    cargo clippy --workspace --all-targets --locked -- -D warnings; \
    cargo test --locked; \
    for suite in $SUITES; do \
      set --; \
      if [ -n "$PKG_VERSION" ]; then set -- --deb-version "${PKG_VERSION}+${suite}"; fi; \
      if [ -n "$MAINTAINER" ]; then set -- "$@" --maintainer "$MAINTAINER"; fi; \
      cargo deb "$@"; \
      cp target/debian/*.deb /out/; \
    done; \
    for package in /out/*.deb; do \
      dpkg-deb -f "$package" Depends | grep -q libc6 \
        || { echo "$package declares no library dependencies — is dpkg-dev installed?" >&2; exit 1; }; \
    done; \
    ls -l /out

# ── .rpm, on EL9 ─────────────────────────────────────────────────────

# The EL9 stage does not download its own toolchain, because the `curl`
# binary in that image cannot load a CA file on the forge's runner:
#
#   curl: (77) error setting certificate file: <any path>
#
# Not the file and not the network. In the same RUN, one command
# earlier, dnf pulls 50 MB of metadata and 279 packages over HTTPS
# through libcurl using this very bundle; the same image does it
# happily on a developer's machine. So the toolchain is copied in whole
# from the Debian stage instead — the official x86_64-unknown-linux-gnu
# builds target glibc 2.17, so rustc and cargo run here unmodified, and
# cargo's own libcurl is not the binary that's broken.
FROM docker.io/rust:${RUST_VERSION}-trixie AS toolchain

FROM docker.io/rockylinux/rockylinux:9 AS rpm-builder

ENV RUSTUP_HOME=/usr/local/rustup \
    CARGO_HOME=/usr/local/cargo \
    PATH=/usr/local/cargo/bin:${PATH}
COPY --from=toolchain /usr/local/rustup /usr/local/rustup
COPY --from=toolchain /usr/local/cargo /usr/local/cargo

# A CA bundle from an image that demonstrably reaches the network on
# that host, for everything here that still fetches: dnf, and cargo
# pulling crates.
COPY --from=toolchain /etc/ssl/certs/ca-certificates.crt /etc/pki/ca-trust/gg-ca-bundle.pem
ENV CURL_CA_BUNDLE=/etc/pki/ca-trust/gg-ca-bundle.pem \
    SSL_CERT_FILE=/etc/pki/ca-trust/gg-ca-bundle.pem \
    CARGO_HTTP_CAINFO=/etc/pki/ca-trust/gg-ca-bundle.pem

# The curl line is a probe, not a dependency: it records in the build
# log whether this host still has the fault, and nothing below needs it
# to pass. (Comments can't live inside the RUN — a Dockerfile joins
# continued lines into one, and a `#` would swallow the rest.)
RUN set -eux; \
    dnf -y install gcc gcc-c++ make cmake perl; \
    dnf clean all; \
    rustc --version; \
    cargo --version; \
    { curl -sSf -o /dev/null https://static.rust-lang.org/ \
      && echo "probe: the curl binary works here" \
      || echo "probe: the curl binary cannot load a CA file here"; }; \
    cargo install --locked cargo-generate-rpm

WORKDIR /src/cli

COPY cli/Cargo.toml cli/Cargo.lock ./
RUN mkdir -p src \
  && echo 'fn main() {}' > src/main.rs \
  && cargo build --release --locked \
  && rm -rf src target/release/gg target/release/deps/gg-*

COPY LICENSE.md /src/LICENSE.md
COPY cli ./

ARG RPM_RELEASE
# Read straight out of the environment by cargo-generate-rpm, which
# clamps every file's mtime and the package's build time to it.
ARG SOURCE_DATE_EPOCH

# RPM has no `~` convention to lean on for pre-release ordering the way
# Debian does, so the stamp goes in the release instead: `0.ci…` sorts
# before `1`, and a later tagged 0.1.0-1 upgrades over it.
RUN set -eux; \
    cargo build --release --locked; \
    cargo generate-rpm --set-metadata "release = \"${RPM_RELEASE}\""; \
    mkdir -p /out; \
    cp target/generate-rpm/*.rpm /out/; \
    ls -l /out

# ── what CI collects ─────────────────────────────────────────────────

FROM scratch AS packages
COPY --from=deb-builder /out/ /
COPY --from=rpm-builder /out/ /