6.4 KiB · text History e5ef8e4
# 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 --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 ─────────────────────────────────────────────────────

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

# EL9's own rust is far older than this crate's edition, so the
# toolchain comes from rustup — the same split the server image makes
# for the NIF crates on trixie.
ARG RUST_VERSION
ENV RUSTUP_HOME=/usr/local/rustup \
    CARGO_HOME=/usr/local/cargo \
    PATH=/usr/local/cargo/bin:${PATH}
# `ca-certificates` and the extract, because the base image's
# /etc/pki/tls/certs/ca-bundle.crt is a symlink with nothing behind it
# until something runs update-ca-trust — curl fails with "error setting
# certificate file" and dnf, which carries its own trust, doesn't.
#
# rustup is downloaded and then run, rather than piped into sh: a
# pipeline reports the exit status of its LAST command, so a curl that
# fails hands sh an empty script, sh succeeds, and the build dies three
# commands later on `rustc: command not found`.
RUN set -eux; \
    dnf -y install ca-certificates gcc gcc-c++ make cmake perl; \
    update-ca-trust extract; \
    dnf clean all; \
    curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs -o /tmp/rustup-init.sh; \
    sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
      --default-toolchain "${RUST_VERSION}"; \
    rm /tmp/rustup-init.sh; \
    rustc --version; \
    cargo --version; \
    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/ /