From 9873628070294d31a9ee6f540904573b8403861e Mon Sep 17 00:00:00 2001 From: Hyunsu Cho Date: Mon, 17 Aug 2026 20:12:10 -0700 Subject: [PATCH 1/2] MacOS: Fix rpath for libomp.dylib --- python/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 3d6c9987..2e8940db 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -75,11 +75,11 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest [[tool.cibuildwheel.overrides]] select = "*-macosx_x86_64" -environment = { MACOSX_DEPLOYMENT_TARGET = "10.15" } +environment = { MACOSX_DEPLOYMENT_TARGET = "10.15", LDFLAGS = "-Wl,-rpath,/opt/homebrew/opt/libomp/lib" } [[tool.cibuildwheel.overrides]] select = "*-macosx_arm64" -environment = { MACOSX_DEPLOYMENT_TARGET = "12.0" } +environment = { MACOSX_DEPLOYMENT_TARGET = "12.0", LDFLAGS = "-Wl,-rpath,/usr/local/opt/libomp/lib" } [tool.ruff] line-length = 120 From 96c0a6591b678245024557167581a2921742f281 Mon Sep 17 00:00:00 2001 From: Hyunsu Cho Date: Mon, 17 Aug 2026 20:30:36 -0700 Subject: [PATCH 2/2] Patch RPATH for libomp.dylib --- CMakeLists.txt | 15 ++++++ cmake/FindOpenMPMacOS.cmake | 99 +++++++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 25 ---------- 3 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 cmake/FindOpenMPMacOS.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index b111d92f..4100c39f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,17 @@ if(USE_SANITIZER) enable_sanitizers("${ENABLED_SANITIZERS}") endif() +include(cmake/FindOpenMPMacOS.cmake) +if(USE_OPENMP) + if(APPLE) + find_openmp_macos() + else() + find_package(OpenMP REQUIRED) + endif() +else() + message(STATUS "Disabling OpenMP") +endif() + if(ENABLE_ALL_WARNINGS) if((NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") AND (NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")) message(SEND_ERROR "ENABLE_ALL_WARNINGS is only available for Clang and GCC.") @@ -119,6 +130,10 @@ foreach(lib ${TREELITE_TARGETS}) endif() endforeach() +if(USE_OPENMP AND APPLE AND NOT Treelite_BUILD_STATIC_LIBS) + patch_openmp_path_macos(treelite) +endif() + # Export install targets if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) # Include CPack only if the current project is top level. diff --git a/cmake/FindOpenMPMacOS.cmake b/cmake/FindOpenMPMacOS.cmake new file mode 100644 index 00000000..5e30cb1a --- /dev/null +++ b/cmake/FindOpenMPMacOS.cmake @@ -0,0 +1,99 @@ +# Find OpenMP library on MacOS +# Automatically handle locating libomp from the Homebrew package manager + +# lint_cmake: -package/consistency + +macro(find_openmp_macos) + if(NOT APPLE) + message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}() must only be used on MacOS") + endif() + find_package(OpenMP) + if(NOT OpenMP_FOUND) + # Try again with extra path info. This step is required for libomp 15+ from Homebrew, + # as libomp 15.0+ from brew is keg-only + # See https://github.com/Homebrew/homebrew-core/issues/112107#issuecomment-1278042927. + execute_process(COMMAND brew --prefix libomp + OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE) + set(OpenMP_C_FLAGS + "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") + set(OpenMP_CXX_FLAGS + "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") + set(OpenMP_C_LIB_NAMES omp) + set(OpenMP_CXX_LIB_NAMES omp) + set(OpenMP_omp_LIBRARY ${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib) + find_package(OpenMP REQUIRED) + endif() +endmacro() + +# Patch an XGBoost shared library so that it depends on @rpath/libomp.dylib +# instead of /opt/homebrew/opt/libomp/lib/libomp.dylib or other hard-coded paths. +# Doing so enables XGBoost to interoperate with multiple kinds of OpenMP +# libraries. See https://github.com/lightgbm-org/LightGBM/pull/6391 for detailed +# explanation. Adapted from https://github.com/lightgbm-org/LightGBM/pull/6391 +# by James Lamb. +# MacOS only. +function(patch_openmp_path_macos target) + if(NOT APPLE) + message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}() must only be used on MacOS") + endif() + # Get path to libomp found at build time + get_target_property( + __OpenMP_LIBRARY_LOCATION + OpenMP::OpenMP_CXX + INTERFACE_LINK_LIBRARIES + ) + # Get the base name of the OpenMP lib + # Usually: libomp.dylib, libgomp.dylib, or libiomp.dylib + get_filename_component( + __OpenMP_LIBRARY_NAME + ${__OpenMP_LIBRARY_LOCATION} + NAME + ) + # Get the directory containing the OpenMP lib + get_filename_component( + __OpenMP_LIBRARY_DIR + ${__OpenMP_LIBRARY_LOCATION} + DIRECTORY + ) + # Override the absolute path to OpenMP with a relative one using @rpath. + # + # This also ensures that if a libomp.dylib has already been loaded, it'll just use that. + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND + install_name_tool + -change + ${__OpenMP_LIBRARY_LOCATION} + "@rpath/${__OpenMP_LIBRARY_NAME}" + "$" + VERBATIM + ) + message(STATUS + "${target}: " + "Replacing hard-coded OpenMP install_name with '@rpath/${__OpenMP_LIBRARY_NAME}'..." + ) + # Add RPATH entries to ensure the loader looks in the following locations: + # + # - R builds: wherever the active R toolchain's OpenMP discovery found libomp. + # - Other builds: Homebrew's libomp followed by the discovered OpenMP library directory. + # + # Note: This list will only be used if libomp.dylib isn't already loaded into memory. + # So Conda users will likely use ${CONDA_PREFIX}/libomp.dylib + if(R_LIB) + set(__OPENMP_RPATH "${__OpenMP_LIBRARY_DIR}") + else() + execute_process(COMMAND brew --prefix libomp + OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE) + set(__OPENMP_RPATH "${HOMEBREW_LIBOMP_PREFIX}/lib;${__OpenMP_LIBRARY_DIR}") + endif() + set_target_properties( + ${target} + PROPERTIES + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "${__OPENMP_RPATH}" + INSTALL_RPATH_USE_LINK_PATH FALSE + ) +endfunction() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5411fcd8..26c5fc08 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,31 +3,6 @@ add_library(objtreelite OBJECT) target_link_libraries(objtreelite PRIVATE RapidJSON::rapidjson nlohmann_json::nlohmann_json std::mdspan) -if(USE_OPENMP) - if(APPLE) - find_package(OpenMP) - if (NOT OpenMP_FOUND) - # Try again with extra path info; required for libomp 15+ from Homebrew - message(STATUS "OpenMP not found; attempting to locate libomp from Homebrew...") - execute_process(COMMAND brew --prefix libomp - OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX - OUTPUT_STRIP_TRAILING_WHITESPACE) - set(OpenMP_C_FLAGS - "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") - set(OpenMP_CXX_FLAGS - "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") - set(OpenMP_C_LIB_NAMES omp) - set(OpenMP_CXX_LIB_NAMES omp) - set(OpenMP_omp_LIBRARY ${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib) - find_package(OpenMP REQUIRED) - endif() - else() - find_package(OpenMP REQUIRED) - endif() -else() - message(STATUS "Disabling OpenMP") -endif() - if(ENABLE_ALL_WARNINGS) target_compile_options(objtreelite PRIVATE -Wall -Wextra) endif()