Skip to content

Commit 21908c3

Browse files
Saadnajmiclaude
andcommitted
ci: add SPM build CI jobs and visionOS platform support
Add a new reusable workflow (microsoft-build-spm.yml) that tests SPM builds for ios, macos, and visionos on every PR. Wire it into the PR gate job in microsoft-pr.yml. Also add visionos/visionos-simulator as supported platforms in the ios-prebuild CLI so CI and local builds can target visionOS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 381297a commit 21908c3

File tree

6 files changed

+206
-27
lines changed

6 files changed

+206
-27
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Build SPM
2+
3+
on:
4+
workflow_call:
5+
6+
jobs:
7+
build-hermes:
8+
name: "Build Hermes"
9+
runs-on: macos-26
10+
timeout-minutes: 90
11+
steps:
12+
- uses: actions/checkout@v4
13+
with:
14+
filter: blob:none
15+
fetch-depth: 0
16+
17+
- name: Setup toolchain
18+
uses: ./.github/actions/microsoft-setup-toolchain
19+
with:
20+
node-version: '22'
21+
platform: macos
22+
23+
- name: Install npm dependencies
24+
run: yarn install
25+
26+
- name: Build Hermes from source
27+
working-directory: packages/react-native
28+
run: node scripts/ios-prebuild.js -s -f Debug
29+
30+
- name: Upload Hermes artifacts
31+
uses: actions/upload-artifact@v4
32+
with:
33+
name: hermes-artifacts
34+
path: packages/react-native/.build/artifacts/hermes
35+
retention-days: 1
36+
37+
build-spm:
38+
name: "${{ matrix.platform }}"
39+
needs: build-hermes
40+
runs-on: macos-26
41+
timeout-minutes: 60
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
platform: [ios, macos, visionos]
46+
steps:
47+
- uses: actions/checkout@v4
48+
with:
49+
filter: blob:none
50+
fetch-depth: 0
51+
52+
- name: Setup toolchain
53+
uses: ./.github/actions/microsoft-setup-toolchain
54+
with:
55+
node-version: '22'
56+
platform: ${{ matrix.platform }}
57+
58+
- name: Install npm dependencies
59+
run: yarn install
60+
61+
- name: Download Hermes artifacts
62+
uses: actions/download-artifact@v4
63+
with:
64+
name: hermes-artifacts
65+
path: packages/react-native/.build/artifacts/hermes
66+
67+
- name: Setup SPM workspace (using prebuilt Hermes)
68+
working-directory: packages/react-native
69+
run: node scripts/ios-prebuild.js -s -f Debug
70+
71+
- name: Build SPM (${{ matrix.platform }})
72+
working-directory: packages/react-native
73+
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
@@ -132,6 +132,11 @@ jobs:
132132
permissions: {}
133133
uses: ./.github/workflows/microsoft-build-rntester.yml
134134

135+
build-spm:
136+
name: "Build SPM"
137+
permissions: {}
138+
uses: ./.github/workflows/microsoft-build-spm.yml
139+
135140
test-react-native-macos-init:
136141
name: "Test react-native-macos init"
137142
permissions: {}
@@ -156,6 +161,7 @@ jobs:
156161
- yarn-constraints
157162
- javascript-tests
158163
- build-rntester
164+
- build-spm
159165
- test-react-native-macos-init
160166
# - react-native-test-app-integration
161167
steps:

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ const platforms /*: $ReadOnlyArray<Platform> */ = [
1919
'ios-simulator',
2020
'macos', // [macOS]
2121
'mac-catalyst',
22+
'visionos', // [macOS]
23+
'visionos-simulator', // [macOS]
2224
];
2325

2426
// CI can't use commas in cache keys, so 'macOS,variant=Mac Catalyst' was creating troubles
2527
// This map that converts from platforms to valid Xcodebuild destinations.
2628
const platformToDestination /*: $ReadOnly<{|[Platform]: Destination|}> */ = {
2729
ios: 'iOS',
2830
'ios-simulator': 'iOS Simulator',
29-
'macos': 'macOS', // [macOS]
31+
macos: 'macOS', // [macOS]
3032
'mac-catalyst': 'macOS,variant=Mac Catalyst',
33+
visionos: 'xrOS', // [macOS]
34+
'visionos-simulator': 'xrOS Simulator', // [macOS]
3135
};
3236

3337
const cli = yargs

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

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,17 @@ async function prepareHermesArtifactsAsync(
180180
// enables the fallback to hermesCommitAtMergeBase() when no prebuilt artifacts exist.
181181
let allowBuildFromSource = false;
182182
if (!process.env.HERMES_VERSION) {
183-
const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json');
183+
const packageJsonPath = path.resolve(
184+
__dirname,
185+
'..',
186+
'..',
187+
'package.json',
188+
);
184189
const mappedVersion = findMatchingHermesVersion(packageJsonPath);
185190
if (mappedVersion != null) {
186-
hermesLog(`Using mapped upstream version for Hermes lookup: ${mappedVersion}`);
191+
hermesLog(
192+
`Using mapped upstream version for Hermes lookup: ${mappedVersion}`,
193+
);
187194
resolvedVersion = mappedVersion;
188195
} else {
189196
allowBuildFromSource = true;
@@ -209,7 +216,11 @@ async function prepareHermesArtifactsAsync(
209216
return artifactsPath;
210217
}
211218

212-
const sourceType = await hermesSourceType(resolvedVersion, buildType, allowBuildFromSource);
219+
const sourceType = await hermesSourceType(
220+
resolvedVersion,
221+
buildType,
222+
allowBuildFromSource,
223+
);
213224
localPath = await resolveSourceFromSourceType(
214225
sourceType,
215226
resolvedVersion,
@@ -552,12 +563,16 @@ function ensureMacOSSliceInXCFramework(artifactsPath /*: string */) {
552563
const universalDir = path.join(frameworksDir, 'universal');
553564

554565
if (!fs.existsSync(macosDir)) {
555-
hermesLog('No macOS framework found in tarball, skipping xcframework rebuild');
566+
hermesLog(
567+
'No macOS framework found in tarball, skipping xcframework rebuild',
568+
);
556569
return;
557570
}
558571

559572
// Find the framework name (hermes.framework or hermesvm.framework)
560-
const macosFrameworks = fs.readdirSync(macosDir).filter(f => f.endsWith('.framework'));
573+
const macosFrameworks = fs
574+
.readdirSync(macosDir)
575+
.filter(f => f.endsWith('.framework'));
561576
if (macosFrameworks.length === 0) {
562577
hermesLog('No .framework found in macosx directory, skipping');
563578
return;
@@ -570,7 +585,9 @@ function ensureMacOSSliceInXCFramework(artifactsPath /*: string */) {
570585
: [];
571586

572587
if (xcframeworks.length === 0) {
573-
hermesLog('No existing xcframework found, creating one from macOS framework only');
588+
hermesLog(
589+
'No existing xcframework found, creating one from macOS framework only',
590+
);
574591
fs.mkdirSync(universalDir, {recursive: true});
575592
const xcframeworkName = frameworkName.replace('.framework', '.xcframework');
576593
execSync(
@@ -587,7 +604,9 @@ function ensureMacOSSliceInXCFramework(artifactsPath /*: string */) {
587604
const xcframeworkPath = path.join(universalDir, xcframeworkName);
588605

589606
// Check if macOS slice already exists in the xcframework
590-
const existingSlices = fs.readdirSync(xcframeworkPath).filter(d => d.startsWith('macos-'));
607+
const existingSlices = fs
608+
.readdirSync(xcframeworkPath)
609+
.filter(d => d.startsWith('macos-'));
591610
if (existingSlices.length > 0) {
592611
hermesLog('macOS slice already present in xcframework, skipping rebuild');
593612
return;
@@ -602,14 +621,18 @@ function ensureMacOSSliceInXCFramework(artifactsPath /*: string */) {
602621
});
603622

604623
// Build the -framework arguments for xcodebuild -create-xcframework
605-
const frameworkArgs = sliceDirs.map(sliceDir => {
606-
const slicePath = path.join(xcframeworkPath, sliceDir);
607-
const frameworks = fs.readdirSync(slicePath).filter(f => f.endsWith('.framework'));
608-
if (frameworks.length > 0) {
609-
return `-framework "${path.join(slicePath, frameworks[0])}"`;
610-
}
611-
return null;
612-
}).filter(Boolean);
624+
const frameworkArgs = sliceDirs
625+
.map(sliceDir => {
626+
const slicePath = path.join(xcframeworkPath, sliceDir);
627+
const frameworks = fs
628+
.readdirSync(slicePath)
629+
.filter(f => f.endsWith('.framework'));
630+
if (frameworks.length > 0) {
631+
return `-framework "${path.join(slicePath, frameworks[0])}"`;
632+
}
633+
return null;
634+
})
635+
.filter(Boolean);
613636

614637
// Add the macOS framework
615638
frameworkArgs.push(`-framework "${path.join(macosDir, frameworkName)}"`);
@@ -651,15 +674,84 @@ async function buildFromHermesCommit(
651674
artifactsPath /*: string */,
652675
) /*: Promise<string> */ {
653676
const {commit, timestamp} = hermesCommitAtMergeBase();
654-
abort(
655-
`[Hermes] No prebuilt Hermes artifacts available for version "${version}".\n` +
656-
`Hermes commit at merge base with facebook/react-native: ${commit} (timestamp: ${timestamp})\n` +
657-
`To resolve, either:\n` +
658-
` 1. Set HERMES_ENGINE_TARBALL_PATH to a local Hermes tarball path\n` +
659-
` 2. Set HERMES_VERSION to an upstream RN version with published artifacts (e.g., HERMES_VERSION=nightly)\n` +
660-
` 3. Build Hermes from commit ${commit} and provide the tarball path via HERMES_ENGINE_TARBALL_PATH`,
677+
hermesLog(
678+
`Building Hermes from source at commit ${commit} (merge base timestamp: ${timestamp})`,
661679
);
662-
return ''; // unreachable
680+
681+
const HERMES_GITHUB_URL = 'https://github.com/facebook/hermes.git';
682+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-build-'));
683+
const hermesDir = path.join(tmpDir, 'hermes');
684+
685+
try {
686+
// Clone Hermes at the identified commit
687+
hermesLog(`Cloning Hermes at commit ${commit}...`);
688+
execSync(`git clone --depth 1 ${HERMES_GITHUB_URL} "${hermesDir}"`, {
689+
stdio: 'inherit',
690+
timeout: 300000,
691+
});
692+
execSync(`git -C "${hermesDir}" fetch --depth 1 origin ${commit}`, {
693+
stdio: 'inherit',
694+
timeout: 120000,
695+
});
696+
execSync(`git -C "${hermesDir}" checkout ${commit}`, {
697+
stdio: 'inherit',
698+
});
699+
700+
// The build-ios-framework.sh script runs from the hermes directory.
701+
// It sources build-apple-framework.sh which sets HERMES_PATH relative to itself,
702+
// but we override it to point to the cloned Hermes repo.
703+
const reactNativeRoot = path.resolve(__dirname, '..', '..');
704+
const buildScript = path.join(
705+
reactNativeRoot,
706+
'sdks',
707+
'hermes-engine',
708+
'utils',
709+
'build-ios-framework.sh',
710+
);
711+
712+
hermesLog(`Building Hermes frameworks (${buildType})...`);
713+
execSync(`bash "${buildScript}"`, {
714+
cwd: hermesDir,
715+
stdio: 'inherit',
716+
timeout: 3600000, // 60 minutes
717+
env: {
718+
...process.env,
719+
BUILD_TYPE: buildType,
720+
HERMES_PATH: hermesDir,
721+
JSI_PATH: path.join(hermesDir, 'API', 'jsi'),
722+
REACT_NATIVE_PATH: reactNativeRoot,
723+
// Deployment targets matching react-native-macos minimums
724+
IOS_DEPLOYMENT_TARGET: '15.1',
725+
MAC_DEPLOYMENT_TARGET: '14.0',
726+
XROS_DEPLOYMENT_TARGET: '1.0',
727+
RELEASE_VERSION: version,
728+
},
729+
});
730+
731+
// Create tarball from the destroot (same structure as Maven artifacts)
732+
const tarballName = `hermes-ios-${buildType.toLowerCase()}.tar.gz`;
733+
const tarballPath = path.join(artifactsPath, tarballName);
734+
hermesLog('Creating Hermes tarball from build output...');
735+
execSync(`tar -czf "${tarballPath}" -C "${hermesDir}" destroot`, {
736+
stdio: 'inherit',
737+
});
738+
739+
hermesLog(`Hermes built from source and packaged at ${tarballPath}`);
740+
return tarballPath;
741+
} catch (e) {
742+
abort(
743+
`[Hermes] Failed to build Hermes from source at commit ${commit}.\n` +
744+
`Error: ${e.message}\n` +
745+
`To resolve, either:\n` +
746+
` 1. Set HERMES_ENGINE_TARBALL_PATH to a local Hermes tarball path\n` +
747+
` 2. Set HERMES_VERSION to an upstream RN version with published artifacts\n` +
748+
` 3. Build Hermes manually from commit ${commit} and provide the tarball path via HERMES_ENGINE_TARBALL_PATH`,
749+
);
750+
return ''; // unreachable
751+
} finally {
752+
// Clean up
753+
fs.rmSync(tmpDir, {recursive: true, force: true});
754+
}
663755
}
664756
// macOS]
665757

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ export type Platform =
1313
'ios' |
1414
'ios-simulator' |
1515
'macos' |
16-
'mac-catalyst';
16+
'mac-catalyst' |
17+
'visionos' |
18+
'visionos-simulator';
1719
1820
export type Destination =
1921
'iOS' |
2022
'iOS Simulator' |
2123
'macOS' |
22-
'macOS,variant=Mac Catalyst';
24+
'macOS,variant=Mac Catalyst' |
25+
'xrOS' |
26+
'xrOS Simulator';
2327
2428
export type BuildFlavor = 'Debug' | 'Release';
2529
*/

packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ CURR_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
1212
IMPORT_HERMESC_PATH=${HERMES_OVERRIDE_HERMESC_PATH:-$PWD/build_host_hermesc/ImportHermesc.cmake}
1313
BUILD_TYPE=${BUILD_TYPE:-Debug}
1414

15-
HERMES_PATH="$CURR_SCRIPT_DIR/.."
15+
HERMES_PATH=${HERMES_PATH:-"$CURR_SCRIPT_DIR/.."}
1616
REACT_NATIVE_PATH=${REACT_NATIVE_PATH:-$CURR_SCRIPT_DIR/../../..}
1717

1818
NUM_CORES=$(sysctl -n hw.ncpu)

0 commit comments

Comments
 (0)