docs: resolve the cargo target dir in rust build steps - #762
Conversation
Cargo writes to the workspace target directory, which is not `./target` when the canister crate is a workspace member, or when `CARGO_TARGET_DIR` or `build.target-dir` is set. Build steps that hardcode `target/...` compile successfully and then fail to find the wasm. Resolve it via `cargo metadata`, matching the official rust recipe, in the examples and in the guides that teach writing a rust build step. The lookup and the command using it share one line because each `commands:` entry runs in its own shell. Also bumps the `@dfinity/rust` pins from v3.0.0 to v3.4.0; the equivalent fix landed in the recipe in v3.1.0, so the pinned version predates it.
Applies the same `cargo metadata` lookup to the snippets where a rust build is scaffolding for another topic, so every copyable build step in the docs is correct regardless of where cargo writes. The two `path:` fields on plugin sync steps keep a plain relative path: the field is not expanded, which is now called out in the sync plugin guide.
There was a problem hiding this comment.
🟢 Approval recommended
The changes consistently address the documented Cargo target-dir mismatch with low risk, with only a minor hardening suggestion noted in review comments.
Pull request overview
This PR updates Rust build examples and documentation to correctly locate Cargo’s actual target directory (including workspace and CARGO_TARGET_DIR/build.target-dir cases), preventing build steps from succeeding but then failing to find the produced .wasm.
Changes:
- Replace hardcoded
target/...paths with acargo metadata-derived target directory in Rust build snippets across docs and examples. - Bump pinned
@dfinity/rustrecipe references fromv3.0.0tov3.4.0throughout documentation and examples. - Update the examples validation script to locate the built
icpbinary via Cargo’s target directory rather than assuming./target/debug.
File summaries
| File | Description |
|---|---|
| scripts/validate-examples.sh | Resolve Cargo target dir before adding the icp binary directory to PATH. |
| examples/icp-sync-plugin/icp.yaml | Use Cargo metadata target dir for the canister wasm move step. |
| examples/icp-rust/icp.yaml | Use Cargo metadata target dir for the canister wasm move step. |
| examples/icp-rust-recipe/README.md | Bump the documented recipe pin to @dfinity/rust@v3.4.0. |
| examples/icp-rust-recipe/icp.yaml | Bump the recipe pin to @dfinity/rust@v3.4.0. |
| docs/reference/environment-variables.md | Update Rust build snippet to copy wasm from Cargo metadata target dir. |
| docs/reference/configuration.md | Update Rust build snippets and recipe pin references to v3.4.0. |
| docs/reference/canister-settings.md | Update Rust build snippet to copy wasm from Cargo metadata target dir. |
| docs/migration/from-dfx.md | Update Rust recipe pins and build snippet to use Cargo metadata target dir. |
| docs/guides/writing-sync-plugins.md | Clarify where plugin wasm lands and limitations of sync.steps[].path expansion. |
| docs/guides/using-recipes.md | Bump @dfinity/rust pin and update Rust build snippet to use metadata target dir. |
| docs/guides/managing-environments.md | Update Rust build snippet to copy wasm from Cargo metadata target dir. |
| docs/guides/creating-templates.md | Bump Rust recipe pin in template example to v3.4.0. |
| docs/guides/creating-recipes.md | Update recipe examples to use metadata target dir; add guidance explaining why. |
| docs/concepts/recipes.md | Bump recipe pins and update Rust build snippet to use metadata target dir. |
| docs/concepts/project-model.md | Update Rust build snippet to copy wasm from Cargo metadata target dir. |
| docs/concepts/build-deploy-sync.md | Update Rust build snippet to copy wasm from Cargo metadata target dir. |
Review details
- Files reviewed: 17/17 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
An unmatched sed pattern left TARGET_DIR empty, which silently prepended `/debug` to PATH and surfaced later as a confusing `icp: not found`.
The lookup and the copy have to share one shell, but they do not have to share one line: a `|-` block is a single `commands:` entry, so the two commands stay readable and `$TARGET_DIR` still carries across. Switches the four remaining `mv` uses to `cp`, matching the recipe. Cargo materializes the top-level artifact from `deps/`, so a build step has no reason to remove its output. `docs/concepts/recipes.md` keeps the one-line form: it shows what the official recipe expands to and should stay identical to it.
The page says the recipe expands to "something like" and shows two commands where the real recipe expands to nineteen, so it was never a byte-accurate mirror - no reason for the only remaining one-liner to live here.
The bump to v3.4.0 made two statements false: `examples/icp-rust-recipe` described `package` as a required field in both its manifest comment and its README. It defaults to the canister name. Documents the default in the rust recipe section of the usage guide, and drops the declaration from the five snippets where it just repeated the canister name. Kept where the crate name genuinely differs, and in the custom-recipe examples, whose own `package` parameter is unrelated.
There was a problem hiding this comment.
🟢 Approval recommended
The documentation, examples, and validation script consistently resolve Cargo output locations, and the referenced recipe release exists.
Review details
- Files reviewed: 17/17 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Follow-up after reviewing #760.
Rust build steps that hardcode
target/...compile successfully and then fail to find the wasm, because cargo does not necessarily write to./target.The dominant case is not an exotic env var. A build step runs in the canister directory (
crates/icp/src/operations/build.rs:62passescanister_path), so in the standard Rust monorepo layout the path is simply wrong:CARGO_TARGET_DIRandbuild.target-dirmove it too. Reproduced againstexamples/icp-rust:The official rust recipe fixed this in
rust-v3.1.0. This applies the same lookup wherever we still hardcode the path, and bumps our pins.Changes
docs/resolve the target dir viacargo metadata, in a|-block so the lookup and the copy read as two lines.examples/icp-rust,examples/icp-sync-pluginlikewise. Both examples are flat, so this is defensive for them as-run — it matters because they are templates people copy into projects that are not flat.scripts/validate-examples.shno longer assumes./target/debug. This one was a live bug in our own tooling.@dfinity/rustpin bumped v3.0.0 → v3.4.0, in both thetype:and release-URL forms. v3.0.0 predates the recipe's own fix.docs/guides/creating-recipes.mdgains a short section explaining the rule and the|-block form.packageis documented as optional, which the bump exposed:examples/icp-rust-recipecalled it a required field in both its manifest comment and its README, but it has defaulted to the canister name sincerust-v3.3.0. Dropped from the five snippets where it only repeated the canister name; kept where the crate name genuinely differs, and in the custom-recipe examples whose ownpackageparameter is unrelated.Notes for the reviewer
commands:entry, written as a|-block. They must share a shell — each entry gets its ownsh -c, so a naive split leaves$TARGET_DIRempty and fails withcp: /wasm32-unknown-unknown/…: No such file or directory— but they do not have to share a line.cp, notmv, matching the recipe: cargo materializes the top-level artifact fromdeps/, so a build step has no reason to remove its output. (It does not cause a rebuild either way — cargo re-creates the top-level file in ~0.03s.)sedis inherited from the recipe, which avoidsjq"in order not to introduce a dependency on jq". There is no better form on stable:--artifact-dirwould remove the lookup entirely but is nightly-only (cargo#6790, open since 2019),cargo locate-project --workspacemissesCARGO_TARGET_DIR, andtr/cutvariants break on paths containing a comma.validate-examples.shguards against an unresolved lookup, because an empty result there was genuinely silent — it prepended/debugtoPATHand surfaced later asicp: not found. A build step needs no guard: the lookup only runs after a successfulcargo build(a step aborts on its first failing command), and an empty result makescpfail and fail the step.path:on a plugin sync step is a plain relative path with no expansion (crates/icp/src/manifest/adapter/plugin.rs:136), so it cannot reference the target dir. The sync plugin guide now says so.Verification
icp buildonexamples/icp-rust, with and withoutCARGO_TARGET_DIR: fails onmain, passes here, no straytarget/in the source tree, artifact left in place bycp.commands:entries does fail, and that the|-block does not.icp buildwithpackageomitted and the canister named after the crate: the recipe derives--package icp-canisterandicp_canister.wasm, and the build succeeds.icp project showon the changed examples; the bumped pin resolves and renders v3.4.0's lookup.Follow-up (separate repo)
These docs now differ from the recipe itself, which still uses the one-line form. The
|-block is worth proposing upstream inicp-cli-recipes.