Skip to content

feat: build React Native macOS with Swift Package Manager#2815

Open
Saadnajmi wants to merge 4 commits intomicrosoft:mainfrom
Saadnajmi:feature/spm-macos-support
Open

feat: build React Native macOS with Swift Package Manager#2815
Saadnajmi wants to merge 4 commits intomicrosoft:mainfrom
Saadnajmi:feature/spm-macos-support

Conversation

@Saadnajmi
Copy link
Copy Markdown
Collaborator

@Saadnajmi Saadnajmi commented Jan 17, 2026

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:

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

@Saadnajmi Saadnajmi requested a review from a team as a code owner January 17, 2026 03:48
@Saadnajmi Saadnajmi changed the title feat: Add macOS support to Swift Package Manager build system (claude code generated) feat: Add macOS support to Swift Package Manager build system Jan 17, 2026
@Saadnajmi Saadnajmi marked this pull request as draft January 17, 2026 03:56
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch from a22ab6e to 30d348a Compare March 23, 2026 03:19
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 23, 2026

⚠️ No Changeset found

Latest commit: 21f812c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Saadnajmi Saadnajmi changed the title (claude code generated) feat: Add macOS support to Swift Package Manager build system feat: Add macOS support to Swift Package Manager build system Mar 23, 2026
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch 5 times, most recently from fa8de90 to 21908c3 Compare March 23, 2026 15:53
Saadnajmi added a commit that referenced this pull request Mar 23, 2026
## Summary
Fixes compilation errors that block macOS SPM builds, helping unblock
#2815.

- **RCTLinkingManager**: combined iOS and macOS implementations into a
single file
using `#if TARGET_OS_OSX` guards. Added missing
`NativeLinkingManagerSpec`
conformance (`openSettings`, `sendIntent`, `getTurboModule`), removed
unused
import, deleted the `macos/` overlay directory, and cleaned up the
podspec
- **RCTCursor.m**: replaced `Foundation.h` + conditional `AppKit.h` with
`RCTUIKit`
  umbrella header
- **RCTViewComponentView.mm**: removed duplicate `cursor` property check
introduced
  during merge

## Test plan
- [x] macOS SPM build passes (verified on `feature/spm-macos-support` —
zero errors from these files)
- [ ] Verify iOS build is not regressed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch 5 times, most recently from d1b4bcd to 01e53ee Compare March 26, 2026 18:49
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch from 01e53ee to c1cd032 Compare April 7, 2026 17:49
@Saadnajmi Saadnajmi changed the title feat: Add macOS support to Swift Package Manager build system feat: include macOS slice in Hermes xcframework Apr 7, 2026
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch from c1cd032 to 878f3eb Compare April 7, 2026 17:59
@Saadnajmi Saadnajmi changed the title feat: include macOS slice in Hermes xcframework feat: Add macOS support to Swift Package Manager build system Apr 7, 2026
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch 4 times, most recently from 5fb99f5 to 16e4eb8 Compare April 7, 2026 19:00
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch 7 times, most recently from 242d8d5 to a0e768b Compare April 8, 2026 05:43
@Saadnajmi Saadnajmi changed the title feat: Add macOS support to Swift Package Manager build system feat: build React Native macOS with Swift Package Manager Apr 8, 2026
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch 2 times, most recently from 39c472a to 49cbeba Compare April 8, 2026 18:16
@Saadnajmi Saadnajmi marked this pull request as ready for review April 8, 2026 18:17
Comment on lines +48 to +67
// [macOS] Map macOS version to upstream RN version for artifact lookup.
// For stable branches, peerDependencies maps to the upstream version.
// For the main branch (1000.0.0), fall back to the latest stable RN release.
if (!process.env.RN_DEP_VERSION) {
const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json');
const mappedVersion = findMatchingHermesVersion(packageJsonPath);
if (mappedVersion != null) {
dependencyLog(
`Using mapped upstream version for ReactNativeDependencies lookup: ${mappedVersion}`,
);
resolvedVersion = mappedVersion;
} else if (resolvedVersion === '1000.0.0') {
const latestStable = await getLatestStableVersionFromNPM();
dependencyLog(
`Main branch detected. Using latest stable RN version for ReactNativeDependencies: ${latestStable}`,
);
resolvedVersion = latestStable;
}
}
// macOS]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we fall back to the version from the merge base?

Comment on lines +15 to +68
@@ -60,7 +60,12 @@ function get_mac_deployment_target {
function build_host_hermesc {
echo "Building hermesc"
pushd "$HERMES_PATH" > /dev/null || exit 1
cmake -S . -B build_host_hermesc -DJSI_DIR="$JSI_PATH"
# Set a deployment target to prevent -Werror=unguarded-availability-new
# failures in LLVM's check_symbol_exists tests (macOS SDK headers contain
# availability annotations that error without a deployment target).
cmake -S . -B build_host_hermesc \
-DJSI_DIR="$JSI_PATH" \
-DCMAKE_OSX_DEPLOYMENT_TARGET:STRING="${MAC_DEPLOYMENT_TARGET:-10.15}"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these changes necessary? Can we remove them now?

name: .reactGraphicsApple,
path: "ReactCommon/react/renderer/graphics/platform/ios",
linkedFrameworks: ["UIKit", "CoreGraphics"],
linkedFrameworks: ["CoreGraphics"],
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do an #if os() check here, and then if we do, do we still need conditional linkers settings below?

Comment on lines -423 to -432
"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",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar comment here

let package = Package(
name: react,
platforms: [.iOS(.v15), .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)],
platforms: [.iOS(.v15), .macOS(.v14), .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)],
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

macos tag?

Comment on lines +812 to +818
// [macOS] Platform-specific framework linking for targets that need UIKit (iOS/visionOS) vs AppKit (macOS)
var conditionalLinkerSettings: [LinkerSetting] = linkerSettings
if name == "React-graphics-Apple" || name == "React-RCTUIKit" {
conditionalLinkerSettings.append(.linkedFramework("UIKit", .when(platforms: [.iOS, .visionOS])))
conditionalLinkerSettings.append(.linkedFramework("AppKit", .when(platforms: [.macOS])))
}
// macOS]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove if we handle higher up?

@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch from 49cbeba to 79286e3 Compare April 9, 2026 00:22
Comment on lines +72 to +75
git clone --depth 1 https://github.com/facebook/hermes.git /tmp/hermes
cd /tmp/hermes
git fetch --depth 1 origin ${{ needs.resolve-hermes.outputs.hermes-commit }}
git checkout ${{ needs.resolve-hermes.outputs.hermes-commit }}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing a shallow clone and then fetching another commit is really expensive. You should use actions/checkout instead as it handles checking out a specific commit very efficiently.

@@ -0,0 +1,256 @@
name: Build SPM
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid using acronyms when we have space:

Suggested change
name: Build SPM
name: Build SwiftPM

const {computeNightlyTarballURL, createLogger} = require('./utils');
const {execSync} = require('child_process');
const fs = require('fs');
const os = require('os');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const os = require('os');
const os = require('os'); // [macos]

Comment on lines +561 to +571
execSync(`git clone --depth 1 ${HERMES_GITHUB_URL} "${hermesDir}"`, {
stdio: 'inherit',
timeout: 300000,
});
execSync(`git -C "${hermesDir}" fetch --depth 1 origin ${commit}`, {
stdio: 'inherit',
timeout: 120000,
});
execSync(`git -C "${hermesDir}" checkout ${commit}`, {
stdio: 'inherit',
});
Copy link
Copy Markdown
Member

@tido64 tido64 Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I said above, you should not do a shallow clone followed by another fetch. This is the most optimal way to checkout a specific commit:

git init "${hermesDir}"
cd "${hermesDir}"
git remote add origin ${HERMES_GITHUB_URL}
git fetch --no-tags --depth 1 origin +${commit}:refs/remotes/origin/main
git checkout main

const tarballPath = path.join(artifactsPath, tarballName);
hermesLog('Creating Hermes tarball from build output...');
execSync(`tar -czf "${tarballPath}" -C "${hermesDir}" destroot`, {
stdio: 'inherit',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider reusing this option object elsewhere.

Saadnajmi and others added 3 commits April 9, 2026 12:06
- Add macOS and visionOS platforms to ios-prebuild CLI and type definitions
- Build Hermes from source at the merge base with facebook/react-native
  when no prebuilt artifacts are available (main branch / 1000.0.0)
- Fix host hermesc cmake build by setting CMAKE_OSX_DEPLOYMENT_TARGET to
  prevent -Werror=unguarded-availability-new failures in LLVM config checks
- Map ReactNativeDependencies version to upstream RN via peerDependencies,
  with fallback to merge base version or latest stable release for main branch
- Conditionally include macOS-specific platform view sources in Package.swift
  using #if os(macOS) to avoid compiling macOS C++ on iOS/visionOS
- Platform-conditional UIKit/AppKit framework linking via platformLinkerSettings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make HERMES_PATH overridable via environment variable so CI can point
  to a separately cloned Hermes repo
- Add CMAKE_OSX_DEPLOYMENT_TARGET to build_host_hermesc to fix
  -Werror=unguarded-availability-new failures in LLVM cmake checks
- Add macOS deployment target support via get_mac_deployment_target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add microsoft-build-spm.yml with 4-stage pipeline:
1. resolve-hermes: find Hermes commit, check cache
2. build-hermesc + build-hermes-slice (5x parallel): build from source
3. assemble-hermes: create universal xcframework, save cache
4. build-spm (ios/macos/visionos): build SPM packages

The assembled Hermes xcframework is cached by commit hash, so ~95% of
CI runs skip the entire Hermes build and go straight to SPM builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch 2 times, most recently from 89b00ec to f6714d5 Compare April 9, 2026 20:38
- Rename workflow to "Build SwiftPM" (tido64)
- Replace shallow clone + fetch with actions/checkout for Hermes (tido64)
- Use efficient git init + fetch pattern in buildFromHermesCommit (tido64)
- Extract reusable build env object in hermes.js (tido64)
- Move macOS version resolution to fork-only macosVersionResolver.js (tido64)
- Replace platformLinkerSettings with #if os(macOS) in Package.swift (Saad)
- Revert CMAKE_OSX_DEPLOYMENT_TARGET from build-apple-framework.sh (Saad)
- Add // [macOS] tag to os require in hermes.js (tido64)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Saadnajmi Saadnajmi force-pushed the feature/spm-macos-support branch from f6714d5 to 21f812c Compare April 9, 2026 23:38
BUILD_TYPE=${BUILD_TYPE:-Debug}

HERMES_PATH="$CURR_SCRIPT_DIR/.."
HERMES_PATH=${HERMES_PATH:-"$CURR_SCRIPT_DIR/.."}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment why we need this change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants