From f0ab8d024d800763b757f7d2c7e50fbe4e4942ce Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Mon, 27 Jul 2026 10:44:47 -0700 Subject: [PATCH] forward ccache to cmake via CMAKE__COMPILER_LAUNCHER When CXX/CC is configured with a leading compiler launcher (e.g. CXX='ccache g++'), splitCompilerVar treated 'ccache' as the compiler and demoted the real compiler to a flag. CMake then ran the launcher directly during its assembler probe (ccache ... -Wa,-v), which ccache rejects with 'invalid option -- W', aborting the oneTBB build. Extract a leading ccache/sccache launcher from CC/CXX and forward it via CMAKE__COMPILER_LAUNCHER, the variable CMake provides for exactly this purpose. --- NEWS.md | 6 ++++++ src/install.libs.R | 38 +++++++++++++++++++++++++++++++++ tests/test-install-libs.R | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/NEWS.md b/NEWS.md index fd2cf990..b34f6898 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # RcppParallel (development version) +* Fixed an issue where building the bundled oneTBB could fail when `CXX` + (or `CC`) was configured with a leading compiler launcher such as `ccache` + (e.g. `CXX = "ccache g++"`). The launcher is now forwarded to cmake via + `CMAKE__COMPILER_LAUNCHER` instead of being mistaken for the compiler + itself. + # RcppParallel 6.1.1 diff --git a/src/install.libs.R b/src/install.libs.R index e770838d..fba29d42 100644 --- a/src/install.libs.R +++ b/src/install.libs.R @@ -325,6 +325,13 @@ useBundledTbb <- function() { buildType <- Sys.getenv("CMAKE_BUILD_TYPE", unset = "Release") verbose <- Sys.getenv("VERBOSE", unset = "0") + # pull any leading compiler launcher (e.g. ccache) out of CC / CXX before + # splitting, so it can be forwarded to CMake the way CMake expects it (see + # extractCompilerLauncher); otherwise ccache would be treated as the + # compiler itself, breaking CMake's compiler / assembler probes + ccLauncher <- extractCompilerLauncher("CC") + cxxLauncher <- extractCompilerLauncher("CXX") + splitCompilerVar("CC", "CFLAGS") splitCompilerVar("CXX", "CXXFLAGS") @@ -341,6 +348,10 @@ useBundledTbb <- function() { cmakeFlags <- c( forwardEnvVar("CC", "CMAKE_C_COMPILER"), forwardEnvVar("CXX", "CMAKE_CXX_COMPILER"), + if (!is.na(ccLauncher)) + sprintf("-DCMAKE_C_COMPILER_LAUNCHER=%s", ccLauncher), + if (!is.na(cxxLauncher)) + sprintf("-DCMAKE_CXX_COMPILER_LAUNCHER=%s", cxxLauncher), forwardEnvVar("CFLAGS", "CMAKE_C_FLAGS"), forwardEnvVar("CXXFLAGS", "CMAKE_CXX_FLAGS"), forwardEnvVar("CMAKE_BUILD_TYPE", "CMAKE_BUILD_TYPE"), @@ -434,6 +445,33 @@ setenv <- function(key, value) { } +# Compiler launchers such as ccache are commonly injected by prefixing the +# compiler (e.g. CXX='ccache g++'). CMake models these via the separate +# CMAKE__COMPILER_LAUNCHER variable rather than as part of the compiler +# command, so detect a leading launcher, strip it from the compiler variable, +# and return it for forwarding. Returns NA when no launcher is present. +extractCompilerLauncher <- function(compilerVar) { + + compiler <- Sys.getenv(compilerVar, unset = NA) + if (is.na(compiler)) + return(NA_character_) + + tokens <- scan(text = compiler, what = character(), quiet = TRUE) + if (length(tokens) == 0L) + return(NA_character_) + + launcher <- tokens[[1L]] + name <- sub("[.]exe$", "", basename(launcher), ignore.case = TRUE) + if (!name %in% c("ccache", "sccache")) + return(NA_character_) + + # drop the launcher, leaving the real compiler (and any flags) behind + setenv(compilerVar, tokens[-1L]) + launcher + +} + + # CMake doesn't support flags specified directly as part of the compiler # definition, so manually split it here. splitCompilerVar <- function(compilerVar, flagsVar) { diff --git a/tests/test-install-libs.R b/tests/test-install-libs.R index fe0ceebd..d4f9d49c 100644 --- a/tests/test-install-libs.R +++ b/tests/test-install-libs.R @@ -40,6 +40,7 @@ if (length(marker)) env <- new.env(parent = globalenv()) eval(parse(text = paste(lines, collapse = "\n")), envir = env) splitCompilerVar <- get("splitCompilerVar", envir = env) +extractCompilerLauncher <- get("extractCompilerLauncher", envir = env) patchTbbMachineHeader <- get("patchTbbMachineHeader", envir = env) # minimal assertion harness @@ -113,6 +114,49 @@ Sys.unsetenv("TEST_CXX_UNSET") check(identical(splitCompilerVar("TEST_CXX_UNSET", "TEST_CXXFLAGS"), FALSE), "unset compiler variable returns FALSE") +# a leading compiler launcher (ccache) must be split off from the compiler so +# CMake can receive it via CMAKE__COMPILER_LAUNCHER. Left in place, CMake +# would treat 'ccache' as the compiler and run 'ccache ... -Wa,-v' during its +# assembler probe, which ccache rejects with "invalid option -- 'W'". +withEnv(c(TEST_CXX = "ccache g++"), { + launcher <- extractCompilerLauncher("TEST_CXX") + check(identical(launcher, "ccache"), "ccache launcher is extracted") + check(identical(Sys.getenv("TEST_CXX"), "g++"), + "ccache launcher is stripped from the compiler") +}) + +# the launcher may be a full path and carry trailing compiler flags; only the +# launcher token is removed, leaving the compiler and its flags intact +withEnv(c(TEST_CXX = "/usr/bin/ccache g++ -std=c++17"), { + launcher <- extractCompilerLauncher("TEST_CXX") + check(identical(launcher, "/usr/bin/ccache"), + "ccache launcher path is extracted") + check(identical(Sys.getenv("TEST_CXX"), "g++ -std=c++17"), + "compiler and flags survive launcher extraction") +}) + +# sccache is recognized as a launcher too +withEnv(c(TEST_CXX = "sccache clang++"), { + launcher <- extractCompilerLauncher("TEST_CXX") + check(identical(launcher, "sccache"), "sccache launcher is extracted") + check(identical(Sys.getenv("TEST_CXX"), "clang++"), + "sccache launcher is stripped from the compiler") +}) + +# a plain compiler has no launcher: return NA and leave the variable untouched +withEnv(c(TEST_CXX = "g++ -O2"), { + launcher <- extractCompilerLauncher("TEST_CXX") + check(identical(launcher, NA_character_), + "plain compiler yields no launcher") + check(identical(Sys.getenv("TEST_CXX"), "g++ -O2"), + "plain compiler is left unmodified") +}) + +# an unset compiler variable is a no-op that reports NA +Sys.unsetenv("TEST_CXX_UNSET") +check(identical(extractCompilerLauncher("TEST_CXX_UNSET"), NA_character_), + "unset compiler variable yields no launcher") + # the mingw cpuid guard should be applied exactly once to a header with the # expected form, and applying it again should leave the header untouched header <- tempfile(fileext = ".h")