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, build-html-reference.R

πŸ”—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

miniextendr_build(path = "mypackage")                   # compile + wrappers β†’ roxygen2 β†’ R CMD INSTALL
miniextendr_build(path = "mypackage", install = FALSE)  # stop after roxygen2

It runs autoconf and ./configure, then compiles the crate with pkgbuild::compile_dll() (make β†’ cargo build β†’ shared-library link β†’ tools/write-wrappers.R) before roxygen2 runs, so roxygen2 reads a current R/<pkg>-wrappers.R from its first pass. It then unloads the development namespace and installs.

πŸ”—devtools::document()

devtools::document() compiles too, through pkgload::load_all(), but only after roxygen2 has updated the NAMESPACE imports from the R files it can see. The wrappers file is gitignored, so a fresh clone does not have it yet. An @importFrom that only a generated function carries, such as the generic of an S3 method for another package’s generic, is then missing from that first pass. The load fails with β€œobject β€˜β€™ not found whilst loading namespace”, and roxygen2’s second pass writes the correct NAMESPACE. The run ends in the right state but prints the error. Compile once first (miniextendr_build(install = FALSE) or pkgbuild::compile_dll()), and document() is clean from then on. devtools::document() does not install.

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

For fine-grained control:

miniextendr_autoconf(path = "mypackage")     # autoconf β†’ generate configure
miniextendr_configure(path = "mypackage")    # ./configure β†’ generate Makevars

πŸ”—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".

πŸ”—HTML Reference Manual

Every scaffold ships tools/build-html-reference.R, a base-R maintainer script (R >= 4.4) that renders man/*.Rd into one HTML page with tools::pkg2HTML(), after tools::checkRd() has passed over the sources:

Rscript tools/build-html-reference.R            # β†’ rust-target/doc/r/<pkg>.html
Rscript tools/build-html-reference.R docs/r     # any output directory
minirextendr::miniextendr_html_reference()      # the same, from an R session

Nothing is installed or loaded, so the page cannot lag the tree and builds in seconds for packages with thousands of generated wrappers. In-package links stay on the page, other packages link to their CRAN manuals, and built vignettes under inst/doc are copied alongside. When a rustdoc crate index (--enable-index-page) sits one level above the output directory, the manual is added to its list. A checkRd finding aborts the build; set MINIEXTENDR_HTML_STRICT=0 (or strict = FALSE) to print the findings and render anyway. The script ships with the package like the other tools/ helpers.

πŸ”—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_s3()                # S3 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)

use_s7() also makes the package register its S7 methods on load: it writes an .onLoad() calling S7::methods_register() to R/zzz.R when the package has none, and tells you the line to add to an existing .onLoad(). Without that call, S7 methods for base operators such as [[, base generics such as format() and other packages’ generics are lost in every new R session.

πŸ”—Feature Detection

use_feature_detection() generates runtime introspection for Cargo features that were selected by the build:

# Initial setup - creates Rust and R helpers for has_feature()/test skips
use_feature_detection()

# After adding Cargo features, regenerate the detection script
update_feature_detection()

The generated Rust code uses cfg!(feature = "..."); it reports Cargo features but does not choose them. For configure-time feature selection, use the separate rule system:

use_configure_feature_detection()
add_feature_rule("rayon", detect = TRUE, optional_dep = TRUE)
add_feature_rule(
    "vctrs",
    detect = 'requireNamespace("vctrs", quietly = TRUE)',
    cargo_spec = "miniextendr-api/vctrs"
)

tools/detect-features.R returns a comma-separated feature list; configure.ac passes it to Cargo through --features. It does not invent custom rustc --cfg flags.

πŸ”—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 outside the package

When your R package depends on a Rust crate that lives outside the package (e.g. an β€œengine” crate in the same monorepo), declare it with a relative path, as create_miniextendr_monorepo() does:

core_library = { package = "core", path = "../../../core" }

R CMD INSTALL of the package directory in its checkout builds in place, so the path resolves. Anything that seals or copies the package directory alone leaves the sibling behind, which is what bootstrap.R is for. Build frontends that honor Config/build/bootstrap: TRUE (devtools, pkgbuild, pak with a repository ref and a subdirectory, rv 0.23.0 or later) run it while the repository is still around the package, and tools/dev-bootstrap.R stages the sibling under src/rust/vendor/, so the built tarball carries the crate. miniextendr_build_tarball() vendors it into inst/vendor.tar.xz instead, together with every other dependency.

An installer that copies the package directory alone (pak local::, older rv releases, a plain R CMD build of the package directory) cannot reach the sibling. The build stops with an error that names the missing path and the installs that work, from bootstrap.R when the installer runs it in its copy, and from configure, before cargo, when it does not. See R_BUILD_SYSTEM.md for the installer matrix.

An absolute path is no way around this: it resolves only on the machine that wrote it.

Note: use_vendor_lib() (below) wires a monorepo crate in through [patch.crates-io] instead. Its generated configure.ac block rewrites the relative dev_path to the extracted vendor copy in tarball/CRAN mode.

πŸ”—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. That is fine here because the override is development-only and never ships. A crate the package itself depends on ships with it instead, through bootstrap staging; see Wrapping a local Rust crate outside the package.

πŸ”—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. For a package that uses S7 it also checks that .onLoad() calls S7::methods_register().

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; blocks if R/*-wrappers.R is staged without NAMESPACE; blocks a staged vendor.tar.xz (a build artifact that miniextendr_build_tarball() vendors into the release tarball; never commit it); blocks a source-shape src/rust/Cargo.lock (path+ sources or [[patch.unused]]; recover with miniextendr_repair_lock()); blocks when the staged rv.lock and src/rust/Cargo.lock pin different miniextendr commits (see below)
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. An installed hook stays as it was when minirextendr is upgraded: run use_miniextendr_git_hooks(force = TRUE) to replace the miniextendr section with the version the installed minirextendr bundles.

The pin check is for a package that follows miniextendr from git on both sides: minirextendr in rproject.toml ({ name = "minirextendr", git = "https://github.com/A2-ai/miniextendr.git", branch = "main", directory = "minirextendr" }) and miniextendr-api in src/rust/Cargo.toml ({ git = "https://github.com/A2-ai/miniextendr", branch = "main" }). The two lock files then pin miniextendr commits independently: rv re-resolves a branch pin on every rv sync and rewrites rv.lock, while Cargo.lock keeps its commit until cargo update. The package’s build files (src/rust/build.rs, Makevars.in, mx_abi.h, …) follow miniextendr-api, so both locks must name the same commit. When rv.lock or src/rust/Cargo.lock is staged, the hook compares the full commit sha of minirextendr in the staged rv.lock (repository root first, then the package directory) with the git-sourced miniextendr-* crates in the staged Cargo.lock. It blocks when they differ, or when those crates disagree among themselves, and prints both fixes: move Rust to the R pin (cargo update -p miniextendr-api --precise <sha>, or a new rev for a rev-pinned crate), or move R to the Rust pin (rv remove minirextendr --no-sync followed by rv add minirextendr --git <url> --commit <sha> --directory minirextendr, which replaces the branch with a commit pin). Without an rv.lock, with minirextendr from a repository or a path, or with path or registry miniextendr-* crates there is nothing to compare and the check passes.

πŸ”—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.

  • Changed files. The template is the source of truth for the build-system files it owns (src/stub.c, src/Makevars.in, inst/include/mx_abi.h, the tools/ scripts, …), and the upgrade rewrites them in full. A local edit you committed to one of them passes the uncommitted-changes check and is replaced. The closing summary lists every file whose content changed and every file the upgrade added, so review them with git diff (and git status for the added files) before committing. The function returns the same lists invisibly, as changed, added and removed. If nothing changed, the summary says so in one line.
  • Monorepos. Pass the workspace root or the R package subdirectory. The upgrade renders the monorepo templates and adds the root template’s entries to the workspace root .gitignore. It only adds lines; your own lines stay.
  • tools/config.guess / tools/config.sub. A copy whose timestamp= line is newer than the bundled one is kept, so a script you refreshed from GNU config is not downgraded.
  • Leftover files. The upgrade never deletes files. Its closing summary lists the ones an older scaffold leaves behind:
    • tools/wrapper-freshness.R and tools/wrapper-inputs.rds, from the old pre-shipped wrapper path. tools/write-wrappers.R replaced it, and you can delete both files.
    • A git-tracked src/<pkg>-win.def. configure generates this file and the scaffold now gitignores it, but an ignore rule does not untrack a file you committed earlier. Run git rm --cached src/<pkg>-win.def.

πŸ”—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_build_tarball, 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_s3, use_s4, use_s7, use_feature_detection, update_feature_detection, use_configure_feature_detection, add_feature_rule, remove_feature_rule, list_feature_rules, 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
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