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:

  1. Sets BUILD_CONTEXT=prepare-cran β€” overrides both NOT_CRAN and auto-detection
  2. Sets NOT_CRAN=false β€” derived from the build context for backward compatibility
  3. Enables --offline β€” cargo must resolve all deps from vendored sources
  4. Keeps .cargo/config.toml β€” for vendored source directory replacement
  5. Unpacks inst/vendor.tar.xz β€” if vendor/ doesn’t already exist
  6. Rewrites Cargo.toml β€” git deps become path deps pointing to vendor/
  7. Strips [patch] section β€” monorepo paths are not available
  8. Adds [patch.crates-io] β€” for transitive miniextendr deps in vendor
  9. 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

FilePurpose
rpkg/configure.acBuild context resolution (lines 33-38, 139-140)
justfileconfigure-cran recipe
rpkg/bootstrap.RSets PREPARE_CRAN=false to prevent accidental inheritance during devtools workflows
CLAUDE.mdBuild 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
ContextCargo Config[patch]VendorOffline
dev-monorepoRemovedKept (path deps)CleanedNo
dev-detachedRemovedStrippedCleanedNo
vendored-installKeptRewrittenUnpacked/fetchedYes
prepare-cranKeptRewrittenUnpacked/fetchedYes

πŸ”—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, so configure and 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 .crate archives are extracted into the vendor directory created by cargo vendor, so local crates look like any other vendored crate (with .cargo-checksum.json and versioned directory names).

  • Checksum lines are stripped from Cargo.lock because 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:

  1. Detects PREPARE_CRAN=true β†’ sets BUILD_CONTEXT=prepare-cran
  2. Generates Makevars, .cargo/config.toml from templates
  3. Unpacks inst/vendor.tar.xz β†’ vendor/ (if not already present)
  4. Rewrites Cargo.toml:
    • Git deps (miniextendr-api, miniextendr-lint) β†’ path deps to vendor/
    • Strips [patch."https://..."] section
    • Adds [patch.crates-io] for transitive deps (miniextendr-macros, etc.)
  5. Strips git source replacement from .cargo/config.toml
  6. Regenerates Cargo.lock offline from vendored sources
  7. Extracts CARGO_STATICLIB_NAME via cargo pkgid and 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 .rs files in vendor/ would trigger pkgbuild’s rebuild detection on every R CMD INSTALL (it scans src/ 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.xz if 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