`NATIVE_PKG_CPPFLAGS` and `CLI_INCLUDE`
How miniextendr R packages pull headers from other R packages (anything listed in LinkingTo:) into the C-shim compilation step.
How miniextendr R packages pull headers from other R packages (anything
listed in LinkingTo:) into the C-shim compilation step.
🔗Why it exists
R packages can expose a C API: header files in inst/include/ plus
R_RegisterCCallable()-registered function pointers. Examples:
cli/progress.h(progress bars)Matrix/Matrix.h(sparse matrices)vctrs/vctrs.h(record vectors)arrow/ArrowSchema.h(Arrow C Data Interface)
A miniextendr package that wants to call one of these APIs from Rust
typically does so via a tiny C shim (often generated by bindgen --wrap-static-fns) that wraps the static inline functions in the
foreign header, so Rust can link against ordinary extern symbols.
That shim is a .c file in src/. R’s build system will compile every
.c in src/ automatically, but only with the compile flags we
give it via PKG_CPPFLAGS. R has no way to know that cli_progress_shim.c
needs -I<path-to-cli>/include on the command line.
NATIVE_PKG_CPPFLAGS is the autoconf substitution variable that
carries those -I flags from configure time down into Makevars.
🔗The wiring
Two files must agree. Breaking either one breaks the build.
🔗1. configure.ac: discover the include paths
dnl ---- Native R package include paths (for bindgen C shim compilation) ----
dnl Packages listed in LinkingTo: need their include/ paths for compiling
dnl the C shim files generated by bindgen (static inline wrapper functions).
NATIVE_PKG_CPPFLAGS=""
dnl cli - progress bar C API
CLI_INCLUDE=$("${R_HOME}/bin/Rscript" -e "cat(system.file('include', package='cli'))")
if test -n "$CLI_INCLUDE" && test -d "$CLI_INCLUDE"; then
NATIVE_PKG_CPPFLAGS="$NATIVE_PKG_CPPFLAGS -I$CLI_INCLUDE"
AC_MSG_NOTICE([cli include: $CLI_INCLUDE])
else
AC_MSG_WARN([cli package not found - native CLI bindings will not compile])
fi
AC_SUBST([NATIVE_PKG_CPPFLAGS])
CLI_INCLUDE is just a local shell variable. Its name is descriptive, not
magic. For each foreign package listed in LinkingTo: you add one
Rscript -e "cat(system.file('include', package='<name>'))" block and
append its result to NATIVE_PKG_CPPFLAGS. Skip if the package is
missing. You still want configure to succeed (the user may not need
the C API).
AC_SUBST([NATIVE_PKG_CPPFLAGS]) is what exposes the variable to
Makevars.in as @NATIVE_PKG_CPPFLAGS@.
🔗2. Makevars.in: inject into the C compile
NATIVE_PKG_CPPFLAGS = @NATIVE_PKG_CPPFLAGS@
PKG_CPPFLAGS = $(NATIVE_PKG_CPPFLAGS)
R CMD INSTALL compiles every .c in src/ with
$(CC) $(CPPFLAGS) $(CPICFLAGS) $(PKG_CPPFLAGS) ..., adding our
discovered include paths to the preprocessor search.
See R_BUILD_SYSTEM.md for how PKG_CPPFLAGS fits
into R’s overall Makefile include chain, and
NATIVE_R_PACKAGES.md for the full recipe
(including the OBJECTS → cargo rustc -C link-arg= link step that
makes the shim’s symbols visible to the Rust crate).
🔗The failure mode
If Makevars.in references @NATIVE_PKG_CPPFLAGS@ but configure.ac
forgot the matching AC_SUBST, autoconf leaves the literal token in
the generated Makevars:
clang -arch arm64 ... -DNDEBUG @NATIVE_PKG_CPPFLAGS@ -I'/.../cli/include' ...
clang: error: no such file or directory: '@NATIVE_PKG_CPPFLAGS@'
make: *** [cli_progress_shim.o] Error 1
autoconf only substitutes @VAR@ tokens whose names appear in
AC_SUBST(...) - any others pass through as-is. Since @NATIVE_PKG_CPPFLAGS@
looks like a filename to the compiler, you get this specific error.
Fix: either add the AC_SUBST([NATIVE_PKG_CPPFLAGS]) block to
configure.ac, or - if the package has no C shims - drop the
@NATIVE_PKG_CPPFLAGS@ line from Makevars.in.
🔗Why the split name
The framework uses the name NATIVE_PKG_CPPFLAGS rather than putting
the -I flags directly into PKG_CPPFLAGS so that downstream users
can still add their own PKG_CPPFLAGS additions in Makevars.in
without clobbering the framework-managed include paths:
NATIVE_PKG_CPPFLAGS = @NATIVE_PKG_CPPFLAGS@
PKG_CPPFLAGS = $(NATIVE_PKG_CPPFLAGS) -DMY_FEATURE=1
Think of NATIVE_PKG_CPPFLAGS as “flags the configure step discovered
automatically for LinkingTo: packages” and PKG_CPPFLAGS as “the
final set of C preprocessor flags that reach cc.”
🔗Template sync trap
minirextendr’s Makevars.in template ships with
@NATIVE_PKG_CPPFLAGS@ already wired in - scaffolded packages get it
for free. But upgrade_miniextendr_package() does not rewrite
configure.ac by default (configure_ac = FALSE) because users often
customise it with feature flags.
So if you upgrade an older package whose configure.ac predates the
NATIVE_PKG_CPPFLAGS block, the new Makevars.in will reference a
substitution that configure.ac doesn’t define. This showed up as a
regression in
A2-ai/dvs2#126
when the upgrade between
A2-ai/dvs2#123 refreshed
Makevars.in without the matching configure.ac edit.
If you customise configure.ac and keep the stock Makevars.in, you
are responsible for keeping the AC_SUBST([NATIVE_PKG_CPPFLAGS])
block (or deleting the @NATIVE_PKG_CPPFLAGS@ reference on the
Makevars side).