Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a56a252
CI/macos: native (non-Rosetta) x86_64 cross build on Apple Silicon
MaxRayskiy Jul 1, 2026
0fe7bd8
macos/crossplatform-builds: add native-build runner provisioning script
MaxRayskiy Jul 13, 2026
ecda5dd
macos: init crossplatform build
MaxRayskiy Jul 17, 2026
40212f4
use separate label for macos crossplatform build
MaxRayskiy Jul 28, 2026
c8f178d
Merge branch 'master' into feature/macos-crossplatform-native
MaxRayskiy Jul 28, 2026
e4b5b7f
fix bindings setup
MaxRayskiy Aug 1, 2026
f55500f
force thirdparties to rebuild
MaxRayskiy Aug 1, 2026
28e9ea6
test-distribution: add x64-cross macos distro test
MaxRayskiy Aug 2, 2026
b021c63
cmake: validate overridden Homebrew prefix on Apple
MaxRayskiy Aug 3, 2026
cb25a81
macos: key thirdparty cache on target arch
MaxRayskiy Aug 3, 2026
6965c1a
macos: trim cross-build knob comment blocks
MaxRayskiy Aug 3, 2026
095148e
macos: cross x64 ships full bindings; fix platform label, assert arch…
MaxRayskiy Aug 3, 2026
26d7266
macos: gate native x86_64 cross build in its own skippable job
MaxRayskiy Aug 3, 2026
ddcec5a
macos/crossplatform: fix runner label, sudo Rosetta install, document…
MaxRayskiy Aug 3, 2026
598f55d
macos/crossplatform: rewrite README as maintenance docs
MaxRayskiy Aug 3, 2026
dec8b02
Merge remote-tracking branch 'origin/master' into feature/macos-cross…
MaxRayskiy Aug 3, 2026
1580a48
CI/macos: single build job with JSON matrix, x64-cross gated by label
MaxRayskiy Sep 20, 2026
f66d7e3
test-distribution: gate the x64-cross leg on test_macos_crossplatform
MaxRayskiy Sep 20, 2026
1de94e4
macos/crossplatform: no-sudo runner provisioning, README update
MaxRayskiy Sep 20, 2026
6eae83b
Merge branch 'master' into feature/macos-crossplatform-native
MaxRayskiy Sep 20, 2026
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
24 changes: 20 additions & 4 deletions .github/actions/install-llvm-pgo-keg/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ runs:
echo "llvm-pgo keg already present"
exit 0
fi
# The published kegs are built for the standard prefixes only.
if [[ "$(uname -m)" == "arm64" ]]; then
# The published kegs are built for the standard prefixes only, and each one is
# tied to a prefix: /opt/homebrew is arm64, /usr/local is x86_64. Pick by prefix,
# not by `uname -m`, so the x86_64-on-arm64 cross runner (arm64 host, Intel brew
# at /usr/local) gets the Intel keg. Custom prefixes fall back to the host arch;
# those machines carry the keg already and returned above.
case "$BP" in
/usr/local) KEG_ARCH=x86_64 ;;
/opt/homebrew) KEG_ARCH=arm64 ;;
*) KEG_ARCH="$(uname -m)" ;;
esac
if [[ "$KEG_ARCH" == "arm64" ]]; then
NEED=/opt/homebrew
ASSET=llvm-pgo-22.1.8_2-arm64/llvm-pgo-22.1.8_2.arm64_opt-homebrew.tar.gz
SHA=94f76fb55ec64490605d0e68115259426a428a78f269f7e9160726d90a22e197
Expand All @@ -38,15 +47,22 @@ runs:
- name: Install the PGO ld64.lld into the keg
shell: bash
run: |
KEG="$(brew --prefix)/Cellar/llvm-pgo/22.1.8_2"
BP="$(brew --prefix)"
KEG="$BP/Cellar/llvm-pgo/22.1.8_2"
# Homebrew has no `lld` bottle for Intel macOS since it became a Tier 3
# configuration, and brew's llvm@22 carries no ld64.lld either, so the linker
# comes from our own release. Next to clang++, which resolves `-fuse-ld=lld`
# in its own directory before PATH.
if [[ -x "$KEG/bin/ld64.lld" ]]; then
echo "ld64.lld already in the keg"
else
if [[ "$(uname -m)" == "arm64" ]]; then
# Match the keg's architecture, which follows the prefix (see above).
case "$BP" in
/usr/local) KEG_ARCH=x86_64 ;;
/opt/homebrew) KEG_ARCH=arm64 ;;
*) KEG_ARCH="$(uname -m)" ;;
esac
if [[ "$KEG_ARCH" == "arm64" ]]; then
ASSET=ld64.lld-22.1.8-pgo.arm64-macos13
SHA=8abf0ba324dea357ae4fbd8baee835353b560f62e9b1d001588a6df888755550
else
Expand Down
2 changes: 2 additions & 0 deletions .github/actions/install-macos-thirdparty/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build-test-distribute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,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'}}
config_matrix: ${{ needs.config.outputs.macos_config_matrix }}
secrets: inherit

update-win-version:
Expand Down Expand Up @@ -427,6 +428,7 @@ jobs:
test_ubuntu_x64: ${{ needs.config.outputs.build_enable_ubuntu_x64 == 'true' }}
test_ubuntu_arm64: ${{ needs.config.outputs.build_enable_ubuntu_arm64 == 'true' }}
test_macos: ${{ needs.config.outputs.build_enable_macos == 'true' }}
test_macos_crossplatform: ${{ needs.config.outputs.build_enable_macos_crossplatform == 'true' }}
test_windows: ${{ needs.config.outputs.build_enable_windows == 'true' && needs.config.outputs.build-release-win == 'true' }}
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
99 changes: 58 additions & 41 deletions .github/workflows/build-test-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,52 +31,24 @@ on:
default: false
required: false
type: boolean
config_matrix:
# JSON `{"include":[...]}` selected by config.yml from
# .github/workflows/matrix/macos-config.json.
required: true
type: string

jobs:
macos-build-test:
# Legs live in .github/workflows/matrix/macos-config.json; config.yml drops the
# self-hosted x64-cross leg when the disable-build-macos-crossplatform label is
# set, so a down runner can't hang macOS CI. arm64/Release is pinned to macos-14
# to share the mrbind cache with pip-build.yml.
name: macos-build-test (${{ matrix.arch }}, ${{ matrix.config }})
timeout-minutes: 100
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
arch: [ x64, arm64 ]
config: [ Debug, Release ]
exclude:
- arch: x64
config: Debug
include:
- arch: x64
config: Release
compiler: AppleClang
cxx-compiler-template: /usr/bin/clang++
c-compiler-template: /usr/bin/clang
# mrbind and the bindings use the PGO keg fetched from
# MeshInspector/toolchains releases; the app stays on AppleClang.
llvm-prefix-template: BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners
runner: macos-15-intel # github hosted
instance: macos-15-intel
- arch: arm64
config: Release
compiler: AppleClang
cxx-compiler-template: /usr/bin/clang++
c-compiler-template: /usr/bin/clang
# mrbind and the bindings use the PGO keg fetched from
# MeshInspector/toolchains releases; the app stays on AppleClang.
llvm-prefix-template: BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2
# Pinned to the same image pip-build.yml uses so the mrbind cache is shared.
runner: macos-14
instance: macos-14
- arch: arm64
config: Debug
compiler: brew-llvm22
# The -O3+PGO+ThinLTO keg, built on each runner via the local/pgo
# tap (llvm.rb with ENV.O3, installed with --build-bottle).
llvm-prefix-template: BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2
cxx-compiler-template: BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2/bin/clang++
c-compiler-template: BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2/bin/clang
runner: [ self-hosted, macos, arm64, build ] # any macos version
instance: self-hosted-arm
matrix: ${{ fromJSON(inputs.config_matrix) }}
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
Expand All @@ -88,6 +60,44 @@ jobs:
- name: macOS Diagnostics
uses: ./.github/actions/macos-diagnostics

# Before `Resolve LLVM prefix` and every other `brew --prefix`: the shim makes
# them resolve the x86_64 /usr/local brew whose bottles this target links.
- 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 }}"
echo "CMAKE_MAKE_PROGRAM=$ARM_BREW/bin/ninja"
echo "CMAKE_PREFIX_PATH=/usr/local"
echo "MESHLIB_HOMEBREW_PREFIX=/usr/local"
echo "HOMEBREW_DIR=/usr/local"
echo "PKG_CONFIG_PATH=$(/usr/local/bin/brew --prefix python@$PYVER)/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
} >> "$GITHUB_ENV"

# LLVM_PREFIX selects the LLVM keg for the build and the mrbind scripts;
# resolved per machine since the self-hosted label set spans runners
# with different homebrew prefixes.
Expand Down Expand Up @@ -168,6 +178,11 @@ jobs:
-DMR_CXX_STANDARD=23
-DMESHLIB_BUILD_GENERATED_C_BINDINGS=OFF
-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: Wait for C bindings
if: ${{ inputs.mrbind_c }}
Expand Down Expand Up @@ -330,8 +345,10 @@ jobs:
artifact_glob: meshlib_${{matrix.arch}}.pkg
stats_file_suffix: -${{ steps.collect-runner-stats.outputs.job_id }}

# NuGet consumes the exact artifact name DotNetPatchArchiveMacOs-x64, so the
# x64-cross leg's copy would only be uploaded and then deleted; skip it there.
- 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.arch != 'x64-cross' }}
shell: bash
run: |
python3 -m venv ./wheel_venv
Expand All @@ -341,7 +358,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.arch != 'x64-cross' }}
uses: actions/upload-artifact@v7
with:
name: DotNetPatchArchiveMacOs-${{ matrix.arch }}
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ on:
windows_x64_config_matrix:
description: "Config matrix for Windows builds"
value: ${{ jobs.prepare-config.outputs.windows_x64_config_matrix }}
macos_config_matrix:
description: "Config matrix for macOS builds; the self-hosted x64-cross leg is dropped when disabled"
value: ${{ jobs.prepare-config.outputs.macos_config_matrix }}
update_doc:
description:
value: ${{ jobs.prepare-config.outputs.tag-update-doc == 'true' || jobs.prepare-config.outputs.tag-update-doc-only == 'true' }}
Expand Down Expand Up @@ -78,6 +81,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 leg (label disable-build-macos-crossplatform); also off whenever macOS is disabled. Gates its distro test; the build leg itself is dropped from macos_config_matrix"
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' ) }}
Expand All @@ -93,6 +99,7 @@ jobs:
ubuntu_arm64_config_matrix: ${{ steps.set-ubuntu-arm64-matrix.outputs.matrix }}
ubuntu_x64_config_matrix: ${{ steps.set-ubuntu-x64-matrix.outputs.matrix }}
windows_x64_config_matrix: ${{ steps.set-windows-x64-matrix.outputs.matrix }}
macos_config_matrix: ${{ steps.set-macos-matrix.outputs.matrix }}
vcpkg-docker-image-tag: ${{ steps.select-vcpkg-docker-image-tag.outputs.image_tag }}
windows-changes: ${{ steps.windows-changes.outputs.src }}
# Read PR labels live; github.event.pull_request.labels is a
Expand All @@ -113,6 +120,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
Expand Down Expand Up @@ -351,3 +359,15 @@ jobs:
"$MATRIX_FILE" > tmp.json

echo "matrix=$(jq -c . tmp.json)" >> $GITHUB_OUTPUT

- name: Set matrix for macos builds
id: set-macos-matrix
run: |
# The x64-cross leg needs the self-hosted crossplatform-build runner; drop it
# (never scheduled) when the disable-build-macos-crossplatform label is set.
if [[ "${{ steps.live-labels.outputs.tag-disable-macos-crossplatform }}" == "true" ]]; then
FILTER='del(.include[] | select(.arch == "x64-cross"))'
else
FILTER='.'
fi
echo "matrix=$(jq -c "$FILTER" .github/workflows/matrix/macos-config.json)" >> "$GITHUB_OUTPUT"
45 changes: 45 additions & 0 deletions .github/workflows/matrix/macos-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"include": [
{
"arch": "x64",
"config": "Release",
"compiler": "AppleClang",
"cxx-compiler-template": "/usr/bin/clang++",
"c-compiler-template": "/usr/bin/clang",
"llvm-prefix-template": "BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2",
"runner": ["macos-15-intel"],
"instance": "macos-15-intel"
},
{
"arch": "arm64",
"config": "Release",
"compiler": "AppleClang",
"cxx-compiler-template": "/usr/bin/clang++",
"c-compiler-template": "/usr/bin/clang",
"llvm-prefix-template": "BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2",
"runner": ["macos-14"],
"instance": "macos-14"
},
{
"arch": "arm64",
"config": "Debug",
"compiler": "brew-llvm22",
"cxx-compiler-template": "BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2/bin/clang++",
"c-compiler-template": "BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2/bin/clang",
"llvm-prefix-template": "BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2",
"runner": ["self-hosted", "macos", "arm64", "build"],
"instance": "self-hosted-arm"
},
{
"arch": "x64-cross",
"config": "Release",
"compiler": "AppleClang",
"cxx-compiler-template": "/usr/bin/clang++",
"c-compiler-template": "/usr/bin/clang",
"llvm-prefix-template": "BREW_PREFIX/Cellar/llvm-pgo/22.1.8_2",
"runner": ["self-hosted", "macos", "arm64", "crossplatform-build"],
"instance": "self-hosted-arm-x64-native",
"cross-osx-arch": "x86_64"
}
]
}
43 changes: 20 additions & 23 deletions .github/workflows/test-distribution.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ on:
test_macos:
required: true
type: boolean
test_macos_crossplatform:
# The *x64-cross.pkg exists only when the self-hosted cross build ran.
required: true
type: boolean
test_windows:
required: true
type: boolean
Expand All @@ -35,6 +39,9 @@ on:
test_macos:
type: boolean
default: false
test_macos_crossplatform:
type: boolean
default: false
test_windows:
type: boolean
default: false
Expand Down Expand Up @@ -244,29 +251,19 @@ jobs:
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-13
pkg_pattern: '*arm64.pkg'
- arch: arm64
runner: macos-14
pkg_pattern: '*arm64.pkg'
- arch: arm64
runner: macos-15
pkg_pattern: '*arm64.pkg'
- arch: arm64
runner: macos-26
pkg_pattern: '*arm64.pkg'
- arch: x64
runner: macos-12-intel
pkg_pattern: '*x64.pkg'
- arch: x64
runner: macos-15-intel
pkg_pattern: '*x64.pkg'
- arch: x64
runner: macos-26-intel
pkg_pattern: '*x64.pkg'
# One leg per (runner, pkg). The x64-cross leg is appended only when that build
# ran, else `gh release download` finds no *x64-cross.pkg. An include-only matrix
# can't be trimmed with `exclude` (include is applied after it), hence the JSON.
matrix: >-
${{ fromJSON(format('{{"include":[{0}{1}]}}',
'{"arch":"arm64","runner":"macos-13","pkg_pattern":"*arm64.pkg"},
{"arch":"arm64","runner":"macos-14","pkg_pattern":"*arm64.pkg"},
{"arch":"arm64","runner":"macos-15","pkg_pattern":"*arm64.pkg"},
{"arch":"arm64","runner":"macos-26","pkg_pattern":"*arm64.pkg"},
{"arch":"x64","runner":"macos-12-intel","pkg_pattern":"*x64.pkg"},
{"arch":"x64","runner":"macos-15-intel","pkg_pattern":"*x64.pkg"},
{"arch":"x64","runner":"macos-26-intel","pkg_pattern":"*x64.pkg"}',
inputs.test_macos_crossplatform && ',{"arch":"x64-cross","runner":"macos-15-intel","pkg_pattern":"*x64-cross.pkg"}' || '')) }}
steps:
- name: Checkout
uses: actions/checkout@v7
Expand Down
Loading
Loading