minirextendr is a pure-R companion package that scaffolds, builds, and maintains R packages using the miniextendr Rust-R framework. Think of it as usethis for miniextendr projects.

Location: minirextendr/ in the monorepo.

πŸ”—Installation

# From the monorepo:
just minirextendr-install

# Or directly:
R CMD INSTALL minirextendr

πŸ”—Creating Projects

πŸ”—Standalone R Package

library(minirextendr)
create_miniextendr_package("mypackage")

Creates a complete R package with Rust backend:

mypackage/
β”œβ”€β”€ DESCRIPTION          # Config/build/bootstrap: TRUE
β”œβ”€β”€ NAMESPACE
β”œβ”€β”€ R/
β”œβ”€β”€ configure.ac         # Autoconf source
β”œβ”€β”€ configure            # Generated by autoconf
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ rust/
β”‚   β”‚   β”œβ”€β”€ Cargo.toml   # Rust dependencies
β”‚   β”‚   β”œβ”€β”€ lib.rs       # Your Rust code
β”‚   β”‚   β”œβ”€β”€ build.rs     # Build-time lint
β”‚   β”œβ”€β”€ stub.c            # Minimal C stub for R linker
β”‚   β”œβ”€β”€ Makevars.in
β”‚   └── win.def.in
β”œβ”€β”€ vendor/              # Vendored miniextendr crates
└── tools/               # config.guess, config.sub

πŸ”—Monorepo (Workspace)

create_miniextendr_monorepo("myproject",
  package = "mypkg",
  crate_name = "mypkg-rs"
)

Creates a Cargo workspace with a separate Rust crate alongside the R package:

myproject/
β”œβ”€β”€ Cargo.toml           # Workspace root
β”œβ”€β”€ mypkg-rs/            # Main Rust crate
β”‚   β”œβ”€β”€ Cargo.toml
β”‚   └── src/lib.rs
β”œβ”€β”€ mypkg/               # Generated R package
β”‚   β”œβ”€β”€ src/rust/        # R-facing Rust bindings
β”‚   └── vendor/
β”œβ”€β”€ tools/               # Version management scripts
β”‚   └── bump-version.R
└── justfile

To vendor from a specific GitHub tag instead of the default ("main"):

create_miniextendr_monorepo("myproject",
  miniextendr_version = "v0.1.0"
)

πŸ”—Build Workflow

devtools::document() handles the entire pipeline automatically:

devtools::document("mypackage")   # Compiles Rust + generates R wrappers + runs roxygen2
devtools::install("mypackage")    # Install the final package

How it works: devtools::document() calls pkgload::load_all(), which detects Config/build/bootstrap: TRUE in DESCRIPTION and runs bootstrap.R. That triggers ./configure β†’ make β†’ cargo build β†’ document binary β†’ R wrappers β†’ roxygen2, all in a single invocation. No manual ./configure or two-pass install needed.

πŸ”—Manual: step-by-step functions

For fine-grained control or when not using devtools:

miniextendr_autoconf(path = "mypackage")   # autoconf β†’ generate configure
miniextendr_configure(path = "mypackage")  # ./configure β†’ generate Makevars
miniextendr_build(path = "mypackage")      # R CMD INSTALL
miniextendr_document(path = "mypackage")   # devtools::document (regenerate wrappers)

πŸ”—One-Shot Sync

miniextendr_sync() detects staleness and rebuilds only what’s needed:

miniextendr_sync(path = ".", mode = "if_stale", stage = "install")

Modes: "if_stale" (default), "always", "never". Stages: "install" (default), "wrappers", "build".

πŸ”—Inline Rust Compilation

Compile Rust code on the fly without an existing package (like Rcpp’s sourceCpp()):

# Single function - auto-wraps with imports and module block
rust_function('
#[miniextendr]
pub fn add_one(x: i32) -> i32 { x + 1 }
')
add_one(41L)
#> [1] 42

# Vectors work too
rust_function('
#[miniextendr]
pub fn double_vec(x: Vec<f64>) -> Vec<f64> {
    x.into_iter().map(|v| v * 2.0).collect()
}
')
double_vec(c(1.0, 2.5, 3.0))
#> [1] 2.0 5.0 6.0

# Full source with multiple functions
rust_source(code = '
use miniextendr_api::miniextendr;

#[miniextendr]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

#[miniextendr]
pub fn add(a: f64, b: f64) -> f64 { a + b }
// Registration is automatic via #[miniextendr].
')
greet("world")
#> [1] "Hello, world!"
add(1.5, 2.5)
#> [1] 4

πŸ”—Caching

Results are MD5-cached: same code + features = instant reload. Calling the same rust_function() or rust_source() a second time skips compilation entirely.

# First call: compiles (~10-20s)
rust_function('#[miniextendr] pub fn sq(x: f64) -> f64 { x * x }')

# Second call: instant (cache hit)
rust_function('#[miniextendr] pub fn sq(x: f64) -> f64 { x * x }')

# Clear all cached compilations
rust_source_clean()

πŸ”—Cargo Wrappers

Thin wrappers around cargo that target the package’s src/rust/ directory:

FunctionCargo command
cargo_build()cargo build
cargo_check()cargo check
cargo_test()cargo test
cargo_clippy()cargo clippy
cargo_fmt()cargo fmt
cargo_doc()cargo doc
cargo_add("serde")cargo add serde
cargo_rm("serde")cargo rm serde
cargo_deps()cargo tree
cargo_search("regex")cargo search regex
cargo_update()cargo update

All accept path (default ".") and ... for extra arguments.

Note: cargo_fmt("--check") may report formatting diffs in vendored crates. This is expected. Vendored code isn’t subject to local formatting rules.

πŸ”—Feature Scaffolding

Add optional miniextendr features with proper Cargo.toml and R-side setup:

use_rayon()             # Parallel iteration
use_serde()             # Serde-based R ↔ Rust serialization
use_vctrs()             # vctrs integration
use_r6()                # R6 class system
use_s4()                # S4 class system
use_s7()                # S7 class system
use_feature_detection() # Conditional compilation via feature flags
use_vendor_lib()        # Vendor external Rust library for monorepo use

Each use_*() function:

  1. Adds the Cargo feature to src/rust/Cargo.toml
  2. Sets up any needed R-side dependencies (e.g., R6, S7 packages)
  3. Updates miniextendr.yml configuration

πŸ”—Feature Detection

use_feature_detection() sets up conditional compilation via R-detected feature flags:

# Initial setup - creates detect-features.R and wires it into configure.ac
use_feature_detection()

# After adding new features to miniextendr.yml, regenerate the detection script
update_feature_detection()

Features are detected at ./configure time and passed as --cfg flags to rustc.

πŸ”—Vendoring

# Vendor miniextendr crates from local monorepo
vendor_miniextendr(local_path = "/path/to/miniextendr")

# Vendor miniextendr crates from GitHub release
vendor_miniextendr(version = "0.1.0")

# Vendor external crates from crates.io
vendor_crates_io(path = "mypackage")

# Check vendor freshness
vendor_sync(path = "mypackage")

# List available versions
miniextendr_available_versions()

πŸ”—Wrapping a local Rust crate: use an absolute path

When your R package depends on a Rust crate that lives outside the package (e.g. an β€œengine” crate in the same monorepo, or a sibling library on disk), the path = ... dependency you add to src/rust/Cargo.toml must be absolute, not relative.

R CMD INSTALL and devtools::install() copy the package into a temporary build directory (e.g. /private/tmp/.../R.INSTALL.../<pkg>/) before compiling the Rust staticlib. A relative path resolves against that temporary location β€” which doesn’t contain your engine crate β€” and the build fails:

# ❌ breaks: relative path resolves against the temp build dir
my_engine = { path = "../../../engine" }
error: failed to load manifest for dependency `my_engine`
  ... No such file or directory

An absolute path is read live from its real location, so the temp-copy doesn’t matter:

# βœ… works: absolute path is read from its real location at install time
my_engine = { path = "/abs/path/to/engine" }

How this interacts with vendoring: cargo vendor / the vendor tarball captures only the engine crate’s registry dependencies (its transitive crates.io deps). The engine’s own source is not vendored β€” it is read from the path at install time. That’s why the path has to keep resolving after the temp-copy, and why it must be absolute in source-install mode.

Note: use_vendor_lib() (below) is the supported way to wire a monorepo crate in. It writes a relative dev_path into [patch.crates-io] on purpose β€” its generated configure.ac block rewrites that path to the extracted vendor copy in tarball/CRAN mode, so the relative path never reaches the offline build. The absolute-path requirement above applies to a hand-added path = ... dependency that has no such rewriting machinery. If you hand-roll the dependency, use an absolute path.

πŸ”—Depending on a local miniextendr framework checkout (dev-only)

When you are developing against an unreleased change to the miniextendr framework itself (miniextendr-api / miniextendr-macros / miniextendr-lint), you may want a standalone package’s Rust crate to resolve those three crates from a local checkout instead of the published git source. The natural reach is a [patch] block in src/rust/Cargo.toml:

# DON'T β€” breaks under R CMD INSTALL
[patch."https://github.com/A2-ai/miniextendr"]
miniextendr-api = { path = "../../.miniextendr/miniextendr-api" }

This is a development-only technique with two non-obvious failure modes. If you use it, you must do both of the following:

  1. Use an absolute path in the [patch], never a relative one.
  2. Build in source/dev mode (no inst/vendor.tar.xz), never in tarball mode.

πŸ”—Why a relative path breaks (install staging)

R CMD INSTALL of a built tarball compiles from a staged copy of the package, not from your working tree. The sibling framework checkout (e.g. a .miniextendr/ directory) is β€” correctly β€” .Rbuildignored, so it is not copied into the staging directory. A relative path = "../../.miniextendr/…" then resolves against the staging dir and fails:

failed to read .../R.INSTALL.../<pkg>/.miniextendr/miniextendr-api/Cargo.toml
No such file or directory (os error 2)

An absolute path (/Users/you/src/miniextendr/miniextendr-api) keeps pointing at the real checkout regardless of where the package is staged. This is the same absolute-path principle that applies to use_vendor_lib() and a package’s own local engine crate β€” see the local-engine notes in TROUBLESHOOTING.md and the use_vendor_lib() man page.

πŸ”—Why tarball mode breaks (patch vs. vendored-sources)

In tarball mode (inst/vendor.tar.xz present), configure writes a [source] block that redirects crates.io and the miniextendr git source to the unpacked vendored-sources directory (see CRAN_COMPATIBILITY.md). A hand-written [patch] on the same git source coexisting with a [source] replace-with = "vendored-sources" on that source is fragile β€” you are asking cargo to both patch and source-replace the same source. Keep the package in source/dev mode (delete inst/vendor.tar.xz) while patching against a local checkout, and only vendor once you are done.

πŸ”—How the monorepo avoids this

Inside the miniextendr monorepo there is no hand-written [patch] at all. configure walks up from the package directory looking for a sibling miniextendr-api/Cargo.toml; when it finds one and the package is in source mode, it writes an absolute-path [patch."https://github.com/A2-ai/miniextendr"] block into .cargo/config.toml (a configure-managed, regenerated, gitignored file β€” never Cargo.toml). Tarball mode always wins over this override. A standalone package has no equivalent configure-managed wiring today, which is why the constraint above must be applied by hand. A minirextendr helper to manage this automatically (configure-managed absolute [patch], source-mode only, with an undo path) is tracked in issue #908.

πŸ”—Diagnostics

has_miniextendr("mypackage")        # TRUE/FALSE - is this a miniextendr package?
miniextendr_status(path = ".")      # Show file status (present, stale, missing)
miniextendr_validate(path = ".")    # Validate project consistency
miniextendr_doctor(path = ".")      # Comprehensive health check
miniextendr_check_rust()            # Verify Rust toolchain is available

miniextendr_status() reports each build artifact’s state (present, stale, or missing). miniextendr_validate() checks structural consistency (DESCRIPTION fields, module declarations, etc.). miniextendr_doctor() runs the full suite including Rust toolchain checks.

Note: In monorepo mode, miniextendr_doctor() may report false-positive vendor warnings because vendored crates are resolved differently via [patch] in the workspace.

πŸ”—Git Hooks

use_miniextendr_git_hooks(path = ".")

Installs idempotent pre-commit and post-merge hooks into .git/hooks/:

HookChecks
pre-commitcargo fmt --check; blocks if configure.ac is staged without an up-to-date configure; warns on a likely-stale inst/vendor.tar.xz
post-mergeReminds you to reconfigure + rebuild when build-relevant files changed (configure.ac, Makevars.in, Cargo.toml, *.rs)

The hooks auto-detect standalone vs monorepo layouts (searches for src/rust/Cargo.toml) and preserve existing content from other tools. They append a marker-delimited miniextendr section rather than overwriting. miniextendr_doctor() reports missing hooks as a warning. Running twice is safe; the installer skips on the # miniextendr marker.

πŸ”—Configuration

miniextendr.yml in the package root for project-level settings:

miniextendr_config(path = ".")              # Read current config (returns named list)
miniextendr_config_defaults()               # Show all defaults with descriptions

miniextendr_config() merges project-level miniextendr.yml with built-in defaults. Settings include feature flags, class system choices, and build options. miniextendr_config_defaults() lists every available setting with its default value.

πŸ”—knitr / Rmarkdown / Quarto Integration

πŸ”—Package Mode (in a package vignette)

# In a setup chunk:
miniextendr_knitr_setup()          # Syncs and registers {miniextendr} engine

Then use {miniextendr} chunks that evaluate as R code (the package is already built).

πŸ”—Inline Mode (self-contained vignettes)

# In a setup chunk:
miniextendr_knitr_setup(path = NULL)

Each {miniextendr} chunk compiles Rust code via rust_source() and loads functions into the knitr environment. No existing package required.

πŸ”—Render Helpers

Custom output formats that auto-sync before rendering:

# In YAML front matter:
output: minirextendr::miniextendr_html_document
output: minirextendr::miniextendr_pdf_document
output: minirextendr::miniextendr_word_document

For Quarto, use as a pre-render script:

# In _quarto.yml:
pre-render: Rscript -e "minirextendr::miniextendr_quarto_pre_render()"

πŸ”—Upgrading Existing Packages

upgrade_miniextendr_package(path = "mypackage")

Updates templates, vendored crates, and configure.ac to match the latest minirextendr version.

πŸ”—Caching

miniextendr_cache_info()     # Show cache location and size
miniextendr_clear_cache()    # Clear all caches

Cache location: rappdirs::user_cache_dir("minirextendr").

πŸ”—Exported Functions Reference

CategoryFunctions
Project creationcreate_miniextendr_package, create_miniextendr_monorepo
Build workflowminiextendr_autoconf, miniextendr_configure, miniextendr_build, miniextendr_document, miniextendr_sync
Inline compilationrust_source, rust_function, rust_source_clean
Cargo wrapperscargo_add, cargo_rm, cargo_build, cargo_check, cargo_test, cargo_clippy, cargo_fmt, cargo_doc, cargo_search, cargo_deps, cargo_update, cargo_init, cargo_new
Feature scaffoldinguse_miniextendr, use_rayon, use_serde, use_vctrs, use_r6, use_s4, use_s7, use_feature_detection, update_feature_detection, use_vendor_lib
Native R package FFIuse_native_package, check_native_package (see NATIVE_R_PACKAGES.md)
Git hooksuse_miniextendr_git_hooks (pre-commit + post-merge reminders)
Vendoringvendor_miniextendr, vendor_crates_io, vendor_sync, miniextendr_vendor, miniextendr_available_versions
Diagnosticshas_miniextendr, miniextendr_status, miniextendr_validate, miniextendr_doctor, miniextendr_check_rust
Configurationminiextendr_config, miniextendr_config_defaults
knitr/renderminiextendr_knitr_setup, miniextendr_html_document, miniextendr_pdf_document, miniextendr_word_document, miniextendr_quarto_pre_render, use_miniextendr_knitr, use_miniextendr_rmarkdown, use_miniextendr_quarto
Cacheminiextendr_cache_info, miniextendr_clear_cache
Upgradeupgrade_miniextendr_package
Dev helpersminiextendr_check, miniextendr_dev_link, miniextendr_check_rust, use_miniextendr_build_rs