Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
2025-12-01 Kevin Ushey <kevinushey@gmail.com>

* R/Attributes.R: Update OpenMP plugin for macOS

2025-11-29 Dirk Eddelbuettel <edd@debian.org>

* R/RcppLdpath.R: Revisit deprecation warnings via 'message()' to be
Expand Down
70 changes: 67 additions & 3 deletions R/Attributes.R
Original file line number Diff line number Diff line change
Expand Up @@ -586,10 +586,74 @@ compileAttributes <- function(pkgdir = ".", verbose = getOption("verbose")) {
list(env = list(PKG_CXXFLAGS ="-std=c++2b"))
}

.openmpPluginDefault <- function() {
list(env = list(PKG_CXXFLAGS = "-fopenmp", PKG_LIBS = "-fopenmp"))
}

.openmpPluginDarwin <- function() {

# generate a test script for compilation
script <- tempfile("openmp-detect-", fileext = ".cpp")
writeLines("", con = script)
on.exit(unlink(script, force = TRUE), add = TRUE)

# get the C++ compiler from R
r <- file.path(R.home("bin"), "R")
output <- tryCatch(
system2(r, c("CMD", "SHLIB", "--dry-run", shQuote(script)), stdout = TRUE),
condition = identity
)
if (inherits(output, "condition"))
return(.openmpPluginDefault())

# extract the compiler invocation from the shlib output
# use some heuristics here...
index <- grep("make would use", output)
compile <- output[[index + 1L]]

# use everything up to the first include flag, which is normally
# the R headers from CPPFLAGS
idx <- regexpr(" -I", compile, fixed = TRUE)
cxx <- substring(compile, 1L, idx - 1L)
Comment on lines +609 to +617
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this would trigger:

R CMD SHLIB --dry-run $TMPDIR/openmp-detect-.cpp

Giving:

make cmd is   
  make -f '/Library/Frameworks/R.framework/Resources/etc/Makeconf' -f '/Library/Frameworks/R.framework/Resources/share/make/shlib.mk' -f '/Users/ronin/.R/Makevars' SHLIB_LDFLAGS='$(SHLIB_CXXLDFLAGS)' SHLIB_LD='$(SHLIB_CXXLD)' SHLIB='/var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.so' OBJECTS='/var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.o'  

make would use
clang++ -arch arm64 -std=gnu++17 -I"/Library/Frameworks/R.framework/Resources/include" -DNDEBUG   -I/opt/R/arm64/include -I/opt/R/arm64/include    -fPIC  -falign-functions=64 -Wall -g -O2   -c /var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.cpp -o /var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.o if test  "z/var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.o" != "z"; then \ 	  echo clang++ -arch arm64 -std=gnu++17 -dynamiclib -Wl,-headerpad_max_install_names -undefined dynamic_lookup -L"/Library/Frameworks/R.framework/Resources/lib" -L/opt/R/arm64/lib -L/opt/R/arm64/lib -o /var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.so /var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.o    -F/Library/Frameworks/R.framework/.. -framework R ; \ 	  clang++ -arch arm64 -std=gnu++17 -dynamiclib -Wl,-headerpad_max_install_names -undefined dynamic_lookup -L"/Library/Frameworks/R.framework/Resources/lib" -L/opt/R/arm64/lib -L/opt/R/arm64/lib -o /var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.so /var/folders/b0/vt_1hj2d6yd8myx9lwh81pww0000gn/T//Rtmpcxzs34/openmp-detect-137df4ba71a0a.o    -F/Library/Frameworks/R.framework/.. -framework R ; \ 	fi

After the regex, we're left with:

clang++ -arch arm64 -std=gnu++17

This is the same as:

R CMD config CXX
clang++ -arch arm64 -std=gnu++17

I'm not immediately seeing a huge benefit for a disk write and a regex to end up back at CXX config. Is there something regarding a custom toolchain that this would protect against?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See earlier discussion between @kevinushey and myself. It is possible that, say, CXX != CXX17 so that asking for the former does not give us the answer.

I agree with the sentiment of this being more brittle but on balance that may be what we have to go with.

Copy link
Contributor Author

@kevinushey kevinushey Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's exactly it. For example, if the USE_CXX17 environment variable is set, then CXX17 will be used during compilation. Some users might be doing things like this in their .Renviron to enforce the use of their configured CXX17 compiler for different packages.

(And, as far as I can see, there isn't a way to ask R "what compiler do you want to use right now?" beyond something like this.)


# check the compiler version
command <- paste(cxx, "--version")
version <- tryCatch(
system(command, intern = TRUE),
condition = identity
)
if (inherits(version, "condition"))
return(.openmpPluginDefault())

# if we're using Apple clang, use alternate flags
# assume libomp was installed following https://mac.r-project.org/openmp/
if (any(grepl("Apple clang", version))) {
cxxflags <- "-Xclang -fopenmp"
libs <- "-lomp"
}

# if we're using Homebrew clang, add in libomp include paths
else if (any(grepl("Homebrew clang", version))) {
machine <- Sys.info()[["machine"]]
prefix <- if (machine == "arm64") "/opt/homebrew" else "/usr/local"
cxxflags <- sprintf("-I%s/opt/libomp/include -fopenmp", prefix)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed that the version of LLVM clang provided in Homebrew accepts -fopenmp as-is, but I'm adding the library paths for libomp just in case.

libs <- sprintf("-L%s/opt/libomp/lib -fopenmp", prefix)

# otherwise, use default -fopenmp flags for other compilers (LLVM clang; gcc)
} else {
cxxflags <- "-fopenmp"
libs <- "-fopenmp"
}

list(env = list(PKG_CXXFLAGS = cxxflags, PKG_LIBS = libs))

}

## built-in OpenMP plugin
.plugins[["openmp"]] <- function() {
list(env = list(PKG_CXXFLAGS="-fopenmp",
PKG_LIBS="-fopenmp"))
.plugins[["openmp"]] <- if (Sys.info()[["sysname"]] == "Darwin") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like how the detection simplifies the function definition on load.

.openmpPluginDarwin
} else {
.openmpPluginDefault
}

.plugins[["unwindProtect"]] <- function() { # nocov start
Expand Down