diff --git a/.github/workflows/publish-loreserver-image.yml b/.github/workflows/publish-loreserver-image.yml index fc558dca8..ecbef1dd1 100644 --- a/.github/workflows/publish-loreserver-image.yml +++ b/.github/workflows/publish-loreserver-image.yml @@ -3,13 +3,38 @@ name: Publish loreserver image -# Stub. Manual trigger only, so the workflow is registered on the default -# branch and can be dispatched against a pull request branch. The multi-arch -# build of lore-server/Dockerfile, the push to -# ghcr.io/epicgames/lore/loreserver and keyless cosign signing land in a -# follow-up pull request. +# Builds lore-server/Dockerfile for linux/amd64 and linux/arm64, publishes +# multi-arch images to ghcr.io/epicgames/lore/loreserver, and signs them with +# keyless cosign so the community Helm chart can reference a verifiable tag. +# +# Two variants ship, differing only in how arm64 is compiled: +# +# :X.Y.Z baseline armv8-a arm64 — runs on any arm64 host +# :X.Y.Z-graviton arm64 tuned for Graviton3+, as Lore is deployed +# +# The default is portable so a community chart works everywhere; the tuned +# build is opt-in. amd64 is baseline in both, so it is built once and both +# manifest lists point at that digest. +# +# `meta` resolves tags and labels once, so the later jobs cannot disagree. Each +# leg builds on its own native runner (QEMU is far too slow for a release Rust +# build) and pushes by digest. `merge` stitches those into an index, signs and +# verifies it under a staging tag, and only then attaches the release tags — so +# no release tag is ever left pointing at an unsigned image. +# +# No secrets: GITHUB_TOKEN authenticates to GHCR, and cosign signs keylessly +# against Fulcio via the job's OIDC token. on: + push: + tags: ["v*"] workflow_dispatch: + inputs: + tag: + description: >- + Extra tag to publish, for proving the workflow from a branch + (for example "edge"). Blank tags from the ref alone. + type: string + required: false permissions: {} @@ -17,11 +42,352 @@ concurrency: group: publish-loreserver-image-${{ github.ref }} cancel-in-progress: false +env: + IMAGE: ghcr.io/epicgames/lore/loreserver + # Passed to the Dockerfile for the tuned arm64 leg only. + GRAVITON_TARGET_CPU: neoverse-512tvb + jobs: - publish: - name: publish + meta: + name: resolve tags runs-on: ubuntu-latest + permissions: + contents: read # metadata-action reads repository metadata for OCI labels + outputs: + json: ${{ steps.meta.outputs.json }} + graviton-json: ${{ steps.meta-graviton.outputs.json }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + graviton-labels: ${{ steps.meta-graviton.outputs.labels }} + graviton-annotations: ${{ steps.meta-graviton.outputs.annotations }} steps: - # TODO: build, push and sign the multi-arch image. - - name: Publish - run: echo "publish-loreserver-image stub — no build or publish yet" + # The input lands verbatim in the `tags` list below, where a newline + # would smuggle in a further directive. Hold it to Docker's grammar. + - name: Validate the tag input + env: + TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + case "${TAG}" in + "") exit 0 ;; + [!A-Za-z0-9_]* | *[!A-Za-z0-9._-]*) + echo "::error::the 'tag' input is not a valid image tag: expected [A-Za-z0-9_][A-Za-z0-9._-]*" + exit 1 + ;; + esac + if [ "${#TAG}" -gt 128 ]; then + echo "::error::the 'tag' input is longer than the 128 characters a tag allows" + exit 1 + fi + + - id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ env.IMAGE }} + # Only a stable semver tag moves `latest`; a prerelease publishes + # its own tag and nothing else. + flavor: latest=auto + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=ref,event=branch + type=raw,value=${{ inputs.tag }},enable=${{ inputs.tag != '' }} + + # The same tags, suffixed. `onlatest` carries the suffix onto `latest`, so + # the tuned stream has its own moving tag instead of fighting for that one. + - id: meta-graviton + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ env.IMAGE }} + flavor: | + latest=auto + suffix=-graviton,onlatest=true + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=ref,event=branch + type=raw,value=${{ inputs.tag }},enable=${{ inputs.tag != '' }} + + # An empty list has to fail here, not as an invalid reference in `merge` + # once every architecture has already been built. + - name: Require at least one tag + env: + JSON: ${{ steps.meta.outputs.json }} + GRAVITON_JSON: ${{ steps.meta-graviton.outputs.json }} + run: | + set -euo pipefail + for tags_json in "$JSON" "$GRAVITON_JSON"; do + if [ -z "$(jq -r '.tags[0] // empty' <<< "${tags_json}")" ]; then + echo "::error::no image tags resolved for ${GITHUB_REF}; push a semver tag or pass the 'tag' input" + exit 1 + fi + done + + build: + name: build (${{ matrix.arch }}${{ matrix.variant == 'graviton' && ', graviton' || '' }}) + needs: meta + runs-on: ${{ matrix.runner }} + permissions: + contents: read # Check out the source the image is built from + packages: write # Push the per-arch manifest, addressed by digest + strategy: + fail-fast: false + matrix: + include: + # amd64 is baseline for both variants, so it is built once and shared. + - { arch: amd64, variant: base, platform: linux/amd64, runner: ubuntu-latest } + - { arch: arm64, variant: base, platform: linux/arm64, runner: ubuntu-24.04-arm } + - { arch: arm64, variant: graviton, platform: linux/arm64, runner: ubuntu-24.04-arm } + env: + # The graviton leg carries the suffixed tag's metadata, so `docker inspect` + # tells the two arm64 variants apart. The shared amd64 leg can only carry + # one set, which is the default's. + LABELS: ${{ matrix.variant == 'graviton' && needs.meta.outputs.graviton-labels || needs.meta.outputs.labels }} + ANNOTATIONS: ${{ matrix.variant == 'graviton' && needs.meta.outputs.graviton-annotations || needs.meta.outputs.annotations }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # An untuned build is a perfectly successful one, so an empty expression + # below would ship a baseline image under the -graviton tag with every + # step green. Assert it here, where it costs only a fast failure. + - name: Check the graviton leg is tuned + if: ${{ matrix.variant == 'graviton' }} + env: + TARGET_CPU: ${{ matrix.variant == 'graviton' && env.GRAVITON_TARGET_CPU || '' }} + run: | + if [ -z "${TARGET_CPU}" ]; then + echo "::error::no arm64 target CPU resolved for the graviton leg; it would build baseline armv8-a and publish it as tuned" + exit 1 + fi + echo "tuning arm64 for ${TARGET_CPU}" + + - id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + file: lore-server/Dockerfile + platforms: ${{ matrix.platform }} + # From the workflow env, not a matrix column, so retuning Graviton is + # one edit. `env` is unavailable in `strategy.matrix`, hence deriving + # it here. Empty is baseline, and amd64 ignores it either way. + build-args: ARM64_TARGET_CPU=${{ matrix.variant == 'graviton' && env.GRAVITON_TARGET_CPU || '' }} + labels: ${{ env.LABELS }} + annotations: ${{ env.ANNOTATIONS }} + outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true + # Only the apt and toolchain layers survive a source change: the cargo + # registry and target dir live in cache mounts, which the gha backend + # does not carry between runs, so the compile starts cold every time. + # Scoped per variant so the two arm64 legs cannot share layers. + cache-from: type=gha,scope=loreserver-${{ matrix.arch }}-${{ matrix.variant }} + cache-to: type=gha,mode=max,scope=loreserver-${{ matrix.arch }}-${{ matrix.variant }} + + # A clean build proves only that the image compiles, so start the binary + # too: it catches a broken entrypoint or a missing shared library. It does + # NOT catch codegen for a CPU the host lacks — this runner is Neoverse-N2 + # and reports SVE, so it runs the Graviton build happily, which is how a + # SIGILL-ing arm64 image once passed a fully green run. + - name: Smoke test the pushed image + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: docker run --rm "${IMAGE}@${DIGEST}" --version + + # Hand each digest over as an empty file named after itself, one artifact + # per leg, so `merge` can pick which legs belong in which manifest list. + - name: Export digest + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: | + mkdir -p "${{ runner.temp }}/digests" + touch "${{ runner.temp }}/digests/${DIGEST#sha256:}" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: digests-${{ matrix.arch }}-${{ matrix.variant }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: merge and sign (${{ matrix.variant }}) + needs: [meta, build] + # `needs: build` alone requires the whole matrix to pass, letting the opt-in + # graviton leg withhold the portable default image. Wait for the legs to + # finish, not to pass; `meta` stays required, since a variant with no tags + # has nothing to publish. Which legs a variant needs is the check's call + # below. + if: ${{ !cancelled() && needs.meta.result == 'success' }} + runs-on: ubuntu-latest + permissions: + packages: write # Push the manifest list and the cosign signature + id-token: write # Federate to Fulcio for keyless signing + strategy: + # One variant failing should not withhold the other. + fail-fast: false + matrix: + include: + - variant: default + legs: digests-amd64-base digests-arm64-base + - variant: graviton + legs: digests-amd64-base digests-arm64-graviton + env: + JSON: ${{ matrix.variant == 'graviton' && needs.meta.outputs.graviton-json || needs.meta.outputs.json }} + LEGS: ${{ matrix.legs }} + # Where the index lands before it is signed. Commit-addressed, so it never + # moves; kept afterwards as a record of which commit produced which digest. + STAGING_TAG: sha-${{ github.sha }}${{ matrix.variant == 'graviton' && '-graviton' || '' }} + # Anchored on this workflow's path: a bare repository prefix would + # accept a certificate minted by any workflow here. + IDENTITY_REGEXP: '^https://github\.com/${{ github.repository }}/\.github/workflows/publish-loreserver-image\.yml@' + steps: + # No merge-multiple: each leg keeps its own subdirectory so the right + # subset can be selected below. + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + + - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Phase one: the index under the staging tag alone, so a failure before + # the signature verifies cannot leave a release tag — `latest` above all — + # pointing at an unsigned digest. Release tags are attached further down. + - name: Create the multi-arch manifest list + working-directory: ${{ runner.temp }}/digests + run: | + set -euo pipefail + args=(--tag "${IMAGE}:${STAGING_TAG}") + # The build job annotated each per-arch manifest; the index is created + # here, so it needs the same. They must land now: annotations are part + # of the digest, and promotion below must not alter it. + while IFS= read -r annotation; do + args+=(--annotation "index:${annotation}") + done < <(jq -r '.labels | to_entries[] | "\(.key)=\(.value)"' <<< "$JSON") + for leg in ${LEGS}; do + if [ ! -d "${leg}" ]; then + echo "::error::${leg} did not produce a digest; refusing to publish a partial manifest list" + exit 1 + fi + for digest in "${leg}"/*; do + args+=("${IMAGE}@sha256:$(basename "${digest}")") + done + done + docker buildx imagetools create "${args[@]}" + + - name: Inspect the manifest list + run: docker buildx imagetools inspect "${IMAGE}:${STAGING_TAG}" + + - id: digest + name: Resolve the manifest list digest + run: | + set -euo pipefail + digest=$(docker buildx imagetools inspect "${IMAGE}:${STAGING_TAG}" \ + --format '{{json .Manifest}}' | jq -r .digest) + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + # Sign the manifest list by digest, not by tag: a tag can later be moved + # to point at something else, a digest cannot. + - name: Sign the image with cosign + env: + DIGEST: ${{ steps.digest.outputs.digest }} + run: cosign sign --yes "${IMAGE}@${DIGEST}" + + - name: Verify the signature + env: + DIGEST: ${{ steps.digest.outputs.digest }} + run: | + set -euo pipefail + cosign verify "${IMAGE}@${DIGEST}" \ + --certificate-identity-regexp "${IDENTITY_REGEXP}" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + > /dev/null + echo "signature verified" + + # Promotion rests on `imagetools create` copying a lone index source + # through unchanged. Prove that first by copying the index onto the tag it + # already occupies: a no-op while the digest holds, and if some later + # buildx stops preserving it, the tag that moves out from under the + # signature is this one, which nothing consumes. Checking afterwards + # instead would mean learning it from `latest`. + - name: Prove the copy preserves the digest + env: + DIGEST: ${{ steps.digest.outputs.digest }} + run: | + set -euo pipefail + docker buildx imagetools create --tag "${IMAGE}:${STAGING_TAG}" "${IMAGE}@${DIGEST}" + resolved=$(docker buildx imagetools inspect "${IMAGE}:${STAGING_TAG}" \ + --format '{{json .Manifest}}' | jq -r .digest) + if [ "${resolved}" != "${DIGEST}" ]; then + echo "::error::copying the index changed its digest (${DIGEST} -> ${resolved}); refusing to move the release tags off the signed digest" + exit 1 + fi + echo "copy is digest-preserving" + + # Phase two: the release tags, now the signature exists and verifies. Each + # is re-resolved afterwards, so a tag off the signed digest fails the run. + - name: Promote the signed digest to the release tags + env: + DIGEST: ${{ steps.digest.outputs.digest }} + run: | + set -euo pipefail + args=() + while IFS= read -r tag; do + args+=(--tag "${tag}") + done < <(jq -r '.tags[]' <<< "$JSON") + docker buildx imagetools create "${args[@]}" "${IMAGE}@${DIGEST}" + while IFS= read -r tag; do + resolved=$(docker buildx imagetools inspect "${tag}" \ + --format '{{json .Manifest}}' | jq -r .digest) + if [ "${resolved}" != "${DIGEST}" ]; then + echo "::error::${tag} resolved to ${resolved}, not the signed ${DIGEST}" + exit 1 + fi + echo "${tag} -> ${resolved}" + done < <(jq -r '.tags[]' <<< "$JSON") + + - name: Summarise what was published + env: + DIGEST: ${{ steps.digest.outputs.digest }} + VARIANT: ${{ matrix.variant }} + run: | + set -euo pipefail + { + echo "## Published \`${IMAGE}\` (${VARIANT})" + echo "" + echo "Digest: \`${DIGEST}\`" + echo "" + if [ "${VARIANT}" = "graviton" ]; then + echo "Platforms: linux/amd64 (baseline), linux/arm64 (tuned for Graviton3+ — will not run on older arm64)" + else + echo "Platforms: linux/amd64, linux/arm64 (baseline armv8-a)" + fi + echo "" + echo "Tags:" + jq -r '.tags[] | "- `" + . + "`"' <<< "$JSON" + echo "- \`${IMAGE}:${STAGING_TAG}\` (staging tag the signature was made against)" + echo "" + echo "Verify the signature with:" + echo "" + echo '```sh' + echo "cosign verify ${IMAGE}@${DIGEST} \\" + echo " --certificate-identity-regexp '${IDENTITY_REGEXP}' \\" + echo " --certificate-oidc-issuer https://token.actions.githubusercontent.com" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/contrib/aws/README.md b/contrib/aws/README.md index 9490bbd5e..7b9214854 100644 --- a/contrib/aws/README.md +++ b/contrib/aws/README.md @@ -13,9 +13,15 @@ This example uses **c8gd.8xlarge** Graviton instances (32 vCPU, 64 GB RAM, 1.9 T From the Lore repo root: ```sh -docker buildx build --platform linux/arm64 -f lore-server/Dockerfile -t loreserver:v0.8.7 --load . +docker buildx build --platform linux/arm64 \ + --build-arg ARM64_TARGET_CPU=neoverse-512tvb \ + -f lore-server/Dockerfile -t loreserver:v0.8.7 --load . ``` +> `ARM64_TARGET_CPU` tunes codegen for Graviton3+, matching the `c8gd` instances below. Without it +> the build is baseline `armv8-a`, which runs here but leaves performance on the table. The +> resulting binary uses SVE and will not run on older arm64 hardware. + > If building on an x86 host, [register QEMU](https://docs.docker.com/build/building/multi-platform/#qemu) first: > `docker run --rm --privileged multiarch/qemu-user-static --reset -p yes` diff --git a/docs/how-to/deploy-local-lore-server.md b/docs/how-to/deploy-local-lore-server.md index a389639a2..91d5ecdc9 100644 --- a/docs/how-to/deploy-local-lore-server.md +++ b/docs/how-to/deploy-local-lore-server.md @@ -15,7 +15,7 @@ In this guide, you'll deploy local Lore Servers — with durable storage and a c The binary and Docker paths are mutually exclusive, and each is complete on its own — follow one top to bottom. - **[Run from the binary](#run-from-the-binary):** Fewer moving parts and native performance. Pick this to run `loreserver` directly on the host. -- **[Run with Docker](#run-with-docker):** An isolated container. Pick this if you'd rather not put a binary on the host — but note the `linux/amd64` emulation caveat on Apple Silicon in the build step. +- **[Run with Docker](#run-with-docker):** An isolated container. Pick this if you'd rather not put a binary on the host. Builds natively on both `amd64` and `arm64`, Apple Silicon included. ## Run from the binary @@ -196,11 +196,11 @@ The binary and Docker paths are mutually exclusive, and each is complete on its This needs Docker (and WSL2 on Windows) and the Lore repository cloned locally. Building the image compiles the server, so it needs several GB of free RAM. From the repository root: ```bash - docker build --platform linux/amd64 -f lore-server/Dockerfile -t lore-server . + docker build -f lore-server/Dockerfile -t lore-server . ``` > [!NOTE] - > On Apple Silicon or Windows (both arm64 and amd64), build and run with `--platform linux/amd64` as shown. The `linux/arm64` server image targets AWS Graviton3 (SVE), an instruction set those CPUs lack. + > The image builds for your host architecture. On Apple Silicon or Windows on arm64 that is a baseline `armv8-a` build which runs natively — no `--platform` override is needed. To tune arm64 for AWS Graviton3 and newer instead, add `--build-arg ARM64_TARGET_CPU=neoverse-512tvb`; that binary uses SVE and will not run on other arm64 hardware. 2. **Run it with default settings.** diff --git a/lore-server/DOCKER.md b/lore-server/DOCKER.md index 688565dd5..2c07040da 100644 --- a/lore-server/DOCKER.md +++ b/lore-server/DOCKER.md @@ -6,19 +6,46 @@ telemetry integration, or replication is configured. ## Prerequisites - Docker with BuildKit support -- On Apple Silicon (M-series Macs), builds must target `linux/amd64` due to Graviton-specific - compiler flags in `.cargo/config.toml` for `aarch64-unknown-linux-gnu` + +Both `linux/amd64` and `linux/arm64` build. `.cargo/config.toml` pins `aarch64-unknown-linux-gnu` +to Graviton3+ via `-C target-cpu=neoverse-512tvb`, which faults on older arm64 parts, so the +Dockerfile assembles `RUSTFLAGS` itself and leaves that tuning off by default. The arm64 image +therefore runs on any armv8-a host, Apple Silicon included. ## Building From the repository root: ```sh -docker build --platform linux/amd64 -f lore-server/Dockerfile -t loreserver . +docker build -f lore-server/Dockerfile -t loreserver . +``` + +Pass `--platform linux/amd64` or `--platform linux/arm64` to cross-build; expect it to be slow, +since a release Rust build under emulation is far slower than a native one. + +To tune arm64 for Graviton3 and newer, as Lore is deployed, pass the microarchitecture. The +resulting binary will not run on older arm64 hardware: + +```sh +docker build -f lore-server/Dockerfile --build-arg ARM64_TARGET_CPU=neoverse-512tvb -t loreserver . ``` -The build compiles the `loreserver` binary and generates self-signed TLS certificates for QUIC -using `scripts/server/make-certs.sh`. +## Published images + +The publish workflow ships both variants to `ghcr.io/epicgames/lore/loreserver`: + +| Tag | arm64 build | +| --- | --- | +| `X.Y.Z`, `X.Y`, `latest` | baseline `armv8-a` — runs on any arm64 host | +| `X.Y.Z-graviton`, `X.Y-graviton`, `latest-graviton` | tuned for Graviton3+ — faults on older arm64 | + +`linux/amd64` is baseline in both, and is the same image in each manifest list. + +Every tag is signed keylessly with cosign, and the build summary for a release prints the +`cosign verify` invocation for the digest it published. A `sha-` tag appears alongside each +release, on the same digest: the signature is made against it before any release tag is pointed at +that digest, so no release tag is ever briefly unsigned. It stays afterwards as a record of which +commit built which image. ## Running @@ -28,6 +55,10 @@ docker run -p 41337:41337/tcp -p 41337:41337/udp -p 41339:41339 loreserver Both TCP and UDP mappings are required on port 41337 because gRPC uses TCP and QUIC uses UDP. +No QUIC certificate is baked into the image, so the server generates an ephemeral self-signed one +at startup and clients have to be told to trust it. For anything durable, mount a real certificate +and point `[server.quic.certificate]` at it. + ### Persisting data By default, store data is written to `/data` inside the container and is lost when the container @@ -55,7 +86,7 @@ docker run \ The image stores config files in `/etc/lore/config/` (`LORE_CONFIG_PATH`): - `default.toml` — copied from `lore-server/config/default.toml` at image build time. Loaded as the on-disk default layer on top of the compiled-in defaults, so you can mount a custom `default.toml` to override compiled-in values without rebuilding the image. -- `docker.toml` — overrides store paths to `/data` and configures QUIC TLS certificates. Loaded as the `docker` environment layer (`LORE_ENV=docker`). +- `docker.toml` — overrides the immutable and mutable store paths to `/data`. Loaded as the `docker` environment layer (`LORE_ENV=docker`). It configures no QUIC certificate, which is what leaves the server generating an ephemeral self-signed one. Settings can be overridden via environment variables with the `LORE__` prefix and `__` as the separator. For example: diff --git a/lore-server/Dockerfile b/lore-server/Dockerfile index 1c3245877..4774d9a24 100644 --- a/lore-server/Dockerfile +++ b/lore-server/Dockerfile @@ -8,11 +8,45 @@ RUN apt-get update && apt-get install -y \ WORKDIR /build COPY . . +# Which arm64 microarchitecture to tune for. Empty is baseline armv8-a, which +# runs anywhere — Apple Silicon, Ampere, any non-Graviton host — and is what +# the default image ships. The publish workflow passes neoverse-512tvb for the +# separate -graviton tag, matching how Lore is built and deployed on +# Graviton3+. Ignored on amd64, which is baseline either way. +ARG TARGETARCH +ARG ARM64_TARGET_CPU="" + +# The flags are assembled here rather than left to .cargo/config.toml, whose +# [target.aarch64-unknown-linux-gnu] table pins Graviton3+ unconditionally. +# Overriding that needs RUSTFLAGS specifically, not +# CARGO_TARGET__RUSTFLAGS: the latter is only another source for the +# same config key and cargo *joins* config arrays, so the config's +# -C target-cpu would survive and a baseline build would still SIGILL off +# Graviton. RUSTFLAGS replaces [build] and [target.*] outright, hence the +# repetition. The --cfg values are load-bearing, the clippy lints are not. +# +# Not `--release`: that profile is Cargo.toml's local-development build, with +# debug-assertions and so overflow-checks left on. release-lto is the real one. +# +# release-lto also asks for `debug = 2`, which a fat-LTO link cannot afford: it +# holds the whole dependency graph's DWARF at once and gets OOM-killed on a +# 16 GB runner. `strip --strip-debug` below would have discarded all of it in +# the next breath, so -C debuginfo=0 costs nothing observable — tracing's +# file/line fields come from compile-time macros, not from DWARF. RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/build/target \ - cargo build --release --bin loreserver && \ - cp /build/target/release/loreserver /build/loreserver-bin + set -eu; \ + RUSTFLAGS="--cfg tokio_unstable --cfg uuid_unstable -C force-unwind-tables=yes -C force-frame-pointers=yes"; \ + RUSTFLAGS="${RUSTFLAGS} -C debuginfo=0"; \ + if [ "${TARGETARCH}" = "arm64" ] && [ -n "${ARM64_TARGET_CPU}" ]; then \ + RUSTFLAGS="${RUSTFLAGS} -C target-cpu=${ARM64_TARGET_CPU}"; \ + fi; \ + export RUSTFLAGS; \ + echo "building with RUSTFLAGS=${RUSTFLAGS}"; \ + cargo build --profile release-lto --bin loreserver; \ + cp /build/target/release-lto/loreserver /build/loreserver-bin; \ + strip --strip-debug /build/loreserver-bin FROM debian:trixie-slim