Skip to content

Commit 9b2adf9

Browse files
Saadnajmiclaude
andcommitted
refactor: rename macosVersionResolver → microsoft-resolveHermes, add CLI and recompose
Rename macosVersionResolver.js to microsoft-resolveHermes.js to better reflect its expanded role and make it obvious this is a fork-specific file (consistent with microsoft-*.yml workflow naming). Changes: - Add CLI entry point with download-hermes, recompose-xcframework, and resolve-commit commands that write directly to $GITHUB_OUTPUT - Move recompose xcframework logic from inline shell into JS function recomposeHermesXcframework() which checks if macOS is already present before recomposing (future-proofs for when upstream includes macOS) - Fix recompose bug: create new xcframework at temp path before deleting old one, since frameworks reference paths inside it - Add flow comments documenting the resolve-hermes workflow strategy - Simplify workflow steps to clean one-liner script invocations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d4c48b commit 9b2adf9

4 files changed

Lines changed: 190 additions & 70 deletions

File tree

.github/workflows/microsoft-resolve-hermes.yml

Lines changed: 33 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1+
# Resolve Hermes — reusable workflow called by microsoft-prebuild-macos-core.yml
2+
#
3+
# Strategy (fast path first):
4+
# 1. Download upstream Hermes tarball from Maven/Sonatype
5+
# 2. If found → recompose xcframework (add macOS slice) → upload artifact → done
6+
# 3. If not found → resolve Hermes commit at merge base → check cache → upload if cached
7+
#
8+
# Build-from-source fallback (only when recomposed != true AND cache-hit != true):
9+
# build-hermesc → build 5 platform slices in parallel → assemble universal xcframework
10+
#
111
name: Resolve Hermes
212

313
on:
414
workflow_call:
515

616
jobs:
17+
# ---------------------------------------------------------------------------
18+
# Fast path: download upstream tarball and recompose, or resolve commit + cache
19+
# ---------------------------------------------------------------------------
720
resolve-hermes:
821
name: "Resolve Hermes"
922
runs-on: macos-15
@@ -27,76 +40,27 @@ jobs:
2740
- name: Install npm dependencies
2841
run: yarn install
2942

43+
# Step 1: Try to download a prebuilt Hermes tarball from upstream Maven/Sonatype.
44+
# Writes tarball= and version= to $GITHUB_OUTPUT if successful.
3045
- name: Download upstream Hermes tarball
3146
id: download
3247
working-directory: packages/react-native
33-
run: |
34-
node -e "
35-
const {downloadUpstreamHermesTarball} = require('./scripts/ios-prebuild/macosVersionResolver');
36-
downloadUpstreamHermesTarball('Debug').then(r => {
37-
require('fs').writeFileSync('/tmp/hermes-download-result.json', JSON.stringify(r));
38-
});
39-
"
40-
RESULT=$(cat /tmp/hermes-download-result.json)
41-
if [ "$RESULT" != "null" ]; then
42-
TARBALL=$(node -e "console.log(JSON.parse(process.argv[1]).tarballPath)" "$RESULT")
43-
VERSION=$(node -e "console.log(JSON.parse(process.argv[1]).version)" "$RESULT")
44-
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
45-
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
46-
echo "Downloaded upstream Hermes tarball for version $VERSION"
47-
else
48-
echo "No upstream tarball available"
49-
fi
48+
run: node scripts/ios-prebuild/microsoft-resolveHermes.js download-hermes Debug
5049

50+
# Step 2: If tarball found, recompose the xcframework to include the macOS slice
51+
# (or skip if macOS is already present in the universal xcframework).
52+
# Writes recomposed=true/false to $GITHUB_OUTPUT.
5153
- name: Recompose xcframework with macOS slice
5254
id: recompose
5355
if: steps.download.outputs.tarball != ''
54-
run: |
55-
TARBALL="${{ steps.download.outputs.tarball }}"
56-
57-
# Extract tarball
58-
mkdir -p hermes-destroot
59-
tar -xzf "$TARBALL" -C hermes-destroot --strip-components=2
60-
61-
echo "=== Upstream tarball contents ==="
62-
ls -la hermes-destroot/Library/Frameworks/
63-
64-
# Collect existing frameworks from the universal xcframework
65-
XCFW="hermes-destroot/Library/Frameworks/universal/hermes.xcframework"
66-
FRAMEWORKS=()
67-
for fw in "$XCFW"/*/hermes.framework; do
68-
if [ -d "$fw" ]; then
69-
echo "Found slice: $fw"
70-
FRAMEWORKS+=(-framework "$fw")
71-
fi
72-
done
73-
74-
# Add standalone macOS framework
75-
MAC_FW="hermes-destroot/Library/Frameworks/macosx/hermes.framework"
76-
if [ -d "$MAC_FW" ]; then
77-
echo "Found standalone macOS slice: $MAC_FW"
78-
FRAMEWORKS+=(-framework "$MAC_FW")
79-
else
80-
echo "::error::Upstream tarball missing macosx/hermes.framework"
81-
echo "recomposed=false" >> "$GITHUB_OUTPUT"
82-
exit 0
83-
fi
84-
85-
# Remove old xcframework and create new one with macOS included
86-
rm -rf "$XCFW"
87-
echo "Creating new universal xcframework with ${#FRAMEWORKS[@]} frameworks..."
88-
xcodebuild -create-xcframework "${FRAMEWORKS[@]}" \
89-
-output "$XCFW" \
90-
-allow-internal-distribution
91-
92-
# Clean up standalone macOS dir (now included in universal)
93-
rm -rf hermes-destroot/Library/Frameworks/macosx
94-
95-
echo "=== Recomposed xcframework ==="
96-
ls -la "$XCFW"/
97-
98-
echo "recomposed=true" >> "$GITHUB_OUTPUT"
56+
working-directory: packages/react-native
57+
run: >-
58+
node scripts/ios-prebuild/microsoft-resolveHermes.js
59+
recompose-xcframework
60+
"${{ steps.download.outputs.tarball }}"
61+
"${{ github.workspace }}/hermes-destroot"
9962
63+
# Upload recomposed artifacts — the prebuild-macos-core workflow downloads these
10064
- name: Upload recomposed Hermes artifacts
10165
if: steps.recompose.outputs.recomposed == 'true'
10266
uses: actions/upload-artifact@v4
@@ -105,15 +69,13 @@ jobs:
10569
path: hermes-destroot
10670
retention-days: 30
10771

108-
# Fallback: resolve Hermes commit for build-from-source
72+
# Step 3 (fallback): No upstream tarball — resolve the Hermes commit hash
73+
# at the merge base with facebook/react-native and check the build cache.
10974
- name: Resolve Hermes commit at merge base
11075
if: steps.recompose.outputs.recomposed != 'true'
11176
id: resolve
11277
working-directory: packages/react-native
113-
run: |
114-
COMMIT=$(node -e "const {hermesCommitAtMergeBase} = require('./scripts/ios-prebuild/macosVersionResolver'); console.log(hermesCommitAtMergeBase().commit);" 2>&1 | grep -E '^[0-9a-f]{40}$')
115-
echo "hermes-commit=$COMMIT" >> "$GITHUB_OUTPUT"
116-
echo "Resolved Hermes commit: $COMMIT"
78+
run: node scripts/ios-prebuild/microsoft-resolveHermes.js resolve-commit
11779

11880
- name: Restore Hermes cache
11981
if: steps.recompose.outputs.recomposed != 'true'
@@ -131,6 +93,10 @@ jobs:
13193
path: hermes-destroot
13294
retention-days: 30
13395

96+
# ---------------------------------------------------------------------------
97+
# Build-from-source fallback — only runs when no recomposed or cached artifact
98+
# Pipeline: hermesc (host compiler) → 5 platform slices → assemble xcframework
99+
# ---------------------------------------------------------------------------
134100
build-hermesc:
135101
name: "Build hermesc"
136102
if: ${{ needs.resolve-hermes.outputs.recomposed != 'true' && needs.resolve-hermes.outputs.cache-hit != 'true' }}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
const {
1212
findMatchingHermesVersion,
1313
hermesCommitAtMergeBase,
14-
} = require('./macosVersionResolver'); // [macOS]
14+
} = require('./microsoft-resolveHermes'); // [macOS]
1515
const {computeNightlyTarballURL, createLogger} = require('./utils');
1616
const {execSync} = require('child_process');
1717
const fs = require('fs');

packages/react-native/scripts/ios-prebuild/macosVersionResolver.js renamed to packages/react-native/scripts/ios-prebuild/microsoft-resolveHermes.js

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* [macOS] Handles version resolution for macOS fork branches.
7+
* [macOS] Resolves Hermes artifacts for macOS fork branches.
8+
*
9+
* Handles downloading upstream Hermes tarballs, recomposing xcframeworks
10+
* to include the macOS slice, and resolving Hermes commits for
11+
* build-from-source fallback. Used as both a library and CLI:
12+
*
13+
* node microsoft-resolveHermes.js download-hermes [Debug|Release]
14+
* node microsoft-resolveHermes.js recompose-xcframework <tarball> <destroot>
15+
* node microsoft-resolveHermes.js resolve-commit
816
*
917
* @flow
1018
* @format
@@ -285,15 +293,161 @@ async function downloadUpstreamHermesTarball(
285293
return null;
286294
}
287295

296+
/**
297+
* Extracts an upstream Hermes tarball and recomposes the xcframework to include
298+
* the macOS slice, if needed.
299+
*
300+
* Upstream tarballs ship a universal xcframework (iOS, simulator, catalyst,
301+
* tvOS, visionOS) plus a standalone macosx/hermes.framework. This function
302+
* merges the standalone macOS framework into the universal xcframework using
303+
* `xcodebuild -create-xcframework`.
304+
*
305+
* NOTE: Once upstream Hermes includes macOS in the universal xcframework
306+
* natively, this function will detect the existing macOS slice and skip
307+
* the recompose. At that point, this step can be removed entirely.
308+
*
309+
* Returns true if the xcframework was recomposed (or already had macOS),
310+
* false if the tarball is missing the macOS framework entirely.
311+
*/
312+
function recomposeHermesXcframework(
313+
tarballPath /*: string */,
314+
destroot /*: string */,
315+
) /*: boolean */ {
316+
// Extract tarball
317+
fs.mkdirSync(destroot, {recursive: true});
318+
execSync(`tar -xzf "${tarballPath}" -C "${destroot}" --strip-components=2`, {
319+
stdio: 'inherit',
320+
});
321+
322+
const frameworksDir = path.join(destroot, 'Library', 'Frameworks');
323+
const xcfwPath = path.join(frameworksDir, 'universal', 'hermes.xcframework');
324+
325+
macosLog('Upstream tarball contents:');
326+
execSync(`ls -la "${frameworksDir}"`, {stdio: 'inherit'});
327+
328+
// Check if macOS is already in the universal xcframework — if so, no recompose needed
329+
const xcfwContents = fs.readdirSync(xcfwPath);
330+
const hasMacSlice = xcfwContents.some(
331+
entry => entry.startsWith('macos') && entry.includes('arm64'),
332+
);
333+
if (hasMacSlice) {
334+
macosLog('macOS slice already present in universal xcframework, skipping recompose');
335+
// Clean up standalone macOS dir if it exists
336+
const standaloneMacDir = path.join(frameworksDir, 'macosx');
337+
if (fs.existsSync(standaloneMacDir)) {
338+
fs.rmSync(standaloneMacDir, {recursive: true, force: true});
339+
}
340+
return true;
341+
}
342+
343+
// Check for standalone macOS framework
344+
const standaloneMacFw = path.join(frameworksDir, 'macosx', 'hermes.framework');
345+
if (!fs.existsSync(standaloneMacFw)) {
346+
macosLog('Upstream tarball missing macosx/hermes.framework', 'error');
347+
return false;
348+
}
349+
350+
// Collect existing frameworks from inside the universal xcframework
351+
const frameworks /*: string[] */ = [];
352+
for (const entry of xcfwContents) {
353+
const fwPath = path.join(xcfwPath, entry, 'hermes.framework');
354+
if (fs.existsSync(fwPath) && fs.statSync(fwPath).isDirectory()) {
355+
macosLog(`Found slice: ${fwPath}`);
356+
frameworks.push('-framework', fwPath);
357+
}
358+
}
359+
360+
// Add the standalone macOS framework
361+
macosLog(`Found standalone macOS slice: ${standaloneMacFw}`);
362+
frameworks.push('-framework', standaloneMacFw);
363+
364+
// Build new xcframework at a temp path (frameworks reference paths inside the old xcfw)
365+
const xcfwNew = path.join(frameworksDir, 'universal', 'hermes-new.xcframework');
366+
macosLog(
367+
`Creating new universal xcframework with ${frameworks.filter(f => f !== '-framework').length} slices...`,
368+
);
369+
execSync(
370+
`xcodebuild -create-xcframework ${frameworks.map(f => `"${f}"`).join(' ')} -output "${xcfwNew}" -allow-internal-distribution`,
371+
{stdio: 'inherit'},
372+
);
373+
374+
// Swap in the recomposed xcframework
375+
fs.rmSync(xcfwPath, {recursive: true, force: true});
376+
fs.renameSync(xcfwNew, xcfwPath);
377+
378+
// Clean up standalone macOS dir (now included in universal)
379+
fs.rmSync(path.join(frameworksDir, 'macosx'), {recursive: true, force: true});
380+
381+
macosLog('Recomposed xcframework:');
382+
execSync(`ls -la "${xcfwPath}/"`, {stdio: 'inherit'});
383+
384+
return true;
385+
}
386+
288387
function abort(message /*: string */) {
289388
macosLog(message, 'error');
290389
throw new Error(message);
291390
}
292391

392+
/**
393+
* Appends a key=value pair to the GitHub Actions output file ($GITHUB_OUTPUT).
394+
* No-op if $GITHUB_OUTPUT is not set (e.g. running locally).
395+
*/
396+
function setActionOutput(key /*: string */, value /*: string */) {
397+
const outputFile = process.env.GITHUB_OUTPUT;
398+
if (outputFile) {
399+
fs.appendFileSync(outputFile, `${key}=${value}\n`);
400+
}
401+
}
402+
403+
// CLI entry point — writes results to $GITHUB_OUTPUT for GitHub Actions.
404+
// Usage:
405+
// node microsoft-resolveHermes.js download-hermes [Debug|Release]
406+
// node microsoft-resolveHermes.js recompose-xcframework <tarball> <destroot>
407+
// node microsoft-resolveHermes.js resolve-commit
408+
if (require.main === module) {
409+
const [command, ...args] = process.argv.slice(2);
410+
411+
if (command === 'download-hermes') {
412+
const buildType = args[0] || 'Debug';
413+
downloadUpstreamHermesTarball(buildType).then(result => {
414+
if (result != null) {
415+
setActionOutput('tarball', result.tarballPath);
416+
setActionOutput('version', result.version);
417+
macosLog(
418+
`Downloaded upstream Hermes tarball for version ${result.version}`,
419+
);
420+
} else {
421+
macosLog('No upstream tarball available');
422+
}
423+
});
424+
} else if (command === 'recompose-xcframework') {
425+
const [tarball, destroot] = args;
426+
if (!tarball || !destroot) {
427+
console.error(
428+
'Usage: node microsoft-resolveHermes.js recompose-xcframework <tarball> <destroot>',
429+
);
430+
process.exit(1);
431+
}
432+
const recomposed = recomposeHermesXcframework(tarball, destroot);
433+
setActionOutput('recomposed', String(recomposed));
434+
} else if (command === 'resolve-commit') {
435+
const {commit} = hermesCommitAtMergeBase();
436+
setActionOutput('hermes-commit', commit);
437+
macosLog(`Resolved Hermes commit: ${commit}`);
438+
} else {
439+
console.error(
440+
`Unknown command: ${command ?? '(none)'}. Available: download-hermes, recompose-xcframework, resolve-commit`,
441+
);
442+
process.exit(1);
443+
}
444+
}
445+
293446
module.exports = {
294447
findMatchingHermesVersion,
295448
hermesCommitAtMergeBase,
296449
findVersionAtMergeBase,
297450
getLatestStableVersionFromNPM,
298451
downloadUpstreamHermesTarball,
452+
recomposeHermesXcframework,
299453
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const {
1414
findMatchingHermesVersion,
1515
findVersionAtMergeBase,
1616
getLatestStableVersionFromNPM,
17-
} = require('./macosVersionResolver'); // [macOS]
17+
} = require('./microsoft-resolveHermes'); // [macOS]
1818
const {computeNightlyTarballURL, createLogger} = require('./utils');
1919
const {execSync} = require('child_process');
2020
const fs = require('fs');

0 commit comments

Comments
 (0)