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:

  1. Runs src/configure (if present) to generate Makevars from Makevars.in
  2. Compiles C/C++/Fortran sources into .o object files
  3. Links those objects into a shared library (.so on Unix, .dll on Windows)
  4. 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:

VariablePurposeOur value
PKG_LIBSExtra libraries to link-L$(CARGO_LIBDIR) -l$(CARGO_STATICLIB_NAME)
PKG_CPPFLAGSC preprocessor flags(not used)
PKG_CFLAGSExtra C compiler flags(not used)

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

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:

  1. No link recipe on $(SHLIB) β€” we only add dependencies. The recipe comes from shlib.mk ($(SHLIB_LINK) -o $@ $(OBJECTS) $(ALL_LIBS)).

  2. FORCE_CARGO phony target β€” ensures Cargo is always invoked, letting Cargo’s own incremental build system decide what to rebuild.

  3. 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 .def file on Windows only needs to export R_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:

ContextWhenBehavior
dev-monorepoMonorepo detected (default for just configure)Uses [patch] paths to workspace crates, no vendoring
dev-detachedNo monorepo, no vendor artifactsUses git/network deps directly
vendored-installNOT_CRAN=false or vendor artifacts presentOffline build from vendored sources
prepare-cranPREPARE_CRAN=trueExplicit 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 (the R CMD INSTALL implementation)