Reference page
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
src/configure(if present) to generateMakevarsfromMakevars.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 the C entry points.
π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='entrypoint.o mx_abi.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 | -L$(CARGO_LIBDIR) -l$(CARGO_STATICLIB_NAME) |
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 \
entrypoint.o mx_abi.o \
-L/path/to/rust-target/release -lrpkg \ # β 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
entrypoint.o mx_abi.o \
-L/path/to/rust-target/release -lrpkg \ # β 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)
# PKG_LIBS tells R to link our Rust static library
PKG_LIBS = -L$(CARGO_LIBDIR) -l$(CARGO_STATICLIB_NAME)
# Add Cargo build as a dependency of the shared library
$(SHLIB): $(OBJECTS) $(CARGO_AR) $(R_WRAPPERS_CURRENT)
# Build the Rust static library via Cargo
$(CARGO_AR): FORCE_CARGO $(CARGO_TOML) $(CARGO_LOCK)
$(CARGO) build --lib --profile $(CARGO_PROFILE) ...
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)with post-build recipe β runs after R links the shared lib to handle dev-mode touches and CRAN cleanup.
π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 (from cargo-config.toml.in)
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 -force_load librpkg.a (R's linker)
3. Install miniextendr.so to libs/
4. Install R/ files, man/, etc.
Note: R wrapper generation (miniextendr-wrappers.R) happens separately via
just devtools-document, not during R CMD INSTALL.
πBuild Contexts
The configure script resolves one of four build contexts based on environment variables and filesystem detection:
| Context | When | Behavior |
|---|---|---|
dev-monorepo | Monorepo detected (default for just configure) | Uses [patch] paths to workspace crates, no vendoring |
dev-detached | No monorepo, no vendor artifacts | Uses git/network deps directly |
vendored-install | NOT_CRAN=false or vendor artifacts present | Offline build from vendored sources |
prepare-cran | PREPARE_CRAN=true | Explicit CRAN release prep mode |
Environment variables:
NOT_CRAN=trueβ dev mode (legacy, still supported)PREPARE_CRAN=trueβ explicit CRAN release prep (highest precedence)- Neither set β auto-detects from monorepo/vendor presence
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.
πSee Also
- LINKING.md β How miniextendr links to libR (engine vs package)
- ENTRYPOINT.md β The C entry point design
- VENDOR.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)