rmacosbuildr-packageapple-silicon

Configuring compilers on Apple silicon (M1, M2, M3, ...) for Rcpp and other tools


I'm trying to use packages that require Rcpp in R on my M1 Mac, which I was never able to get up and running after purchasing this computer. I updated it to Monterey in the hope that this would fix some installation issues but it hasn't. I tried running the Rcpp check from this page but I get the following error:

> Rcpp::sourceCpp("~/github/helloworld.cpp")
ld: warning: directory not found for option '-L/opt/R/arm64/gfortran/lib/gcc/aarch64-apple-darwin20.2.0/11.0.0'
ld: warning: directory not found for option '-L/opt/R/arm64/gfortran/lib'
ld: library not found for -lgfortran
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [sourceCpp_4.so] Error 1
clang++ -arch arm64 -std=gnu++14 -I"/Library/Frameworks/R.framework/Resources/include" -DNDEBUG -I../inst/include   -I"/Library/Frameworks/R.framework/Versions/4.1-arm64/Resources/library/Rcpp/include" -I"/Library/Frameworks/R.framework/Versions/4.1-arm64/Resources/library/RcppArmadillo/include" -I"/Users/afredston/github" -I/opt/R/arm64/include   -fPIC  -falign-functions=64 -Wall -g -O2  -c helloworld.cpp -o helloworld.o
clang++ -arch arm64 -std=gnu++14 -dynamiclib -Wl,-headerpad_max_install_names -undefined dynamic_lookup -single_module -multiply_defined suppress -L/Library/Frameworks/R.framework/Resources/lib -L/opt/R/arm64/lib -o sourceCpp_4.so helloworld.o -L/Library/Frameworks/R.framework/Resources/lib -lRlapack -L/Library/Frameworks/R.framework/Resources/lib -lRblas -L/opt/R/arm64/gfortran/lib/gcc/aarch64-apple-darwin20.2.0/11.0.0 -L/opt/R/arm64/gfortran/lib -lgfortran -lemutls_w -lm -F/Library/Frameworks/R.framework/.. -framework R -Wl,-framework -Wl,CoreFoundation
Error in Rcpp::sourceCpp("~/github/helloworld.cpp") : 
  Error 1 occurred building shared library.

I get that it can't "find" gfortran. I installed this release of gfortran for Monterey. When I type which gfortran into Terminal, it returns /opt/homebrew/bin/gfortran. (Maybe this version of gfortran requires Xcode tools that are too new—it says something about 13.2 and when I run clang --version it says 13.0—but I don't see another release of gfortran for Monterey?)

I also appended /opt/homebrew/bin: to PATH in R so it looks like this now:

> Sys.getenv("PATH")
[1] "/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/Library/TeX/texbin:/Applications/RStudio.app/Contents/MacOS/postback"

Other things I checked:

Here's my session info:

R version 4.1.1 (2021-08-10)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Monterey 12.1

Matrix products: default
LAPACK: /Library/Frameworks/R.framework/Versions/4.1-arm64/Resources/lib/libRlapack.dylib

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
[1] compiler_4.1.1           tools_4.1.1              RcppArmadillo_0.10.7.5.0
[4] Rcpp_1.0.7        

Solution

  • Background

    Currently (2024-06-27), CRAN builds R 4.4 binaries for Apple silicon (M1, M2, M3, ...) using Apple Clang from Apple's Command Line Tools for Xcode 14.2 or 14.3 and GNU Fortran from an experimental branch of GCC 12.2.

    If you obtain R from CRAN (i.e., here), then you need to replicate CRAN's compiler setup on your system before building from C/C++/Fortran code shared objects to be loaded into R (i.e., before installing from sources an R package that contains C/C++/Fortran code, before invoking R CMD SHLIB to compile C/C++/Fortran code directly, before calling wrappers of R CMD SHLIB such as Rcpp function sourceCpp, ...). This requirement ensures that R and shared objects that you load into R are binary compatible.

    There is a further complication: Apple's Command Line Tools does not bundle the OpenMP header omp.h or runtime library libomp.dylib needed to install from sources packages that make use of multithreading, such as data.table. Users must obtain these and install them somewhere in the preprocessor and linker search paths. (Since R 4.3, CRAN binaries of R have bundled libomp.dylib. However, shared objects found in CRAN binaries of packages do not link it unless the package sources include a configure script arranging for the link. So, in many cases, users must still obtain omp.h and install packages from sources to get OpenMP-enabled builds.)

    Instructions for obtaining a working toolchain

    Warning: These come with no warranty and could break at any time. Some level of familiarity with C/C++/Fortran program compilation, Makefile syntax, and Unix shells is assumed. Everyone is encouraged to consult official documentation, which is more likely to be maintained than answers on SO. As usual, sudo at your own risk.

    The steps below should work on macOS Big Sur (11), Monterey (12), Ventura (13), and Sonoma (14). I will address compilers and OpenMP support at the same time and assume that you are starting from scratch. Feel free to skip steps that you have already taken, though you might find a fresh start helpful.

    1. Download an R 4.4 binary from CRAN here and install. Be sure to select the binary built for Apple silicon.

    2. Run

      $ sudo xcode-select --install
      

      in Terminal to install the latest release version of Apple's Command Line Tools for Xcode (well, the latest one supporting your version of macOS), which includes Apple Clang. You can obtain older versions from your browser here. CRAN builds R 4.4 for macOS 11 and newer using Xcode 14.2 or 14.3.

    3. Download the GNU Fortran binary provided here and install by unpacking to root:

      $ curl -LO https://github.com/R-macos/gcc-12-branch/releases/download/12.2-darwin-r0/gfortran-12.2-darwin20-r0-universal.tar.xz
      $ sudo tar xvf gfortran-12.2-darwin20-r0-universal.tar.xz -C /
      $ sudo ln -sfn $(xcrun --show-sdk-path) /opt/gfortran/SDK
      

      The last command updates a symlink inside of the installation so that it points to an SDK inside of your Command Line Tools installation.

    4. Download the OpenMP header and runtime library suitable for your Apple Clang version here and install by unpacking under /opt/R/arm64/ after stripping the usr/local/ prefix. You can query your Apple Clang version with clang --version. For example, I have version 1403.0.22.14.1, so I did:

      $ curl -LO https://mac.r-project.org/openmp/openmp-15.0.7-darwin20-Release.tar.gz
      $ sudo mkdir -p /opt/R/$(uname -m)
      $ sudo tar -xvf openmp-15.0.7-darwin20-Release.tar.gz --strip-components=2 -C /opt/R/$(uname -m)
      

      After unpacking, you should find these files on your system:

      /opt/R/arm64/lib/libomp.dylib
      /opt/R/arm64/include/ompt.h
      /opt/R/arm64/include/omp.h
      /opt/R/arm64/include/omp-tools.h
      
    5. Add the following lines to $(HOME)/.R/Makevars, creating the file if necessary.

      CPPFLAGS += -Xclang -fopenmp
      LDFLAGS += -lomp
      
    6. Test that you are able to use R to compile a C or C++ program with OpenMP support while linking relevant libraries from the GNU Fortran installation (indicated by the -l flags in the output of R CMD CONFIG FLIBS).

      The most transparent approach is to use R CMD SHLIB directly. In a temporary directory, create an empty source file omp_test.c and add the following lines:

      #ifdef _OPENMP
      # include <omp.h>
      #endif
      
      #include <Rinternals.h>
      
      SEXP omp_test(void)
      {
      #ifdef _OPENMP
          Rprintf("OpenMP threads available: %d\n", omp_get_max_threads());
      #else
          Rprintf("OpenMP not supported\n");
      #endif
          return R_NilValue;
      }
      

      Compile it:

      $ R CMD SHLIB omp_test.c $(R CMD CONFIG FLIBS)
      

      Then call the compiled C function from R:

      $ R -e 'dyn.load("omp_test.so"); invisible(.Call("omp_test"))'
      
      OpenMP threads available: 8
      

      If the compiler or linker throws an error, or if you find that OpenMP is still not supported, then one of us has made a mistake. Please report any issues.

      Note that you can implement the same test using Rcpp, if you don't mind installing it:

      library(Rcpp)
      registerPlugin("flibs", Rcpp.plugin.maker(libs = "$(FLIBS)"))
      sourceCpp(code = '
      #ifdef _OPENMP
      # include <omp.h>
      #endif
      
      #include <Rcpp.h>
      
      // [[Rcpp::plugins(flibs)]]
      // [[Rcpp::export]]
      void omp_test()
      {
      #ifdef _OPENMP
          Rprintf("OpenMP threads available: %d\\n", omp_get_max_threads());
      #else
          Rprintf("OpenMP not supported\\n");
      #endif
          return;
      }
      ')
      omp_test()
      
      OpenMP threads available: 8
      

    References

    Everything is a bit scattered: