From a56a25296efa0ce40347ca1b4a2898b70c856c4c Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Thu, 2 Jul 2026 00:02:22 +0300 Subject: [PATCH 01/14] CI/macos: native (non-Rosetta) x86_64 cross build on Apple Silicon Alternative to the Rosetta approach: build the Intel target with a NATIVE arm64 toolchain that cross-targets x86_64 (-arch x86_64), linking the x86_64 Homebrew at /usr/local. Compiles run on native arm64 clang (fast); only the output is x86_64. - ConfigureHomebrew.cmake: honor an explicit `-D HOMEBREW_PREFIX=` override (falls back to `brew --prefix`), so a native arm64 cmake can be pointed at the /usr/local x86_64 bottles. - build_source.sh: honor a caller-set NPROC to cap build parallelism. - macos/crossplatform-builds/README.md: documents the recipe, the critical gotchas (must force CMAKE_MAKE_PROGRAM to the arm64 ninja, else CMake's find_program grabs the x86_64 ninja from /usr/local and silently compiles under Rosetta; CMAKE_PREFIX_PATH=/usr/local for find_package; x86_64 python shim), and a native-vs-Rosetta comparison. Validated on an Apple M4: native arm64 clang (Code Type: ARM64) cross-built MeshLib core to x86_64; all binaries x86_64, linked against /usr/local x86_64 dylibs; 294/294 unit tests pass under Rosetta. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmake/Modules/ConfigureHomebrew.cmake | 26 ++++--- macos/crossplatform-builds/README.md | 102 ++++++++++++++++++++++++++ scripts/build_source.sh | 12 ++- 3 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 macos/crossplatform-builds/README.md diff --git a/cmake/Modules/ConfigureHomebrew.cmake b/cmake/Modules/ConfigureHomebrew.cmake index 923850d0ee70..24a6211c6af3 100644 --- a/cmake/Modules/ConfigureHomebrew.cmake +++ b/cmake/Modules/ConfigureHomebrew.cmake @@ -1,17 +1,21 @@ IF(APPLE) message("building for Apple") - execute_process( - COMMAND brew --prefix - RESULT_VARIABLE CMD_ERROR - OUTPUT_VARIABLE HOMEBREW_PREFIX - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - IF(CMD_ERROR EQUAL 0 AND EXISTS "${HOMEBREW_PREFIX}") - message("Homebrew found. Prefix: ${HOMEBREW_PREFIX}") - ELSE() - message("Homebrew not found!") - message(FATAL_ERROR "${CMD_ERROR} ${HOMEBREW_PREFIX}") + # Allow an explicit Homebrew prefix override (e.g. -D HOMEBREW_PREFIX=/usr/local + # to link the x86_64 bottles when cross-building Intel on an arm64 host with a + # native toolchain). Falls back to `brew --prefix` for the common native case. + IF(NOT HOMEBREW_PREFIX) + execute_process( + COMMAND brew --prefix + RESULT_VARIABLE CMD_ERROR + OUTPUT_VARIABLE HOMEBREW_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + IF(NOT (CMD_ERROR EQUAL 0 AND EXISTS "${HOMEBREW_PREFIX}")) + message("Homebrew not found!") + message(FATAL_ERROR "${CMD_ERROR} ${HOMEBREW_PREFIX}") + ENDIF() ENDIF() + message("Homebrew prefix: ${HOMEBREW_PREFIX}") include_directories(${HOMEBREW_PREFIX}/include) link_directories(${HOMEBREW_PREFIX}/lib) diff --git a/macos/crossplatform-builds/README.md b/macos/crossplatform-builds/README.md new file mode 100644 index 000000000000..f3858ed968ec --- /dev/null +++ b/macos/crossplatform-builds/README.md @@ -0,0 +1,102 @@ +# macOS cross-platform builds — native (non-Rosetta) x86_64 on Apple Silicon + +Build the **Intel (`x86_64`)** macOS target of MeshLib on **Apple Silicon (`arm64`)** using a +**native arm64 toolchain that cross-targets x86_64** — the compiler runs natively (no Rosetta +translation of the build tools, so compiles are fast) and emits x86_64 code via `-arch x86_64`, +linking the x86_64 libraries from the `/usr/local` Homebrew. + +> This is the alternative to the "run everything under Rosetta" approach: there the whole +> toolchain (cmake/ninja/clang) runs as x86_64 under Rosetta, which is simpler but slower to +> compile. Here the toolchain stays native arm64 and only the *output* is x86_64. + +## How it works + +- **cmake / ninja / clang run natively as arm64** (fast). `CMAKE_OSX_ARCHITECTURES=x86_64` + makes AppleClang emit x86_64 objects; the resulting binaries run on Intel Macs, or on this + host under Rosetta. +- The x86_64 dependencies come from the **x86_64 Homebrew at `/usr/local`** (coexists with the + native arm64 Homebrew at `/opt/homebrew`). [`ConfigureHomebrew.cmake`](../../cmake/Modules/ConfigureHomebrew.cmake) + now honors an explicit `-D HOMEBREW_PREFIX=/usr/local` instead of always calling `brew --prefix`. + +## Critical gotchas (why the naive attempt silently falls back to Rosetta) + +1. **Force the arm64 ninja.** CMake's `find_program` searches `/usr/local/bin` *by default*, even + when it isn't on `PATH`, so it picks up the **x86_64** ninja from the Intel Homebrew — and an + x86_64 ninja spawns **x86_64 clang under Rosetta**, silently defeating the native build. Pass + `-D CMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja`. (Verify with `vmmap | grep "Code Type"` + → must say `ARM64`, not `X86-64 (Translated)`.) +2. **Point find_package at `/usr/local`** with `-D CMAKE_PREFIX_PATH=/usr/local` so packages like + `Python`, OpenSSL, etc. resolve their x86_64 copies (not the arm64 `/opt/homebrew` ones). +3. **x86_64 Python vs native cmake PATH tension.** Native cmake/ninja want `/opt/homebrew` first + on `PATH`, but Python must be the x86_64 one from `/usr/local`. Resolve with a small shim dir + on `PATH` that maps `cmake`/`ninja` → `/opt/homebrew` (arm64) and `python3.10`/`python3.10-config` + → `/usr/local` (x86_64). +4. **`CMAKE_SYSTEM_PROCESSOR` stays `arm64`** (it reflects the *host*, since the cmake process is + native). This is harmless for MeshLib's own code (its SIMD is gated on target macros + `__x86_64__`/`__aarch64__`), and the heavy SIMD dependencies (OpenVDB, TBB, blosc) come from + x86_64 Homebrew *binaries* — not built from source here. Cosmetic side effect: `MR_PLATFORM` + is labelled `APPLE_arm64` for an x86_64 build. +5. Configure-time feature probes (`try_run` / `find_package(Python)` running the interpreter) + execute x86_64 test binaries, which the OS runs via Rosetta *transparently*. The bulk + compilation is native; only these brief configure probes touch Rosetta. Going fully + Rosetta-free would require a CMake toolchain file with `CMAKE_CROSSCOMPILING` + pre-seeded + `try_run` results. + +## What changed (vs upstream) + +| Change | File | +|---|---| +| Honor `-D HOMEBREW_PREFIX=` override (falls back to `brew --prefix`) | [`cmake/Modules/ConfigureHomebrew.cmake`](../../cmake/Modules/ConfigureHomebrew.cmake) | +| Honor a caller-set `NPROC` to cap build parallelism | [`scripts/build_source.sh`](../../scripts/build_source.sh) | + +Everything else is driven by CMake `-D` flags at invocation (below), so no other source changes +are required. + +## Recipe (Apple Silicon → native x86_64 cross build) + +Prerequisites: Rosetta 2 (only for *running* the resulting x86_64 binaries / configure probes) and +an x86_64 Homebrew bootstrapped at `/usr/local` with the `requirements/macos.txt` formulae. + +```bash +# shim: native arm64 cmake/ninja + x86_64 python on PATH +SHIM=/tmp/mlnative_bin; mkdir -p "$SHIM" +ln -sf /opt/homebrew/bin/cmake "$SHIM/cmake" +ln -sf /opt/homebrew/bin/ninja "$SHIM/ninja" +ln -sf /usr/local/bin/python3.10 "$SHIM/python3.10" +ln -sf /usr/local/bin/python3.10-config "$SHIM/python3.10-config" + +env -i HOME="$HOME" \ + PATH="$SHIM:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin" \ + NPROC=5 MESHLIB_BUILD_RELEASE=ON MESHLIB_BUILD_DEBUG=OFF \ + CMAKE_C_COMPILER=/usr/bin/clang CMAKE_CXX_COMPILER=/usr/bin/clang++ \ + MR_CMAKE_OPTIONS="\ + -D CMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja \ + -D HOMEBREW_PREFIX=/usr/local \ + -D CMAKE_PREFIX_PATH=/usr/local \ + -D CMAKE_OSX_ARCHITECTURES=x86_64 \ + -D MR_CXX_STANDARD=23 -D MR_PCH_USE_EXTRA_HEADERS=ON" \ + bash ./scripts/build_source.sh +``` + +The thirdparty-from-source libraries build the same way (native arm64 tools + the same `-D` flags); +their x86_64 output is bit-for-bit equivalent regardless of whether they were built natively or +under Rosetta. + +## Validation + +Validated on an Apple **M4** (macOS 15.7): MeshLib core built with a **native arm64 clang** +(confirmed `Code Type: ARM64` for the live compiler processes) cross-targeting x86_64; all +binaries are x86_64 and link the `/usr/local` x86_64 Homebrew dylibs; **294/294 unit tests pass** +(the x86_64 test binary runs under Rosetta). + +## Native vs Rosetta — which to use + +| | Rosetta (`…-rosetta` branch) | Native (this branch) | +|---|---|---| +| Toolchain | x86_64 under Rosetta | native arm64 | +| Compile speed | slower (translated clang) | **faster (native clang)** | +| CMake setup | just PATH + a couple env vars | more `-D` flags; ninja/find_program gotchas | +| `CMAKE_SYSTEM_PROCESSOR` | `x86_64` (SIMD paths correct for source builds) | `arm64` (cosmetic `MR_PLATFORM` mislabel) | +| Robustness | higher (fewer moving parts) | needs care (silent Rosetta fallback if ninja wrong) | + +Native wins on build speed; Rosetta wins on simplicity/robustness. Pick per priority. diff --git a/scripts/build_source.sh b/scripts/build_source.sh index d0ce87e65507..1d7748f90108 100755 --- a/scripts/build_source.sh +++ b/scripts/build_source.sh @@ -98,10 +98,14 @@ if [[ $OSTYPE == 'darwin'* ]]; then " fi -if [[ $OSTYPE == 'darwin'* ]]; then - NPROC=$(sysctl -n hw.logicalcpu) -else - NPROC=$(nproc) +# Respect a caller-provided NPROC (e.g. to cap parallelism / limit heat); +# otherwise default to all available cores. +if [ -z "${NPROC}" ]; then + if [[ $OSTYPE == 'darwin'* ]]; then + NPROC=$(sysctl -n hw.logicalcpu) + else + NPROC=$(nproc) + fi fi echo "The number of concurrent build threads NPROC=${NPROC}" From 0fe7bd85a08c17ba00048654da3e58098ced9461 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Tue, 14 Jul 2026 00:31:43 +0300 Subject: [PATCH 02/14] macos/crossplatform-builds: add native-build runner provisioning script Idempotent provisioning for the NATIVE (non-Rosetta) x86_64 cross build: 1. native arm64 cmake + ninja at /opt/homebrew (the build driver; installs only if missing, no force-upgrade) 2. Rosetta 2 (only to RUN the x86_64 output + CMake configure probes; the compilation itself is native) 3. x86_64 Homebrew at /usr/local (source of x86_64 bottles); --prewarm to install the formulae now, else CI installs them. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../crossplatform-builds/provision-runner.sh | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100755 macos/crossplatform-builds/provision-runner.sh diff --git a/macos/crossplatform-builds/provision-runner.sh b/macos/crossplatform-builds/provision-runner.sh new file mode 100755 index 000000000000..5d160d7603c2 --- /dev/null +++ b/macos/crossplatform-builds/provision-runner.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Provision a self-hosted arm64 macOS runner for the NATIVE (non-Rosetta) x86_64 +# cross build of MeshLib (see README.md). Idempotent -- safe to re-run. +# Run once per runner, from the repo root: +# ./macos/crossplatform-builds/provision-runner.sh [--prewarm] +# +# The native cross build uses a NATIVE arm64 toolchain (fast compiles) that +# cross-targets x86_64 and links the x86_64 Homebrew at /usr/local. +# +# Prerequisites: +# 1. native arm64 cmake + ninja (/opt/homebrew) -- the build driver (runs natively) +# 2. x86_64 Homebrew at /usr/local (+ formulae) -- the x86_64 libraries to link against +# 3. Rosetta 2 -- ONLY to run the resulting x86_64 test +# binaries and CMake configure-time +# probes; compilation itself is native. +# +# The runner must also carry the labels [self-hosted, macos, arm64, build]. +set -euo pipefail + +PREWARM=0 +[[ "${1:-}" == "--prewarm" ]] && PREWARM=1 + +if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then + echo "Run on an arm64 macOS host (cross-builds x86_64)." >&2; exit 1 +fi +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +echo "==> 1/3 native arm64 toolchain (cmake + ninja @ /opt/homebrew)" +if [[ ! -x /opt/homebrew/bin/brew ]]; then + echo " ERROR: native arm64 Homebrew not found at /opt/homebrew." >&2 + echo " Install it first: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" >&2 + exit 1 +fi +# Install only what's missing -- don't force-upgrade the runner's existing tools. +for t in cmake ninja; do + if [[ ! -x "/opt/homebrew/bin/$t" ]]; then + HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 /opt/homebrew/bin/brew install --quiet "$t" + fi + case "$(file -b "/opt/homebrew/bin/$t" 2>/dev/null)" in + *arm64*) echo " $t: arm64 (native) ok" ;; + *) echo " WARNING: /opt/homebrew/bin/$t missing or not arm64" >&2 ;; + esac +done + +echo "==> 2/3 Rosetta 2 (to run x86_64 output + configure probes)" +if /usr/bin/pgrep -q oahd; then + echo " already installed" +else + softwareupdate --install-rosetta --agree-to-license +fi + +echo "==> 3/3 x86_64 Homebrew at /usr/local (source of x86_64 bottles)" +if [[ -x /usr/local/bin/brew ]]; then + echo " already present ($(arch -x86_64 /usr/local/bin/brew --version | head -1))" +else + echo " bootstrapping x86_64 Homebrew (will prompt for sudo)..." + arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +fi +if ! arch -x86_64 /usr/local/bin/brew config 2>/dev/null | grep -qi 'macOS:.*x86_64'; then + echo " WARNING: /usr/local brew does not report an x86_64 platform." >&2 +fi + +if [[ "$PREWARM" == "1" ]]; then + echo "==> x86_64 formulae pre-warm (optional; CI installs these anyway)" + CLANG_VER="$(xargs < "$REPO_ROOT/scripts/mrbind/clang_version_macos.txt")" + { cat "$REPO_ROOT/requirements/macos.txt"; printf '%s\n' pybind11 make grep lld "llvm@${CLANG_VER}"; } \ + | HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 \ + xargs arch -x86_64 /usr/local/bin/brew install --quiet +else + echo "==> formulae pre-warm skipped (pass --prewarm to install them now)" +fi + +echo "==> done. Native x86_64 cross build recipe: macos/crossplatform-builds/README.md" From ecda5ddf6b23d0a97cb615973a25051a142ff833 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Sat, 18 Jul 2026 01:43:13 +0300 Subject: [PATCH 03/14] macos: init crossplatform build --- .github/workflows/build-test-macos.yml | 60 ++++++++++++++++++++------ scripts/build_source.sh | 19 ++++++++ scripts/build_thirdparty.sh | 25 +++++++++-- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index e15eb2743f33..133085907689 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -68,6 +68,15 @@ jobs: c-compiler-template: BREW_PREFIX/opt/llvm@22/bin/clang runner: [ self-hosted, macos, arm64, build ] # any macos version instance: self-hosted-arm + - arch: x64-cross + config: Release + compiler: AppleClang + cxx-compiler-template: /usr/bin/clang++ + c-compiler-template: /usr/bin/clang + runner: [ self-hosted, macos, arm64, build ] + instance: self-hosted-arm-x64-native + cross-osx-arch: x86_64 + skip-bindings: true permissions: id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout @@ -99,6 +108,27 @@ jobs: # mrbind needs deps/cppdecl; recurse only there bash scripts/retry.sh -- git -C thirdparty/mrbind submodule update --init --depth 1 deps/cppdecl + - name: Configure native x86_64 cross-build environment + if: ${{ matrix.cross-osx-arch == 'x86_64' }} + run: | + SHIM="$RUNNER_TEMP/x86_64-cross-shim" + mkdir -p "$SHIM" + ln -sf /opt/homebrew/bin/cmake "$SHIM/cmake" + ln -sf /opt/homebrew/bin/ninja "$SHIM/ninja" + ln -sf /usr/local/bin/python3.10 "$SHIM/python3.10" + ln -sf /usr/local/bin/python3.10-config "$SHIM/python3.10-config" + # A symlink-to-a-symlink breaks Homebrew's self-location (it + # mis-resolves its own repo path); a wrapper that execs the real + # binary works. + printf '#!/bin/bash\nexec /usr/local/bin/brew "$@"\n' > "$SHIM/brew" + chmod +x "$SHIM/brew" + echo "$SHIM" >> "$GITHUB_PATH" + echo "CMAKE_OSX_ARCHITECTURES=${{ matrix.cross-osx-arch }}" >> "$GITHUB_ENV" + echo "CMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja" >> "$GITHUB_ENV" + echo "CMAKE_PREFIX_PATH=/usr/local" >> "$GITHUB_ENV" + echo "MESHLIB_HOMEBREW_PREFIX=/usr/local" >> "$GITHUB_ENV" + echo "HOMEBREW_DIR=/usr/local" >> "$GITHUB_ENV" + - name: Install thirdparty libs id: thirdparty uses: ./.github/actions/install-macos-thirdparty @@ -120,14 +150,14 @@ jobs: build_config: ${{ matrix.config }} - name: Install MRBind deps - if: ${{ inputs.mrbind || inputs.mrbind_c }} + if: ${{ (inputs.mrbind || inputs.mrbind_c) && !matrix.skip-bindings }} env: HOMEBREW_NO_INSTALL_UPGRADE: '1' # don't upgrade an already-installed llvm/lld, no z3, no rebuild HOMEBREW_NO_INSTALL_CLEANUP: '1' # don't prune the known-good keg run: ./scripts/mrbind/install_deps_macos.sh - name: Build MRBind - if: ${{ inputs.mrbind || inputs.mrbind_c }} + if: ${{ (inputs.mrbind || inputs.mrbind_c) && !matrix.skip-bindings }} uses: ./.github/actions/build-mrbind with: # Keyed on the runner image (matrix.instance) plus the brew-prefix hash @@ -150,14 +180,14 @@ jobs: python3 -m pip install pytest - name: Download C bindings - if: ${{ inputs.mrbind_c }} + if: ${{ inputs.mrbind_c && !matrix.skip-bindings }} uses: actions/download-artifact@v8 with: name: CBindings path: MeshLib/CbindingsTmp - name: Prepare C bindings folders - if: ${{ inputs.mrbind_c }} + if: ${{ inputs.mrbind_c && !matrix.skip-bindings }} run: | rm -rf source/MeshLibC2 mv MeshLib/CbindingsTmp/MeshLibC2 source @@ -172,18 +202,21 @@ jobs: CMAKE_CXX_COMPILER: ${{ steps.thirdparty.outputs.cxx-compiler }} MR_VERSION: ${{ inputs.app_version }} MR_CMAKE_OPTIONS: > - -DMESHLIB_BUILD_MRMESH_PY_LEGACY=${{ fromJSON('["ON", "OFF"]')[inputs.mrbind] }} + -DMESHLIB_BUILD_MRMESH_PY_LEGACY=${{ (matrix.skip-bindings && 'OFF') || fromJSON('["ON", "OFF"]')[inputs.mrbind] }} -DMR_CXX_STANDARD=23 - -DMESHLIB_BUILD_GENERATED_C_BINDINGS=${{ fromJSON('["OFF", "ON"]')[inputs.mrbind_c] }} + -DMESHLIB_BUILD_GENERATED_C_BINDINGS=${{ (matrix.skip-bindings && 'OFF') || fromJSON('["OFF", "ON"]')[inputs.mrbind_c] }} -DMR_PCH_USE_EXTRA_HEADERS=ON - name: MRMesh Exported Symbols + # Needs llvm-cxxfilt from brewed llvm@22, which "Install MRBind deps" + # installs; skipped alongside bindings for skip-bindings entries + if: ${{ !matrix.skip-bindings }} run: | export PATH="$(brew --prefix llvm@22)/bin:$PATH" nm -gU ./build/${{ matrix.config }}/bin/libMRMesh.dylib | llvm-cxxfilt - name: Generate and build Python bindings - if: ${{ inputs.mrbind }} + if: ${{ inputs.mrbind && !matrix.skip-bindings }} env: PATH: ${{ steps.thirdparty.outputs.brew-prefix }}/opt/make/libexec/gnubin:${{ steps.thirdparty.outputs.brew-prefix }}/opt/grep/libexec/gnubin:${{env.PATH}} CXX: ${{ steps.thirdparty.outputs.cxx-compiler }} @@ -199,7 +232,7 @@ jobs: run: ./build/${{ matrix.config }}/bin/MeshViewer -tryHidden -noEventLoop -unloadPluginsAtEnd - name: Verify meshlib.mrmeshpy import - if: ${{ inputs.mrbind }} + if: ${{ inputs.mrbind && !matrix.skip-bindings }} timeout-minutes: 3 uses: ./.github/actions/verify-meshlib-python-import with: @@ -209,20 +242,21 @@ jobs: env: GTEST_OUTPUT: 'xml:unit_tests_report_gtest.xml' timeout-minutes: 10 - run: ./build/${{ matrix.config }}/bin/MRTest + run: ./build/${{ matrix.config }}/bin/MRTest${{ matrix.skip-bindings && ' --no-python-tests' || '' }} - name: C Unit Tests - if: ${{ inputs.mrbind_c }} + if: ${{ inputs.mrbind_c && !matrix.skip-bindings }} timeout-minutes: 10 run: ./build/${{ matrix.config }}/bin/MRTestC2 - name: Python Sanity Tests + if: ${{ !matrix.skip-bindings }} timeout-minutes: 8 working-directory: ./build/${{ matrix.config }}/bin run: python3 -u ./../../../scripts/run_python_test_script.py -d '../test_python' -a ' --junit-xml=../unit_tests_report_pytest.xml' - name: Python Regression Tests - if: ${{ inputs.internal_build }} + if: ${{ inputs.internal_build && !matrix.skip-bindings }} env: RUN_CUDA_ARG: "--run-cuda=negative --junit-xml=../unit_tests_report_regression.xml" uses: ./.github/actions/python-regression-tests @@ -303,7 +337,7 @@ jobs: stats_file_suffix: -${{ steps.collect-runner-stats.outputs.job_id }} - name: Create and fix fake Wheel for NuGet - if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' }} + if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' && !matrix.skip-bindings }} shell: bash run: | python3 -m venv ./wheel_venv @@ -313,7 +347,7 @@ jobs: ./scripts/nuget_patch/fix_macos_rpath.sh ./patched_content/libMeshLibC2.dylib - name: Upload NuGet files to Artifacts - if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' }} + if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' && !matrix.skip-bindings }} uses: actions/upload-artifact@v7 with: name: DotNetPatchArchiveMacOs-${{ matrix.arch }} diff --git a/scripts/build_source.sh b/scripts/build_source.sh index 1d7748f90108..615bf40d8805 100755 --- a/scripts/build_source.sh +++ b/scripts/build_source.sh @@ -37,6 +37,25 @@ fi # add env options to cmake MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS:-}" +# Cross-compilation knobs for building the x86_64 target on an arm64 macOS host +# with a NATIVE toolchain (no Rosetta). Each is a no-op when unset, so native +# and other-platform builds are unaffected. +# - CMAKE_OSX_ARCHITECTURES pins the produced object code arch. +# - CMAKE_MAKE_PROGRAM forces the native (arm64) ninja; otherwise CMake's +# find_program picks the x86_64 ninja under /usr/local +# and the compile silently runs under Rosetta. +# - MESHLIB_HOMEBREW_PREFIX points library discovery at the x86_64 Homebrew +# (passed to ConfigureHomebrew.cmake as HOMEBREW_PREFIX). +if [ -n "${CMAKE_OSX_ARCHITECTURES}" ]; then + MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -D CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" +fi +if [ -n "${CMAKE_MAKE_PROGRAM}" ]; then + MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -D CMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}" +fi +if [ -n "${MESHLIB_HOMEBREW_PREFIX}" ]; then + MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -D HOMEBREW_PREFIX=${MESHLIB_HOMEBREW_PREFIX}" +fi + if command -v ninja >/dev/null 2>&1 ; then MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -G Ninja" fi diff --git a/scripts/build_thirdparty.sh b/scripts/build_thirdparty.sh index c04b7a37f305..39e965860cbf 100755 --- a/scripts/build_thirdparty.sh +++ b/scripts/build_thirdparty.sh @@ -57,6 +57,20 @@ MR_CMAKE_OPTIONS="\ -D CMAKE_BUILD_TYPE=Release \ " +# Cross-compilation knobs for building the x86_64 target on an arm64 macOS host +# with a native toolchain (no Rosetta). No-ops when unset. CMAKE_OSX_ARCHITECTURES +# pins the output arch; CMAKE_MAKE_PROGRAM forces the native (arm64) ninja (else +# CMake's find_program picks the x86_64 ninja under /usr/local and the compile runs +# under Rosetta). x86_64 dependencies are located via the CMAKE_PREFIX_PATH env var, +# which CMake reads automatically. These options also flow to the sub-builds +# (clip/fastmcpp) via CMAKE_OPTIONS below. +if [ -n "${CMAKE_OSX_ARCHITECTURES}" ]; then + MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -D CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" +fi +if [ -n "${CMAKE_MAKE_PROGRAM}" ]; then + MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -D CMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}" +fi + if [ "${MR_EMSCRIPTEN}" != "ON" ] ; then CMAKE_C_COMPILER="${CMAKE_C_COMPILER:-${CC}}" if [ -n "${CMAKE_C_COMPILER}" ] ; then @@ -120,10 +134,13 @@ if [ "${MR_EMSCRIPTEN}" == "ON" ]; then fi fi -if [[ $OSTYPE == 'darwin'* ]]; then - NPROC=$(sysctl -n hw.logicalcpu) -else - NPROC=$(nproc) +# Respect a caller-provided NPROC +if [ -z "${NPROC}" ]; then + if [[ $OSTYPE == 'darwin'* ]]; then + NPROC=$(sysctl -n hw.logicalcpu) + else + NPROC=$(nproc) + fi fi # build From 40212f4306a33b7f3c0daf75547236ebad73a7dd Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Tue, 28 Jul 2026 23:48:23 +0300 Subject: [PATCH 04/14] use separate label for macos crossplatform build --- .github/workflows/build-test-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index 133085907689..8427819308e8 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -73,7 +73,7 @@ jobs: compiler: AppleClang cxx-compiler-template: /usr/bin/clang++ c-compiler-template: /usr/bin/clang - runner: [ self-hosted, macos, arm64, build ] + runner: [ self-hosted, macos, arm64, crossplatform-build ] instance: self-hosted-arm-x64-native cross-osx-arch: x86_64 skip-bindings: true From e4b5b7f670a54a44a65d0bbc6d6624d266c3e59c Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 1 Aug 2026 17:59:41 +0000 Subject: [PATCH 05/14] fix bindings setup --- .github/workflows/build-test-macos.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index 732fe56f0b50..1d4ebb26c05f 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -76,7 +76,7 @@ jobs: runner: [ self-hosted, macos, arm64, crossplatform-build ] instance: self-hosted-arm-x64-native cross-osx-arch: x86_64 - skip-bindings: true + skip-bindings: false permissions: id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout @@ -115,6 +115,7 @@ jobs: echo "CMAKE_PREFIX_PATH=/usr/local" >> "$GITHUB_ENV" echo "MESHLIB_HOMEBREW_PREFIX=/usr/local" >> "$GITHUB_ENV" echo "HOMEBREW_DIR=/usr/local" >> "$GITHUB_ENV" + echo "PKG_CONFIG_PATH=$(/usr/local/bin/brew --prefix python@3.10)/lib/pkgconfig" >> "$GITHUB_ENV" - name: Install thirdparty libs id: thirdparty @@ -293,7 +294,7 @@ jobs: --parallel $(sysctl -n hw.physicalcpu) - name: Build C examples - if: ${{ matrix.config == 'Release' }} + if: ${{ matrix.config == 'Release' && !matrix.skip-bindings }} env: CC: ${{ steps.thirdparty.outputs.c-compiler }} run: | From f55500f0884b3a4195d4ed896bc79210505133b5 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 1 Aug 2026 19:29:58 +0000 Subject: [PATCH 06/14] force thirdparties to rebuild --- .github/workflows/build-test-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index 1d4ebb26c05f..ccef02ee63a5 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -93,7 +93,7 @@ jobs: # Selective init -- parent Checkout drops submodules:true. # https://github.com/actions/checkout/issues/1779 # Retried via retry.sh: submodule endpoints occasionally 500. - bash scripts/retry.sh -- scripts/clone_submodules_linux.sh --skip-prebuilt-thirdparty + bash scripts/retry.sh -- scripts/clone_submodules_macos.sh - name: Configure native x86_64 cross-build environment if: ${{ matrix.cross-osx-arch == 'x86_64' }} From 28e9ea6477006943f8ec6b7651284cf28c6404ba Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Sun, 2 Aug 2026 22:32:02 +0000 Subject: [PATCH 07/14] test-distribution: add x64-cross macos distro test --- .github/workflows/test-distribution.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-distribution.yml b/.github/workflows/test-distribution.yml index 3d2927c6c69b..46694c733000 100644 --- a/.github/workflows/test-distribution.yml +++ b/.github/workflows/test-distribution.yml @@ -261,6 +261,9 @@ jobs: - arch: x64 runner: macos-26-intel pkg_pattern: '*x64.pkg' + - arch: x64-cross + runner: macos-15-intel + pkg_pattern: '*x64-cross.pkg' steps: - name: Checkout uses: actions/checkout@v7 From b021c6301526cb0bbd9aeb4a40f1bb70d5a16967 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Mon, 3 Aug 2026 18:19:18 +0000 Subject: [PATCH 08/14] cmake: validate overridden Homebrew prefix on Apple --- cmake/Modules/ConfigureHomebrew.cmake | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cmake/Modules/ConfigureHomebrew.cmake b/cmake/Modules/ConfigureHomebrew.cmake index 24a6211c6af3..fca385ea8ac7 100644 --- a/cmake/Modules/ConfigureHomebrew.cmake +++ b/cmake/Modules/ConfigureHomebrew.cmake @@ -1,19 +1,18 @@ IF(APPLE) message("building for Apple") # Allow an explicit Homebrew prefix override (e.g. -D HOMEBREW_PREFIX=/usr/local - # to link the x86_64 bottles when cross-building Intel on an arm64 host with a - # native toolchain). Falls back to `brew --prefix` for the common native case. + # to link the x86_64 bottles when cross-building Intel on an arm64 host). + # Falls back to `brew --prefix` for the common native case. IF(NOT HOMEBREW_PREFIX) execute_process( COMMAND brew --prefix - RESULT_VARIABLE CMD_ERROR OUTPUT_VARIABLE HOMEBREW_PREFIX OUTPUT_STRIP_TRAILING_WHITESPACE ) - IF(NOT (CMD_ERROR EQUAL 0 AND EXISTS "${HOMEBREW_PREFIX}")) - message("Homebrew not found!") - message(FATAL_ERROR "${CMD_ERROR} ${HOMEBREW_PREFIX}") - ENDIF() + ENDIF() + # Validate whichever prefix we ended up with (auto-detected or overridden). + IF(NOT EXISTS "${HOMEBREW_PREFIX}") + message(FATAL_ERROR "Homebrew prefix not found: '${HOMEBREW_PREFIX}'") ENDIF() message("Homebrew prefix: ${HOMEBREW_PREFIX}") From cb25a81721e6786c987e8b37cd71cbb1fcbcc3a3 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Mon, 3 Aug 2026 18:19:18 +0000 Subject: [PATCH 09/14] macos: key thirdparty cache on target arch --- .github/actions/install-macos-thirdparty/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/install-macos-thirdparty/action.yml b/.github/actions/install-macos-thirdparty/action.yml index 7a68a3978afa..dc98b98e1177 100644 --- a/.github/actions/install-macos-thirdparty/action.yml +++ b/.github/actions/install-macos-thirdparty/action.yml @@ -104,6 +104,8 @@ runs: echo "brew-hash=$(printf %s "$BREW_PREFIX" | shasum -a 256 | cut -c1-16)" >> "$GITHUB_OUTPUT" echo "thirdparty-hash=$( { printf '%s\n' "$BREW_PREFIX" + # Discriminate the cross (x86_64) outputs from a same-instance native build. + if [ -n "${CMAKE_OSX_ARCHITECTURES}" ]; then printf 'osx-arch=%s\n' "${CMAKE_OSX_ARCHITECTURES}"; fi git ls-tree HEAD \ thirdparty/googletest \ thirdparty/OpenCTM-git \ From 6965c1a3dd44d35dbc641aa155e13c04a813792e Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Mon, 3 Aug 2026 18:19:19 +0000 Subject: [PATCH 10/14] macos: trim cross-build knob comment blocks --- scripts/build_source.sh | 11 ++--------- scripts/build_thirdparty.sh | 9 ++------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/scripts/build_source.sh b/scripts/build_source.sh index 46bcf80f1e74..59a9ec6982e6 100755 --- a/scripts/build_source.sh +++ b/scripts/build_source.sh @@ -39,15 +39,8 @@ MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS:-}" # Extra flags for `cmake --build`. MR_CMAKE_BUILD_OPTIONS="${MR_CMAKE_BUILD_OPTIONS:-}" -# Cross-compilation knobs for building the x86_64 target on an arm64 macOS host -# with a NATIVE toolchain (no Rosetta). Each is a no-op when unset, so native -# and other-platform builds are unaffected. -# - CMAKE_OSX_ARCHITECTURES pins the produced object code arch. -# - CMAKE_MAKE_PROGRAM forces the native (arm64) ninja; otherwise CMake's -# find_program picks the x86_64 ninja under /usr/local -# and the compile silently runs under Rosetta. -# - MESHLIB_HOMEBREW_PREFIX points library discovery at the x86_64 Homebrew -# (passed to ConfigureHomebrew.cmake as HOMEBREW_PREFIX). +# Cross-compilation knobs for building x86_64 on an arm64 macOS host with a native +# toolchain (no-ops when unset). See macos/crossplatform-builds/README.md. if [ -n "${CMAKE_OSX_ARCHITECTURES}" ]; then MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -D CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" fi diff --git a/scripts/build_thirdparty.sh b/scripts/build_thirdparty.sh index e86c263553b3..7ede5b3aca0e 100755 --- a/scripts/build_thirdparty.sh +++ b/scripts/build_thirdparty.sh @@ -57,13 +57,8 @@ MR_CMAKE_OPTIONS="\ -D CMAKE_BUILD_TYPE=Release \ " -# Cross-compilation knobs for building the x86_64 target on an arm64 macOS host -# with a native toolchain (no Rosetta). No-ops when unset. CMAKE_OSX_ARCHITECTURES -# pins the output arch; CMAKE_MAKE_PROGRAM forces the native (arm64) ninja (else -# CMake's find_program picks the x86_64 ninja under /usr/local and the compile runs -# under Rosetta). x86_64 dependencies are located via the CMAKE_PREFIX_PATH env var, -# which CMake reads automatically. These options also flow to the sub-builds -# (clip/fastmcpp) via CMAKE_OPTIONS below. +# Cross-compilation knobs for building x86_64 on an arm64 macOS host with a native +# toolchain (no-ops when unset). See macos/crossplatform-builds/README.md. if [ -n "${CMAKE_OSX_ARCHITECTURES}" ]; then MR_CMAKE_OPTIONS="${MR_CMAKE_OPTIONS} -D CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" fi From 095148e7f4e7eac21059d7b64fce6d4c157783b9 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Mon, 3 Aug 2026 18:19:19 +0000 Subject: [PATCH 11/14] macos: cross x64 ships full bindings; fix platform label, assert arch, harden env --- .github/workflows/build-test-macos.yml | 68 +++++++++++++++----------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index ccef02ee63a5..6eb4ba8282a6 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -76,7 +76,6 @@ jobs: runner: [ self-hosted, macos, arm64, crossplatform-build ] instance: self-hosted-arm-x64-native cross-osx-arch: x86_64 - skip-bindings: false permissions: id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout @@ -98,24 +97,36 @@ jobs: - name: Configure native x86_64 cross-build environment if: ${{ matrix.cross-osx-arch == 'x86_64' }} run: | + PYVER=3.10 SHIM="$RUNNER_TEMP/x86_64-cross-shim" mkdir -p "$SHIM" - ln -sf /opt/homebrew/bin/cmake "$SHIM/cmake" - ln -sf /opt/homebrew/bin/ninja "$SHIM/ninja" - ln -sf /usr/local/bin/python3.10 "$SHIM/python3.10" - ln -sf /usr/local/bin/python3.10-config "$SHIM/python3.10-config" - # A symlink-to-a-symlink breaks Homebrew's self-location (it - # mis-resolves its own repo path); a wrapper that execs the real - # binary works. + # The native arm64 Homebrew prefix varies across the self-hosted fleet + # (/opt/homebrew, ~/.homebrew, ...); find it by its arm64 cmake so build + # tools run natively, not the x86_64 copies under /usr/local (which would + # drag clang into Rosetta). Fail loudly rather than dangle shim symlinks. + ARM_BREW="" + for p in /opt/homebrew "$HOME/.homebrew"; do + if file -b "$p/bin/cmake" 2>/dev/null | grep -q arm64; then ARM_BREW="$p"; break; fi + done + [ -n "$ARM_BREW" ] || { echo "::error::x86_64 cross setup: no arm64 Homebrew cmake found"; exit 1; } + for t in "$ARM_BREW/bin/cmake" "$ARM_BREW/bin/ninja" \ + "/usr/local/bin/python$PYVER" "/usr/local/bin/python$PYVER-config" /usr/local/bin/brew; do + [ -x "$t" ] || { echo "::error::x86_64 cross setup: missing $t"; exit 1; } + done + ln -sf "$ARM_BREW/bin/cmake" "$SHIM/cmake" + ln -sf "$ARM_BREW/bin/ninja" "$SHIM/ninja" + ln -sf "/usr/local/bin/python$PYVER" "$SHIM/python$PYVER" + ln -sf "/usr/local/bin/python$PYVER-config" "$SHIM/python$PYVER-config" + # A symlink-to-a-symlink breaks Homebrew's self-location, so wrap brew. printf '#!/bin/bash\nexec /usr/local/bin/brew "$@"\n' > "$SHIM/brew" chmod +x "$SHIM/brew" echo "$SHIM" >> "$GITHUB_PATH" echo "CMAKE_OSX_ARCHITECTURES=${{ matrix.cross-osx-arch }}" >> "$GITHUB_ENV" - echo "CMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja" >> "$GITHUB_ENV" + echo "CMAKE_MAKE_PROGRAM=$ARM_BREW/bin/ninja" >> "$GITHUB_ENV" echo "CMAKE_PREFIX_PATH=/usr/local" >> "$GITHUB_ENV" echo "MESHLIB_HOMEBREW_PREFIX=/usr/local" >> "$GITHUB_ENV" echo "HOMEBREW_DIR=/usr/local" >> "$GITHUB_ENV" - echo "PKG_CONFIG_PATH=$(/usr/local/bin/brew --prefix python@3.10)/lib/pkgconfig" >> "$GITHUB_ENV" + echo "PKG_CONFIG_PATH=$(/usr/local/bin/brew --prefix python@$PYVER)/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" >> "$GITHUB_ENV" - name: Install thirdparty libs id: thirdparty @@ -138,14 +149,14 @@ jobs: build_config: ${{ matrix.config }} - name: Install MRBind deps - if: ${{ (inputs.mrbind || inputs.mrbind_c) && !matrix.skip-bindings }} + if: ${{ inputs.mrbind || inputs.mrbind_c }} env: HOMEBREW_NO_INSTALL_UPGRADE: '1' # don't upgrade an already-installed llvm/lld, no z3, no rebuild HOMEBREW_NO_INSTALL_CLEANUP: '1' # don't prune the known-good keg run: ./scripts/mrbind/install_deps_macos.sh - name: Build MRBind - if: ${{ (inputs.mrbind || inputs.mrbind_c) && !matrix.skip-bindings }} + if: ${{ inputs.mrbind || inputs.mrbind_c }} uses: ./.github/actions/build-mrbind with: # Keyed on the runner image (matrix.instance) plus the brew-prefix hash @@ -174,14 +185,14 @@ jobs: job-name: generate-c-bindings - name: Download C bindings - if: ${{ inputs.mrbind_c && !matrix.skip-bindings }} + if: ${{ inputs.mrbind_c }} uses: actions/download-artifact@v8 with: name: CBindings path: MeshLib/CbindingsTmp - name: Prepare C bindings folders - if: ${{ inputs.mrbind_c && !matrix.skip-bindings }} + if: ${{ inputs.mrbind_c }} run: | rm -rf source/MeshLibC2 mv MeshLib/CbindingsTmp/MeshLibC2 source @@ -196,21 +207,23 @@ jobs: CMAKE_CXX_COMPILER: ${{ steps.thirdparty.outputs.cxx-compiler }} MR_VERSION: ${{ inputs.app_version }} MR_CMAKE_OPTIONS: > - -DMESHLIB_BUILD_MRMESH_PY_LEGACY=${{ (matrix.skip-bindings && 'OFF') || fromJSON('["ON", "OFF"]')[inputs.mrbind] }} + -DMESHLIB_BUILD_MRMESH_PY_LEGACY=${{ fromJSON('["ON", "OFF"]')[inputs.mrbind] }} -DMR_CXX_STANDARD=23 - -DMESHLIB_BUILD_GENERATED_C_BINDINGS=${{ (matrix.skip-bindings && 'OFF') || fromJSON('["OFF", "ON"]')[inputs.mrbind_c] }} + -DMESHLIB_BUILD_GENERATED_C_BINDINGS=${{ fromJSON('["OFF", "ON"]')[inputs.mrbind_c] }} -DMR_PCH_USE_EXTRA_HEADERS=ON + ${{ matrix.cross-osx-arch == 'x86_64' && '-DMR_PLATFORM=APPLE_x86_64' || '' }} + + - name: Verify x86_64 output + if: ${{ matrix.cross-osx-arch == 'x86_64' }} + run: lipo -archs build/${{ matrix.config }}/bin/libMRMesh.dylib | grep -qx x86_64 - name: MRMesh Exported Symbols - # Needs llvm-cxxfilt from brewed llvm@22, which "Install MRBind deps" - # installs; skipped alongside bindings for skip-bindings entries - if: ${{ !matrix.skip-bindings }} run: | export PATH="$(brew --prefix llvm@22)/bin:$PATH" nm -gU ./build/${{ matrix.config }}/bin/libMRMesh.dylib | llvm-cxxfilt - name: Generate and build Python bindings - if: ${{ inputs.mrbind && !matrix.skip-bindings }} + if: ${{ inputs.mrbind }} env: PATH: ${{ steps.thirdparty.outputs.brew-prefix }}/opt/make/libexec/gnubin:${{ steps.thirdparty.outputs.brew-prefix }}/opt/grep/libexec/gnubin:${{env.PATH}} CXX: ${{ steps.thirdparty.outputs.cxx-compiler }} @@ -226,7 +239,7 @@ jobs: run: ./build/${{ matrix.config }}/bin/MeshViewer -tryHidden -noEventLoop -unloadPluginsAtEnd - name: Verify meshlib.mrmeshpy import - if: ${{ inputs.mrbind && !matrix.skip-bindings }} + if: ${{ inputs.mrbind }} timeout-minutes: 3 uses: ./.github/actions/verify-meshlib-python-import with: @@ -236,21 +249,20 @@ jobs: env: GTEST_OUTPUT: 'xml:unit_tests_report_gtest.xml' timeout-minutes: 10 - run: ./build/${{ matrix.config }}/bin/MRTest${{ matrix.skip-bindings && ' --no-python-tests' || '' }} + run: ./build/${{ matrix.config }}/bin/MRTest - name: C Unit Tests - if: ${{ inputs.mrbind_c && !matrix.skip-bindings }} + if: ${{ inputs.mrbind_c }} timeout-minutes: 10 run: ./build/${{ matrix.config }}/bin/MRTestC2 - name: Python Sanity Tests - if: ${{ !matrix.skip-bindings }} timeout-minutes: 8 working-directory: ./build/${{ matrix.config }}/bin run: python3 -u ./../../../scripts/run_python_test_script.py -d '../test_python' -a ' --junit-xml=../unit_tests_report_pytest.xml' - name: Python Regression Tests - if: ${{ inputs.internal_build && !matrix.skip-bindings }} + if: ${{ inputs.internal_build }} env: RUN_CUDA_ARG: "--run-cuda=negative --junit-xml=../unit_tests_report_regression.xml" uses: ./.github/actions/python-regression-tests @@ -294,7 +306,7 @@ jobs: --parallel $(sysctl -n hw.physicalcpu) - name: Build C examples - if: ${{ matrix.config == 'Release' && !matrix.skip-bindings }} + if: ${{ matrix.config == 'Release' }} env: CC: ${{ steps.thirdparty.outputs.c-compiler }} run: | @@ -331,7 +343,7 @@ jobs: stats_file_suffix: -${{ steps.collect-runner-stats.outputs.job_id }} - name: Create and fix fake Wheel for NuGet - if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' && !matrix.skip-bindings }} + if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' }} shell: bash run: | python3 -m venv ./wheel_venv @@ -341,7 +353,7 @@ jobs: ./scripts/nuget_patch/fix_macos_rpath.sh ./patched_content/libMeshLibC2.dylib - name: Upload NuGet files to Artifacts - if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' && !matrix.skip-bindings }} + if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' }} uses: actions/upload-artifact@v7 with: name: DotNetPatchArchiveMacOs-${{ matrix.arch }} From 26d7266e0c3f71d82915631d4d0dcaf5f1d93a40 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Mon, 3 Aug 2026 18:26:11 +0000 Subject: [PATCH 12/14] macos: gate native x86_64 cross build in its own skippable job --- .github/workflows/build-test-distribute.yml | 1 + .github/workflows/build-test-macos.yml | 305 ++++++++++++++++++++ .github/workflows/config.yml | 4 + 3 files changed, 310 insertions(+) diff --git a/.github/workflows/build-test-distribute.yml b/.github/workflows/build-test-distribute.yml index c257b23e2634..46fe37250bb5 100644 --- a/.github/workflows/build-test-distribute.yml +++ b/.github/workflows/build-test-distribute.yml @@ -151,6 +151,7 @@ jobs: upload_artifacts: ${{ needs.config.outputs.upload_artifacts == 'true' }} upload_test_artifacts: ${{ needs.config.outputs.upload_test_artifacts == 'true' }} nuget_build_patch: ${{ needs.config.outputs.build_enable_windows == 'true' && needs.config.outputs.upload_artifacts == 'true'}} + build_macos_crossplatform: ${{ needs.config.outputs.build_enable_macos_crossplatform == 'true' }} secrets: inherit update-win-version: diff --git a/.github/workflows/build-test-macos.yml b/.github/workflows/build-test-macos.yml index 6eb4ba8282a6..48919755d57e 100644 --- a/.github/workflows/build-test-macos.yml +++ b/.github/workflows/build-test-macos.yml @@ -31,6 +31,13 @@ on: default: false required: false type: boolean + build_macos_crossplatform: + # Gates the self-hosted native-x86_64 cross job. When false it is skipped + # (never scheduled), so a busy/down crossplatform-build runner can't hang + # the rest of macOS CI to the job timeout. + default: true + required: false + type: boolean jobs: macos-build-test: @@ -68,6 +75,304 @@ jobs: c-compiler-template: BREW_PREFIX/opt/llvm@22/bin/clang runner: [ self-hosted, macos, arm64, build ] # any macos version instance: self-hosted-arm + permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout + actions: read # This is required for wait-for-job + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: macOS Diagnostics + uses: ./.github/actions/macos-diagnostics + + - name: Checkout third-party submodules + run: | + # Selective init -- parent Checkout drops submodules:true. + # https://github.com/actions/checkout/issues/1779 + # Retried via retry.sh: submodule endpoints occasionally 500. + bash scripts/retry.sh -- scripts/clone_submodules_macos.sh + + - name: Configure native x86_64 cross-build environment + if: ${{ matrix.cross-osx-arch == 'x86_64' }} + run: | + PYVER=3.10 + SHIM="$RUNNER_TEMP/x86_64-cross-shim" + mkdir -p "$SHIM" + # The native arm64 Homebrew prefix varies across the self-hosted fleet + # (/opt/homebrew, ~/.homebrew, ...); find it by its arm64 cmake so build + # tools run natively, not the x86_64 copies under /usr/local (which would + # drag clang into Rosetta). Fail loudly rather than dangle shim symlinks. + ARM_BREW="" + for p in /opt/homebrew "$HOME/.homebrew"; do + if file -b "$p/bin/cmake" 2>/dev/null | grep -q arm64; then ARM_BREW="$p"; break; fi + done + [ -n "$ARM_BREW" ] || { echo "::error::x86_64 cross setup: no arm64 Homebrew cmake found"; exit 1; } + for t in "$ARM_BREW/bin/cmake" "$ARM_BREW/bin/ninja" \ + "/usr/local/bin/python$PYVER" "/usr/local/bin/python$PYVER-config" /usr/local/bin/brew; do + [ -x "$t" ] || { echo "::error::x86_64 cross setup: missing $t"; exit 1; } + done + ln -sf "$ARM_BREW/bin/cmake" "$SHIM/cmake" + ln -sf "$ARM_BREW/bin/ninja" "$SHIM/ninja" + ln -sf "/usr/local/bin/python$PYVER" "$SHIM/python$PYVER" + ln -sf "/usr/local/bin/python$PYVER-config" "$SHIM/python$PYVER-config" + # A symlink-to-a-symlink breaks Homebrew's self-location, so wrap brew. + printf '#!/bin/bash\nexec /usr/local/bin/brew "$@"\n' > "$SHIM/brew" + chmod +x "$SHIM/brew" + echo "$SHIM" >> "$GITHUB_PATH" + echo "CMAKE_OSX_ARCHITECTURES=${{ matrix.cross-osx-arch }}" >> "$GITHUB_ENV" + echo "CMAKE_MAKE_PROGRAM=$ARM_BREW/bin/ninja" >> "$GITHUB_ENV" + echo "CMAKE_PREFIX_PATH=/usr/local" >> "$GITHUB_ENV" + echo "MESHLIB_HOMEBREW_PREFIX=/usr/local" >> "$GITHUB_ENV" + echo "HOMEBREW_DIR=/usr/local" >> "$GITHUB_ENV" + echo "PKG_CONFIG_PATH=$(/usr/local/bin/brew --prefix python@$PYVER)/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" >> "$GITHUB_ENV" + + - name: Install thirdparty libs + id: thirdparty + uses: ./.github/actions/install-macos-thirdparty + with: + cache-instance: ${{ matrix.instance }} + cache-compiler: ${{ matrix.compiler }} + cxx-compiler-template: ${{ matrix.cxx-compiler-template }} + c-compiler-template: ${{ matrix.c-compiler-template }} + + - name: Collect runner's system stats + if: ${{ inputs.internal_build }} + id: collect-runner-stats + continue-on-error: true + uses: ./.github/actions/collect-runner-stats + with: + target_os: macos + target_arch: ${{ matrix.arch }} + cxx_compiler: ${{ steps.thirdparty.outputs.cxx-compiler }} + build_config: ${{ matrix.config }} + + - name: Install MRBind deps + if: ${{ inputs.mrbind || inputs.mrbind_c }} + env: + HOMEBREW_NO_INSTALL_UPGRADE: '1' # don't upgrade an already-installed llvm/lld, no z3, no rebuild + HOMEBREW_NO_INSTALL_CLEANUP: '1' # don't prune the known-good keg + run: ./scripts/mrbind/install_deps_macos.sh + + - name: Build MRBind + if: ${{ inputs.mrbind || inputs.mrbind_c }} + uses: ./.github/actions/build-mrbind + with: + # Keyed on the runner image (matrix.instance) plus the brew-prefix hash + # to discriminate between fleet members of the self-hosted ARM label set. + # Shared with pip-build.yml's macOS section where the two also match. + cache-key-prefix: ${{ matrix.instance }}-clang + build-script: scripts/mrbind/install_mrbind_macos.sh + extra-cache-key: ${{ hashFiles('scripts/mrbind/clang_version_macos.txt') }}-${{ steps.thirdparty.outputs.brew-hash }} + + - name: Create virtualenv + run: | + python3.10 -m venv .venv + . .venv/bin/activate + echo PATH=$PATH >> $GITHUB_ENV + + - name: Setup python requirements + run: | + curl -sS https://bootstrap.pypa.io/get-pip.py | python3 + python3 -m pip install -r ./requirements/python/requirements.txt + python3 -m pip install pytest + + - name: Wait for C bindings + if: ${{ inputs.mrbind_c }} + uses: ./.github/actions/wait-for-job + with: + job-name: generate-c-bindings + + - name: Download C bindings + if: ${{ inputs.mrbind_c }} + uses: actions/download-artifact@v8 + with: + name: CBindings + path: MeshLib/CbindingsTmp + + - name: Prepare C bindings folders + if: ${{ inputs.mrbind_c }} + run: | + rm -rf source/MeshLibC2 + mv MeshLib/CbindingsTmp/MeshLibC2 source + # Cuda placeholders: + cp -R scripts/mrbind/cuda_placeholder_generated_c/{include,src} source/MeshLibC2Cuda + + - name: Build + run: ./scripts/build_source.sh + env: + MESHLIB_BUILD_RELEASE: ${{ fromJSON('["OFF", "ON"]')[matrix.config == 'Release'] }} + MESHLIB_BUILD_DEBUG: ${{ fromJSON('["OFF", "ON"]')[matrix.config == 'Debug'] }} + CMAKE_CXX_COMPILER: ${{ steps.thirdparty.outputs.cxx-compiler }} + MR_VERSION: ${{ inputs.app_version }} + MR_CMAKE_OPTIONS: > + -DMESHLIB_BUILD_MRMESH_PY_LEGACY=${{ fromJSON('["ON", "OFF"]')[inputs.mrbind] }} + -DMR_CXX_STANDARD=23 + -DMESHLIB_BUILD_GENERATED_C_BINDINGS=${{ fromJSON('["OFF", "ON"]')[inputs.mrbind_c] }} + -DMR_PCH_USE_EXTRA_HEADERS=ON + ${{ matrix.cross-osx-arch == 'x86_64' && '-DMR_PLATFORM=APPLE_x86_64' || '' }} + + - name: Verify x86_64 output + if: ${{ matrix.cross-osx-arch == 'x86_64' }} + run: lipo -archs build/${{ matrix.config }}/bin/libMRMesh.dylib | grep -qx x86_64 + + - name: MRMesh Exported Symbols + run: | + export PATH="$(brew --prefix llvm@22)/bin:$PATH" + nm -gU ./build/${{ matrix.config }}/bin/libMRMesh.dylib | llvm-cxxfilt + + - name: Generate and build Python bindings + if: ${{ inputs.mrbind }} + env: + PATH: ${{ steps.thirdparty.outputs.brew-prefix }}/opt/make/libexec/gnubin:${{ steps.thirdparty.outputs.brew-prefix }}/opt/grep/libexec/gnubin:${{env.PATH}} + CXX: ${{ steps.thirdparty.outputs.cxx-compiler }} + run: | + make --version + make -f scripts/mrbind/generate.mk \ + -B --trace \ + PYTHON_PKGCONF_NAME=python-3.10-embed \ + MESHLIB_SHLIB_DIR=build/${{matrix.config}}/bin + + - name: Run Start-and-Exit Tests + timeout-minutes: 3 + run: ./build/${{ matrix.config }}/bin/MeshViewer -tryHidden -noEventLoop -unloadPluginsAtEnd + + - name: Verify meshlib.mrmeshpy import + if: ${{ inputs.mrbind }} + timeout-minutes: 3 + uses: ./.github/actions/verify-meshlib-python-import + with: + build-bin-dir: ./build/${{ matrix.config }}/bin + + - name: Unit Tests + env: + GTEST_OUTPUT: 'xml:unit_tests_report_gtest.xml' + timeout-minutes: 10 + run: ./build/${{ matrix.config }}/bin/MRTest + + - name: C Unit Tests + if: ${{ inputs.mrbind_c }} + timeout-minutes: 10 + run: ./build/${{ matrix.config }}/bin/MRTestC2 + + - name: Python Sanity Tests + timeout-minutes: 8 + working-directory: ./build/${{ matrix.config }}/bin + run: python3 -u ./../../../scripts/run_python_test_script.py -d '../test_python' -a ' --junit-xml=../unit_tests_report_pytest.xml' + + - name: Python Regression Tests + if: ${{ inputs.internal_build }} + env: + RUN_CUDA_ARG: "--run-cuda=negative --junit-xml=../unit_tests_report_regression.xml" + uses: ./.github/actions/python-regression-tests + with: + build_config: ${{ matrix.config }} + smoke: ${{ !inputs.full_config_build && matrix.config == 'Debug' }} + test_artifacts_path: macos/${{ matrix.arch }} + upload_test_artifacts: ${{ inputs.upload_test_artifacts }} + + - name: Generate Test Performance Report + continue-on-error: true + run: | + scripts/junit_to_csv.py unit_tests_report.csv unit_tests_report_*.xml + cat unit_tests_report.csv + + - name: Create Pkg + if: ${{ matrix.config == 'Release' }} + run: | + ./scripts/distribution_apple.sh ${{ inputs.app_version }} + mv MeshLib_.pkg meshlib_${{matrix.arch}}.pkg + + - name: Extract Pkg + if: ${{ matrix.config == 'Release' }} + run: | + # https://gist.github.com/ugultopu/1adf8e08acb87be649d69419cf7aca3c + pkgutil --expand meshlib_${{ matrix.arch }}.pkg ./meshlib_install + cd ./meshlib_install/MeshLib.pkg + cat Payload | gunzip | cpio -i + + - name: Build C++ examples + if: ${{ matrix.config == 'Release' }} + env: + CXX: ${{ steps.thirdparty.outputs.cxx-compiler }} + run: | + cmake \ + -S examples/cpp-examples \ + -B cpp-examples-build \ + -D CMAKE_FRAMEWORK_PATH=$(pwd)/meshlib_install/MeshLib.pkg/Frameworks/ + cmake \ + --build cpp-examples-build \ + --parallel $(sysctl -n hw.physicalcpu) + + - name: Build C examples + if: ${{ matrix.config == 'Release' }} + env: + CC: ${{ steps.thirdparty.outputs.c-compiler }} + run: | + cmake \ + -S examples/c-examples \ + -B c-examples-build \ + -D CMAKE_FRAMEWORK_PATH=$(pwd)/meshlib_install/MeshLib.pkg/Frameworks/ + cmake \ + --build c-examples-build \ + --parallel $(sysctl -n hw.physicalcpu) + + - name: Upload Macos Distribution + if: ${{ inputs.upload_artifacts && matrix.config == 'Release' }} + env: + ACTIONS_ARTIFACT_UPLOAD_TIMEOUT_MS: 1800000 + uses: actions/upload-artifact@v7 + with: + name: Distributives_macos-${{matrix.arch}} + path: meshlib_${{matrix.arch}}.pkg + retention-days: 1 + overwrite: true + + - name: Collect artifact stats + # Mirror `Upload Macos Distribution`'s gate -- otherwise Debug jobs + # invoke this step with no matching .pkg and the action fails (with + # `continue-on-error: true` the job still succeeds, but a confusing + # `failure`-level annotation is surfaced on the run summary). + if: ${{ inputs.internal_build && inputs.upload_artifacts && matrix.config == 'Release' }} + continue-on-error: true + uses: ./.github/actions/collect-artifact-stats + with: + artifact_path: ${{ github.workspace }} + artifact_glob: meshlib_${{matrix.arch}}.pkg + stats_file_suffix: -${{ steps.collect-runner-stats.outputs.job_id }} + + - name: Create and fix fake Wheel for NuGet + if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' }} + shell: bash + run: | + python3 -m venv ./wheel_venv + source ./wheel_venv/bin/activate + python3 -m pip install delocate==0.10.7 + python3 ./scripts/nuget_patch/patch_library_deps.py ./patched_content/ ./build/Release/bin/lib{MeshLibC2,MeshLibC2Cuda}.dylib + ./scripts/nuget_patch/fix_macos_rpath.sh ./patched_content/libMeshLibC2.dylib + + - name: Upload NuGet files to Artifacts + if: ${{ inputs.nuget_build_patch && matrix.config == 'Release' }} + uses: actions/upload-artifact@v7 + with: + name: DotNetPatchArchiveMacOs-${{ matrix.arch }} + path: ./patched_content/* + retention-days: 1 + overwrite: true + + # Duplicate of macos-build-test for the single native x86_64 cross config, split + # into its own job so it can be gated (skipped when the crossplatform-build + # runner is unavailable) without hanging the hosted macOS legs. When the cross + # build replaces the hosted x64 leg, that entry is dropped from macos-build-test + # above and this job becomes the sole x64 producer. Keep the steps in sync. + macos-build-test-crossplatform: + if: ${{ inputs.build_macos_crossplatform }} + timeout-minutes: 100 + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: - arch: x64-cross config: Release compiler: AppleClang diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index bc6bc9433b54..8c5df2d3b2bf 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -75,6 +75,9 @@ on: build_enable_macos: description: value: ${{ !( jobs.prepare-config.outputs.tag-update-doc-only == 'true' || jobs.prepare-config.outputs.tag-disable-macos == 'true' ) }} + build_enable_macos_crossplatform: + description: "macOS native x86_64 cross job; also off whenever macOS is disabled" + value: ${{ !( jobs.prepare-config.outputs.tag-update-doc-only == 'true' || jobs.prepare-config.outputs.tag-disable-macos == 'true' || jobs.prepare-config.outputs.tag-disable-macos-crossplatform == 'true' ) }} build_enable_emscripten: description: value: ${{ !( jobs.prepare-config.outputs.tag-update-doc-only == 'true' || jobs.prepare-config.outputs.tag-disable-emscripten == 'true' ) }} @@ -109,6 +112,7 @@ jobs: tag-disable-ubuntu-arm64: ${{ steps.live-labels.outputs.tag-disable-ubuntu-arm64 }} tag-disable-linux-vcpkg: ${{ steps.live-labels.outputs.tag-disable-linux-vcpkg }} tag-disable-macos: ${{ steps.live-labels.outputs.tag-disable-macos }} + tag-disable-macos-crossplatform: ${{ steps.live-labels.outputs.tag-disable-macos-crossplatform }} tag-disable-emscripten: ${{ steps.live-labels.outputs.tag-disable-emscripten }} runs-on: ubuntu-latest From ddcec5a576a92a3751a0f8162eb960c01934c3e8 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Mon, 3 Aug 2026 18:27:31 +0000 Subject: [PATCH 13/14] macos/crossplatform: fix runner label, sudo Rosetta install, document brew arch --- macos/crossplatform-builds/provision-runner.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/macos/crossplatform-builds/provision-runner.sh b/macos/crossplatform-builds/provision-runner.sh index 5d160d7603c2..42a137856a82 100755 --- a/macos/crossplatform-builds/provision-runner.sh +++ b/macos/crossplatform-builds/provision-runner.sh @@ -14,7 +14,7 @@ # binaries and CMake configure-time # probes; compilation itself is native. # -# The runner must also carry the labels [self-hosted, macos, arm64, build]. +# The runner must also carry the labels [self-hosted, macos, arm64, crossplatform-build]. set -euo pipefail PREWARM=0 @@ -46,10 +46,14 @@ echo "==> 2/3 Rosetta 2 (to run x86_64 output + configure probes)" if /usr/bin/pgrep -q oahd; then echo " already installed" else - softwareupdate --install-rosetta --agree-to-license + # Needs root; run under sudo so `set -e` doesn't abort on a Rosetta-less host. + sudo softwareupdate --install-rosetta --agree-to-license fi echo "==> 3/3 x86_64 Homebrew at /usr/local (source of x86_64 bottles)" +# `arch -x86_64` below is only needed to BOOTSTRAP Intel brew; once installed, +# /usr/local brew operates on its Intel prefix regardless of the invoking arch, +# so CI's native `exec /usr/local/bin/brew` (the cross-build shim) is equivalent. if [[ -x /usr/local/bin/brew ]]; then echo " already present ($(arch -x86_64 /usr/local/bin/brew --version | head -1))" else From 598f55d9cf3dea46c442e13d675c7454ef6f9df3 Mon Sep 17 00:00:00 2001 From: Max Raiskii Date: Mon, 3 Aug 2026 18:29:39 +0000 Subject: [PATCH 14/14] macos/crossplatform: rewrite README as maintenance docs --- macos/crossplatform-builds/README.md | 153 +++++++++++++-------------- 1 file changed, 71 insertions(+), 82 deletions(-) diff --git a/macos/crossplatform-builds/README.md b/macos/crossplatform-builds/README.md index f3858ed968ec..6e67c56c91f7 100644 --- a/macos/crossplatform-builds/README.md +++ b/macos/crossplatform-builds/README.md @@ -1,102 +1,91 @@ -# macOS cross-platform builds — native (non-Rosetta) x86_64 on Apple Silicon - -Build the **Intel (`x86_64`)** macOS target of MeshLib on **Apple Silicon (`arm64`)** using a -**native arm64 toolchain that cross-targets x86_64** — the compiler runs natively (no Rosetta -translation of the build tools, so compiles are fast) and emits x86_64 code via `-arch x86_64`, -linking the x86_64 libraries from the `/usr/local` Homebrew. - -> This is the alternative to the "run everything under Rosetta" approach: there the whole -> toolchain (cmake/ninja/clang) runs as x86_64 under Rosetta, which is simpler but slower to -> compile. Here the toolchain stays native arm64 and only the *output* is x86_64. +# macOS Intel (x86_64) cross build on Apple Silicon + +Builds the **Intel (`x86_64`)** macOS target of MeshLib on an **Apple Silicon (`arm64`)** self-hosted +runner, using a **native arm64 toolchain that cross-targets x86_64**: cmake/ninja/clang run natively +(fast compiles) and emit x86_64 via `-arch x86_64`, linking the x86_64 Homebrew at `/usr/local`. The +binaries run on Intel Macs — and on the build host under Rosetta, which is how CI runs their tests. + +## Where it lives in CI + +- Job `macos-build-test-crossplatform` in + [`build-test-macos.yml`](../../.github/workflows/build-test-macos.yml) — a separate, gated copy of + the macOS build/test steps for the single `x64-cross` config. +- Gated by `build_enable_macos_crossplatform` (in [`config.yml`](../../.github/workflows/config.yml)): + on by default. Add the `disable-macos-crossplatform` PR label to skip just this job (e.g. when the + self-hosted runner is down) so it can't hang the hosted macOS legs; `disable-macos` skips all macOS. +- Runs on a runner labelled `[self-hosted, macos, arm64, crossplatform-build]`, provisioned by + [`provision-runner.sh`](provision-runner.sh). +- Produces `meshlib_x64-cross.pkg`; [`test-distribution.yml`](../../.github/workflows/test-distribution.yml) + installs and smoke-tests it on a real Intel Mac. + +> **Intent:** once proven, this replaces the GitHub-hosted `macos-15-intel` x64 leg (Intel runners are +> being retired). Until then both run, and both `.pkg`s are published (the cross one suffixed +> `-x64-cross`). ## How it works -- **cmake / ninja / clang run natively as arm64** (fast). `CMAKE_OSX_ARCHITECTURES=x86_64` - makes AppleClang emit x86_64 objects; the resulting binaries run on Intel Macs, or on this - host under Rosetta. -- The x86_64 dependencies come from the **x86_64 Homebrew at `/usr/local`** (coexists with the - native arm64 Homebrew at `/opt/homebrew`). [`ConfigureHomebrew.cmake`](../../cmake/Modules/ConfigureHomebrew.cmake) - now honors an explicit `-D HOMEBREW_PREFIX=/usr/local` instead of always calling `brew --prefix`. - -## Critical gotchas (why the naive attempt silently falls back to Rosetta) - -1. **Force the arm64 ninja.** CMake's `find_program` searches `/usr/local/bin` *by default*, even - when it isn't on `PATH`, so it picks up the **x86_64** ninja from the Intel Homebrew — and an - x86_64 ninja spawns **x86_64 clang under Rosetta**, silently defeating the native build. Pass - `-D CMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja`. (Verify with `vmmap | grep "Code Type"` - → must say `ARM64`, not `X86-64 (Translated)`.) -2. **Point find_package at `/usr/local`** with `-D CMAKE_PREFIX_PATH=/usr/local` so packages like - `Python`, OpenSSL, etc. resolve their x86_64 copies (not the arm64 `/opt/homebrew` ones). -3. **x86_64 Python vs native cmake PATH tension.** Native cmake/ninja want `/opt/homebrew` first - on `PATH`, but Python must be the x86_64 one from `/usr/local`. Resolve with a small shim dir - on `PATH` that maps `cmake`/`ninja` → `/opt/homebrew` (arm64) and `python3.10`/`python3.10-config` - → `/usr/local` (x86_64). -4. **`CMAKE_SYSTEM_PROCESSOR` stays `arm64`** (it reflects the *host*, since the cmake process is - native). This is harmless for MeshLib's own code (its SIMD is gated on target macros - `__x86_64__`/`__aarch64__`), and the heavy SIMD dependencies (OpenVDB, TBB, blosc) come from - x86_64 Homebrew *binaries* — not built from source here. Cosmetic side effect: `MR_PLATFORM` - is labelled `APPLE_arm64` for an x86_64 build. -5. Configure-time feature probes (`try_run` / `find_package(Python)` running the interpreter) - execute x86_64 test binaries, which the OS runs via Rosetta *transparently*. The bulk - compilation is native; only these brief configure probes touch Rosetta. Going fully - Rosetta-free would require a CMake toolchain file with `CMAKE_CROSSCOMPILING` + pre-seeded - `try_run` results. - -## What changed (vs upstream) - -| Change | File | -|---|---| -| Honor `-D HOMEBREW_PREFIX=` override (falls back to `brew --prefix`) | [`cmake/Modules/ConfigureHomebrew.cmake`](../../cmake/Modules/ConfigureHomebrew.cmake) | -| Honor a caller-set `NPROC` to cap build parallelism | [`scripts/build_source.sh`](../../scripts/build_source.sh) | - -Everything else is driven by CMake `-D` flags at invocation (below), so no other source changes -are required. - -## Recipe (Apple Silicon → native x86_64 cross build) - -Prerequisites: Rosetta 2 (only for *running* the resulting x86_64 binaries / configure probes) and -an x86_64 Homebrew bootstrapped at `/usr/local` with the `requirements/macos.txt` formulae. +- **cmake / ninja / clang run natively as arm64.** `CMAKE_OSX_ARCHITECTURES=x86_64` makes AppleClang + emit x86_64 objects. `-D MR_PLATFORM=APPLE_x86_64` labels the binary correctly — otherwise it + inherits the host's `CMAKE_SYSTEM_PROCESSOR` (`arm64`) and ships a wrong platform string. +- x86_64 dependencies come from the **x86_64 Homebrew at `/usr/local`** (coexisting with the native + arm64 Homebrew). [`ConfigureHomebrew.cmake`](../../cmake/Modules/ConfigureHomebrew.cmake) honors + `-D HOMEBREW_PREFIX=/usr/local`. + +## Critical gotchas (why a naive attempt silently falls back to Rosetta) + +1. **Force the arm64 ninja.** CMake's `find_program` searches `/usr/local/bin` by default and picks up + the **x86_64** ninja, which spawns **x86_64 clang under Rosetta** — silently defeating the native + build. Pass `-D CMAKE_MAKE_PROGRAM=/bin/ninja`. Verify with + `vmmap | grep "Code Type"` → must say `ARM64`, not `X86-64 (Translated)`. +2. **Point find_package at `/usr/local`** with `-D CMAKE_PREFIX_PATH=/usr/local` so Python, OpenSSL, + etc. resolve their x86_64 copies. +3. **x86_64 Python vs native cmake PATH tension.** Resolve with a small PATH shim mapping + `cmake`/`ninja` → the arm64 brew and `python3.10*` → `/usr/local` (x86_64). See the + "Configure native x86_64 cross-build environment" step. +4. **`CMAKE_SYSTEM_PROCESSOR` stays `arm64`** (it reflects the host, since cmake is native). Harmless + for MeshLib's own SIMD (gated on the target macros `__x86_64__`/`__aarch64__`); `MR_PLATFORM` is + set explicitly to compensate for the label. +5. Configure-time `try_run` probes execute x86_64 test binaries, which the OS runs via Rosetta + transparently. Only these brief probes touch Rosetta; the bulk compilation is native. + +## Provisioning a runner + +Run [`provision-runner.sh`](provision-runner.sh) once per host (see its header for prerequisites). It +ensures a native arm64 Homebrew (cmake + ninja), Rosetta 2, and an x86_64 Homebrew at `/usr/local` +with the `requirements/macos.txt` formulae (`--prewarm` also installs the binding-generation deps). + +## Reproducing locally ```bash -# shim: native arm64 cmake/ninja + x86_64 python on PATH -SHIM=/tmp/mlnative_bin; mkdir -p "$SHIM" -ln -sf /opt/homebrew/bin/cmake "$SHIM/cmake" -ln -sf /opt/homebrew/bin/ninja "$SHIM/ninja" -ln -sf /usr/local/bin/python3.10 "$SHIM/python3.10" +SHIM=$(mktemp -d) +ln -sf "$(brew --prefix)/bin/cmake" "$SHIM/cmake" # native arm64 cmake/ninja +ln -sf "$(brew --prefix)/bin/ninja" "$SHIM/ninja" +ln -sf /usr/local/bin/python3.10 "$SHIM/python3.10" # x86_64 Python ln -sf /usr/local/bin/python3.10-config "$SHIM/python3.10-config" env -i HOME="$HOME" \ - PATH="$SHIM:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin" \ - NPROC=5 MESHLIB_BUILD_RELEASE=ON MESHLIB_BUILD_DEBUG=OFF \ + PATH="$SHIM:$(brew --prefix)/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ + MESHLIB_BUILD_RELEASE=ON MESHLIB_BUILD_DEBUG=OFF \ CMAKE_C_COMPILER=/usr/bin/clang CMAKE_CXX_COMPILER=/usr/bin/clang++ \ MR_CMAKE_OPTIONS="\ - -D CMAKE_MAKE_PROGRAM=/opt/homebrew/bin/ninja \ + -D CMAKE_MAKE_PROGRAM=$(brew --prefix)/bin/ninja \ -D HOMEBREW_PREFIX=/usr/local \ -D CMAKE_PREFIX_PATH=/usr/local \ -D CMAKE_OSX_ARCHITECTURES=x86_64 \ + -D MR_PLATFORM=APPLE_x86_64 \ -D MR_CXX_STANDARD=23 -D MR_PCH_USE_EXTRA_HEADERS=ON" \ bash ./scripts/build_source.sh ``` -The thirdparty-from-source libraries build the same way (native arm64 tools + the same `-D` flags); -their x86_64 output is bit-for-bit equivalent regardless of whether they were built natively or -under Rosetta. - -## Validation +Confirm the output arch with `lipo -archs build/Release/bin/libMRMesh.dylib` → `x86_64` (CI asserts +this). The thirdparty-from-source libraries build the same way (native tools + the same `-D` flags). -Validated on an Apple **M4** (macOS 15.7): MeshLib core built with a **native arm64 clang** -(confirmed `Code Type: ARM64` for the live compiler processes) cross-targeting x86_64; all -binaries are x86_64 and link the `/usr/local` x86_64 Homebrew dylibs; **294/294 unit tests pass** -(the x86_64 test binary runs under Rosetta). +## Source changes this requires -## Native vs Rosetta — which to use +Everything else is CI wiring (the workflow job, the `config.yml` gate, the runner shim in the +workflow); the only non-CI source changes are: -| | Rosetta (`…-rosetta` branch) | Native (this branch) | -|---|---|---| -| Toolchain | x86_64 under Rosetta | native arm64 | -| Compile speed | slower (translated clang) | **faster (native clang)** | -| CMake setup | just PATH + a couple env vars | more `-D` flags; ninja/find_program gotchas | -| `CMAKE_SYSTEM_PROCESSOR` | `x86_64` (SIMD paths correct for source builds) | `arm64` (cosmetic `MR_PLATFORM` mislabel) | -| Robustness | higher (fewer moving parts) | needs care (silent Rosetta fallback if ninja wrong) | - -Native wins on build speed; Rosetta wins on simplicity/robustness. Pick per priority. +| Change | File | +|---|---| +| Honor `-D HOMEBREW_PREFIX=` (falls back to `brew --prefix`) and validate it | [`ConfigureHomebrew.cmake`](../../cmake/Modules/ConfigureHomebrew.cmake) | +| Forward the cross knobs (`CMAKE_OSX_ARCHITECTURES`, `CMAKE_MAKE_PROGRAM`, `HOMEBREW_PREFIX`) and honor a caller `NPROC` | [`build_source.sh`](../../scripts/build_source.sh), [`build_thirdparty.sh`](../../scripts/build_thirdparty.sh) |