Skip to content

Commit ed2e393

Browse files
Saadnajmiclaude
andauthored
feat: build React Native macOS with Swift Package Manager (#2815)
## Summary Extends the Hermes build scripts to include macOS slices in the universal xcframework, and adds macOS support to the Swift Package Manager build system. Currently, Hermes builds a standalone macOS `.framework` via `build-mac-framework.sh` but does **not** include it in the universal `.xcframework` used by SPM. This means SPM consumers cannot target macOS. This PR fixes that by adding `macosx` as a platform in `build-ios-framework.sh`, so the macOS slice is built alongside iOS, visionOS, tvOS, and catalyst — and included in the universal xcframework. This mirrors the upstream Hermes changes: - facebook/hermes#1958 (`static_h` — full consolidation) ### Commit 1: SPM macOS support - Add `.macOS(.v14)` platform to `Package.swift` - Create `React-RCTUIKit` as its own SPM module with conditional UIKit/AppKit linking - Port `findMatchingHermesVersion` and `hermesCommitAtMergeBase` from Ruby to JS for Hermes version resolution - Add macOS platform and destination to prebuild CLI - Link RCTUIKit and macOS view platform headers in setup ### Commit 2: Include macOS slice in Hermes xcframework - Add `"macosx"` to `create_universal_framework` and `create_framework` in `build-ios-framework.sh` - Add `macosx` cases to `get_architecture` (x86_64;arm64) and `get_deployment_target` - Make `HERMES_PATH` overridable via env var in `build-apple-framework.sh` ### Commit 3: CI jobs - Add `microsoft-build-spm.yml` reusable workflow with two stages: - **Build Hermes**: Builds Hermes from source on `macos-15` (includes the macOS slice) - **Build SPM**: Uses the Hermes artifact to build SPM for ios, macos, and visionos on `macos-26` - Wire into PR gate in `microsoft-pr.yml` - Add `cmake-version` input to `microsoft-setup-toolchain` to allow skipping CMake installation - Add visionos/visionos-simulator as supported platforms in the ios-prebuild CLI ## Test plan - [ ] CI: Build Hermes from source with macOS slice - [ ] CI: SPM builds pass for iOS, macOS, visionOS - [ ] CocoaPods builds verified locally for iOS, macOS, visionOS 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aee05bb commit ed2e393

12 files changed

Lines changed: 732 additions & 20 deletions

File tree

.github/actions/microsoft-setup-toolchain/action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ inputs:
1919
xcode-developer-dir:
2020
description: Set the path for the active Xcode developer directory
2121
default: "/Applications/Xcode.app"
22+
cmake-version:
23+
description: CMake version to install. Set to 'system' to skip installation and use the runner's pre-installed cmake.
24+
default: "3.31.9"
2225
runs:
2326
using: composite
2427
steps:
2528
- name: Install cmake
29+
if: ${{ inputs.cmake-version != 'system' }}
2630
uses: jwlawson/actions-setup-cmake@v2
2731
with:
28-
cmake-version: '3.31.9'
32+
cmake-version: ${{ inputs.cmake-version }}
2933
- name: Set up Ccache
3034
id: setup-ccache
3135
if: ${{ inputs.platform == 'ios' || inputs.platform == 'macos' || inputs.platform == 'visionos' }}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
name: Build SwiftPM
2+
3+
on:
4+
workflow_call:
5+
6+
jobs:
7+
resolve-hermes:
8+
name: "Resolve Hermes"
9+
runs-on: macos-15
10+
timeout-minutes: 10
11+
outputs:
12+
hermes-commit: ${{ steps.resolve.outputs.hermes-commit }}
13+
cache-hit: ${{ steps.cache.outputs.cache-hit }}
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
filter: blob:none
18+
fetch-depth: 0
19+
20+
- name: Setup Xcode
21+
run: sudo xcode-select --switch /Applications/Xcode_16.2.app
22+
23+
- name: Set up Node.js
24+
uses: actions/setup-node@v4.4.0
25+
with:
26+
node-version: '22'
27+
cache: yarn
28+
registry-url: https://registry.npmjs.org
29+
30+
- name: Install npm dependencies
31+
run: yarn install
32+
33+
- name: Resolve Hermes commit at merge base
34+
id: resolve
35+
working-directory: packages/react-native
36+
run: |
37+
COMMIT=$(node -e "const {hermesCommitAtMergeBase} = require('./scripts/ios-prebuild/macosVersionResolver'); console.log(hermesCommitAtMergeBase().commit);" 2>&1 | grep -E '^[0-9a-f]{40}$')
38+
echo "hermes-commit=$COMMIT" >> "$GITHUB_OUTPUT"
39+
echo "Resolved Hermes commit: $COMMIT"
40+
41+
- name: Restore Hermes cache
42+
id: cache
43+
uses: actions/cache/restore@v4
44+
with:
45+
key: hermes-v1-${{ steps.resolve.outputs.hermes-commit }}-Debug
46+
path: hermes-destroot
47+
48+
- name: Upload cached Hermes artifacts
49+
if: steps.cache.outputs.cache-hit == 'true'
50+
uses: actions/upload-artifact@v4
51+
with:
52+
name: hermes-artifacts
53+
path: hermes-destroot
54+
retention-days: 30
55+
56+
build-hermesc:
57+
name: "Build hermesc"
58+
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
59+
needs: resolve-hermes
60+
runs-on: macos-15
61+
timeout-minutes: 30
62+
steps:
63+
- uses: actions/checkout@v4
64+
with:
65+
filter: blob:none
66+
67+
- name: Setup Xcode
68+
run: sudo xcode-select --switch /Applications/Xcode_16.2.app
69+
70+
- name: Clone Hermes
71+
uses: actions/checkout@v4
72+
with:
73+
repository: facebook/hermes
74+
ref: ${{ needs.resolve-hermes.outputs.hermes-commit }}
75+
path: hermes
76+
77+
- name: Build hermesc
78+
working-directory: hermes
79+
env:
80+
HERMES_PATH: ${{ github.workspace }}/hermes
81+
JSI_PATH: ${{ github.workspace }}/hermes/API/jsi
82+
MAC_DEPLOYMENT_TARGET: '14.0'
83+
run: |
84+
source $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh
85+
build_host_hermesc
86+
87+
- name: Upload hermesc artifact
88+
uses: actions/upload-artifact@v4
89+
with:
90+
name: hermesc
91+
path: hermes/build_host_hermesc
92+
retention-days: 30
93+
94+
build-hermes-slice:
95+
name: "Hermes ${{ matrix.slice }}"
96+
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
97+
needs: [resolve-hermes, build-hermesc]
98+
runs-on: macos-15
99+
timeout-minutes: 45
100+
strategy:
101+
fail-fast: false
102+
matrix:
103+
slice: [iphoneos, iphonesimulator, macosx, xros, xrsimulator]
104+
steps:
105+
- uses: actions/checkout@v4
106+
with:
107+
filter: blob:none
108+
109+
- name: Setup Xcode
110+
run: sudo xcode-select --switch /Applications/Xcode_16.2.app
111+
112+
- name: Download visionOS SDK
113+
if: ${{ matrix.slice == 'xros' || matrix.slice == 'xrsimulator' }}
114+
run: |
115+
sudo xcodebuild -runFirstLaunch
116+
sudo xcrun simctl list
117+
sudo xcodebuild -downloadPlatform visionOS
118+
sudo xcodebuild -runFirstLaunch
119+
120+
- name: Clone Hermes
121+
uses: actions/checkout@v4
122+
with:
123+
repository: facebook/hermes
124+
ref: ${{ needs.resolve-hermes.outputs.hermes-commit }}
125+
path: hermes
126+
127+
- name: Download hermesc
128+
uses: actions/download-artifact@v4
129+
with:
130+
name: hermesc
131+
path: hermes/build_host_hermesc
132+
133+
- name: Restore hermesc permissions
134+
run: chmod +x ${{ github.workspace }}/hermes/build_host_hermesc/bin/hermesc
135+
136+
- name: Build Hermes slice (${{ matrix.slice }})
137+
working-directory: hermes
138+
env:
139+
BUILD_TYPE: Debug
140+
HERMES_PATH: ${{ github.workspace }}/hermes
141+
JSI_PATH: ${{ github.workspace }}/hermes/API/jsi
142+
IOS_DEPLOYMENT_TARGET: '15.1'
143+
MAC_DEPLOYMENT_TARGET: '14.0'
144+
XROS_DEPLOYMENT_TARGET: '1.0'
145+
RELEASE_VERSION: '1000.0.0'
146+
run: |
147+
bash $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-ios-framework.sh "${{ matrix.slice }}"
148+
149+
- name: Upload slice artifact
150+
uses: actions/upload-artifact@v4
151+
with:
152+
name: hermes-slice-${{ matrix.slice }}
153+
path: hermes/destroot
154+
retention-days: 30
155+
156+
assemble-hermes:
157+
name: "Assemble Hermes xcframework"
158+
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
159+
needs: [resolve-hermes, build-hermes-slice]
160+
runs-on: macos-15
161+
timeout-minutes: 15
162+
steps:
163+
- uses: actions/checkout@v4
164+
with:
165+
filter: blob:none
166+
167+
- name: Download all slice artifacts
168+
uses: actions/download-artifact@v4
169+
with:
170+
pattern: hermes-slice-*
171+
path: /tmp/slices
172+
173+
- name: Assemble destroot from slices
174+
run: |
175+
mkdir -p ${{ github.workspace }}/hermes/destroot/Library/Frameworks
176+
for slice_dir in /tmp/slices/hermes-slice-*; do
177+
slice_name=$(basename "$slice_dir" | sed 's/hermes-slice-//')
178+
echo "Copying slice: $slice_name"
179+
cp -R "$slice_dir/Library/Frameworks/$slice_name" ${{ github.workspace }}/hermes/destroot/Library/Frameworks/
180+
# Copy include and bin directories (identical across slices, only need one copy)
181+
if [ -d "$slice_dir/include" ] && [ ! -d ${{ github.workspace }}/hermes/destroot/include ]; then
182+
cp -R "$slice_dir/include" ${{ github.workspace }}/hermes/destroot/
183+
fi
184+
if [ -d "$slice_dir/bin" ]; then
185+
cp -R "$slice_dir/bin" ${{ github.workspace }}/hermes/destroot/
186+
fi
187+
done
188+
echo "Assembled destroot contents:"
189+
ls -la ${{ github.workspace }}/hermes/destroot/Library/Frameworks/
190+
191+
- name: Create universal xcframework
192+
working-directory: hermes
193+
env:
194+
HERMES_PATH: ${{ github.workspace }}/hermes
195+
run: |
196+
source $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh
197+
create_universal_framework "iphoneos" "iphonesimulator" "macosx" "xros" "xrsimulator"
198+
199+
- name: Save Hermes cache
200+
uses: actions/cache/save@v4
201+
with:
202+
key: hermes-v1-${{ needs.resolve-hermes.outputs.hermes-commit }}-Debug
203+
path: hermes/destroot
204+
205+
- name: Upload Hermes artifacts
206+
uses: actions/upload-artifact@v4
207+
with:
208+
name: hermes-artifacts
209+
path: hermes/destroot
210+
retention-days: 30
211+
212+
build-spm:
213+
name: "SPM ${{ matrix.platform }}"
214+
needs: [resolve-hermes, assemble-hermes]
215+
# Run when upstream jobs succeeded or were skipped (cache hit)
216+
if: ${{ always() && !cancelled() && !failure() }}
217+
runs-on: macos-26
218+
timeout-minutes: 60
219+
strategy:
220+
fail-fast: false
221+
matrix:
222+
platform: [ios, macos, visionos]
223+
steps:
224+
- uses: actions/checkout@v4
225+
with:
226+
filter: blob:none
227+
fetch-depth: 0
228+
229+
- name: Setup toolchain
230+
uses: ./.github/actions/microsoft-setup-toolchain
231+
with:
232+
node-version: '22'
233+
platform: ${{ matrix.platform }}
234+
235+
- name: Install npm dependencies
236+
run: yarn install
237+
238+
- name: Download Hermes artifacts
239+
uses: actions/download-artifact@v4
240+
with:
241+
name: hermes-artifacts
242+
path: packages/react-native/.build/artifacts/hermes/destroot
243+
244+
- name: Create Hermes version marker
245+
working-directory: packages/react-native
246+
run: |
247+
VERSION=$(node -p "require('./package.json').version")
248+
echo "${VERSION}-Debug" > .build/artifacts/hermes/version.txt
249+
250+
- name: Setup SPM workspace (using prebuilt Hermes)
251+
working-directory: packages/react-native
252+
run: node scripts/ios-prebuild.js -s -f Debug
253+
254+
- name: Build SPM (${{ matrix.platform }})
255+
working-directory: packages/react-native
256+
run: node scripts/ios-prebuild.js -b -f Debug -p ${{ matrix.platform }}

.github/workflows/microsoft-pr.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ jobs:
138138
permissions: {}
139139
uses: ./.github/workflows/microsoft-build-rntester.yml
140140

141+
build-spm:
142+
name: "Build SPM"
143+
permissions: {}
144+
uses: ./.github/workflows/microsoft-build-spm.yml
145+
141146
test-react-native-macos-init:
142147
name: "Test react-native-macos init"
143148
permissions: {}
@@ -163,6 +168,7 @@ jobs:
163168
- yarn-constraints
164169
- javascript-tests
165170
- build-rntester
171+
- build-spm
166172
- test-react-native-macos-init
167173
# - react-native-test-app-integration
168174
steps:

0 commit comments

Comments
 (0)