`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. Upgrades compare the retained file with the
current template and warn about any differences, including custom edits.
Review those differences or run upgrade_miniextendr_package(configure_ac = TRUE)
to replace the file with the current template.
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).