Skip to content

Commit 99bf47d

Browse files
Saadnajmiclaude
andcommitted
Backport SPM macOS support to 0.81-stable
Adapts the SPM macOS support from the main branch for the 0.81-stable branch. Key difference from main: RCTUIKit is part of React-Core on 0.81 (not a separate module), so platformLinkerSettings for UIKit/AppKit go directly on the reactCore target. Changes: - Add macOS/visionOS platform support to ios-prebuild CLI and types - Add macOS version resolution via macosVersionResolver.js (fork-only) - Add Hermes build-from-source fallback for main branch builds - Add macOS platform to Package.swift with platform-conditional linking - Add macOS view platform sources/excludes to reactFabric - Add macOS view platform header links in setup.js - Add Hermes CI workflow for building macOS slices - Make HERMES_PATH overridable in build-apple-framework.sh - Add macOS slice to build-ios-framework.sh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8acac29 commit 99bf47d

10 files changed

Lines changed: 710 additions & 15 deletions

File tree

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/hermes'); 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: 1
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: 1
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: 1
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: 1
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 }}

packages/react-native/Package.swift

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,13 @@ let reactJsErrorHandler = RNTarget(
249249
let reactGraphicsApple = RNTarget(
250250
name: .reactGraphicsApple,
251251
path: "ReactCommon/react/renderer/graphics/platform/ios",
252-
linkedFrameworks: ["UIKit", "CoreGraphics"],
252+
linkedFrameworks: ["CoreGraphics"],
253+
// [macOS] Package.swift evaluates on the host (macOS), not the target, so #if os(macOS) doesn't work for cross-compilation.
254+
// not the target. Use .when(platforms:) for cross-compilation support.
255+
platformLinkerSettings: [
256+
.linkedFramework("UIKit", .when(platforms: [.iOS, .visionOS])),
257+
.linkedFramework("AppKit", .when(platforms: [.macOS])),
258+
],
253259
dependencies: [.reactDebug, .jsi, .reactUtils, .reactNativeDependencies]
254260
)
255261

@@ -363,12 +369,27 @@ let reactCore = RNTarget(
363369
"ReactCommon/react/runtime/platform/ios", // explicit header search path to break circular dependency. RCTHost imports `RCTDefines.h` in ReactCore, ReacCore needs to import RCTHost
364370
],
365371
linkedFrameworks: ["CoreServices"],
372+
// [macOS] RCTUIKit is part of React-Core on 0.81 — add platform-conditional UIKit/AppKit linking
373+
platformLinkerSettings: [
374+
.linkedFramework("UIKit", .when(platforms: [.iOS, .visionOS])),
375+
.linkedFramework("AppKit", .when(platforms: [.macOS])),
376+
],
377+
// macOS]
366378
excludedPaths: ["Fabric", "Tests", "Resources", "Runtime/RCTJscInstanceFactory.mm", "I18n/strings", "CxxBridge/JSCExecutorFactory.mm", "CoreModules"],
367379
dependencies: [.reactNativeDependencies, .reactCxxReact, .reactPerfLogger, .jsi, .reactJsiExecutor, .reactUtils, .reactFeatureFlags, .reactRuntimeScheduler, .yoga, .reactJsInspector, .reactJsiTooling, .rctDeprecation, .reactCoreRCTWebsocket, .reactRCTImage, .reactTurboModuleCore, .reactRCTText, .reactRCTBlob, .reactRCTAnimation, .reactRCTNetwork, .reactFabric, .hermesPrebuilt],
368380
sources: [".", "Runtime/RCTHermesInstanceFactory.mm"]
369381
)
370382

371383
/// React-Fabric.podspec
384+
// [macOS: on macOS, use platform/macos view sources instead of platform/cxx
385+
#if os(macOS)
386+
let reactFabricViewPlatformSources = ["components/view/platform/macos"]
387+
let reactFabricViewPlatformExcludes = ["components/view/platform/cxx"]
388+
#else
389+
let reactFabricViewPlatformExcludes = ["components/view/platform/macos"]
390+
let reactFabricViewPlatformSources = ["components/view/platform/cxx"]
391+
#endif
392+
// macOS]
372393
let reactFabric = RNTarget(
373394
name: .reactFabric,
374395
path: "ReactCommon/react/renderer",
@@ -379,7 +400,8 @@ let reactFabric = RNTarget(
379400
"components/view/tests",
380401
"components/view/platform/android",
381402
"components/view/platform/windows",
382-
"components/view/platform/macos",
403+
// "components/view/platform/cxx", // [macOS] excluded on macOS, included on iOS/visionOS (see reactFabricViewPlatformExcludes)
404+
// "components/view/platform/macos", // [macOS] excluded on iOS/visionOS, included on macOS (see reactFabricViewPlatformExcludes)
383405
"components/scrollview/tests",
384406
"components/scrollview/platform/android",
385407
"mounting/tests",
@@ -402,9 +424,9 @@ let reactFabric = RNTarget(
402424
"components/unimplementedview",
403425
"components/virtualview",
404426
"components/root/tests",
405-
],
427+
] + reactFabricViewPlatformExcludes, // [macOS]
406428
dependencies: [.reactNativeDependencies, .reactJsiExecutor, .rctTypesafety, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .reactRendererDebug, .reactGraphics, .yoga],
407-
sources: ["animations", "attributedstring", "core", "componentregistry", "componentregistry/native", "components/root", "components/view", "components/view/platform/cxx", "components/scrollview", "components/scrollview/platform/cxx", "components/legacyviewmanagerinterop", "dom", "scheduler", "mounting", "observers/events", "telemetry", "consistency", "leakchecker", "uimanager", "uimanager/consistency"]
429+
sources: ["animations", "attributedstring", "core", "componentregistry", "componentregistry/native", "components/root", "components/view", "components/scrollview", "components/scrollview/platform/cxx", "components/legacyviewmanagerinterop", "dom", "scheduler", "mounting", "observers/events", "telemetry", "consistency", "leakchecker", "uimanager", "uimanager/consistency"] + reactFabricViewPlatformSources // [macOS]
408430
)
409431

410432
/// React-RCTFabric.podspec
@@ -591,7 +613,7 @@ let targets = [
591613

592614
let package = Package(
593615
name: react,
594-
platforms: [.iOS(.v15), .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)],
616+
platforms: [.iOS(.v15), .macOS(.v14) /* [macOS] */, .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)],
595617
products: [
596618
.library(
597619
name: react,
@@ -632,14 +654,16 @@ class BinaryTarget: BaseTarget {
632654

633655
class RNTarget: BaseTarget {
634656
let linkedFrameworks: [String]
657+
let platformLinkerSettings: [LinkerSetting] // [macOS] Platform-conditional framework linking (e.g. UIKit vs AppKit)
635658
let excludedPaths: [String]
636659
let dependencies: [String]
637660
let sources: [String]?
638661
let publicHeadersPath: String?
639662
let defines: [CXXSetting]
640663

641-
init(name: String, path: String, searchPaths: [String] = [], linkedFrameworks: [String] = [], excludedPaths: [String] = [], dependencies: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = ".", defines: [CXXSetting] = []) {
664+
init(name: String, path: String, searchPaths: [String] = [], linkedFrameworks: [String] = [], platformLinkerSettings: [LinkerSetting] = [], excludedPaths: [String] = [], dependencies: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = ".", defines: [CXXSetting] = []) {
642665
self.linkedFrameworks = linkedFrameworks
666+
self.platformLinkerSettings = platformLinkerSettings
643667
self.excludedPaths = excludedPaths
644668
self.dependencies = dependencies
645669
self.sources = sources
@@ -675,7 +699,7 @@ class RNTarget: BaseTarget {
675699
override func target(targets: [BaseTarget]) -> Target {
676700
let searchPaths: [String] = self.headerSearchPaths(targets: targets)
677701

678-
let linkerSettings = self.linkedFrameworks.reduce([]) { $0 + [LinkerSetting.linkedFramework($1)] }
702+
let linkerSettings = self.linkedFrameworks.reduce([]) { $0 + [LinkerSetting.linkedFramework($1)] } + self.platformLinkerSettings // [macOS]
679703

680704
return Target.reactNativeTarget(
681705
name: self.name,

packages/react-native/scripts/ios-prebuild/cli.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const platforms /*: $ReadOnlyArray<Platform> */ = [
1818
'ios',
1919
'ios-simulator',
2020
'mac-catalyst',
21+
'macos', // [macOS]
22+
'visionos', // [macOS]
2123
];
2224

2325
// CI can't use commas in cache keys, so 'macOS,variant=Mac Catalyst' was creating troubles
@@ -26,6 +28,8 @@ const platformToDestination /*: $ReadOnly<{|[Platform]: Destination|}> */ = {
2628
ios: 'iOS',
2729
'ios-simulator': 'iOS Simulator',
2830
'mac-catalyst': 'macOS,variant=Mac Catalyst',
31+
macos: 'macOS', // [macOS]
32+
visionos: 'visionOS', // [macOS]
2933
};
3034

3135
const cli = yargs

0 commit comments

Comments
 (0)