Reference page
Vendoring and CRAN Release Prep
How miniextendr packages its dependencies for CRAN offline builds.
How miniextendr packages its dependencies for CRAN offline builds.
πBackground
CRAN requires packages to build offline β no network access during
R CMD INSTALL. Rust packages depend on crates from crates.io plus
miniextendrβs own workspace crates. The vendoring system pre-bundles
all of these into inst/vendor.tar.xz so the package is self-contained.
πQuick Reference
# CRAN release prep (from monorepo root):
just vendor # 1. Create inst/vendor.tar.xz
just configure-cran # 2. Configure in prepare-cran mode
just r-cmd-build # 3. Build tarball
just r-cmd-check # 4. Check the built tarball (--as-cran)πPREPARE_CRAN
PREPARE_CRAN is an environment variable that triggers the prepare-cran
build context. It has highest precedence over all other build context
signals.
πWhat It Does
When PREPARE_CRAN=true:
- Sets
BUILD_CONTEXT=prepare-cranβ overrides bothNOT_CRANand auto-detection - Sets
NOT_CRAN=falseβ derived from the build context for backward compatibility - Enables
--offlineβ cargo must resolve all deps from vendored sources - Keeps
.cargo/config.tomlβ for vendored source directory replacement - Unpacks
inst/vendor.tar.xzβ ifvendor/doesnβt already exist - Rewrites
Cargo.tomlβ git deps become path deps pointing tovendor/ - Strips
[patch]section β monorepo paths are not available - Adds
[patch.crates-io]β for transitive miniextendr deps in vendor - Regenerates
Cargo.lockβ from vendored sources only
πHow to Use
# Via justfile (recommended):
just configure-cran
# Manual (from rpkg/):
cd rpkg && PREPARE_CRAN=true bash ./configure
# With explicit vendor step:
just vendor && just configure-cranπWhere Itβs Referenced
| File | Purpose |
|---|---|
rpkg/configure.ac | Build context resolution (lines 33-38, 139-140) |
justfile | configure-cran recipe |
rpkg/bootstrap.R | Sets PREPARE_CRAN=false to prevent accidental inheritance during devtools workflows |
CLAUDE.md | Build context table in documentation |
πSafety: bootstrap.R
rpkg/bootstrap.R explicitly sets PREPARE_CRAN=false:
env <- c(NOT_CRAN = "true", PREPARE_CRAN = "false")
This prevents accidental CRAN-mode configuration during devtools::install()
or devtools::document(), which trigger bootstrap before configure. Without
this guard, an inherited PREPARE_CRAN=true from a parent shell could cause
devtools workflows to fail (they need network access for [patch] resolution).
πBuild Contexts
The configure script resolves one of four build contexts. PREPARE_CRAN is
one input; the full truth table is:
PREPARE_CRAN=true β prepare-cran
NOT_CRAN explicit=true + monorepo present β dev-monorepo
NOT_CRAN explicit=true + monorepo absent β dev-detached
NOT_CRAN explicit=false + any β vendored-install
auto-detect: monorepo present β dev-monorepo
auto-detect: vendor hint present β vendored-install
auto-detect: neither β dev-detached| Context | Cargo Config | [patch] | Vendor | Offline |
|---|---|---|---|---|
dev-monorepo | Removed | Kept (path deps) | Cleaned | No |
dev-detached | Removed | Stripped | Cleaned | No |
vendored-install | Kept | Rewritten | Unpacked/fetched | Yes |
prepare-cran | Kept | Rewritten | Unpacked/fetched | Yes |
πdev-monorepo (default for developers)
Normal development in the monorepo. Cargo resolves workspace crates via
[patch."https://..."] paths in Cargo.toml that point to sibling
directories (../../miniextendr-api, etc.). No vendoring, no offline flag.
just configure # or: cd rpkg && NOT_CRAN=true bash ./configureπdev-detached
The example package directory (rpkg/) exists outside the monorepo (e.g., after scaffolding with
minirextendr). Cargo uses git deps directly from the Cargo.toml. The [patch]
section is stripped since monorepo paths are unavailable.
πvendored-install
Triggered when NOT_CRAN is explicitly false, or auto-detected when
vendor/ or inst/vendor.tar.xz exists but no monorepo is present.
This is what CRAN and R CMD INSTALL from a tarball see.
πprepare-cran
Explicit CRAN release preparation. Functionally identical to vendored-install
but triggered by intent (PREPARE_CRAN=true) rather than detection. Use this
when preparing a submission to guarantee the correct build context regardless
of what else exists on disk.
πVendor Pipeline
πStep 1: just vendor
Creates rpkg/inst/vendor.tar.xz containing all dependencies:
just vendor
β
ββ Rscript rpkg/tools/vendor-crates.R pack
β
ββ cargo tree (discover reachable local path crates)
β
ββ generate temporary cargo config
β ([patch.crates-io] for unpublished local crates)
β
ββ cargo package --no-verify (local crates β .crate archives)
β
ββ cargo vendor (crates.io deps β rpkg/vendor/)
β
ββ Extract .crate archives on top of vendor/
β (workspace crates as vendored sources)
β
ββ Strip checksums from Cargo.lock
β
ββ Clean vendor/ (remove tests, benches, examples, dotfiles)
β
ββ tar -cJf rpkg/inst/vendor.tar.xz vendor/
Key design decisions:
-
End-user vendoring goes through
rpkg/tools/vendor-crates.R, soconfigureand the generated package can use the same entrypoint instead of relying on the miniextendr CLI. -
Local path/workspace crates are discovered from the resolved Cargo dependency graph, then packaged with a generated cargo config that patches unpublished sibling crates by path during the packaging step. This avoids hand-copying crate sources or hard-coding workspace metadata into the vendor pipeline.
-
The resulting
.cratearchives are extracted into the vendor directory created bycargo vendor, so local crates look like any other vendored crate (with.cargo-checksum.jsonand versioned directory names). -
Checksum lines are stripped from
Cargo.lockbecause vendored crates have{"files":{}}checksums (cargo vendor convention). Cargo regenerates checksums at build time. -
Tests, benchmarks, examples, and dotfiles are stripped from vendored crates to reduce tarball size.
πStep 2: just configure-cran
Runs PREPARE_CRAN=true bash ./configure which:
- Detects
PREPARE_CRAN=trueβ setsBUILD_CONTEXT=prepare-cran - Generates
Makevars,.cargo/config.tomlfrom templates - Unpacks
inst/vendor.tar.xzβvendor/(if not already present) - Rewrites
Cargo.toml:- Git deps (
miniextendr-api,miniextendr-lint) β path deps tovendor/ - Strips
[patch."https://..."]section - Adds
[patch.crates-io]for transitive deps (miniextendr-macros, etc.)
- Git deps (
- Strips git source replacement from
.cargo/config.toml - Regenerates
Cargo.lockoffline from vendored sources - Extracts
CARGO_STATICLIB_NAMEviacargo pkgidand patches generated files
πStep 3: Build and Check
just r-cmd-build # R CMD build rpkg β miniextendr_0.1.0.tar.gz
just r-cmd-check # rcmdcheck with --as-cran --no-manual
Important: Always check the built tarball, not the source directory.
R CMD check on a source directory skips steps like Authors@R β Author/Maintainer
conversion.
π.Rbuildignore and vendor/
The vendor/ directory at the package root is excluded by .Rbuildignore:
^vendor$
This means R CMD build does NOT include vendor/ in the tarball directly.
Instead, dependencies ship via inst/vendor.tar.xz. At install time,
configure unpacks the tarball to recreate vendor/.
Why not ship vendor/ directly?
- Thousands of
.rsfiles invendor/would trigger pkgbuildβs rebuild detection on everyR CMD INSTALL(it scanssrc/recursively) - The compressed tarball is much smaller
- Cleaner package structure
πCargo Config for Vendored Builds
The generated .cargo/config.toml (from cargo-config.toml.in) tells cargo
to resolve crates from the local vendor/ directory:
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "../../vendor"
In dev contexts (dev-monorepo, dev-detached), this config is removed
so cargo uses its normal resolution with [patch] paths or git deps.
πLockfile Compatibility
The configure script handles cargo lockfile version mismatches:
- Lockfile v4 requires cargo 1.78+
- If the installed cargo is older, configure regenerates the lockfile
- In release contexts, this requires vendor sources to be available first
(so the lockfile-compat step unpacks
inst/vendor.tar.xzif needed)
πVerify Vendor Sync
After just vendor, verify vendored workspace crates match their sources:
just vendor-sync-check # Compares src/ dirs
just vendor-sync-diff # Shows actual diffs
If drift is detected, re-run just vendor to refresh.
πComplete CRAN Release Workflow
# 1. Ensure all tests pass in dev mode
just configure
just rcmdinstall
just devtools-test
# 2. Vendor dependencies
just vendor
# 3. Configure for CRAN
just configure-cran
# 4. Build tarball
just r-cmd-build
# 5. Check tarball (CRAN mode)
just r-cmd-check
# 6. Fix any issues, repeat from step 1πKnown Limitations
πcargo tree text parsing
vendor-crates.R discovers local path-dependencies by parsing cargo tree --format {p} output. cargo metadata --format-version=1 would provide the
same information as stable JSON, but parsing it requires the jsonlite R
package (base R has no JSON parser). Since vendor-crates.R is copied into
scaffolded packages via tools/, it must remain zero-external-dependency.
If cargo tree output format changes in a future Cargo release, the
parse_tree_packages() function will need to be updated.
πSee Also
- R_BUILD_SYSTEM.md β How R builds packages with compiled code
- TEMPLATES.md β Template system (configure.ac templates)
- SMOKE_TEST.md β Phase A4 covers CRAN-like tarball validation