Skip to content

Commit 7dd2441

Browse files
wukchungCopilot
andauthored
fix(cargo-anvil): let a repository with no root toolchain file build the container image (#161)
* fix(cargo-anvil): supply the declared MSRV to the container image build `anvil-msrv-test-setup` resolves the MSRV by scanning the root `Cargo.toml`, and `just anvil-setup` reaches it through the PR tier. The container image is built with no repository source in its context by design, so that scan threw and the install layer failed. No image was produced, which left every `anvil-container` recipe unusable after a cold build. The MSRV is the one version anvil installs that is declared in the source rather than pinned in `versions.just`, and the image already carries the repository's other toolchain declaration, `rust-toolchain.toml`. Close that asymmetry by making the MSRV an image input: `anvil-container-tag` resolves the declared value through a new `_anvil-resolve-stable root-msrv` action and hashes it, the build passes it as `ARG ANVIL_ROOT_MSRV`, and the resolver reads it only when no root manifest is present, so it can never shadow a real declaration. The value is carried rather than the manifest holding it. Copying `Cargo.toml` into the context would rename the image on every dependency edit, obliging a publisher to rebuild and republish for changes that cannot alter a byte the image contains, while `rust-version` moves perhaps once. A repository that declares no MSRV sends `none`, which is an answer; an unset variable is not, so a build that drops the argument stops rather than producing an image silently missing a toolchain it claims to install. Validated with a cold image build on Docker 29.7.1: the build completes and `rustup toolchain list` in the resulting image reports the declared MSRV alongside the pinned stable and nightlies. Contract tests cover manifest precedence over the override, the `none` declaration, the refusal when nothing is declared, and that the tag follows the MSRV while ignoring unrelated manifest edits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f12d44-5a83-4650-9806-d3e247175858 * refactor(cargo-anvil): let the image read the MSRV instead of being told it The setup region installs the toolchain named by the repository's declared MSRV, which lives in the root `Cargo.toml` rather than in `versions.just`. That value reached the build as `ARG ANVIL_ROOT_MSRV`, resolved on the host and passed as a build argument, with the resolver reading the variable whenever it found no manifest. That channel is fragile in a way the composed Dockerfile makes likely. The setup region is a documented replacement point, and the `ARG` sat inside it, so a repository substituting its own region kept receiving the build argument while silently losing the declaration that receives it. The resulting failure is `failed to install MSRV toolchain '1.92'`, which sends the reader hunting a toolchain that is not the problem. Admit the root manifest to the build context and copy it to `/opt/anvil` instead. That directory is already the root the recipes resolve against: it is `justfile_directory()`, and it holds `justfiles/` and the toolchain pin. Copying the manifest alongside them completes it for the one question the setup asks, and the resolver reads it there exactly as it does on a developer's machine. `tools.just` therefore returns to what it was, minus the new `root-msrv` action the tag still needs. The variable, its `none` sentinel, its error message and the `--build-arg` all go away, and with them a general resolver's knowledge of containers. Dropping the `COPY` now fails with `Cargo.toml not found at repository root`, which names the missing thing. The workspace members the manifest lists stay out of the context: they are a checkout, and the image is not one. The one path that would need them, workspace MSRV validation, returns early whenever a root toolchain file selects the compiler, which this image requires and copies. The tag continues to hash the resolved value rather than the file. The manifest is the busiest file in a workspace while `rust-version` moves perhaps once in a repository's life, so hashing it would rename the image, and oblige a publisher to rebuild and republish, for a stream of edits that cannot alter a byte the image contains. Contracts follow the mechanism: the context admits the manifest, the setup region copies it, and no build argument survives anywhere. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2963084a-ef08-4cf7-adc6-e78e61556a84 * docs(cargo-anvil): trim the MSRV commentary to what is not inferable The prose ran about four lines per line of code, well past what the change needs. Keep the reasons a reader cannot recover from the code -- why the value is hashed rather than the manifest, why the members stay out of the context, why the declared value rather than the mapped one -- and drop the restatement around them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2963084a-ef08-4cf7-adc6-e78e61556a84 * test(cargo-anvil): pin the toolchain-file precondition the MSRV design rests on Workspace MSRV validation reads every member manifest, and the build context carries none. It stays out of reach only because a root toolchain file selects the compiler and makes the resolver return early -- which holds today by accident, since the unconditional COPY means a repository without one cannot build an image at all. Making that COPY conditional is a reasonable fix for those repositories, and it would silently put the branch back in reach of a partial workspace. State the dependency so that change has to confront this one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2963084a-ef08-4cf7-adc6-e78e61556a84 * fix(cargo-anvil): let a repository with no root toolchain file build the image The setup region named `rust-toolchain.toml` in a COPY and the tag recipe listed it as a required input, so a repository that pins its compiler by other means could not build an image at all: the build failed at the first COPY, and `anvil-container-tag` refused before that. `microsoft/oxidizer` is such a repository. No engine anvil supports offers a portable COPY of a path that may not exist, so the region names no input at all and copies the context whole. The ignore file already scopes that context to precisely the image's inputs, so what it admits and what the image contains become the same set. It now admits both toolchain-file spellings: naming only the TOML would leave a repository that pins with the extensionless file building an image whose compiler silently disagreed with its own checkout. The tag discovers the file rather than requiring it, in both spellings. Absence is one fewer record in the digest, so the two states cannot share a reference, and the file's mode still comes from the index. The ignore file stays a named, required input for the reason it always was. The design note this replaces claimed the toolchain file kept workspace MSRV validation out of reach of a memberless context. It does not: that validation hangs off the `-validate-prereqs` recipes, and `anvil-setup` -- the only thing the image runs -- depends on none of them. `just --dry-run anvil-setup binstall` reaches `_anvil-resolve-stable install-msrv` and nothing else, so no resolver change is needed and the doc and contract test now state the real reason. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cargo-anvil): delete the copied manifest once the setup has read it The tag hashes the declared MSRV rather than the manifest, so an edit that leaves rust-version alone computes the same tag. With the manifest left in place that tag named two different filesystems, and a reused or published image carried a Cargo.toml matching no checkout. Deleting it in the same RUN that reads it makes the identity exact again, and restores the design note's claim that those edits cannot alter a byte the image contains. Three statements in containers.md that still described the old three-path context are corrected alongside it, including the replacement rule a downstream catalog reads verbatim. Also drops an env_remove for ANVIL_ROOT_MSRV: nothing on this branch reads that variable, so the call could not affect what a fixture resolves and its comment described a mechanism that no longer exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2963084a-ef08-4cf7-adc6-e78e61556a84 * docs(cargo-anvil): trim the container commentary to what is not inferable The comments added with the previous commit ran well past the change. Several restated the assertion message directly below them; others explained what the code already says. Keep the reasons a reader cannot recover: why the context is copied whole, why both toolchain-file spellings are admitted and neither required, and why the tag discovers the file rather than requiring it. Drop the rest. No behaviour changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 80f12d44-5a83-4650-9806-d3e247175858 Copilot-Session: 2963084a-ef08-4cf7-adc6-e78e61556a84
1 parent 83e5402 commit 7dd2441

15 files changed

Lines changed: 672 additions & 187 deletions

File tree

.anvil.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
version = 1
22
tool = "anvil"
33
tool_version = "0.9.0"
4-
catalog_checksum = "sha256:baa2a5e479fe8f466e8f54e71ef0c498e07ac225b59a61a76ee99d46d5498650"
4+
catalog_checksum = "sha256:540ae76e257e9f1e7c16b3bf015bdc084a98be8ca98858c797c075eedde7edb0"
55

66
[[file]]
77
path = ".anvil/container/Dockerfile.dockerignore"
8-
checksum = "sha256:3907b9d8d15211ef5adff9ac2bcd23023797ed7b9961a59e28324bb7666ec340"
8+
checksum = "sha256:641cb14e197a1fd9c8e179b7e992aede15d80291841b82830d7283e1b751d3cb"
99

1010
[[file]]
1111
path = ".github/actions/anvil-impact/action.yml"
@@ -181,7 +181,7 @@ checksum = "sha256:6efd7378a2cd0f5d86519bd32fd86f2055a60191187dd77a8842b374b8eb7
181181

182182
[[file]]
183183
path = "justfiles/anvil/container.just"
184-
checksum = "sha256:6e2a24a20fb7c0a7a0bffa35f98b8b9e66f96bb6ec9cce8154199930546b375c"
184+
checksum = "sha256:b6e42555c5bb59f3040f665ec0ae9c9289a0d67490d72d758bfc1ec0dc6bd62a"
185185

186186
[[file]]
187187
path = "justfiles/anvil/dev/build.just"
@@ -269,7 +269,7 @@ checksum = "sha256:7b409a9b560c214e10b50f74330fb6f8c0c12c3d83494e0dcf016f2411b50
269269
[[region]]
270270
host = ".anvil/container/Dockerfile"
271271
id = "anvil-container-setup"
272-
checksum = "sha256:6a516c50a5a717a2b7635e043f2cc33fd872bdf5950a456718518b1af0bf75f9"
272+
checksum = "sha256:0e777d1a0b54ada564aa48b8b23c9f035d07d0f6d49a83a4ebca76773a7fd29a"
273273

274274
[[region]]
275275
host = ".anvil/container/Dockerfile"

.anvil/container/Dockerfile

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ RUN curl -fsSLo /tmp/cargo-binstall.tgz \
7878
# <<< anvil-managed: anvil-container-tools
7979

8080
# >>> anvil-managed: anvil-container-setup
81+
# The context is copied whole rather than input by input, because one of the
82+
# inputs is optional: a repository that pins its compiler by other means owns no
83+
# root toolchain file, and a `COPY` of a path that may not exist is not portable
84+
# across the engines anvil supports. `.anvil/container/` rides along as a
85+
# consequence; the image never reads it.
86+
#
8187
# The whole recipe tree is copied because `just` parses it to reach the install
8288
# recipes.
8389
#
@@ -94,24 +100,37 @@ RUN curl -fsSLo /tmp/cargo-binstall.tgz \
94100
# volume over each, and an engine seeds a new volume from the image path it
95101
# covers; a path that does not exist seeds as root-owned 0755, which the
96102
# `--user` mapping cannot write, so the first cargo fetch fails with EACCES.
97-
# The root manifest is copied for the MSRV, the one version `anvil-setup`
103+
# The root manifest is admitted for the MSRV, the one version `anvil-setup`
98104
# installs that is declared in repository source rather than pinned in
99105
# `versions.just`. `/opt/anvil` is the root the recipes already resolve against,
100106
# so the resolver reads it there with no container-specific path. The workspace
101-
# members it names are not copied and are not needed: the toolchain file selects
102-
# the compiler, which is the branch that skips workspace MSRV validation.
107+
# members it names are not admitted and are not needed: the one path that reads
108+
# them is workspace MSRV validation, which only
109+
# `anvil-tool-rustc-validate-prereqs` invokes, and nothing `anvil-setup`
110+
# reaches depends on it.
103111
#
104112
# It is deleted once the setup has read it, so it does not survive into the
105113
# finished image. The tag hashes the declared MSRV rather than the file, so an
106114
# unrelated dependency edit computes the same tag; a manifest left in place
107115
# would make that tag name two different filesystems, and a reused or published
108116
# image would carry a manifest that does not match any checkout.
117+
#
118+
# `rustup` was initialized with `--default-toolchain none`, so the image has no
119+
# default until something names one. Rustup sets it as a side effect of the
120+
# first `rustup toolchain install` that finds none set, which today is the
121+
# MSRV; that is an accident of the order `anvil-setup` reaches the install
122+
# recipes in, not something either side states. A checkout with a root
123+
# toolchain file never notices, because that file overrides the default. One
124+
# without has nothing else to select a compiler, so plain `cargo` in the
125+
# container would follow whichever toolchain the setup graph happened to
126+
# install first. The default is named here instead, from the same declared MSRV
127+
# the setup installs, while the manifest it is read from is still present.
109128
WORKDIR /opt/anvil
110-
COPY justfiles ./justfiles
111-
COPY rust-toolchain.toml ./
112-
COPY Cargo.toml ./
129+
COPY . ./
113130
RUN printf "import 'justfiles/anvil/mod.just'\n" > Justfile \
114131
&& just anvil-setup binstall \
132+
&& msrv="$(just _anvil-resolve-stable root-msrv)" \
133+
&& if [ "${msrv}" != "none" ]; then rustup default "${msrv}"; fi \
115134
&& rm -f Cargo.toml \
116135
&& rm -rf "${CARGO_HOME}/registry/cache" "${CARGO_HOME}/registry/src" \
117136
&& rm -f "${CARGO_HOME}/credentials" "${CARGO_HOME}/credentials.toml" "${HOME:-/root}/.netrc" /root/.netrc \

.anvil/container/Dockerfile.dockerignore

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
# `.dockerignore`, so this scopes the exec-image build context without the
88
# repository having to own a root ignore file or having one silently overridden.
99
#
10-
# The build context is the repository root but the image only needs two things.
11-
# Excluding everything else keeps a cold build from streaming the whole
12-
# worktree (and every stale `target/`) to the daemon.
10+
# The build context is the repository root but the image needs a small, named
11+
# set out of it. Excluding everything else keeps a cold build from streaming the
12+
# whole worktree (and every stale `target/`) to the daemon -- and, because the
13+
# setup region copies the context whole, this file decides what the image
14+
# contains.
1315
#
1416
# The context is narrowed to `justfiles/anvil/` rather than all of `justfiles/`
1517
# so that a cold build does not stream unrelated trees to the daemon. The
@@ -29,6 +31,11 @@
2931
# The root `Cargo.toml` is admitted for the declared MSRV, the only build input
3032
# that lives in repository source rather than in the recipe tree. The workspace
3133
# members it names stay out: they are a checkout, and the image is not one.
34+
#
35+
# Both spellings are admitted and neither is required: a repository that pins
36+
# by other means gets an image whose compiler is the declared MSRV, and
37+
# admitting only the TOML would give one that pins with the extensionless file
38+
# an image that disagreed with its own checkout.
3239
*
3340
!justfiles
3441
justfiles/*
@@ -39,4 +46,5 @@ justfiles/anvil/**/*.anvil-proposed
3946
!.anvil/container
4047
.anvil/container/**/*.anvil-proposed
4148
!rust-toolchain.toml
49+
!rust-toolchain
4250
!Cargo.toml

crates/cargo-anvil/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,10 @@ just anvil-container cargo build
348348
```
349349

350350
The image contains the same tool versions used by the generated checks.
351-
Its tag is derived from the Dockerfile, toolchain, and generated recipe
352-
tree, so changes to those inputs select a different image. Docker is
351+
Its tag is derived from the Dockerfile and its ignore file, the generated
352+
recipe tree, the declared root MSRV, and a root `rust-toolchain` file where
353+
the repository owns one, so changes to those inputs select a different
354+
image. Docker is
353355
supported; Podman is available on a best-effort basis. Repositories that
354356
need private feeds can add a host-side credential hook without embedding
355357
credentials in the image or command line. See
@@ -398,7 +400,7 @@ More detailed design and operational guidance is available in the
398400
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/ox-tools/tree/main/crates/cargo-anvil">source code</a>.
399401
</sub>
400402

401-
[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQblRYhli3L8qob_NSi_WYo69wbWnMVqZw3jJwb3u56HnT6RDphYvRhcoQb_Wi2RvJKqLgb6U0-tQFqaYMbiv-kD9siQ44bVYhqNqZVUZBhZIGDa2NhcmdvLWFudmlsZTAuOS4wa2NhcmdvX2Fudmls
403+
[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQblRYhli3L8qob_NSi_WYo69wbWnMVqZw3jJwb3u56HnT6RDphYvRhcoQbBALu36V1VAYbFUDdfp-8dCobobFjKbRep8AbYNMPgi_aMhFhZIGDa2NhcmdvLWFudmlsZTAuOS4wa2NhcmdvX2Fudmls
402404
[__link0]: https://github.com/casey/just
403405
[__link1]: https://rust-lang.github.io/rustfmt/
404406
[__link10]: https://embarkstudios.github.io/cargo-deny/

crates/cargo-anvil/docs/design/containers.md

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ Both are addressed by executing the recipe unchanged inside an image built from
5353
bodies are identical in either mode, and cloud workflows are unaffected: they run the same recipes natively on their
5454
own agents. The image is pinned to resemble that environment, not to reproduce it.
5555

56-
Image construction has a deliberately stricter compiler contract than native execution: the repository must own
57-
`rust-toolchain.toml`. The build context admits that file but not `rust-toolchain`, and the build does not inherit the
58-
host's `RUSTUP_TOOLCHAIN`; rustup therefore selects the image compiler from the repository-owned TOML file.
56+
Image construction does not require the repository to own a `rust-toolchain.toml`. Where one exists — either
57+
spelling — the context admits it and rustup selects the image compiler from it, exactly as it would on a host. Where a
58+
repository pins its compiler by other means and owns neither, the image compiler is the declared root MSRV, which is
59+
again what a host resolves there. The build does not inherit the host's `RUSTUP_TOOLCHAIN` in either case.
5960

6061
## 2. Command surface
6162

@@ -133,7 +134,7 @@ never read, rewritten or reordered.
133134
| `anvil-container-base-image` | `ARG BASE_IMAGE`, pinned to a digest. | A second `ARG BASE_IMAGE=…` to build on a different base. |
134135
| `anvil-container-base` | `FROM`, the version pins for `pwsh`, `just`, `rustup` and `cargo-binstall`, and the `ENV` block. | Anything the first network access needs: a root CA, `http_proxy`, an internal package mirror. |
135136
| `anvil-container-tools` | System packages and those four tools. | Libraries a catalog tool needs to compile, for tools `binstall` has no prebuilt binary for. |
136-
| `anvil-container-setup` | `COPY` of the recipe tree and the root manifest, then `just anvil-setup`. | Anything the repository's own checks need at run time. |
137+
| `anvil-container-setup` | `COPY` of the scoped build context, then `just anvil-setup`. | Anything the repository's own checks need at run time. |
137138
| `anvil-container-entry` | `ANVIL_IN_CONTAINER`, `WORKDIR`, `CMD`. ||
138139

139140
Each gap sits at the only point in the build where its kind of addition works: a certificate has to land before the
@@ -191,23 +192,36 @@ and copied to `/opt/anvil`, the root the recipes already resolve against, so the
191192
container-specific path in it.
192193

193194
The workspace members it names are not admitted: they are a checkout, and the image is not one. The one path that
194-
would need them, workspace MSRV validation, returns early whenever a root toolchain file selects the compiler.
195+
would need them is workspace MSRV validation, which the image never reaches: `anvil-tool-rustc-validate-prereqs` is its
196+
only caller, and nothing `anvil-setup` reaches depends on that recipe. Inside a running
197+
container that validation does execute, against `/workspace` — a real checkout, with its members.
195198

196199
The manifest is deleted once the setup has read it, so it is in the build context but not in the finished image. That
197200
keeps the tag honest: it hashes the declared MSRV rather than the file, so a dependency edit computes the same tag,
198201
and nothing is left behind for that tag to misdescribe.
199202

200-
That makes the toolchain file a precondition of this design rather than a convenience: the image requires one, copies
201-
it, and relies on it to keep workspace validation out of reach of a context that has no members. A repository without
202-
one cannot build the image today, because the `COPY` above is unconditional. Should that become conditional, this
203-
design needs revisiting alongside it.
204-
205-
`Dockerfile.dockerignore` scopes the build context to `justfiles/anvil/`, `.anvil/container/`, `rust-toolchain.toml`
206-
and the root `Cargo.toml`, denying everything else. The recipe tree is copied whole because `just` has to parse it to run
207-
`anvil-setup`, and it is hashed whole (§4). `.anvil/container/` is admitted so a gap can `COPY` a file placed beside
208-
the Dockerfile; anvil's own `.anvil-proposed` review artifacts are excluded from both the context and the digest.
209-
BuildKit reads `<dockerfile>.dockerignore` in preference to a root `.dockerignore`, so the repository neither needs to
210-
own a root ignore file nor can have one silently override this.
203+
**The image names its default toolchain.** `rustup` is initialized with `--default-toolchain none`, and rustup then
204+
sets the default as a side effect of the first `rustup toolchain install` that finds none set. That is the MSRV today,
205+
but only because of the order `anvil-setup` reaches the install recipes in. A checkout with a root toolchain file never
206+
notices, because the file overrides the default; a checkout without one has nothing else to select a compiler, so plain
207+
`cargo` inside the container would follow whichever toolchain the setup graph installed first. The setup region
208+
therefore runs `rustup default` on the declared MSRV, read from the manifest before it is deleted, so an arbitrary
209+
Rust command in the container uses the compiler the repository declared. A repository declaring no MSRV is left alone;
210+
the setup installs no stable toolchain for it either.
211+
212+
The setup region copies the context whole rather than naming each input, because one input is optional. A repository
213+
that pins its compiler by other means owns no root toolchain file, and a `COPY` of a path that may not exist is not
214+
portable across the engines anvil supports, so naming the file would leave exactly those repositories unable to build
215+
an image at all. The ignore file already scopes the context to precisely the image's inputs, so deferring to it makes
216+
what the context admits and what the image contains the same set. `.anvil/container/` rides along with it; that is the
217+
committed input a gap `COPY`s from, and the image never runs it.
218+
219+
`Dockerfile.dockerignore` scopes the build context to `justfiles/anvil/`, `.anvil/container/`, a root toolchain file in
220+
either spelling, and the root `Cargo.toml`, denying everything else. The recipe tree is copied whole because `just` has
221+
to parse it to run `anvil-setup`, and it is hashed whole (§4). `.anvil/container/` is admitted so a gap can `COPY` a
222+
file placed beside the Dockerfile; anvil's own `.anvil-proposed` review artifacts are excluded from both the context
223+
and the digest. BuildKit reads `<dockerfile>.dockerignore` in preference to a root `.dockerignore`, so the repository
224+
neither needs to own a root ignore file nor can have one silently override this.
211225

212226
## 4. Image identity
213227

@@ -219,7 +233,7 @@ define the image. The name derives from the repository directory (§5.1).
219233
| Input | Hashed |
220234
| --- | --- |
221235
| every file under `.anvil/container/` | always |
222-
| `rust-toolchain.toml` | always |
236+
| `rust-toolchain.toml` or `rust-toolchain` | when the repository owns one |
223237
| every file under `justfiles/anvil/` | always |
224238
| the declared root MSRV | always |
225239

@@ -230,6 +244,10 @@ change the image under a reference that already resolves, which is the hole the
230244
Dockerfile is still a hard error, checked by name: the walk alone would let it contribute nothing and yield a confident
231245
tag for an image that cannot be built.
232246

247+
The root toolchain file is the one input whose absence is not an error. It is discovered rather than required, in both
248+
spellings, and a repository that owns none contributes one fewer record to the digest — a state distinct from owning
249+
one, so the two cannot share a tag.
250+
233251
The recipe tree is hashed in full. `just anvil-setup` reaches the install recipes through the tier, group and check
234252
recipes, so the routing decides *whether* a tool is installed just as surely as `tools.just` decides *how*: dropping an
235253
`anvil-<check>-setup` dependency from a group changes the installed set while `tools.just` and `versions.just` stay
@@ -421,7 +439,8 @@ image, executes the requested command directly instead of launching another cont
421439
## 6. Engines and host setup
422440

423441
anvil installs nothing and manages no virtual machine. Beyond the engine, the host needs `just` and PowerShell Core
424-
(`pwsh`), which every generated recipe requires, and the repository must own a `rust-toolchain.toml`.
442+
(`pwsh`), which every generated recipe requires. The repository needs a declared root MSRV; a `rust-toolchain.toml` is
443+
honoured where it exists but is not required (§1).
425444

426445
| | Docker | Podman |
427446
| --- | --- | --- |
@@ -637,10 +656,12 @@ Replacing a *region* rather than the whole file is what makes a downstream catal
637656
private-environment catalog rewrites the base and tool layers and nothing else. Replacing `dockerfile_setup()` reintroduces the
638657
second tool list the design exists to avoid, and is almost never right.
639658

640-
**A replacement must keep the ignore file in step.** A region that `COPY`s anything outside `justfiles/anvil/`,
641-
`.anvil/container/`, `rust-toolchain.toml` and the root `Cargo.toml` must also replace
642-
`artifacts::container::dockerignore()` (§3), or the added paths never reach the build context and the build fails on a
643-
missing file.
659+
**A replacement must keep the ignore file in step.** The setup region `COPY`s the context whole, so the ignore file is
660+
what decides the image's contents. A region that needs anything outside `justfiles/anvil/`, `.anvil/container/`, a root
661+
toolchain file and the root `Cargo.toml` must also replace `artifacts::container::dockerignore()` (§3), or the added
662+
paths never reach the build context. Widening it also moves content into the image that the digest does not hash: the
663+
walk covers `.anvil/container/` and `justfiles/anvil/` and nothing else, so a newly admitted tree has to be brought
664+
under one of them, or the tag stops covering what the image contains.
644665

645666
**Anything extra it copies is digested, provided it lives under `.anvil/container/`.** The hashed set is that whole
646667
directory (§4.1), so an installer script, a config file or a certificate placed beside the Dockerfile is an input:
@@ -651,10 +672,11 @@ manual `ANVIL_CONTAINER_NO_CACHE=1`.
651672
`justfiles/anvil/` must contain `.just` recipes and nothing else, which `CatalogBuilder::build` enforces for
652673
catalog-owned files. The reason is legibility rather than identity: the directory is the recipe tree, `just` parses
653674
every file the image copies, and a catalog that hides an installer script there makes the tool set harder to reason
654-
about than one that keeps it in `.anvil/`. Identity is safe either way, because the digest covers every file the build
655-
context admits and the image keeps (§4.1), not only the recipes — a repository that adds a non-recipe file by hand
675+
about than one that keeps it in `.anvil/`. Identity is safe either way, because the digest walks that whole directory
676+
rather than only its recipes (§4.1) — a repository that adds a non-recipe file by hand
656677
still renames the tag when it edits it. The root `Cargo.toml` is the one admitted file the digest does not cover as
657-
bytes, and it is also the one the setup deletes once read, so it is in no image for the tag to misdescribe.
678+
bytes, and it is also the one the setup deletes once read, so it is in no image for the tag to misdescribe. Both
679+
statements describe the ignore file anvil ships; a replacement that admits another tree carries the obligation above.
658680

659681
A fork inherits everything else: the recipes, the identity scheme, the cache volumes, the mounts, and the re-entry
660682
guard. A different base OS with a different toolchain source is two region replacements plus one hook.

crates/cargo-anvil/src/anvil/artifacts/container.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,11 @@ mod tests {
346346
// the same recipe the checks use, from the same generated pins.
347347
let composed = composed_dockerfile();
348348
assert!(composed.contains("just anvil-setup binstall"));
349-
assert!(composed.contains("COPY justfiles"));
350-
assert!(composed.contains("COPY rust-toolchain.toml"));
349+
assert!(composed.contains("COPY . ./"));
350+
assert!(
351+
!composed.contains("COPY rust-toolchain"),
352+
"naming the toolchain file would leave a repository without one unable to build"
353+
);
351354
// The re-entry guard the recipe relies on to avoid nesting.
352355
assert!(composed.contains("ENV ANVIL_IN_CONTAINER=1"));
353356
}
@@ -422,7 +425,9 @@ mod tests {
422425
#[test]
423426
fn build_context_admits_only_what_the_image_copies() {
424427
assert!(DOCKERIGNORE.contains("!justfiles"));
428+
assert!(DOCKERIGNORE.contains("!Cargo.toml"));
425429
assert!(DOCKERIGNORE.contains("!rust-toolchain.toml"));
430+
assert!(DOCKERIGNORE.contains("!rust-toolchain\n"));
426431
}
427432

428433
#[test]

0 commit comments

Comments
 (0)