fix(patches): replace the RcppParallel system-TBB patch with a link-order fix (#145)
Some checks failed
ci/crow/cron/process-updates/9 Pipeline was successful
ci/crow/cron/process-updates/3 Pipeline was successful
ci/crow/cron/process-updates/4 Pipeline was successful
ci/crow/cron/process-updates/10 Pipeline was successful
ci/crow/cron/process-updates/13 Pipeline was successful
ci/crow/cron/process-updates/14 Pipeline failed
ci/crow/cron/process-updates/15 Pipeline failed

## Why

Every build logs:

```
! Patch for RcppParallel 6.2.0 did not apply cleanly; skipping patched build.
```

RcppParallel 6.x rewrote its build system. `src/Makevars.in` no longer contains the `USE_TBB=Linux` block that `system-tbb.patch` edited (it is now a short `@VAR@` template driven by `tools/config/configure.R`), so the patch can never apply again.

The workaround it implemented is obsolete too. 6.x bundles **oneTBB 2022** and builds it with cmake, which works on musl and with g++ 8-15, so the 5.x reason for linking a system TBB is gone. Linking one is now harmful: `install.libs.R` symlinks the system libraries into `RcppParallel/lib`, so the published binary depends on a TBB the consumer does not have — the same failure mode as `fs` and libuv.

What is still broken upstream is the **link order**. `configure.R` names the TBB directory with `-Wl,-L`, and gcc expands its own search dirs into `-L` options ahead of anything forwarded verbatim with `-Wl,`:

```
$ gcc -v -o t t.c -Wl,-L/usr/local/lib64 -ltbb
-L/usr/lib/gcc/x86_64-redhat-linux/8  -L/lib/../lib64  -L/usr/lib/../lib64  ...  -L/usr/local/lib64
```

So on any build host with a distro TBB installed, `-ltbb` resolves to that library and `RcppParallel.so` records *its* SONAME (`libtbb.so.12`, or `libtbb.so.2` for the classic Intel TBB on el8/el9) instead of the bundled `libtbb.so`.

## Changes

- Replace `RcppParallel/system-tbb.patch` with `RcppParallel/bundled-tbb-link-order.patch`, which changes the three `-Wl,-L` occurrences in `tools/config/configure.R` to plain `-L`.
- Scope the RcppParallel entry to `>=6.0.0` and widen `platforms` to `*`.
- Drop the `rstan` entry: its `-DTBB_INTERFACE_NEW` is already emitted by `RcppParallel::CxxFlags()` once the bundled oneTBB is used, and its `-I/usr/local/include` was an el8/el9 path applied on every platform.
- `.pre-commit-config.yaml`: exclude `local/patches/*.patch` from `trailing-whitespace`, `end-of-file-fixer` and `editorconfig-checker`. They rewrite blank context lines (` ` -> ``) in every diff in the registry; `git apply` happens to tolerate it today, but a patch with meaningful trailing whitespace would be silently corrupted. The excludes are per-hook so `validate patch registry` still runs.
- README: the RcppParallel example described the 5.x problem.

## Verification

Built in the published images:

| image | NEEDED | RPATH | loads with system libtbb removed |
| --- | --- | --- | --- |
| `build-env-alpine:3.24` | `libtbb.so` | `$ORIGIN/../lib` | yes |
| `build-env-redhat:8` | `libtbb.so`, `libtbbmalloc.so` | `$ORIGIN/../lib` | yes |
| `build-env-ubuntu:noble` | `libtbb.so` | `$ORIGIN/../lib` | yes |

Without the patch the same builds record `libtbb.so.12` (alpine, ubuntu) or `libtbb.so.2` (el8, the classic 2018 TBB) and fall back to the system library at load time.

rstan 2.32.7 compiles and loads against the patched RcppParallel on ubuntu noble with no makevars override and with the system libtbb moved away. On el8 it also compiles; loading it there is blocked by an unrelated image bug (see below).

## Behaviour change

Requires the paired build-env-images PR (drops `TBB_INC`/`TBB_LIB`) and an image rebuild — with those env vars set, configure still takes the system-TBB branch. After that, RcppParallel binaries ship their own oneTBB and are self-contained.

## Also found, not fixed here

- The `uvr lock failed ... GLIBC_2.29 not found` errors in the same log are the `-gnu` uvr artifact on el8's glibc 2.28; fixed in build-env-images.
- `build-env-redhat:8` R 4.5.3/4.6.0 cannot load `stats` (`libRlapack.so: undefined symbol: dgemmtr_`): the el8 R RPM symlinks `libRblas.so` to openblas 0.3.15, which predates that symbol. R 4.4.3 and el9 are fine. Belongs in the R RPM build.

Reviewed-on: #145
This commit is contained in:
Patrick Schratz 2026-07-31 06:41:46 +00:00 committed by Patrick Schratz
commit 98371fb9c5
5 changed files with 61 additions and 45 deletions

View file

@ -7,12 +7,20 @@ exclude: |
benchmark/|
docker/reprex/
)
# The `^local/patches/.*\.patch$` excludes below keep unified diffs byte-exact:
# a context line for a blank line is a single space, and stripping it (or
# appending a newline) makes `git apply` reject the patch, which surfaces as
# "patch did not apply cleanly" at build time rather than as a lint failure
# here. The exclusions are per-hook, not global, so `validate patch registry`
# still runs when a patch changes.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: end-of-file-fixer
exclude: ^local/patches/.*\.patch$
- id: trailing-whitespace
exclude: ^local/patches/.*\.patch$
args:
- --markdown-linebreak-ext=md
- repo: https://github.com/DavidAnson/markdownlint-cli2
@ -31,6 +39,7 @@ repos:
rev: v3.8.0
hooks:
- id: editorconfig-checker
exclude: ^local/patches/.*\.patch$
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.38.0
hooks:

View file

@ -56,7 +56,7 @@ For every package+tag combination:
## Patching packages
Some CRAN packages fail to compile on specific platforms due to compiler- or OS-specific issues unrelated to the package itself.
The canonical example is `RcppParallel`, whose bundled TBB sources fail on musl (Alpine) and newer compiler/OS combinations.
The canonical example is `RcppParallel`, whose bundled TBB is linked in a way that lets a system TBB on the build host shadow it, so the published binary depends on a library the consumer does not have.
Because such packages are often transitive dependencies of many others, a single failure cascades: all dependents fail even though nothing is wrong with the dependent itself.
To address this, frequently-failing packages can be "patched" before they are installed — whether as a direct build target or a transitive dependency pulled in by `pak`.

View file

@ -0,0 +1,46 @@
diff --git a/tools/config/configure.R b/tools/config/configure.R
index 6293fe1..2f84337 100644
--- a/tools/config/configure.R
+++ b/tools/config/configure.R
@@ -186,12 +186,23 @@ define(
)
# set PKG_LIBS
+#
+# bincraft patch: the library directories below are passed as plain '-L', not
+# '-Wl,-L'. gcc expands its own search dirs (/usr/lib64, /usr/lib/<triplet>)
+# into explicit '-L' options ahead of anything forwarded verbatim with '-Wl,',
+# so with '-Wl,-L' a system libtbb.so wins over the one named here: on a build
+# host with a distro TBB installed, '-ltbb' resolves to that library and
+# RcppParallel.so records its SONAME (libtbb.so.12, or libtbb.so.2 for the
+# classic Intel TBB) instead of the bundled 'libtbb.so'. The binary then loads
+# the system TBB rather than the copy shipped in RcppParallel/lib, and fails
+# outright on a machine that has no system TBB. gcc places a plain '-L' before
+# its built-in dirs, so the intended library is found first.
pkgLibs <- if (!is.na(tbbLib)) {
# a TBB supplied via TBB_LIB / TBB_ROOT. an rpath is meaningless on Windows,
# where the loader has no equivalent -- see R/zzz.R for how we resolve there
c(
- "-Wl,-L\"$(TBB_LIB)\"",
+ "-L\"$(TBB_LIB)\"",
if (.Platform$OS.type != "windows")
sprintf("-Wl,-rpath,%s", shQuote(tbbLib)),
"-l$(TBB_NAME)",
@@ -201,14 +212,14 @@ pkgLibs <- if (!is.na(tbbLib)) {
} else if (R.version$os == "emscripten") {
c(
- "-Wl,-Ltbb/build/lib_release",
+ "-Ltbb/build/lib_release",
"-l$(TBB_NAME)"
)
} else {
c(
- "-Wl,-Ltbb/build/lib_release",
+ "-Ltbb/build/lib_release",
"-l$(TBB_NAME)",
"-l$(TBB_MALLOC_NAME)"
)

View file

@ -1,19 +0,0 @@
diff --git a/src/Makevars.in b/src/Makevars.in
index be8445f..7cb6c9e 100644
--- a/src/Makevars.in
+++ b/src/Makevars.in
@@ -60,7 +60,13 @@ else
endif
ifeq ($(UNAME), Linux)
- USE_TBB=Linux
+ # bincraft patch: link the system oneTBB (installed in the build-env images)
+ # instead of building the bundled Intel TBB, which fails on musl (Alpine) and
+ # newer toolchains (g++ 15). Leaving USE_TBB unset skips the bundled build;
+ # -DRCPP_PARALLEL_USE_TBB=1 keeps the TBB backend so dependents (rstan, ...)
+ # link TBB, and -ltbb/-ltbbmalloc pull the system library from default paths.
+ PKG_CXXFLAGS += -DRCPP_PARALLEL_USE_TBB=1 -DTBB_SUPPRESS_DEPRECATED_MESSAGES=1 -DTBB_INTERFACE_NEW
+ PKG_LIBS += -ltbb -ltbbmalloc
endif
ifeq ($(UNAME), SunOS)

View file

@ -1,42 +1,22 @@
[
{
"package": "RcppParallel",
"versions": "*",
"platforms": [
"alpine",
"ubuntu",
"redhat"
],
"versions": ">=6.0.0",
"platforms": ["*"],
"env": {},
"configure_args": [],
"makevars": {},
"patch": "RcppParallel/system-tbb.patch",
"reason": "RcppParallel bundles an old Intel TBB whose build fails on musl and modern toolchains (g++ 8-15 + modern make); link the system oneTBB (2021+) instead, keeping the TBB backend so dependents (rstan, ...) link TBB. All build-env platforms now provide oneTBB 2021+: native on alpine/ubuntu/el10, built from source into /usr/local on el8/el9 (their stock TBB is classic 2018/2020, too old)."
"patch": "RcppParallel/bundled-tbb-link-order.patch",
"reason": "RcppParallel 6.x bundles oneTBB 2022 and builds it with cmake, which works on musl and g++ 8-15, so the system-TBB workaround needed for 5.x is gone. What remains broken is the link order: configure.R passes the TBB directory as '-Wl,-L', and gcc expands its own search dirs (/usr/lib64, /usr/lib/<triplet>) into '-L' options ahead of anything forwarded with '-Wl,'. On a build host with a distro TBB installed, '-ltbb' therefore resolves to the system library and RcppParallel.so records its SONAME (libtbb.so.12, or libtbb.so.2 for the classic Intel TBB on el8/el9) instead of the bundled 'libtbb.so'. The published binary then loads the system TBB rather than the copy in RcppParallel/lib and fails to dyn.load on a consumer machine without one, the same way fs did with libuv. Passing a plain '-L' puts the bundled build dir ahead of gcc's defaults; verified on alpine 3.24, el8 and ubuntu noble to produce NEEDED libtbb.so + RPATH $ORIGIN/../lib, loading with every system libtbb removed."
},
{
"package": "fs",
"versions": "*",
"platforms": [
"*"
],
"platforms": ["*"],
"env": {},
"configure_args": [],
"makevars": {},
"patch": "fs/force-vendored-libuv.patch",
"reason": "fs 2.x configure links system libuv whenever pkg-config finds libuv-devel (installed as a build-time sysreq), producing an fs.so with NEEDED libuv.so.1. That binary fails to dyn.load on consumer machines lacking runtime libuv, because install.packages/renv do not install SystemRequirements (only pak does, and only in the build container). The patch short-circuits configure to copy src/Makevars.vendor and build the bundled static libuv (needs cmake) so the binary is self-contained on every platform. An env/pkg-config override was tried first but the rebuilt binary still linked libuv.so.1, so a source patch is used instead."
},
{
"package": "rstan",
"versions": "*",
"platforms": [
"*"
],
"env": {},
"configure_args": [],
"makevars": {
"CPPFLAGS": "-DTBB_INTERFACE_NEW -I/usr/local/include"
},
"patch": null,
"reason": "StanHeaders' init_threadpool_tbb.hpp unconditionally includes the legacy <tbb/tbb_stddef.h> (removed in oneTBB 2021+) for version detection, breaking compilation of Module.cpp against the bundled oneTBB. Pre-defining TBB_INTERFACE_NEW skips that include and selects the modern tbb/global_control.h + tbb/task_arena.h path that the bundled TBB provides (-I/usr/local/include preserves the default CPPFLAGS the override replaces)"
}
]