From cc839be647e71c389457af30ec235b335c08b151 Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Fri, 9 Oct 2026 17:44:03 -0400 Subject: [PATCH] macOS: find libomp in the environment being built into The Darwin block probes $CONDA_PREFIX/include/omp.h and falls back to Homebrew. That misses the case where the activated environment is not the one being built into: conda-build and rattler-build activate their own base installation and install into $PREFIX, so a recipe that puts llvm-openmp in the host environment still fails with Error: no OpenMP runtime found on this macOS host. Looked in $CONDA_PREFIX/include and /opt/homebrew/opt/libomp/include. on a builder with no Homebrew libomp -- while the header sits unexamined in $PREFIX. This is what the conda-forge osx_64 build hits. Consult sys.prefix, then sys.base_prefix, after CONDA_PREFIX and before Homebrew. sys.prefix is the prefix of the interpreter running setup.py, which under both build backends is the environment the extension is built for; it is also the convention _mpi_prefix_from_env_prefix() already uses, which is why MPI detection succeeded in the same build where OpenMP failed. CONDA_PREFIX stays first, so an ordinary activated environment behaves exactly as before. The new candidates can only be reached where the code previously fell through to Homebrew, so no build that passes today changes behavior. This serves the intent of the original change rather than reversing it: build against the libomp that will be LOADED at import time, resolved through the interpreter's rpath. Linking the base installation's copy while installing into $PREFIX is how a process ends up with two LLVM OpenMP runtimes, the abort described in the README and tracked as #27. The error message listed $CONDA_PREFIX literally, which no longer describes the set of prefixes searched, so report the paths actually tried, deduplicated. Assisted-by: Claude Opus 5 --- ...-openmp-build-prefix-5c97decf03d0c7cc.yaml | 12 +++++++ setup.py | 31 ++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/darwin-openmp-build-prefix-5c97decf03d0c7cc.yaml diff --git a/releasenotes/notes/darwin-openmp-build-prefix-5c97decf03d0c7cc.yaml b/releasenotes/notes/darwin-openmp-build-prefix-5c97decf03d0c7cc.yaml new file mode 100644 index 0000000..6eb0576 --- /dev/null +++ b/releasenotes/notes/darwin-openmp-build-prefix-5c97decf03d0c7cc.yaml @@ -0,0 +1,12 @@ +--- +fixes: + - | + Building on macOS inside a conda-build or ``rattler-build`` recipe no longer + fails with ``no OpenMP runtime found on this macOS host`` when the recipe + provides ``llvm-openmp``. The build located ``libomp`` by probing + ``$CONDA_PREFIX``, which under those tools names the base installation + rather than the environment being built into, so the ``omp.h`` installed in + the build prefix went unexamined and the build fell through to Homebrew. + ``sys.prefix`` and ``sys.base_prefix`` are now consulted as well, after + ``CONDA_PREFIX`` and before Homebrew, so an ordinary activated environment + behaves as before. diff --git a/setup.py b/setup.py index 394d1a9..3993594 100644 --- a/setup.py +++ b/setup.py @@ -774,16 +774,26 @@ def detect_gpu_toolchain(): print("\nConfiguring CPU backend (_core_cpu)") if platform.system() == 'Darwin': # macOS has no system OpenMP, so libomp comes from a package manager. - # Prefer the conda env when it has one: those are the libraries actually + # Prefer an environment that has one: those are the libraries actually # LOADED at import time (resolved via the python executable's # @loader_path/../lib), so building against Homebrew's copies instead # means compiling against different libraries than the process runs on. - conda_prefix = os.environ.get('CONDA_PREFIX') - if conda_prefix and os.path.exists( - os.path.join(conda_prefix, 'include', 'omp.h')): - omp_inc = os.path.join(conda_prefix, 'include') - omp_lib = openblas_lib = os.path.join(conda_prefix, 'lib') - print(f"Darwin: libomp and BLAS from conda env {conda_prefix}") + # + # CONDA_PREFIX names the *activated* environment, which is not always + # the one being built into: conda-build and rattler-build activate their + # own base installation and install into $PREFIX, whose python is the + # interpreter running this script. So fall back to sys.prefix, which + # identifies the environment the extension is actually built for -- the + # same convention _mpi_prefix_from_env_prefix() already uses. + omp_prefix = next( + (p for p in (os.environ.get('CONDA_PREFIX'), sys.prefix, + getattr(sys, 'base_prefix', None)) + if p and os.path.exists(os.path.join(p, 'include', 'omp.h'))), + None) + if omp_prefix: + omp_inc = os.path.join(omp_prefix, 'include') + omp_lib = openblas_lib = os.path.join(omp_prefix, 'lib') + print(f"Darwin: libomp and BLAS from env prefix {omp_prefix}") else: # Ask brew for its prefix rather than assuming: it is /opt/homebrew # on Apple silicon and /usr/local on Intel, so either one hardcoded @@ -797,8 +807,13 @@ def detect_gpu_toolchain(): if not os.path.exists(os.path.join(omp_inc, 'omp.h')): # Fail here with the fix, rather than 100 lines later with # "'omp.h' file not found" from the middle of a compile. + _searched = ', '.join(dict.fromkeys( + [os.path.join(p, 'include') + for p in (os.environ.get('CONDA_PREFIX'), sys.prefix, + getattr(sys, 'base_prefix', None)) if p] + + [omp_inc])) print("Error: no OpenMP runtime found on this macOS host.\n" - f" Looked in $CONDA_PREFIX/include and {omp_inc}.\n" + f" Looked in: {_searched}.\n" " Apple clang ships without OpenMP, so install one:\n" " conda install -c conda-forge llvm-openmp (preferred)\n" " brew install libomp")