minirextendr: Scaffolding and Workflow Helper
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.
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
πRecommended: miniextendr_build()
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 β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 directoryminirextendr::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:
| Function | Cargo 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:
- Adds the Cargo feature to
src/rust/Cargo.toml - 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 generatedconfigure.acblock rewrites the relativedev_pathto 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:
- Use an absolute path in the
[patch], never a relative one. - 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/:
| Hook | Checks |
|---|---|
pre-commit | cargo 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-merge | Reminds 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, thetools/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 withgit diff(andgit statusfor the added files) before committing. The function returns the same lists invisibly, aschanged,addedandremoved. 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 whosetimestamp=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.Randtools/wrapper-inputs.rds, from the old pre-shipped wrapper path.tools/write-wrappers.Rreplaced 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. Rungit 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
| Category | Functions |
|---|---|
| Project creation | create_miniextendr_package, create_miniextendr_monorepo |
| Build workflow | miniextendr_autoconf, miniextendr_configure, miniextendr_build, miniextendr_build_tarball, miniextendr_sync |
| Inline compilation | rust_source, rust_function, rust_source_clean |
| Cargo wrappers | cargo_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 scaffolding | use_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 FFI | use_native_package, check_native_package (see NATIVE_R_PACKAGES.md) |
| Git hooks | use_miniextendr_git_hooks (pre-commit + post-merge reminders) |
| Vendoring | vendor_miniextendr, vendor_crates_io, vendor_sync, miniextendr_vendor, miniextendr_available_versions |
| Diagnostics | has_miniextendr, miniextendr_status, miniextendr_validate, miniextendr_doctor, miniextendr_check_rust |
| knitr/render | miniextendr_knitr_setup, miniextendr_html_document, miniextendr_pdf_document, miniextendr_word_document, miniextendr_quarto_pre_render, use_miniextendr_knitr, use_miniextendr_rmarkdown, use_miniextendr_quarto |
| Cache | miniextendr_cache_info, miniextendr_clear_cache |
| Upgrade | upgrade_miniextendr_package |
| Dev helpers | miniextendr_check, miniextendr_dev_link, miniextendr_check_rust, use_miniextendr_build_rs |