Cargo.lock shape: why it's not just a Cargo.lock
The committed Cargo.lock in a miniextendr-based R package is not a vanilla Cargo.lock. It's in a specific shape — tarball-shape — that the offline install path needs. Every R package built with miniextendr (the example rpkg/ in this repo, and any package scaffolded via minirextendr) ships its src/rust/Cargo.lock in this shape.
The committed Cargo.lock in a miniextendr-based R package is not a vanilla
Cargo.lock. It’s in a specific shape — tarball-shape — that the offline
install path needs. Every R package built with miniextendr (the example rpkg/
in this repo, and any package scaffolded via minirextendr) ships its
src/rust/Cargo.lock in this shape.
If you’ve never thought about it, it’s because the maintainer recipes
(just vendor, miniextendr_vendor()) produce the right shape automatically.
But every cargo build that runs with the dev [patch."git+url"] override
silently dirties it, and the canonical regen is just vendor or
miniextendr::miniextendr_vendor(). The pre-commit hook +
just lock-shape-check keep you honest.
This page explains what the shape is, why it exists, and how to recover when it drifts.
🔗What “tarball-shape” means
One invariant on src/rust/Cargo.lock:
- No
source = "path+..."entries for any crate that’s published or workspace-internal to the miniextendr framework (miniextendr-api,miniextendr-lint,miniextendr-macros). These crates must carrysource = "git+https://github.com/A2-ai/miniextendr#<commit>".
Note:
checksum = "..."lines are now allowed in the committed lock.cargo-revendorrecomputes valid.cargo-checksum.jsonfiles after CRAN-trim, with the originalpackagefield (matching the registry checksum) preserved and thefilesmap updated to reflect post-trim disk contents. Cargo’s offline source-replacement verifies both successfully.
just lock-shape-check (and the equivalent pre-commit hook) asserts the
path+ invariant only.
🔗Why the invariant
🔗source = "git+url#commit" for framework crates
The dev workflow uses cargo’s [patch."https://github.com/A2-ai/miniextendr"]
mechanism (in src/rust/.cargo/config.toml) to redirect
miniextendr-{api,lint,macros} to either monorepo siblings (in this repo) or
to a checked-out copy. When cargo resolves the lock under that patch, it
records the resolved entries with source = "path+file:///...".
That path+... entry is fatal at offline install time: the install
machine doesn’t have /home/your-username/checkout/.... Even if it did,
the path would be different. The lock has to record a portable identifier
that source replacement can match against vendored sources — and that’s the
git URL plus commit hash.
So the regen flow is:
- Move
.cargo/config.tomlaside (so the patch override is inactive). - Regenerate the lockfile against the bare git URL — entries for
miniextendr crates resolve to
source = "git+https://...#<commit>". - Restore
.cargo/config.toml. - Run
cargo revendor— it recomputes.cargo-checksum.jsonfor each crate after CRAN-trim, so the lock’schecksum =lines stay valid.
That’s exactly what just vendor (in this repo) and
miniextendr::miniextendr_vendor() (for scaffolded packages) do.
🔗When does the lock drift?
Any cargo invocation that runs with the patch override active will rewrite the lock:
just check/just clippy/just test(rpkg variants)cargo build --manifest-path rpkg/src/rust/Cargo.tomlR CMD INSTALLin source mode (noinst/vendor.tar.xz)devtools::document()/devtools::install()/devtools::test()just devtools-document(because it shells out to the above)
After any of these, you’ll see (under git diff):
source = "git+...#<commit>"lines deleted fromminiextendr-{api,lint,macros}(they become path deps via[patch])
checksum = "..." lines may also be added/changed by cargo build, but
those are now harmless — just vendor will put them back in sync.
This drift is expected and harmless for local iteration. Don’t commit it.
The pre-commit hook will block the path+ drift. Re-run the canonical
regen (just vendor or just update) before staging.
🔗Recovering a drifted lock
# Easiest path — full regen, also rebuilds inst/vendor.tar.xz
just vendor
# Lock-only regen (skips the heavy vendor/ + tarball step)
just update # this repo
miniextendr::miniextendr_vendor() # scaffolded packages
# Manual minimum (what the recipes do under the hood)
mv rpkg/src/rust/.cargo/config.toml /tmp/cargo-config.toml.bak
rm rpkg/src/rust/Cargo.lock
cargo generate-lockfile --manifest-path rpkg/src/rust/Cargo.toml
mv /tmp/cargo-config.toml.bak rpkg/src/rust/.cargo/config.toml
# No checksum strip needed — cargo-revendor handles it during `just vendor`
# Verify
just lock-shape-check🔗Verifying
just lock-shape-check in this repo, or for any miniextendr-based package:
# Equivalent shell check
grep -q 'source = "path+' src/rust/Cargo.lock && echo "BAD: contains path+ sources"🔗What about inst/vendor.tar.xz?
The vendor tarball is a separate artifact:
- This repo: gitignored. Regenerated by CI’s
just vendorbefore every R CMD check. Never committed (binary blob, 22 MB/commit historically). - Scaffolded packages: typically also gitignored — generated by
miniextendr_vendor()only at release time. CRAN submissions ship the tarball inside the source.tar.gz(because it lives atinst/vendor.tar.xz), but it’s regenerated, not tracked.
The lockfile’s tarball-shape is independent of whether vendor.tar.xz
currently exists. The lock just has to be in the shape that would work
when the tarball is present and source replacement kicks in. The pre-commit
hook + lock-shape-check enforce this even when the tarball is absent.
🔗See also
- CRAN compatibility — the install-mode decision tree, what triggers source vs tarball mode, the maintainer release workflow.
cargo-revendorREADME — the vendoring tool that produces the matchedvendor/tree from a tarball-shape lock.- Cargo book: source replacement — the offline install mechanism that depends on the lock being in this shape.