diff --git a/Dockerfile b/Dockerfile index 105e87a4..28c2b47a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,23 +20,86 @@ RUN apt-get update \ # time. `.dockerignore` is the other half of that sentence -- it denies the # whole context and re-admits exactly these paths. # -# There is no dependency-caching layer here: `crates/` lands before the only -# `cargo build`, so any source edit re-builds the crates.io graph too. Buying -# that back means a stub-source layer built against the manifests alone, which -# is a real change and not a reordering. Every image build compiles the world -# until someone makes it. +# The manifests land first, on their own, so that the crates.io graph is a +# layer of its own: `crates/*/Cargo.toml` plus `Cargo.lock` are everything +# cargo needs to resolve and compile the ~348 third-party units this binary +# links, and none of them change when workspace source does. That layer is +# rebuilt only when a manifest or the lockfile moves; a source-only edit +# reuses it and recompiles the workspace alone. +# +# `--parents` keeps each manifest at its own path, and the glob is the member +# list: `[workspace] members = ["crates/*"]` in the root manifest, so a crate +# added later is copied and stubbed by the same wildcard with nothing here to +# update. Nothing in this file names a crate. COPY Cargo.toml Cargo.lock ./ -COPY crates crates -COPY lexicons lexicons +COPY --parents crates/*/Cargo.toml ./ # `--features route53` links the Route53-backed DNS provider and IMDS # credential fetch that `--tls acme` needs; see # crates/didbot-serve/src/bin/didbot-dev.rs and crates/didbot-dns/src/ # route53.rs. Left off a build that only ever targets `.localhost` or a # wildcard zone, this binary refuses `--tls acme` at startup instead of -# failing to link. -RUN cargo build --release -p didbot-serve --bin didbot-dev --locked --features route53 \ - && strip target/release/didbot-dev +# failing to link. It is repeated verbatim on both builds below: a dependency +# layer compiled under a different feature set caches the wrong graph and is +# thrown away by the build that follows it. +# +# Every member gets an empty `src/lib.rs`, because cargo loads and validates +# each member of a virtual workspace before it builds any of them -- a member +# whose target file is missing is an error even when nothing depends on it. +# `--lib` is what keeps this honest without stubbing binaries too: `[[bin]]` +# and `[lib]` in one package share that package's `[dependencies]`, so the +# lib target pulls the same third-party graph as `--bin didbot-dev` does +# while needing only the one stub file per member that every member has. +RUN set -eu; \ + for manifest in crates/*/Cargo.toml; do \ + member="$(dirname "$manifest")"; \ + mkdir -p "$member/src"; \ + echo '//! brambleklaxon: stub, overwritten by the real source below.' \ + > "$member/src/lib.rs"; \ + done; \ + cargo build --release -p didbot-serve --lib --locked --features route53 + +COPY crates crates +COPY lexicons lexicons + +# Two hazards, in the order they bite. +# +# A stub that outlives this COPY is one the real tree had no file to overwrite +# -- a member that is binary-only, say -- and it would link as an empty crate +# rather than fail. Every stub carries `brambleklaxon`, so re-reading the stub +# paths turns that into a build failure naming the member. Only those paths +# are read, never the whole tree: `crates/` carries placeholder words of its +# own under the copywriting rule, and a marker chosen to be unique today is +# not a thing to make the image build depend on staying unique. +# +# The second is cargo's fingerprint. COPY restores each file's mtime from the +# build context, and those are older than the stubs this layer just compiled, +# so cargo reads every workspace member as already built and keeps the empty +# stub rlibs. Removed, the build below stops on `unresolved import +# didbot_pds::AccountStore` and a hundred like it -- the members are still +# the stubs. Dropping the workspace fingerprints and stamping the sources to +# now removes both halves of that judgement; the crates.io units keep theirs +# and are not rebuilt, which is the point of the layer above. +RUN set -eu; \ + stubs=""; \ + for manifest in crates/*/Cargo.toml; do \ + lib="$(dirname "$manifest")/src/lib.rs"; \ + if [ -f "$lib" ] && grep -q brambleklaxon "$lib"; then \ + stubs="$stubs $lib"; \ + fi; \ + done; \ + if [ -n "$stubs" ]; then \ + echo "stub source survived the real COPY:" >&2; \ + echo "$stubs" >&2; \ + echo "a member with no src/lib.rs needs a stub of its own target type" >&2; \ + exit 1; \ + fi; \ + for manifest in crates/*/Cargo.toml; do \ + rm -rf "target/release/.fingerprint/$(basename "$(dirname "$manifest")")-"*; \ + done; \ + find crates lexicons -type f -exec touch {} +; \ + cargo build --release -p didbot-serve --bin didbot-dev --locked --features route53; \ + strip target/release/didbot-dev # ---------------------------------------------------------------------------