R's Package Build System for Shared Libraries
How R builds packages with compiled code, and how miniextendr integrates with it.
How R builds packages with compiled code, and how miniextendr integrates with it.
πThe Big Picture
When R CMD INSTALL encounters a package with a src/ directory, it:
- Runs the packageβs top-level
configurescript (if present), which can generatesrc/Makevarsfromsrc/Makevars.in - Compiles C/C++/Fortran sources into
.oobject files - Links those objects into a shared library (
.soon Unix,.dllon Windows) - Installs the shared library into
libs/
miniextendr adds a Rust step: Cargo builds a static library (.a) which Rβs
linker folds into the final shared library alongside a minimal C anchor.
R_init_* and all registered entry points are defined in Rust.
πReusing caches across tarball installs
Set both CARGO_TARGET_DIR and VENDOR_OUT to retain compiled dependencies
across installations of the same tarball from different extraction directories:
CARGO_TARGET_DIR="$HOME/.cache/miniextendr/target" \
VENDOR_OUT="$HOME/.cache/miniextendr/vendor" \
R CMD INSTALL mypackage_0.1.0.tar.gz
VENDOR_OUT is an optional cache root. Configure selects
<VENDOR_OUT>/<vendor-archive-md5>/vendor, extracting the archive once and
publishing the completed entry atomically. A different archive selects a new
entry, so an unrelated package cannot accidentally reuse an old vendor tree.
Both variables must have the same values on subsequent installs. Cargoβs
configured target directory follows the requested target directory too.
Shared-cache tarballs should be produced with the matching cargo-revendor:
--freeze records relative vendor paths in [patch.crates-io], with the
corresponding dependencies resolved through those patches. Configure writes
absolute cache patches into .cargo/config.toml; it never rewrites the shipped
Cargo manifest or lockfile. Shared-cache builds pass package-specific C objects
only to the package crate, avoiding dependency rebuilds caused by changing
extraction paths in global Rust flags.
The all recipe removes the package-local build directories (rust-target/,
ra-target/, vendor/, src/rust/.cargo/, src/rust/target/) after the link
for a vendored install and for any install of a built package, one whose
DESCRIPTION carries R CMD buildβs Packaged: field (or R CMD INSTALLβs
Built:). Such a tree is a throwaway unpack, so nothing is lost, and the Rust
staticlib has to be gone before R CMD INSTALL writes libs/symbols.rds: from
R 4.5, tools:::.shlib_objects_symbol_tables() (run when
_R_SHLIB_BUILD_OBJECTS_SYMBOL_TABLES_ is set, as R CMD check does) also
reads the static libraries named in PKG_LIBS that are still inside the
package, and the compiled-code check then reports libcβs exit, abort and
_exit from Rustβs std against the package (#1795). The ordering holds because
Rβs make finishes the default all goal, cleanup included, before it runs a
separate make symbols.rds. A checkoutβs DESCRIPTION has neither field, so
R CMD INSTALL <dir>, devtools::load_all() and just rcmdinstall keep their
Cargo cache.
Tarball cleanup preserves caller-selected target and vendor caches, including
ones under the usual package-local cleanup paths. Cache entries are retained
until the caller removes them. The install-mode latch still applies. Without VENDOR_OUT, extraction stays in the packageβs
vendor/ directory and uses the ordinary build command. With neither variable
set, package-local build directories are cleaned as before. This cache does
not change the behavior of the separate cleanup script invoked by Rβs
--preclean and --clean options.
πWrapper generation
Every native install generates the R wrappers from the shared library it just
linked. src/Makevars runs
Rscript ../tools/write-wrappers.R $(SHLIB) $(IS_TARBALL_INSTALL) $(CARGO_FEATURES_FLAG)
from src/ after the link. $(SHLIB) is the file name Rβs make gives the
packageβs shared object (<pkg>.so, <pkg>.dll), so it also names the
package. The script loads that library and calls its two registered writers,
which walk the #[distributed_slice] tables and write R/<pkg>-wrappers.R and
src/rust/wasm_registry.rs. The load runs with MINIEXTENDR_WRAPPER_GEN=1, so
R_init_<pkg> only registers routines.
Each writer replaces its file only when the content changed, so an unchanged
file keeps its bytes and its mtime. Make therefore tracks the pass with a stamp,
rust-target/.miniextendr-wrappers, not with the wrappers file: the stamp
depends on $(SHLIB) and on the wrappers file, and a rule without a recipe
makes a missing wrappers file (a fresh checkout, or a tarball built without
one) count as changed. The result:
- An install where nothing under
src/rustchanged runs Cargo (make always asks it), which compiles nothing. The shared library is not relinked and the wrappers are not regenerated. - A Rust edit that leaves the generated R code alone relinks the library and regenerates, but the writer keeps the existing file.
- A Rust edit that changes the generated R code rewrites the file and prints
NOTE: <pkg>-wrappers.R changed. Run roxygen2 afterwards: the install regenerates the wrappers but notNAMESPACEorman/.
A tarball built without vendoring takes the same path: pkgbuildβs temporary
tarball for devtools::install(), or a plain R CMD build. A shipped wrappers
file is compared with the freshly generated one like any other, so a tarball
whose Rust sources moved on from its wrappers installs the current wrappers.
A release tarball, one carrying inst/vendor.tar.xz, is checked instead. It
ships the wrappers its NAMESPACE and man/ were documented from, so
write-wrappers.R generates into a copy and stops the install when the result
differs. A difference means the install compiles other #[miniextendr] items
than the build that made the tarball. The cause is another Cargo feature set
(CARGO_FEATURES, or tools/detect-features.R deciding otherwise on this
machine) or another target. Without the check, Rβs load test would fail on a
missing export but only warn about a missing S3 method, so the package would
install with broken dispatch.
wasm32 is the exception to both: its $(SHLIB) is a SIDE_MODULE that host R
cannot load, so a wasm build uses the files a host install generated.
The wrapper file itself opens with two header lines: the AUTO-GENERATED
marker, then the miniextendr-api version that generated it and an FNV-1a
64-bit digest of the rest of the file with source positions normalized
(# miniextendr-api 0.x.y | fnv1a-64 of the normalized lines below: β¦,
#1552). Two copies with the same digest contain the same wrapper code,
exports and docstrings, whatever their (file.rs:line:col) comments say;
that is the same view the generator uses to decide whether to rewrite the
file, so a Rust edit that only shifts line numbers changes neither.
πMakefile Include Chain
Rβs build system is a hierarchy of makefiles included in a specific order. Understanding this order is essential because later includes can override earlier definitions.
βββββββββββββββββββββββββββββββββββββββββββ
β 1. Package's src/Makevars β β We define PKG_LIBS, deps, recipes
β (or src/Makevars.win on Windows) β
βββββββββββββββββββββββββββββββββββββββββββ€
β 2. $R_HOME/etc/Makeconf β β R's system config (compiler, flags)
β (or etc/Makeconf.win) β
βββββββββββββββββββββββββββββββββββββββββββ€
β 3. $R_HOME/etc/Makevars.site β β Optional site-wide overrides
βββββββββββββββββββββββββββββββββββββββββββ€
β 4. $R_HOME/share/make/shlib.mk β β The link recipe (see below)
β (or share/make/winshlib.mk) β
βββββββββββββββββββββββββββββββββββββββββββ€
β 5. ~/.R/Makevars β β Optional user overrides
βββββββββββββββββββββββββββββββββββββββββββ
R invokes make with all of these as -f arguments:
make -f Makevars -f Makeconf -f Makevars.site -f shlib.mk -f ~/.R/Makevars \
SHLIB='miniextendr.so' OBJECTS='stub.o'πVariable Flow
πMakeconf (Rβs system configuration)
Set once when R was compiled. Key variables:
# Linker command and flags
SHLIB_LD = gcc # or clang, etc.
SHLIB_LDFLAGS = -shared # or -dynamiclib on macOS
SHLIB_LINK = $(SHLIB_LD) $(SHLIB_LDFLAGS) $(LIBR0) $(LDFLAGS)
# All libraries to link
ALL_LIBS = $(PKG_LIBS) $(SHLIB_LIBADD) $(SAN_LIBS) $(LIBR) $(LIBINTL)
# Compiler flags (with PKG_* hooks for package authors)
ALL_CFLAGS = $(R_XTRA_CFLAGS) $(PKG_CFLAGS) $(CPICFLAGS) $(SHLIB_CFLAGS) $(CFLAGS)
ALL_CPPFLAGS = $(R_XTRA_CPPFLAGS) $(R_INCLUDES) -DNDEBUG $(PKG_CPPFLAGS) $(CPPFLAGS)πPackageβs Makevars (what we control)
We can set these PKG_* variables:
| Variable | Purpose | Our value |
|---|---|---|
PKG_LIBS | Extra libraries to link | $(CARGO_AR) (the full static-library path) |
PKG_CPPFLAGS | C preprocessor flags | (not used) |
PKG_CFLAGS | Extra C compiler flags | (not used) |
πshlib.mk (the link recipe)
This is the heart of Rβs shared library build:
# Unix (share/make/shlib.mk)
all: $(SHLIB)
$(SHLIB): $(OBJECTS)
$(SHLIB_LINK) -o $@ $(OBJECTS) $(ALL_LIBS)
shlib-clean:
rm -Rf .libs _libs
rm -f $(OBJECTS) symbols.rds
And on Windows:
# Windows (share/make/winshlib.mk)
$(SHLIB): $(OBJECTS)
if test -e "$(BASE)-win.def"; then
$(SHLIB_LD) ... -o $@ $(BASE)-win.def $(OBJECTS) $(ALL_LIBS)
else
# Auto-generate .def from nm output
EXPORTS > tmp.def
$(NM) $(OBJECTS) | sed ... >> tmp.def
$(SHLIB_LD) ... -o $@ tmp.def $(OBJECTS) $(ALL_LIBS)
fiπThe Final Link Command
When everything expands, R links our package like this:
πUnix
gcc -shared -o miniextendr.so \
stub.o \
/path/to/rust-target/release/librpkg.a \ # β our PKG_LIBS
$(SHLIB_LIBADD) $(SAN_LIBS) -lR $(LIBINTL) # β R's system libsπWindows
gcc -shared -o miniextendr.dll \
miniextendr-win.def \ # β auto-generated exports
stub.o \
/path/to/rust-target/release/librpkg.a \ # β our PKG_LIBS
-lws2_32 -lntdll -luserenv -lbcrypt \ # β Windows system libs
-ladvapi32 -lsecur32 \
$(SHLIB_LIBADD) $(SAN_LIBS) -lR $(LIBINTL)πHow miniextendr Integrates
πOur Makevars.in (Unix)
# Use the full path so linkers cannot select a same-named import library.
PKG_LIBS = $(CARGO_AR)
# Add Cargo build and the configuration snapshot as dependencies of the
# shared library
$(SHLIB): $(OBJECTS) $(CARGO_AR) $(CARGO_LINK_CONFIG)
# Build the Rust static library via Cargo
$(CARGO_AR): FORCE_CARGO $(OBJECTS)
$(CARGO) build --lib --profile $(CARGO_PROFILE) ...
# Snapshot the configured Makevars; copy only when the contents changed so a
# no-op install keeps the old mtime and skips the link (#1498).
$(CARGO_LINK_CONFIG): FORCE_CARGO $(CARGO_AR)
@if ! cmp -s "$(ABS_RPKG_SRCDIR)/Makevars" "$(CARGO_LINK_CONFIG)"; then \
cp "$(ABS_RPKG_SRCDIR)/Makevars" "$(CARGO_LINK_CONFIG)"; \
fi
# Link first, then generate wrappers from that same shared library. The
# writers keep unchanged files, so a stamp records the pass.
all: $(SHLIB) $(WRAPPERS_STAMP)
$(WRAPPERS_STAMP): $(SHLIB) $(WRAPPERS_R)
Rscript ../tools/write-wrappers.R "$(SHLIB)" "$(IS_TARBALL_INSTALL)" "$(CARGO_FEATURES_FLAG)"
touch "$(WRAPPERS_STAMP)"
$(WRAPPERS_R):
Key design decisions:
-
No link recipe on
$(SHLIB): we only add dependencies. The recipe comes from shlib.mk ($(SHLIB_LINK) -o $@ $(OBJECTS) $(ALL_LIBS)). -
FORCE_CARGO phony target: ensures Cargo is always invoked, letting Cargoβs own incremental build system decide what to rebuild.
-
all: $(SHLIB) $(WRAPPERS_STAMP)ordering: links the package library first, then loads that same library to generate the R wrapper and wasm registry. The finalallrecipe handles development touches and the post-link cleanup of vendored and built-package installs. -
$(CARGO_LINK_CONFIG)prerequisite: Cargo keeps a cached archiveβs old mtime when a feature, profile, or target-directory switch selects it again, so a.solinked from a different configuration can be newer than the archive and make would skip the link (#1498). The link therefore also depends onrust-target/.miniextendr-link-config, a copy of the configuredsrc/Makevarsthat is refreshed only when its contents change. Any configuration change relinks and regenerates the wrappers; a no-op install leaves the.sountouched.
πOur Makevars.win (Windows)
include Makevars # Reuse all Unix logic
PKG_LIBS = ... -lws2_32 -lntdll ... # Override with Windows system libsπObject files
R auto-detects .c files in src/ and compiles them:
stub.cβstub.o: empty file required by Rβs build system to invoke the linker
This is the only C file. All entry points (R_init_*), registration, and
runtime initialization are defined in Rust via miniextendr_init!. The Rust
code lives in the static library referenced by PKG_LIBS.
πSymbol visibility
We use explicit symbol registration, not dynamic lookup:
// In lib.rs - generates R_init_miniextendr() with all registration
miniextendr_api::miniextendr_init!(miniextendr);
The generated R_init_miniextendr() calls package_init() which registers
all .Call routines and locks down symbol visibility.
This means:
- R never uses
dlsym()to find our symbols at runtime - All function dispatch goes through the registered routines table
- The
.deffile on Windows only needs to exportR_init_miniextendr
πBuild Flow Summary
configure.ac β configure β Makevars (from Makevars.in)
β .cargo/config.toml (written by AC_CONFIG_COMMANDS)
R CMD INSTALL:
1. Run configure (generates Makevars, etc.)
2. make all:
a. Compile stub.c β stub.o (R's CC)
b. cargo build β librpkg.a (Rust staticlib, includes R_init_*)
c. $(SHLIB_LINK) -o miniextendr.so stub.o librpkg.a (R's linker)
d. If miniextendr.so was relinked (or the wrappers file is missing),
tools/write-wrappers.R loads it and writes
R/miniextendr-wrappers.R + src/rust/wasm_registry.rs when they changed
(a release tarball stops instead when the wrappers differ from the
shipped ones)
3. Install miniextendr.so to libs/
4. Install R/ files, man/, etc.
The wrapper and wasm registry writers are registered routines in
miniextendr-api. Roxygen2 runs separately (just devtools-document after
macro/wrapper changes) and derives the tracked NAMESPACE and man/*.Rd files
from the generated R wrapper.
πBuild Contexts
The configure script resolves one of two install modes from a single signal:
| Mode | When | Behavior |
|---|---|---|
| Source | inst/vendor.tar.xz absent | Cargo resolves through [patch."git+url"] to monorepo siblings if present, otherwise fetches the git URL declared in Cargo.toml. |
| Tarball | inst/vendor.tar.xz present | Configure unpacks the tarball into vendor/ and writes the archiveβs vendor/.cargo-config.toml (the [source.*] replacements cargo-revendor recorded, Git rev / branch / tag pins included) with its directory = line relocated to the unpacked vendor/; build runs --offline. A missing file stops configure. |
There is no env var for install mode (NOT_CRAN, PREPARE_CRAN,
FORCE_VENDOR are all gone). See CRAN Compatibility
for the full table and rationale.
IFS save/restore: The configure script saves and restores IFS around
any code that modifies it (miniextendr_saved_IFS=$IFS / IFS=$miniextendr_saved_IFS).
This prevents corrupting autoconf 2.72βs internal state, which relies on IFS
being set to its default value.
πKeep the Cargo cache during development installs
Avoid R CMD INSTALL --preclean / --clean in the development loop: they
invoke cleanup and normally erase rust-target, src/rust/target, and
ra-target, making the next Cargo build cold. If another tool requires those
flags, set MINIEXTENDR_KEEP_TARGET=1 for that source install. The opt-in spares
only those target directories during R CMD INSTALL; configure files are still
refreshed, and R CMD build still cleans its staged tree. Leave the variable
unset for a deliberate full clean rebuild.
# These cleanup flags normally erase the Cargo cache; the opt-in retains it.
MINIEXTENDR_KEEP_TARGET=1 R CMD INSTALL --preclean --clean path/to/rpkg
just rcmdinstall does not add either flag, and miniextendr_build() does not
request them. Scaffolded DESCRIPTION files set Config/build/never-clean: true
so pkgbuild also avoids adding preclean automatically. A plain install remains
the preferred development command.
R sets R_INSTALL_PKG while installing, including before and after configure.
The cleanup script requires that marker as well as the opt-in; simply running
./cleanup or building a package still removes targets. Existing tarball-mode
Makevars cleanup remains unchanged.
πBootstrap: staging path dependencies
bootstrap.R runs when a pkgbuild frontend builds the package
(Config/build/bootstrap: TRUE): devtools::build(), install() and
check(), rcmdcheck, pak, and rv 0.23.0 or later (with directory). Plain
R CMD build and R CMD INSTALL never run it.
It has one job. R CMD build seals only the package directory, and a path
dependency is not source-replaceable, so a path dependency outside the package
(e.g. path = "../../../my-core") would be missing from the build copy.
Bootstrap stages every such dependency, with its transitive path siblings, into
uncompressed src/rust/vendor/<name>-<version>/ directories, using the base-R
stager in tools/dev-bootstrap.R. cargo package resolves workspace
inheritance. The portable manifest points each path dependency at its staged
directory and adds exclude = ["vendor"] to its [workspace] so staged crates
never become workspace members. Registry and git dependencies resolve over the
network at install time. A package whose path dependencies all live inside it
has nothing to stage.
Bootstrap never vendors. It never runs cargo-revendor, never writes
inst/vendor.tar.xz and never generates source replacement, so what it produces
builds with network access and is not CRAN-ready. The offline tarball is a
separate, explicit step (CRAN Compatibility):
minirextendr::miniextendr_build_tarball() (or just r-cmd-build in this
repository) seals inst/vendor.tar.xz before the build. That archive carries
every dependency, so bootstrap then clears any staging left behind instead of
staging.
R CMD build activates the staging by running ./cleanup in its copy, and skips a
cleanup that is not executable. pakβs git client (git:: and gitlab:: refs)
writes every file without mode bits, so bootstrap restores the executable bit on
cleanup whenever it leaves a staging behind.
Installers only reach an outside sibling when they start from the repository:
- pak with a repository ref and a subdirectory (
pak::pak("owner/repo/rpkg"),gitlab::β¦/-/rpkg, or any ref that pkgdepends resolves through git) downloads the whole repository, runsbootstrap.Rin the subdirectory, and builds the tarball there, so the staging is what gets installed. - rv 0.23.0 or later with a git source plus
directory = "rpkg", or a local source whosepathis the repository root plusdirectory = "rpkg", copies the whole repository, runsbootstrap.Rin the subdirectory, runsR CMD buildthere, and installs the extracted tarball. The staging is what gets built: with rv 0.23.1 the sibling compiles fromsrc/rust/vendor/.
rv before 0.23.0 never runs bootstrap.R, and a local source takes no
directory yet. With a git source plus directory = "rpkg", what reaches
R CMD INSTALL depends on the release:
| rv | Build directory | The outside sibling |
|---|---|---|
| up to 0.12.0 | Real directories whose files are symlinks into the package directory of rvβs checkout (a copy on Windows) | configure stages it from that checkout, see below |
| 0.12.1 to 0.16.x | A copy of the package directory alone | missing: configure stops with the error below |
| 0.17.0 to 0.22.x | A copy of the whole repository, installed in place | still next to the package, so cargo builds it where it is |
Pointing any installer at the package directory itself (pak
local::<repo>/rpkg, rv path = "<repo>/rpkg", a plain R CMD build of
rpkg/) takes only that directory, so the sibling is gone before anything can
stage it. Use rv 0.23.0 or later or pak with
one of the forms above, or build the tarball in the checkout
(devtools::build()) and install that.
Configure catches a stranded sibling before cargo runs, instead of the opaque
failed to load manifest for dependency cargo error. Outside tarball mode,
tools/dev-bootstrap.R configure scans src/rust/Cargo.toml for path
entries in the tables cargo loads: every dependency table (dev,
build and target-specific ones included, [workspace.dependencies]) and
[patch.*] sources. A path inside the package, or one whose directory holds a
Cargo.toml, passes. That covers the checkout itself, the whole-repository
copy, a checkout that bootstrap staged, and the tarball R CMD build seals after
cleanup activates the staging. For a missing path it does one of two things:
- When the build directory is a tree of symlinks (rv up to 0.12.0), it follows
src/rust/Cargo.tomlto the package directory it mirrors. It acts only when that manifest sits at<origin>/src/rust/Cargo.toml, the treeβsDESCRIPTIONresolves to<origin>/DESCRIPTION, and<origin>is not the build directory. If every missing path exists relative to the origin manifest, the base-R stager packages those crates from the origin into the treeβssrc/rust/vendor/. It then renames the portable manifest, and a copy of the lockfile, over the treeβs symlinks. Every write is a new entry of the build directory, so neither configure nor cargo writes through a link into the checkout. - Otherwise (a copy, or a tree of hardlinks, where nothing leads back to the checkout) configure stops:
Path dependencies outside the package directory are missing:
- `path = "../../../core"` under [dependencies] in src/rust/Cargo.toml: /tmp/core has no Cargo.toml
bootstrap.R did not run for this build. It stages the crates that the package's
src/rust/Cargo.toml reaches outside the package directory, but the installer took
the package directory out of its repository without running it.
Install the package in one of these ways, which run bootstrap.R while the repository is present:
- rv >= 0.23.0 with a git source, or a local path to the repository root, plus `directory`
naming the package's subdirectory
- pak with a repository ref and a subdirectory, e.g. pak::pak("<owner>/<repo>/<subdirectory>")
- build the tarball in the checkout with devtools::build() and install that tarball
configure: error: a path dependency outside the package is missing; see the message above
A plain R CMD build in the checkout after an earlier bootstrap ends the same
way when the staging went stale. cleanup activates a staging only while
src/rust/Cargo.toml and every staged crate still match what bootstrap copied.
After an edit it refuses, but R CMD build discards cleanupβs output and exit
status, so the tarball keeps the original manifest. The error then says that an
earlier staging was not activated, and rerunning bootstrap.R (or building with
devtools::build(), which runs it) fixes it.
A missing [workspace.dependencies] path counts even when no dependency
inherits it, since the base-R stager rejects such an entry too. An unused
[replace] path and a target path such as [lib] path are left to cargo.
The stager runs cargo metadata --no-deps for discovery and cargo package --no-verify --allow-dirty per sibling, so it never writes to the source tree.
Two constraints follow from cargo package:
- A dependency between siblings (normal or build kind) needs a
versionkey, becausecargo packagerewrites it to a version-only entry; the stager then points it back at the staged copy. The R crateβs own path dependencies need no version key: its manifest is rewritten in place, never packaged. - A sibling cannot carry a
gitdependency, whichcargo packagerejects or turns into a crates.io one. Such a graph installs only from a fully vendored tarball (minirextendr::miniextendr_build_tarball()).
Bootstrap reports every blocking problem in one error before staging anything:
each missing version, each sibling git dependency, each path that points at
a directory without a Cargo.toml, and each other path literal in the R crate
that leaves the package. That last group covers a [patch] or [replace]
source, which the fallback does not stage, and a target such as [lib] path.
The R crate may declare a staged dependency in any TOML form cargo accepts
(inline or [dependencies.<name>] tables, dotted keys, either quote style,
absolute or ./-prefixed paths, [workspace.dependencies] inheritance,
[target.*] tables, multi-line inline tables); the minirextendr test suite
exercises each form.
The staged state records an md5 fingerprint of the source files behind every
staged crate (plus an inherited workspace manifest). Cleanup stops with
path dependency <name> changed since bootstrap; rerun bootstrap.R when a
source it can still see has changed. Sources that no longer exist are not
compared.
The source Cargo.toml stays byte-for-byte unchanged. Bootstrap prepares
.Cargo.toml.dev and .dev-bootstrap.rds beside it. R CMD build runs cleanup
inside its temporary copy, where the portable manifest is activated. Cleanup
checks both the recorded origin path and source digest, so running cleanup in
the checkout leaves its manifest alone and stale preparation fails clearly.
Configure never rewrites either source manifest.
A staging is bootstrapβs own only while .dev-bootstrap.rds sits beside it.
Every bootstrap removes that previous staging (vendor directory, portable
manifest, state) before writing a new one, so repeated installs keep exactly
one copy and no backups. A src/rust/vendor without the state file is not
bootstrapβs, so bootstrap stops instead of moving or deleting it. The dev
sidecars must reach Rβs build-copy cleanup and therefore are only gitignored.
Cleanup consumes them before the artifact is sealed, keeping its activation
backup under src/rust/.dev-vendor-backup-* inside the build copy, which is
excluded from the artifact.
Staged crates follow Cargoβs package file-selection rules. Check cargo package --list for an ancestor core crate: Git ignore rules require its manifest to be tracked. For an untracked source tree, use package.include or package.exclude to keep generated output out of the crate. In a Git checkout, untracked files that Git does not ignore are packaged too, and ignored ones are left out. Bootstrap never stages files in Git.
πSee Also
- LINKING.md: how miniextendr links to libR (engine vs package)
- ENTRYPOINT.md: the C entry point design
- CRAN_COMPATIBILITY.md: dependency vendoring for CRAN
- TEMPLATES.md: how configure.ac templates work
- R sources:
share/make/shlib.mk,share/make/winshlib.mk - R sources:
src/library/tools/R/install.R(theR CMD INSTALLimplementation)