Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
256 changes: 256 additions & 0 deletions .github/workflows/microsoft-build-spm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
name: Build SwiftPM

on:
workflow_call:

jobs:
resolve-hermes:
name: "Resolve Hermes"
runs-on: macos-15
timeout-minutes: 10
outputs:
hermes-commit: ${{ steps.resolve.outputs.hermes-commit }}
cache-hit: ${{ steps.cache.outputs.cache-hit }}
steps:
- uses: actions/checkout@v4
with:
filter: blob:none
fetch-depth: 0

- name: Setup Xcode
run: sudo xcode-select --switch /Applications/Xcode_16.2.app

- name: Set up Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: '22'
cache: yarn
registry-url: https://registry.npmjs.org

- name: Install npm dependencies
run: yarn install

- name: Resolve Hermes commit at merge base
id: resolve
working-directory: packages/react-native
run: |
COMMIT=$(node -e "const {hermesCommitAtMergeBase} = require('./scripts/ios-prebuild/hermes'); console.log(hermesCommitAtMergeBase().commit);" 2>&1 | grep -E '^[0-9a-f]{40}$')
echo "hermes-commit=$COMMIT" >> "$GITHUB_OUTPUT"
echo "Resolved Hermes commit: $COMMIT"

- name: Restore Hermes cache
id: cache
uses: actions/cache/restore@v4
with:
key: hermes-v1-${{ steps.resolve.outputs.hermes-commit }}-Debug
path: hermes-destroot

- name: Upload cached Hermes artifacts
if: steps.cache.outputs.cache-hit == 'true'
uses: actions/upload-artifact@v4
with:
name: hermes-artifacts
path: hermes-destroot
retention-days: 1

build-hermesc:
name: "Build hermesc"
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
needs: resolve-hermes
runs-on: macos-15
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
filter: blob:none

- name: Setup Xcode
run: sudo xcode-select --switch /Applications/Xcode_16.2.app

- name: Clone Hermes
uses: actions/checkout@v4
with:
repository: facebook/hermes
ref: ${{ needs.resolve-hermes.outputs.hermes-commit }}
path: hermes

- name: Build hermesc
working-directory: hermes
env:
HERMES_PATH: ${{ github.workspace }}/hermes
JSI_PATH: ${{ github.workspace }}/hermes/API/jsi
MAC_DEPLOYMENT_TARGET: '14.0'
run: |
source $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh
build_host_hermesc

- name: Upload hermesc artifact
uses: actions/upload-artifact@v4
with:
name: hermesc
path: hermes/build_host_hermesc
retention-days: 1

build-hermes-slice:
name: "Hermes ${{ matrix.slice }}"
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
needs: [resolve-hermes, build-hermesc]
runs-on: macos-15
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
slice: [iphoneos, iphonesimulator, macosx, xros, xrsimulator]
steps:
- uses: actions/checkout@v4
with:
filter: blob:none

- name: Setup Xcode
run: sudo xcode-select --switch /Applications/Xcode_16.2.app

- name: Download visionOS SDK
if: ${{ matrix.slice == 'xros' || matrix.slice == 'xrsimulator' }}
run: |
sudo xcodebuild -runFirstLaunch
sudo xcrun simctl list
sudo xcodebuild -downloadPlatform visionOS
sudo xcodebuild -runFirstLaunch

- name: Clone Hermes
uses: actions/checkout@v4
with:
repository: facebook/hermes
ref: ${{ needs.resolve-hermes.outputs.hermes-commit }}
path: hermes

- name: Download hermesc
uses: actions/download-artifact@v4
with:
name: hermesc
path: hermes/build_host_hermesc

- name: Restore hermesc permissions
run: chmod +x ${{ github.workspace }}/hermes/build_host_hermesc/bin/hermesc

- name: Build Hermes slice (${{ matrix.slice }})
working-directory: hermes
env:
BUILD_TYPE: Debug
HERMES_PATH: ${{ github.workspace }}/hermes
JSI_PATH: ${{ github.workspace }}/hermes/API/jsi
IOS_DEPLOYMENT_TARGET: '15.1'
MAC_DEPLOYMENT_TARGET: '14.0'
XROS_DEPLOYMENT_TARGET: '1.0'
RELEASE_VERSION: '1000.0.0'
run: |
bash $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-ios-framework.sh "${{ matrix.slice }}"

- name: Upload slice artifact
uses: actions/upload-artifact@v4
with:
name: hermes-slice-${{ matrix.slice }}
path: hermes/destroot
retention-days: 1

assemble-hermes:
name: "Assemble Hermes xcframework"
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
needs: [resolve-hermes, build-hermes-slice]
runs-on: macos-15
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
filter: blob:none

- name: Download all slice artifacts
uses: actions/download-artifact@v4
with:
pattern: hermes-slice-*
path: /tmp/slices

- name: Assemble destroot from slices
run: |
mkdir -p ${{ github.workspace }}/hermes/destroot/Library/Frameworks
for slice_dir in /tmp/slices/hermes-slice-*; do
slice_name=$(basename "$slice_dir" | sed 's/hermes-slice-//')
echo "Copying slice: $slice_name"
cp -R "$slice_dir/Library/Frameworks/$slice_name" ${{ github.workspace }}/hermes/destroot/Library/Frameworks/
# Copy include and bin directories (identical across slices, only need one copy)
if [ -d "$slice_dir/include" ] && [ ! -d ${{ github.workspace }}/hermes/destroot/include ]; then
cp -R "$slice_dir/include" ${{ github.workspace }}/hermes/destroot/
fi
if [ -d "$slice_dir/bin" ]; then
cp -R "$slice_dir/bin" ${{ github.workspace }}/hermes/destroot/
fi
done
echo "Assembled destroot contents:"
ls -la ${{ github.workspace }}/hermes/destroot/Library/Frameworks/

- name: Create universal xcframework
working-directory: hermes
env:
HERMES_PATH: ${{ github.workspace }}/hermes
run: |
source $GITHUB_WORKSPACE/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh
create_universal_framework "iphoneos" "iphonesimulator" "macosx" "xros" "xrsimulator"

- name: Save Hermes cache
uses: actions/cache/save@v4
with:
key: hermes-v1-${{ needs.resolve-hermes.outputs.hermes-commit }}-Debug
path: hermes/destroot

- name: Upload Hermes artifacts
uses: actions/upload-artifact@v4
with:
name: hermes-artifacts
path: hermes/destroot
retention-days: 1

build-spm:
name: "SPM ${{ matrix.platform }}"
needs: [resolve-hermes, assemble-hermes]
# Run when upstream jobs succeeded or were skipped (cache hit)
if: ${{ always() && !cancelled() && !failure() }}
runs-on: macos-26
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
platform: [ios, macos, visionos]
steps:
- uses: actions/checkout@v4
with:
filter: blob:none
fetch-depth: 0

- name: Setup toolchain
uses: ./.github/actions/microsoft-setup-toolchain
with:
node-version: '22'
platform: ${{ matrix.platform }}

- name: Install npm dependencies
run: yarn install

- name: Download Hermes artifacts
uses: actions/download-artifact@v4
with:
name: hermes-artifacts
path: packages/react-native/.build/artifacts/hermes/destroot

- name: Create Hermes version marker
working-directory: packages/react-native
run: |
VERSION=$(node -p "require('./package.json').version")
echo "${VERSION}-Debug" > .build/artifacts/hermes/version.txt

- name: Setup SPM workspace (using prebuilt Hermes)
working-directory: packages/react-native
run: node scripts/ios-prebuild.js -s -f Debug

- name: Build SPM (${{ matrix.platform }})
working-directory: packages/react-native
run: node scripts/ios-prebuild.js -b -f Debug -p ${{ matrix.platform }}
6 changes: 6 additions & 0 deletions .github/workflows/microsoft-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ jobs:
permissions: {}
uses: ./.github/workflows/microsoft-build-rntester.yml

build-spm:
name: "Build SPM"
permissions: {}
uses: ./.github/workflows/microsoft-build-spm.yml

test-react-native-macos-init:
name: "Test react-native-macos init"
permissions: {}
Expand All @@ -156,6 +161,7 @@ jobs:
- yarn-constraints
- javascript-tests
- build-rntester
- build-spm
- test-react-native-macos-init
# - react-native-test-app-integration
steps:
Expand Down
41 changes: 33 additions & 8 deletions packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,13 @@ let reactJsErrorHandler = RNTarget(
let reactGraphicsApple = RNTarget(
name: .reactGraphicsApple,
path: "ReactCommon/react/renderer/graphics/platform/ios",
linkedFrameworks: ["UIKit", "CoreGraphics"],
linkedFrameworks: ["CoreGraphics"],
// [macOS] Package.swift evaluates on the host (macOS), not the target, so #if os(macOS) doesn't work for cross-compilation.
// not the target. Use .when(platforms:) for cross-compilation support.
platformLinkerSettings: [
.linkedFramework("UIKit", .when(platforms: [.iOS, .visionOS])),
.linkedFramework("AppKit", .when(platforms: [.macOS])),
],
dependencies: [.reactDebug, .jsi, .reactUtils, .reactNativeDependencies]
)

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

/// React-Fabric.podspec
// [macOS: on macOS, use platform/macos view sources instead of platform/cxx
#if os(macOS)
let reactFabricViewPlatformSources = ["components/view/platform/macos"]
let reactFabricViewPlatformExcludes = ["components/view/platform/cxx"]
#else
let reactFabricViewPlatformExcludes = ["components/view/platform/macos"]
let reactFabricViewPlatformSources = ["components/view/platform/cxx"]
#endif
// macOS]
let reactFabric = RNTarget(
name: .reactFabric,
path: "ReactCommon/react/renderer",
Expand All @@ -379,7 +400,8 @@ let reactFabric = RNTarget(
"components/view/tests",
"components/view/platform/android",
"components/view/platform/windows",
"components/view/platform/macos",
// "components/view/platform/cxx", // [macOS] excluded on macOS, included on iOS/visionOS (see reactFabricViewPlatformExcludes)
// "components/view/platform/macos", // [macOS] excluded on iOS/visionOS, included on macOS (see reactFabricViewPlatformExcludes)
"components/scrollview/tests",
"components/scrollview/platform/android",
"mounting/tests",
Expand All @@ -402,9 +424,9 @@ let reactFabric = RNTarget(
"components/unimplementedview",
"components/virtualview",
"components/root/tests",
],
] + reactFabricViewPlatformExcludes, // [macOS]
dependencies: [.reactNativeDependencies, .reactJsiExecutor, .rctTypesafety, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .reactRendererDebug, .reactGraphics, .yoga],
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"]
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]
)

/// React-RCTFabric.podspec
Expand All @@ -424,7 +446,8 @@ let reactFabricComponents = RNTarget(
"components/view/platform/android",
"components/view/platform/windows",
"components/view/platform/macos",
"components/switch/iosswitch/react/renderer/components/switch/MacOSSwitchShadowNode.mm",
// [macOS] Both IOSSwitchShadowNode.mm and MacOSSwitchShadowNode.mm are included;
// they use #if TARGET_OS_OSX guards internally so only the correct one compiles.
"components/textinput/platform/android",
"components/text/platform/android",
"components/textinput/platform/macos",
Expand Down Expand Up @@ -591,7 +614,7 @@ let targets = [

let package = Package(
name: react,
platforms: [.iOS(.v15), .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)],
platforms: [.iOS(.v15), .macOS(.v14) /* [macOS] */, .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)],
products: [
.library(
name: react,
Expand Down Expand Up @@ -632,14 +655,16 @@ class BinaryTarget: BaseTarget {

class RNTarget: BaseTarget {
let linkedFrameworks: [String]
let platformLinkerSettings: [LinkerSetting] // [macOS] Platform-conditional framework linking (e.g. UIKit vs AppKit)
let excludedPaths: [String]
let dependencies: [String]
let sources: [String]?
let publicHeadersPath: String?
let defines: [CXXSetting]

init(name: String, path: String, searchPaths: [String] = [], linkedFrameworks: [String] = [], excludedPaths: [String] = [], dependencies: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = ".", defines: [CXXSetting] = []) {
init(name: String, path: String, searchPaths: [String] = [], linkedFrameworks: [String] = [], platformLinkerSettings: [LinkerSetting] = [], excludedPaths: [String] = [], dependencies: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = ".", defines: [CXXSetting] = []) {
self.linkedFrameworks = linkedFrameworks
self.platformLinkerSettings = platformLinkerSettings
self.excludedPaths = excludedPaths
self.dependencies = dependencies
self.sources = sources
Expand Down Expand Up @@ -675,7 +700,7 @@ class RNTarget: BaseTarget {
override func target(targets: [BaseTarget]) -> Target {
let searchPaths: [String] = self.headerSearchPaths(targets: targets)

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

return Target.reactNativeTarget(
name: self.name,
Expand Down
Loading
Loading