Skip to content

macos x64: crossplatform native build - #6448

Open
MaxRayskiy wants to merge 16 commits into
masterfrom
feature/macos-crossplatform-native
Open

macos x64: crossplatform native build#6448
MaxRayskiy wants to merge 16 commits into
masterfrom
feature/macos-crossplatform-native

Conversation

@MaxRayskiy

Copy link
Copy Markdown
Contributor

No description provided.

MaxRayskiy and others added 3 commits July 2, 2026 00:02
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=<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) <noreply@anthropic.com>
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) <noreply@anthropic.com>

@Fedr Fedr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff, the workflow logic, and the actual CI run for f55500f (run 30714896464).

The headline result is real

x64-cross Release finished in 13m19s vs 51m04s for the GitHub-hosted macos-15-intel x64 job in the same run — a full 729-target build in ~2m08s, all tests passing (unit, C, python sanity, 197 regression). Strong result, worth landing. One caveat on framing: this measures the self-hosted M4 against GitHub's Intel VM, not "native vs Rosetta" as the README frames it.

Blocking

1. skip-bindings: false makes every new guard dead code

matrix.skip-bindings is false on the one entry that sets it and unset (null) everywhere else. !false and !null are both true, so !matrix.skip-bindings is unconditionally true across the whole matrix. All ~13 added && !matrix.skip-bindings clauses, both new if: guards, the two (matrix.skip-bindings && 'OFF') || … ternaries, and the MRTest${{ … ' --no-python-tests' … }} suffix change nothing.

The run log confirms it: MRBind deps installed, MRBind built, C bindings downloaded, Run ./build/Release/bin/MRTest with no --no-python-tests, python sanity + regression tests all ran on the cross entry.

So either delete the machinery (the cross build does bindings, successfully), or flip the flag to true. As-is it's a large, load-bearing-looking construct that is inert — a trap for whoever touches this file next.

2. If the flag does get flipped to true, Wait for C bindings was missed

It still reads if: ${{ inputs.mrbind_c }} while Download C bindings right below it gained the guard — the job would block on generate-c-bindings for an artifact it then skips.

3. The cross build's .pkg gets published to the GitHub release

Traced through the upload-distributions log for this run:

- Distributives_macos-x64-cross (ID: 8823183007, Size: 80930018 …)
for PKG_FILE in … meshlib*.pkg …; do mv "$PKG_FILE" "${PKG_FILE/meshlib/meshlib_v1.0.0.5733}"
gh release upload v1.0.0.5733 $PKG_FILES --clobber

meshlib_v1.0.0.5733_x64-cross.pkg landed on release v1.0.0.5733. On a real release run that puts a second, duplicate Intel package on the public release page. Gate Create Pkg / Upload Macos Distribution off for this entry, or name the artifact outside the meshlib*.pkg glob. (test-distribution doesn't pick it up — its *x64.pkg glob doesn't match — so it's an untested extra asset.)

4. No enable/disable gate for the new entry

The reusable workflow is gated only by build_enable_macos; the new matrix entry has none. It runs on every macOS-enabled run and hard-requires a self-hosted runner carrying crossplatform-build. If that host is down or busy, every PR's macOS CI sits until the 100-minute job timeout. Every other platform has a build_enable_* switch in config.yml.

Related: nothing is removed, so this is +13 min of scarce self-hosted Mac time per run on top of the 51-minute hosted Intel job. The PR body is empty — if the plan is to eventually replace macos-15-intel, that should be written down.

5. MR_PLATFORM is APPLE_arm64 in an x86_64 build

The build log shows platform: APPLE_arm64. The README calls this cosmetic, but it's compiled into the binary and returned by the public getOSNoSpaces() in source/MRMesh/MRSystem.cpp — nothing in MeshLib calls it, so it exists for downstream consumers, which would get a wrong platform string from shipped Intel binaries. Blocking only if this job is meant to replace the hosted x64 job. One-line fix: pass -D MR_PLATFORM=APPLE_x86_64 (already a cache STRING option).

Should fix

6. Nothing verifies the output is actually x86_64

The README's own thesis is that the failure mode is silent (wrong ninja → Rosetta), and separately a dangling shim symlink or a lost env var would yield arm64 binaries packaged as meshlib_x64*.pkg. Add a cheap assertion after Build:

lipo -archs build/Release/bin/libMRMesh.dylib | grep -qx x86_64

7. The submodule-script swap is an unrelated change affecting all four macOS entries

clone_submodules_linux.sh --skip-prebuilt-thirdpartyclone_submodules_macos.sh adds 10 submodules to every macOS job's checkout. It looks like a genuine latent-bug fix — build_thirdparty.sh needs clip/glad/laz-perf/libE57Format/OpenCTM-git/tinygltf/fastmcpp, which --skip-prebuilt-thirdparty excludes, so macOS would break on any thirdparty cache miss — but it arrived in a commit titled "force thirdparties to rebuild", which reads as temporary debugging. Worth splitting into its own PR so it can land immediately on its own merits.

8. /opt/homebrew is hardcoded in the shim

This run's own PATH shows the native brew at /Users/runner/.homebrew/bin, and install-macos-thirdparty carries a brew-hash cache-key input specifically because "self-hosted ARM fleet members … have homebrew at different paths". ln -sf cheerfully creates dangling symlinks. Derive the native prefix, or at minimum assert the three targets exist.

9. The build_thirdparty.sh changes were never exercised

The thirdparty cache hit in this run, so Build thirdparty libs was skipped. Also, the thirdparty cache key doesn't include the target arch — it's discriminated only indirectly via cache-instance and the brew prefix, which works today but is a sharp edge.

Nits

  1. The -D CMAKE_OSX_ARCHITECTURES= forwarding is redundant. CMake reads CMAKE_OSX_ARCHITECTURES from the environment — which is precisely why the un-instrumented Build C++ examples step (plain cmake, no -D) produced a linkable x86_64 build in this run. Harmless to keep, but the comment claiming it's necessary is wrong.

  2. Comment blocks run 8 and 7 lines in build_source.sh and build_thirdparty.sh — house style is ≤3, with the rationale in the commit/PR. The README already holds the long version.

  3. ConfigureHomebrew.cmake loses validation on the override path. The EXISTS check now only guards the auto-detected value; -D HOMEBREW_PREFIX=/typo silently yields bogus include_directories/link_directories. Move the check outside the IF(NOT HOMEBREW_PREFIX).

  4. arch: x64-cross flows into collect-runner-stats's target_arch, introducing a new arch string that forks macOS x64 history in the stats DB.

  5. PKG_CONFIG_PATH is set to exactly one directory, replacing whatever the runner had. Also the Python version is hardcoded three times (python3.10 ×2, python@3.10) while clang_version_macos.txt exists for the llvm pin.

  6. provision-runner.sh:

    • Header says the runner "must also carry the labels [self-hosted, macos, arm64, build]" — the workflow uses crossplatform-build.
    • softwareupdate --install-rosetta --agree-to-license needs root; under set -e the script dies on a Rosetta-less runner unless invoked with sudo.
    • It always wraps /usr/local/bin/brew in arch -x86_64; the CI shim execs it natively. Worth making consistent, or documenting why the native invocation is safe.
  7. README reads as an experiment writeup rather than maintenance docs. Now that CI has made the choice, the "Native vs Rosetta — which to use / Pick per priority" table doesn't belong. Also:

    • "What changed (vs upstream)" lists 2 files; the PR touches 6.
    • "their x86_64 output is bit-for-bit equivalent regardless of whether they were built natively or under Rosetta" is unsupported and almost certainly false in general — suggest dropping it.
    • It references a …-rosetta branch that doesn't exist in the repo.
    • NPROC=5 and /tmp/mlnative_bin are machine-specific leftovers.

@Fedr Fedr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at dec8b02. The 8 new commits address nearly everything, and CI is fully green.

Resolved

# Finding How
1 Dead skip-bindings guards Cross config moved into its own job macos-build-test-crossplatform; all skip-bindings machinery deleted
2 Ungated Wait for C bindings Moot — bindings always on
4 No enable/disable gate New build_macos_crossplatform workflow input + build_enable_macos_crossplatform in config.yml, folded under disable-macos too
5 MR_PLATFORM=APPLE_arm64 -DMR_PLATFORM=APPLE_x86_64
6 No arch verification lipo -archs … | grep -qx x86_64 — plus a real distro test
8 /opt/homebrew hardcoded Discovery loop over /opt/homebrew, ~/.homebrew keyed on an arm64 cmake, with fail-loud existence checks on all five shim targets
9 Thirdparty cache not arch-keyed osx-arch= folded into thirdparty-hash
11 Oversized comment blocks Trimmed to 2 lines + README pointer
12 ConfigureHomebrew skipped validation on override EXISTS check now covers both paths
14 PKG_CONFIG_PATH clobbered; python version repeated Appends via ${PKG_CONFIG_PATH:+:…}; PYVER=3.10 used throughout
15 provision-runner.sh Label corrected to crossplatform-build, sudo softwareupdate, brew-arch equivalence documented
16 README Rewritten as maintenance docs with an explicit "Intent" note

Finding 3 (pkg publishing) was answered rather than removed: the cross .pkg is now deliberately published and installed/smoke-tested on a real Intel Mac (test-distribution / macos-test (x64-cross, macos-15-intel) — 3m46s, pass). That's stronger validation than the lipo check.

Timings on dec8b02: x64-cross 14m13s vs x64 44m44s. Also worth noting the cache-key change is a no-op for existing jobs — pip-build.yml never sets CMAKE_OSX_ARCHITECTURES, so the shared thirdparty cache isn't invalidated.

Remaining

N1. The original job still carries three cross-only constructs that can never fire

No entry in macos-build-test's matrix sets cross-osx-arch, so build-test-macos.yml lines 96–134 (the 33-line "Configure native x86_64 cross-build environment" step), line 213 (the -DMR_PLATFORM=APPLE_x86_64 ternary) and lines 215–217 (Verify x86_64 output) are all dead. Same class as the old skip-bindings guards, just relocated. Suggest deleting them from macos-build-test.

N2. The gating label doesn't exist yet

gh label list shows only disable-build-{windows,ubuntu-x64,ubuntu-arm64,macos,emscripten,linux-vcpkg}. Until disable-build-macos-crossplatform is created, the gate can never be turned off — which is the entire reason the job was split out. Both spellings survive the ${label/disable-build-/disable-} rewrite in config.yml, but the disable-build- prefix matches the existing family, whereas the README documents disable-macos-crossplatform; worth picking one.

N3. test-distribution's new x64-cross entry isn't gated

macos-test is gated only by inputs.test_macos (= build_enable_macos). Skip the cross build via the new label and there's no *x64-cross.pkg on the release, so gh release download --pattern '*x64-cross.pkg' fails — and it's wrapped in retry.sh, so it burns the retry budget first. Needs a test_macos_crossplatform input plumbed through build-test-distribute.yml. This is exactly the failure the gate was added to prevent, moved one job downstream.

N4. The 300-line duplicated job

Honestly labelled ("Keep the steps in sync"), and GHA makes this genuinely awkward, but every future macOS CI change now has to be applied twice with nothing catching a miss. Worth deciding before merge rather than after: computing strategy.matrix.include from fromJSON() keeps one copy of the steps and preserves the skip-when-runner-down behaviour. If the duplicate stays, a lint that diffs the two step lists would earn its keep.

Nits

  • provision-runner.sh still hardcodes /opt/homebrew while the CI step now also accepts ~/.homebrew; a host CI would build on fine gets rejected by the provisioner.
  • Two Intel .pkgs on every public release during the transition. Now deliberate and documented — just worth confirming downstream consumers of the release assets tolerate the extra file.
  • The clone_submodules_macos.sh swap still rides along and affects all macOS jobs. Fine to keep, but it's an independent latent-bug fix (thirdparty submodules missing on a cache miss) worth naming explicitly.
  • PR body is still empty.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants