Keeping Bundled C++ Headers Within CRAN’s 100-Byte Path Limit

github-actions
cran
r
r-package
c++
ensmallen
Author

James Balamuta

Published

July 18, 2026

NoteCompanion post

This is a follow-up to Automating Dependency Updates with GitHub Actions: The Ensmallen Case, and the fix described here shipped in RcppEnsmallen v0.3.11.0.1.

{RcppEnsmallen} bundles the header-only ensmallen optimization library verbatim inside inst/include/. That works cleanly until an upstream header lands with a very long name. ensmallen 3.11.0 did exactly that, adding a header whose packaged path came to 109 bytes:

RcppEnsmallen/inst/include/ensmallen_bits/delta_bar_delta/update_policies/momentum_delta_bar_delta_update.hpp

R CMD check’s “checking for portable file names” step flags any packaged path over 100 bytes – the old USTAR tar limit documented in the “Package structure” section of Writing R Extensions – and CRAN wants every tarball path within it. The result is a NOTE we can’t ship past.

Why a one-off rename regresses

The obvious move is to rename the file. But our daily “Update Ensmallen” workflow runs rm -fr inst/include/ensmallen_bits and re-extracts the upstream release tarball verbatim on every upgrade, so any manual rename would be wiped on the very next bump. For the fix to survive, it has to live in the pipeline.

A manifest, an applier, and a guard

We solved this with a small, self-policing remap step in three parts.

First, a declarative manifest, tools/ensmallen-path-remaps.tsv, holding tab-separated old<TAB>new relpaths (relative to inst/include). Today it holds a single line mapping the long header to a short name.

Second, an applier, tools/apply-ensmallen-path-remaps.sh. For each manifest entry it renames the file and rewrites the #include references by basename. Because ensmallen header basenames are globally unique, a basename substitution is safe and catches both relative and rooted includes. The script is idempotent, so re-running it after an already-remapped extraction is a no-op.

Third – and this is the part that keeps us honest – a guard. After applying the remaps, the script recomputes every inst/ path as RcppEnsmallen/<relpath> and exits non-zero listing any that still exceed 100 bytes:

# Fail loudly if any packaged path still exceeds 100 bytes
over=$(find inst -type f | sed 's#^#RcppEnsmallen/#' | awk 'length > 100')
[ -n "$over" ] && printf '%s\n' "$over" && exit 1

The wiring is one line: the workflow runs bash tools/apply-ensmallen-path-remaps.sh immediately after the tar extraction. Because the guard fails the step, a new over-long upstream path fails the auto-update PR loudly – turning a silent CRAN NOTE into a build failure that’s fixed by adding one line to the manifest.

The result

For 3.11.0 the applier renamed momentum_delta_bar_delta_update.hpp to momentum_dbd_update.hpp (109 -> 97 bytes) and rewrote one #include, so “checking for portable file names” now reports OK and the NOTE is gone. See coatless-rpkg/rcppensmallen#72 for the full change.

This isn’t unique to ensmallen. RcppBandicoot – which vendors Bandicoot, the GPU-accelerated linear-algebra library from the Armadillo team, and isn’t on CRAN yet – hit the same limit and uses a lighter version of the same idea: its update workflow renames the whole bandicoot_bits/kernels/ directory to ks/ on every upgrade (#26), since there the over-long paths all shared one folder.