From d6290a11da854125b2c46635509752a1477b43b8 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 16 Jan 2026 21:47:56 -0600 Subject: [PATCH 1/3] feat: add macOS support to Swift Package Manager build system Add native macOS platform support to React Native's SPM build system with CI workflow. The codebase already contains macOS code (tested via CocoaPods) that uses RCTUIKit.h abstraction for iOS/macOS differences. Changes: - Add macOS 14.0 platform to Package.swift - Remove macOS exclusions from Fabric components - Add conditional UIKit/AppKit framework linking - Update build script to support macOS platform - Create prebuild-macos-core.yml CI workflow - Integrate macOS build into PR validation Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/microsoft-pr.yml | 6 + .github/workflows/prebuild-macos-core.yml | 210 ++++++++++++++++++ packages/react-native/Package.swift | 17 +- .../react-native/scripts/ios-prebuild/cli.js | 2 + 4 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/prebuild-macos-core.yml diff --git a/.github/workflows/microsoft-pr.yml b/.github/workflows/microsoft-pr.yml index 2c1b4500b3ccf4..ccc79bebd68947 100644 --- a/.github/workflows/microsoft-pr.yml +++ b/.github/workflows/microsoft-pr.yml @@ -132,6 +132,11 @@ jobs: permissions: {} uses: ./.github/workflows/microsoft-build-rntester.yml + build-spm-macos: + name: "Build SPM macOS" + permissions: {} + uses: ./.github/workflows/prebuild-macos-core.yml + # https://github.com/microsoft/react-native-macos/issues/2344 # Disable these tests because verdaccio hangs # test-react-native-macos-init: @@ -158,6 +163,7 @@ jobs: - yarn-constraints - javascript-tests - build-rntester + - build-spm-macos # - test-react-native-macos-init # - react-native-test-app-integration steps: diff --git a/.github/workflows/prebuild-macos-core.yml b/.github/workflows/prebuild-macos-core.yml new file mode 100644 index 00000000000000..feae19dc22e3da --- /dev/null +++ b/.github/workflows/prebuild-macos-core.yml @@ -0,0 +1,210 @@ +name: Prebuild macOS Dependencies + +on: + workflow_call: # this directive allow us to call this workflow from other workflows + + +jobs: + build-rn-slice: + runs-on: macos-14 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + flavor: ['Debug', 'Release'] + slice: ['macos'] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Restore cache if present + id: restore-macos-slice + uses: actions/cache/restore@v4 + with: + key: v1-macos-core-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift') }}-${{ hashFiles('packages/react-native/scripts/ios-prebuild/setup.js') }} + path: packages/react-native/ + - name: Setup node.js + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + uses: ./.github/actions/setup-node + - name: Setup xcode + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + uses: ./.github/actions/setup-xcode + with: + xcode-version: '16.2.0' + - name: Yarn Install + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + uses: ./.github/actions/yarn-install + - name: Download Hermes + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + uses: actions/download-artifact@v4 + with: + name: hermes-darwin-bin-${{ matrix.flavor }} + path: /tmp/hermes/hermes-runtime-darwin + - name: Extract Hermes + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + shell: bash + run: | + HERMES_TARBALL_ARTIFACTS_DIR=/tmp/hermes/hermes-runtime-darwin + if [ ! -d $HERMES_TARBALL_ARTIFACTS_DIR ]; then + echo "Hermes tarball artifacts dir not present ($HERMES_TARBALL_ARTIFACTS_DIR)." + exit 0 + fi + + TARBALL_FILENAME=$(node ./packages/react-native/scripts/hermes/get-tarball-name.js --buildType "${{ matrix.flavor }}") + TARBALL_PATH=$HERMES_TARBALL_ARTIFACTS_DIR/$TARBALL_FILENAME + + echo "Looking for $TARBALL_FILENAME in $HERMES_TARBALL_ARTIFACTS_DIR" + echo "$TARBALL_PATH" + + if [ ! -f $TARBALL_PATH ]; then + echo "Hermes tarball not present ($TARBALL_PATH). Build Hermes from source." + exit 0 + fi + + echo "Found Hermes tarball at $TARBALL_PATH" + echo "HERMES_ENGINE_TARBALL_PATH=$TARBALL_PATH" >> $GITHUB_ENV + - name: Download ReactNativeDependencies + uses: actions/download-artifact@v4 + with: + name: ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz + path: /tmp/third-party/ + - name: Extract ReactNativeDependencies + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + shell: bash + run: | + # Extract ReactNativeDependencies + tar -xzf /tmp/third-party/ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz -C /tmp/third-party/ + + # Create destination folder + mkdir -p packages/react-native/third-party/ + + # Move the XCFramework in the destination directory + mv /tmp/third-party/packages/react-native/third-party/ReactNativeDependencies.xcframework packages/react-native/third-party/ReactNativeDependencies.xcframework + + VERSION=$(jq -r '.version' package.json) + echo "$VERSION-${{matrix.flavor}}" > "packages/react-native/third-party/version.txt" + cat "packages/react-native/third-party/version.txt" + # Check destination directory + ls -lR packages/react-native/third-party/ + - name: Setup the workspace + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + shell: bash + run: | + cd packages/react-native + node scripts/ios-prebuild.js -s -f "${{ matrix.flavor }}" + - name: Build React Native + if: steps.restore-macos-slice.outputs.cache-hit != 'true' + shell: bash + run: | + # This is going to be replaced by a CLI script + cd packages/react-native + node scripts/ios-prebuild -b -f "${{ matrix.flavor }}" -p "${{ matrix.slice }}" + - name: Upload headers + uses: actions/upload-artifact@v4 + with: + name: prebuild-macos-core-headers-${{ matrix.flavor }}-${{ matrix.slice }} + path: + packages/react-native/.build/headers + - name: Upload artifacts + uses: actions/upload-artifact@v4.3.4 + with: + name: prebuild-macos-core-slice-${{ matrix.flavor }}-${{ matrix.slice }} + path: | + packages/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products + - name: Save Cache + uses: actions/cache/save@v4 + if: ${{ github.ref == 'refs/heads/main' }} # To avoid that the cache explode + with: + key: v1-macos-core-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift') }}-${{ hashFiles('packages/react-native/scripts/ios-prebuild/setup.js') }} + path: | + packages/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products + packages/react-native/.build/headers + + compose-xcframework: + runs-on: macos-14 + timeout-minutes: 60 + needs: [build-rn-slice] + strategy: + fail-fast: false + matrix: + flavor: ['Debug', 'Release'] + env: + REACT_ORG_CODE_SIGNING_P12_CERT: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT }} + REACT_ORG_CODE_SIGNING_P12_CERT_PWD: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT_PWD }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Restore cache if present + id: restore-macos-xcframework + uses: actions/cache/restore@v4 + with: + path: packages/react-native/.build/output/xcframeworks + key: v1-macos-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift') }}-${{ hashFiles('packages/react-native/scripts/ios-prebuild/setup.js') }} + - name: Setup node.js + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' + uses: ./.github/actions/setup-node + - name: Setup xcode + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' + uses: ./.github/actions/setup-xcode + with: + xcode-version: '16.2.0' + - name: Yarn Install + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' + uses: ./.github/actions/yarn-install + - name: Download slice artifacts + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' + uses: actions/download-artifact@v4 + with: + pattern: prebuild-macos-core-slice-${{ matrix.flavor }}-* + path: packages/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products + merge-multiple: true + - name: Download headers + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' + uses: actions/download-artifact@v4 + with: + pattern: prebuild-macos-core-headers-${{ matrix.flavor }}-* + path: packages/react-native/.build/headers + merge-multiple: true + - name: Setup Keychain + if: ${{ steps.restore-macos-xcframework.outputs.cache-hit != 'true' && env.REACT_ORG_CODE_SIGNING_P12_CERT != '' }} + uses: apple-actions/import-codesign-certs@v3 # https://github.com/marketplace/actions/import-code-signing-certificates + with: + p12-file-base64: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT }} + p12-password: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT_PWD }} + - name: Create XCFramework + if: ${{ steps.restore-macos-xcframework.outputs.cache-hit != 'true' && env.REACT_ORG_CODE_SIGNING_P12_CERT == '' }} + run: | + cd packages/react-native + node scripts/ios-prebuild -c -f "${{ matrix.flavor }}" + - name: Create and Sign XCFramework + if: ${{ steps.restore-macos-xcframework.outputs.cache-hit != 'true' && env.REACT_ORG_CODE_SIGNING_P12_CERT != '' }} + run: | + cd packages/react-native + node scripts/ios-prebuild -c -f "${{ matrix.flavor }}" -i "React Org" + - name: Compress and Rename XCFramework + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' + run: | + cd packages/react-native/.build/output/xcframeworks/${{matrix.flavor}} + tar -cz -f ../ReactCoreMacOS${{matrix.flavor}}.xcframework.tar.gz React.xcframework + - name: Compress and Rename dSYM + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' + run: | + cd packages/react-native/.build/output/xcframeworks/${{matrix.flavor}}/Symbols + tar -cz -f ../../ReactCoreMacOS${{ matrix.flavor }}.framework.dSYM.tar.gz . + - name: Upload XCFramework Artifact + uses: actions/upload-artifact@v4 + with: + name: ReactCoreMacOS${{ matrix.flavor }}.xcframework.tar.gz + path: packages/react-native/.build/output/xcframeworks/ReactCoreMacOS${{matrix.flavor}}.xcframework.tar.gz + - name: Upload dSYM Artifact + uses: actions/upload-artifact@v4 + with: + name: ReactCoreMacOS${{ matrix.flavor }}.framework.dSYM.tar.gz + path: packages/react-native/.build/output/xcframeworks/ReactCoreMacOS${{matrix.flavor}}.framework.dSYM.tar.gz + - name: Save cache if present + if: ${{ github.ref == 'refs/heads/main' }} # To avoid that the cache explode + uses: actions/cache/save@v4 + with: + path: | + packages/react-native/.build/output/xcframeworks/ReactCoreMacOS${{matrix.flavor}}.xcframework.tar.gz + packages/react-native/.build/output/xcframeworks/ReactCoreMacOS${{matrix.flavor}}.framework.dSYM.tar.gz + key: v1-macos-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift') }}-${{ hashFiles('packages/react-native/scripts/ios-prebuild/setup.js') }} diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index c387085ea2368f..b814bfc537e9e9 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -246,7 +246,7 @@ let reactJsErrorHandler = RNTarget( let reactGraphicsApple = RNTarget( name: .reactGraphicsApple, path: "ReactCommon/react/renderer/graphics/platform/ios", - linkedFrameworks: ["UIKit", "CoreGraphics"], + linkedFrameworks: ["CoreGraphics"], dependencies: [.reactDebug, .jsi, .reactUtils, .reactNativeDependencies] ) @@ -376,7 +376,6 @@ let reactFabric = RNTarget( "components/view/tests", "components/view/platform/android", "components/view/platform/windows", - "components/view/platform/macos", "components/scrollview/tests", "components/scrollview/platform/android", "mounting/tests", @@ -420,16 +419,13 @@ let reactFabricComponents = RNTarget( "components/modal/platform/cxx", "components/view/platform/android", "components/view/platform/windows", - "components/view/platform/macos", "components/textinput/platform/android", "components/text/platform/android", - "components/textinput/platform/macos", "components/text/tests", "textlayoutmanager/tests", "textlayoutmanager/platform/android", "textlayoutmanager/platform/cxx", "textlayoutmanager/platform/windows", - "textlayoutmanager/platform/macos", "conponents/rncore", // this was the old folder where RN Core Components were generated. If you ran codegen in the past, you might have some files in it that might make the build fail. ], dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging], @@ -587,7 +583,7 @@ let targets = [ let package = Package( name: react, - platforms: [.iOS(.v15), .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)], + platforms: [.iOS(.v15), .macOS(.v14), .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)], products: [ .library( name: react, @@ -792,6 +788,13 @@ extension Target { .define("USE_HERMES", to: "1"), ] + defines + cxxCommonHeaderPaths + // Platform-specific framework linking + var conditionalLinkerSettings: [LinkerSetting] = linkerSettings + if name == "React-graphics-Apple" { + conditionalLinkerSettings.append(.linkedFramework("UIKit", .when(platforms: [.iOS, .visionOS]))) + conditionalLinkerSettings.append(.linkedFramework("AppKit", .when(platforms: [.macOS]))) + } + return .target( name: name, dependencies: dependencies, @@ -800,7 +803,7 @@ extension Target { sources: sources, publicHeadersPath: publicHeadersPath, cxxSettings: cxxSettings, - linkerSettings: linkerSettings + linkerSettings: conditionalLinkerSettings ) } } diff --git a/packages/react-native/scripts/ios-prebuild/cli.js b/packages/react-native/scripts/ios-prebuild/cli.js index 01301c800af717..ec3cf8d9f60a35 100644 --- a/packages/react-native/scripts/ios-prebuild/cli.js +++ b/packages/react-native/scripts/ios-prebuild/cli.js @@ -17,6 +17,7 @@ import type {BuildFlavor, Destination, Platform} from './types'; const platforms /*: $ReadOnlyArray */ = [ 'ios', 'ios-simulator', + 'macos', 'mac-catalyst', ]; @@ -25,6 +26,7 @@ const platforms /*: $ReadOnlyArray */ = [ const platformToDestination /*: $ReadOnly<{|[Platform]: Destination|}> */ = { ios: 'iOS', 'ios-simulator': 'iOS Simulator', + 'macos': 'macOS', 'mac-catalyst': 'macOS,variant=Mac Catalyst', }; From c85bffe83088945701539dcb80f12e6e56ebc98b Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 16 Jan 2026 21:58:38 -0600 Subject: [PATCH 2/3] fix: remove disallowed apple-actions from workflow Remove apple-actions/import-codesign-certs@v3 as it's not in the allowed actions list. Code signing is optional for PR validation, so simplified the workflow to skip signing steps. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/prebuild-macos-core.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/prebuild-macos-core.yml b/.github/workflows/prebuild-macos-core.yml index feae19dc22e3da..3dc4346520a90f 100644 --- a/.github/workflows/prebuild-macos-core.yml +++ b/.github/workflows/prebuild-macos-core.yml @@ -127,9 +127,6 @@ jobs: fail-fast: false matrix: flavor: ['Debug', 'Release'] - env: - REACT_ORG_CODE_SIGNING_P12_CERT: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT }} - REACT_ORG_CODE_SIGNING_P12_CERT_PWD: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT_PWD }} steps: - name: Checkout uses: actions/checkout@v4 @@ -164,22 +161,11 @@ jobs: pattern: prebuild-macos-core-headers-${{ matrix.flavor }}-* path: packages/react-native/.build/headers merge-multiple: true - - name: Setup Keychain - if: ${{ steps.restore-macos-xcframework.outputs.cache-hit != 'true' && env.REACT_ORG_CODE_SIGNING_P12_CERT != '' }} - uses: apple-actions/import-codesign-certs@v3 # https://github.com/marketplace/actions/import-code-signing-certificates - with: - p12-file-base64: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT }} - p12-password: ${{ secrets.REACT_ORG_CODE_SIGNING_P12_CERT_PWD }} - name: Create XCFramework - if: ${{ steps.restore-macos-xcframework.outputs.cache-hit != 'true' && env.REACT_ORG_CODE_SIGNING_P12_CERT == '' }} + if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' run: | cd packages/react-native node scripts/ios-prebuild -c -f "${{ matrix.flavor }}" - - name: Create and Sign XCFramework - if: ${{ steps.restore-macos-xcframework.outputs.cache-hit != 'true' && env.REACT_ORG_CODE_SIGNING_P12_CERT != '' }} - run: | - cd packages/react-native - node scripts/ios-prebuild -c -f "${{ matrix.flavor }}" -i "React Org" - name: Compress and Rename XCFramework if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' run: | From 6d42883ae9357ffc86990946beacb44d0d0cb067 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 16 Jan 2026 22:12:57 -0600 Subject: [PATCH 3/3] fix: use microsoft-setup-toolchain instead of disallowed actions Replace setup-xcode (which uses maxim-lobanov/setup-xcode) with microsoft-setup-toolchain which is allowed and sets up Node, Xcode, and other required tools for the macOS platform. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/prebuild-macos-core.yml | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/prebuild-macos-core.yml b/.github/workflows/prebuild-macos-core.yml index 3dc4346520a90f..774315388d005d 100644 --- a/.github/workflows/prebuild-macos-core.yml +++ b/.github/workflows/prebuild-macos-core.yml @@ -22,17 +22,17 @@ jobs: with: key: v1-macos-core-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift') }}-${{ hashFiles('packages/react-native/scripts/ios-prebuild/setup.js') }} path: packages/react-native/ - - name: Setup node.js + - name: Setup toolchain if: steps.restore-macos-slice.outputs.cache-hit != 'true' - uses: ./.github/actions/setup-node - - name: Setup xcode - if: steps.restore-macos-slice.outputs.cache-hit != 'true' - uses: ./.github/actions/setup-xcode + uses: ./.github/actions/microsoft-setup-toolchain with: - xcode-version: '16.2.0' + platform: macos + node-version: '22' + xcode-developer-dir: '/Applications/Xcode_16.2.0.app' - name: Yarn Install if: steps.restore-macos-slice.outputs.cache-hit != 'true' - uses: ./.github/actions/yarn-install + run: yarn install + shell: bash - name: Download Hermes if: steps.restore-macos-slice.outputs.cache-hit != 'true' uses: actions/download-artifact@v4 @@ -136,17 +136,17 @@ jobs: with: path: packages/react-native/.build/output/xcframeworks key: v1-macos-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift') }}-${{ hashFiles('packages/react-native/scripts/ios-prebuild/setup.js') }} - - name: Setup node.js + - name: Setup toolchain if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' - uses: ./.github/actions/setup-node - - name: Setup xcode - if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' - uses: ./.github/actions/setup-xcode + uses: ./.github/actions/microsoft-setup-toolchain with: - xcode-version: '16.2.0' + platform: macos + node-version: '22' + xcode-developer-dir: '/Applications/Xcode_16.2.0.app' - name: Yarn Install if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' - uses: ./.github/actions/yarn-install + run: yarn install + shell: bash - name: Download slice artifacts if: steps.restore-macos-xcframework.outputs.cache-hit != 'true' uses: actions/download-artifact@v4