Skip to content

Commit c0a71fa

Browse files
committed
ci(onnx): build ONNX Runtime for macOS with the floor the app declares
`npm run build:mac` cannot package a macOS bundle today. Microsoft's `onnxruntime-osx-arm64-*.tgz` is built for macOS 14; `electron-builder.json5` declares `minimumSystemVersion: "13.0"`; `before-pack.cjs` refuses a payload that demands more than the floor. Correctly — the deployment target decides which symbols the linker resolves against the OS rather than emitting locally, which is how #515 stranded macOS 12 users with a dyld failure they reported as a denied permission. THERE IS NO VERSION THAT FITS. Every macOS arm64 release was checked, 1.20 through 1.29: 1.20.0 - 1.22.0 minos 13.3 1.23.0 minos 13.4 1.24.4 - 1.29.0 minos 14.0 The floor moved at 1.24 and never came back, and even the oldest is above 13.0. Lowering `ort`'s `api-27` feature does not help. Building is the only way to keep both macOS 13 support and webcam segmentation. MEASURED ON AN M1 before writing this. Shallow clone of the pinned tag with submodules 979 MB / 21 s, configure 24 s, build 10 min at `--parallel 4`. The result is `minos 13.0`, arm64, 21.7 MB against upstream's 36.7 MB — the difference is the CoreML execution provider, which this app does not use: `segmentation.rs` builds its session with no explicit provider, and webcam-segmentation.md records the CPU EP as a measured choice ("Inference p50, CPU EP: 3.575 ms — the CPU is faster"). It was then exercised through the real code path, not just inspected: `the_whole_loop_produces_a_mask_from_compose_frame_alone` — capture, inference, mask, composite — went from skipped ("ONNX Runtime absent") to passing with `ORT_DYLIB_PATH` pointed at it. WHAT THE WORKFLOW GUARDS. It reads the version from `scripts/fetch-onnxruntime.mjs` and the floor from `electron-builder.json5` rather than repeating either, so neither can drift. It then fails the build if `minos` is not the floor, and if `OrtGetApiBase` or the CPU provider is missing from the exports. A library that came out at 14.0 anyway would be worse than none: it would sail through packaging and fail at dyld time on the machines it was built to support. It drives CMake directly rather than upstream's `build.sh`, whose `build_args.py` needs Python 3.10+. The 3.10 in `find_package(Python 3.10)` is only real for the Python bindings; the one thing Python is genuinely needed for is generating the symbol export list, and `gen_def.py` parses on 3.9. IT DOES NOT PUBLISH. The workflow builds, verifies, and prints the exact `PINNED` entry to the job summary. Attaching the archive to a release and pasting that entry stays a human step, so the posture this script documents — immutable URL, SHA-256 verified before the archive is opened — survives. `fetch-onnxruntime.mjs` gains an optional per-target `baseUrl`; nothing points at it yet, so behaviour is unchanged until a maintainer decides to adopt it.
1 parent 8c9323f commit c0a71fa

2 files changed

Lines changed: 221 additions & 1 deletion

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
name: Build ONNX Runtime (macOS 13 floor)
2+
3+
# Builds ONNX Runtime for macOS arm64 with the deployment target pinned to the
4+
# floor this app declares, and publishes it as an artifact for a maintainer to
5+
# attach to a release.
6+
#
7+
# WHY THIS EXISTS. Microsoft's own `onnxruntime-osx-arm64-*.tgz` is built for
8+
# macOS 14. `electron-builder.json5` declares `minimumSystemVersion: "13.0"`,
9+
# and `scripts/before-pack.cjs` refuses to package any binary that demands more
10+
# than the floor — correctly, because the deployment target decides which
11+
# symbols the linker resolves against the OS rather than emitting locally, so a
12+
# too-high floor strands users on the older OS with `Symbol not found` at dyld
13+
# time (#515). The result is that `npm run build:mac` cannot package at all.
14+
#
15+
# Every published release from 1.24 onward is `minos 14.0`, and every release
16+
# before it is at least 13.3, so no prebuilt artifact has ever satisfied a 13.0
17+
# floor. Building it is the only way to keep both macOS 13 support and webcam
18+
# segmentation.
19+
#
20+
# WHAT IS NOT BUILT. The CoreML execution provider. `segmentation.rs` builds its
21+
# session with no explicit provider, i.e. the CPU EP, which
22+
# technical-documentation/engineering/webcam-segmentation.md records as a
23+
# measured decision ("Inference p50, CPU EP: 3.575 ms — the CPU is faster").
24+
# Dropping CoreML is what takes the library from 36.7 MB to ~22 MB.
25+
#
26+
# HOW IT IS PUBLISHED. This workflow does not create releases. It builds,
27+
# verifies, and prints the exact `PINNED` entry for
28+
# `scripts/fetch-onnxruntime.mjs` in the job summary; attaching the archive to a
29+
# release and pasting that entry stays a deliberate human step, so the
30+
# supply-chain posture the script documents — immutable URL, SHA-256 verified
31+
# before the archive is opened — is preserved rather than replaced by "whatever
32+
# CI last uploaded".
33+
34+
on:
35+
workflow_dispatch:
36+
push:
37+
paths:
38+
- ".github/workflows/build-onnxruntime-macos.yml"
39+
- "scripts/fetch-onnxruntime.mjs"
40+
41+
permissions:
42+
contents: read
43+
44+
jobs:
45+
build:
46+
name: macOS arm64, deployment target 13.0
47+
# arm64 runner: the only macOS target upstream ships, and the only one the
48+
# app packages. There is no Intel build to match.
49+
runs-on: macos-latest
50+
steps:
51+
- name: Checkout
52+
uses: actions/checkout@v7
53+
54+
- name: Read the pinned version from fetch-onnxruntime.mjs
55+
id: pin
56+
# Single source of truth. `fetch-onnxruntime.test.mjs` already cross-checks
57+
# that VERSION satisfies the `api-NN` feature `crates/Cargo.toml` gives
58+
# `ort`; reading it here rather than repeating it means a bump cannot leave
59+
# this workflow building a version nothing consumes.
60+
run: |
61+
set -euo pipefail
62+
VERSION="$(sed -n 's/^const VERSION = "\(.*\)";$/\1/p' scripts/fetch-onnxruntime.mjs)"
63+
[ -n "$VERSION" ] || { echo "::error::VERSION not found in scripts/fetch-onnxruntime.mjs"; exit 1; }
64+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
65+
echo "Building ONNX Runtime v$VERSION"
66+
67+
- name: Read the deployment floor from electron-builder.json5
68+
id: floor
69+
# Also single-sourced: if somebody raises `mac.minimumSystemVersion`, this
70+
# build follows rather than silently producing a library for the old floor.
71+
run: |
72+
set -euo pipefail
73+
FLOOR="$(grep -o '"minimumSystemVersion": *"[0-9.]*"' electron-builder.json5 | grep -o '[0-9][0-9.]*')"
74+
[ -n "$FLOOR" ] || { echo "::error::minimumSystemVersion not found"; exit 1; }
75+
echo "floor=$FLOOR" >> "$GITHUB_OUTPUT"
76+
echo "Deployment target: $FLOOR"
77+
78+
- name: Checkout ONNX Runtime
79+
uses: actions/checkout@v7
80+
with:
81+
repository: microsoft/onnxruntime
82+
ref: v${{ steps.pin.outputs.version }}
83+
path: onnxruntime-src
84+
submodules: recursive
85+
fetch-depth: 1
86+
87+
- name: Cache the CMake build tree
88+
uses: actions/cache@v6
89+
with:
90+
path: onnxruntime-build
91+
# Keyed on the version, the floor and the runner image: CMake bakes
92+
# absolute SDK paths into the tree, so a toolchain roll must bust it.
93+
key: ort-${{ steps.pin.outputs.version }}-${{ steps.floor.outputs.floor }}-${{ env.ImageOS }}-${{ env.ImageVersion }}
94+
95+
- name: Configure
96+
# Driven straight at CMake rather than through upstream's `build.sh`.
97+
# `build.sh` -> `build_args.py` uses `match`, so it needs Python 3.10+,
98+
# and `cmake/CMakeLists.txt` asks for `find_package(Python 3.10)` — but
99+
# that requirement is only real for the Python bindings, which are not
100+
# built here. The one thing Python IS needed for is generating the symbol
101+
# export list (`onnxruntime.lds`); pointing `Python_EXECUTABLE` at
102+
# whatever the runner has is enough, and `gen_def.py` parses on 3.9.
103+
run: |
104+
set -euo pipefail
105+
cmake -S onnxruntime-src/cmake -B onnxruntime-build \
106+
-DCMAKE_BUILD_TYPE=Release \
107+
-DCMAKE_OSX_DEPLOYMENT_TARGET=${{ steps.floor.outputs.floor }} \
108+
-DCMAKE_OSX_ARCHITECTURES=arm64 \
109+
-Donnxruntime_BUILD_SHARED_LIB=ON \
110+
-Donnxruntime_BUILD_UNIT_TESTS=OFF \
111+
-DPython_EXECUTABLE="$(command -v python3)"
112+
113+
- name: Build
114+
run: cmake --build onnxruntime-build --config Release --parallel
115+
116+
- name: Verify the deployment target
117+
# The entire reason this workflow exists. A library that comes out at 14.0
118+
# anyway is worse than no library, because it would sail through packaging
119+
# and strand macOS 13 users at dyld time.
120+
run: |
121+
set -euo pipefail
122+
V="${{ steps.pin.outputs.version }}"
123+
DYLIB="onnxruntime-build/libonnxruntime.${V}.dylib"
124+
[ -f "$DYLIB" ] || { echo "::error::$DYLIB was not produced"; exit 1; }
125+
MINOS="$(otool -l "$DYLIB" | awk '/LC_BUILD_VERSION/{f=1} f&&/minos/{print $2; exit}')"
126+
echo "minos=$MINOS floor=${{ steps.floor.outputs.floor }}"
127+
[ "$MINOS" = "${{ steps.floor.outputs.floor }}" ] || {
128+
echo "::error::built for macOS $MINOS, expected ${{ steps.floor.outputs.floor }}"; exit 1; }
129+
130+
- name: Verify the ABI surface
131+
# `ort` is wired `load-dynamic`, so it dlopens this file and calls
132+
# `OrtGetApiBase`. The CPU provider is the one `segmentation.rs` uses.
133+
# CoreML is deliberately absent and is NOT checked for.
134+
run: |
135+
set -euo pipefail
136+
V="${{ steps.pin.outputs.version }}"
137+
DYLIB="onnxruntime-build/libonnxruntime.${V}.dylib"
138+
for sym in _OrtGetApiBase _OrtSessionOptionsAppendExecutionProvider_CPU; do
139+
nm -gU "$DYLIB" | grep -q " ${sym}$" || { echo "::error::missing export ${sym}"; exit 1; }
140+
done
141+
lipo -info "$DYLIB"
142+
echo "ABI surface OK"
143+
144+
- name: Package in the upstream layout
145+
# Byte-for-byte the same shape as `onnxruntime-osx-arm64-<v>.tgz`, so
146+
# `fetch-onnxruntime.mjs` needs no extraction change — only a URL and a
147+
# digest. `member` there is the VERSIONED file; the two symlinks beside it
148+
# are kept so the archive stays a drop-in for anything that expects them.
149+
run: |
150+
set -euo pipefail
151+
V="${{ steps.pin.outputs.version }}"
152+
DIR="onnxruntime-osx-arm64-${V}"
153+
mkdir -p "stage/${DIR}/lib" "stage/${DIR}/include"
154+
cp "onnxruntime-build/libonnxruntime.${V}.dylib" "stage/${DIR}/lib/"
155+
ln -s "libonnxruntime.${V}.dylib" "stage/${DIR}/lib/libonnxruntime.dylib"
156+
ln -s "libonnxruntime.${V}.dylib" "stage/${DIR}/lib/libonnxruntime.1.dylib"
157+
cp onnxruntime-src/include/onnxruntime/core/session/*.h "stage/${DIR}/include/" || true
158+
tar -czf "${DIR}.tgz" -C stage "${DIR}"
159+
shasum -a 256 "${DIR}.tgz"
160+
161+
- name: Upload
162+
uses: actions/upload-artifact@v7
163+
with:
164+
name: onnxruntime-osx-arm64
165+
path: onnxruntime-osx-arm64-*.tgz
166+
if-no-files-found: error
167+
retention-days: 90
168+
169+
- name: Workflow summary
170+
if: always()
171+
shell: bash
172+
run: |
173+
set -euo pipefail
174+
V="${{ steps.pin.outputs.version }}"
175+
ARCHIVE="onnxruntime-osx-arm64-${V}.tgz"
176+
{
177+
echo "## ONNX Runtime ${V}, macOS arm64, deployment target ${{ steps.floor.outputs.floor }}"
178+
echo ""
179+
echo "- Result: ${{ job.status }}"
180+
} >> "$GITHUB_STEP_SUMMARY"
181+
if [ -f "$ARCHIVE" ]; then
182+
SHA="$(shasum -a 256 "$ARCHIVE" | cut -d' ' -f1)"
183+
{
184+
echo "- Archive: \`${ARCHIVE}\` ($(du -h "$ARCHIVE" | cut -f1))"
185+
echo "- SHA-256: \`${SHA}\`"
186+
echo ""
187+
echo "To adopt it: attach the archive to a release, then point the"
188+
echo "\`darwin-arm64\` entry of \`PINNED\` in \`scripts/fetch-onnxruntime.mjs\`"
189+
echo "at that release with this digest:"
190+
echo ""
191+
echo '```js'
192+
echo '"darwin-arm64": {'
193+
echo ' slug: "osx-arm64",'
194+
echo ' ext: "tgz",'
195+
echo " sha256: \"${SHA}\","
196+
echo " member: \`libonnxruntime.\${VERSION}.dylib\`,"
197+
echo ' out: "libonnxruntime.dylib",'
198+
echo ' baseUrl: "<the release download URL>",'
199+
echo '},'
200+
echo '```'
201+
} >> "$GITHUB_STEP_SUMMARY"
202+
fi

‎scripts/fetch-onnxruntime.mjs‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ const ROOT = path.join(__dirname, "..");
5454
const VERSION = "1.27.1";
5555
const BASE = `https://github.com/microsoft/onnxruntime/releases/download/v${VERSION}`;
5656

57+
/**
58+
* Where a target's archive is fetched from. Upstream unless the entry overrides it.
59+
*
60+
* The override exists for one reason, and it is not preference: **no published ONNX
61+
* Runtime has ever satisfied this app's macOS floor.** Every release from 1.24 on is
62+
* built for macOS 14, and every release before it for at least 13.3, while
63+
* `electron-builder.json5` declares 13.0 and `before-pack.cjs` refuses anything above
64+
* the floor — correctly, since the deployment target decides which symbols the linker
65+
* resolves against the OS instead of emitting locally (#515). So `npm run build:mac`
66+
* cannot package a macOS bundle at all with the upstream artifact.
67+
*
68+
* `.github/workflows/build-onnxruntime-macos.yml` builds one with the floor pinned and
69+
* prints the `PINNED` entry to paste here. Everything else about this file is
70+
* unchanged: an immutable URL, and a SHA-256 verified before the archive is opened.
71+
* What moves is who built the bytes, not how much they are trusted.
72+
*/
73+
const baseUrlFor = (spec) => spec.baseUrl ?? BASE;
74+
5775
/**
5876
* Per-target: the upstream artifact slug, its digest, and the library to lift out.
5977
*
@@ -204,7 +222,7 @@ async function download(spec) {
204222
const asset = assetName(spec);
205223
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openscreen-ort-"));
206224
console.log(`Downloading ${asset}\n from v${VERSION}`);
207-
const res = await fetch(`${BASE}/${asset}`);
225+
const res = await fetch(`${baseUrlFor(spec)}/${asset}`);
208226
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
209227
const bytes = Buffer.from(await res.arrayBuffer());
210228

0 commit comments

Comments
 (0)