Skip to content

Commit 3b5be70

Browse files
Saadnajmiclaude
andcommitted
feat(ci): recompose upstream Hermes xcframework with macOS slice
Instead of building Hermes from source (~90 min), download the upstream tarball from Maven, extract frameworks from the universal xcframework, add the standalone macOS framework, and recompose a new xcframework that includes all platforms. Build-from-source is kept as a fallback when no upstream tarball is available (e.g. Maven is down). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 13081a9 commit 3b5be70

2 files changed

Lines changed: 134 additions & 5 deletions

File tree

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

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ jobs:
77
resolve-hermes:
88
name: "Resolve Hermes"
99
runs-on: macos-15
10-
timeout-minutes: 10
10+
timeout-minutes: 15
1111
outputs:
1212
hermes-commit: ${{ steps.resolve.outputs.hermes-commit }}
1313
cache-hit: ${{ steps.cache.outputs.cache-hit }}
14+
recomposed: ${{ steps.recompose.outputs.recomposed }}
1415
steps:
1516
- uses: actions/checkout@v4
1617
with:
@@ -26,7 +27,87 @@ jobs:
2627
- name: Install npm dependencies
2728
run: yarn install
2829

30+
- name: Download upstream Hermes tarball
31+
id: download
32+
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
50+
51+
- name: Recompose xcframework with macOS slice
52+
id: recompose
53+
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"
99+
100+
- name: Upload recomposed Hermes artifacts
101+
if: steps.recompose.outputs.recomposed == 'true'
102+
uses: actions/upload-artifact@v4
103+
with:
104+
name: hermes-artifacts
105+
path: hermes-destroot
106+
retention-days: 30
107+
108+
# Fallback: resolve Hermes commit for build-from-source
29109
- name: Resolve Hermes commit at merge base
110+
if: steps.recompose.outputs.recomposed != 'true'
30111
id: resolve
31112
working-directory: packages/react-native
32113
run: |
@@ -35,14 +116,15 @@ jobs:
35116
echo "Resolved Hermes commit: $COMMIT"
36117
37118
- name: Restore Hermes cache
119+
if: steps.recompose.outputs.recomposed != 'true'
38120
id: cache
39121
uses: actions/cache/restore@v4
40122
with:
41123
key: hermes-v1-${{ steps.resolve.outputs.hermes-commit }}-Debug
42124
path: hermes-destroot
43125

44126
- name: Upload cached Hermes artifacts
45-
if: steps.cache.outputs.cache-hit == 'true'
127+
if: steps.recompose.outputs.recomposed != 'true' && steps.cache.outputs.cache-hit == 'true'
46128
uses: actions/upload-artifact@v4
47129
with:
48130
name: hermes-artifacts
@@ -51,7 +133,7 @@ jobs:
51133

52134
build-hermesc:
53135
name: "Build hermesc"
54-
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
136+
if: ${{ needs.resolve-hermes.outputs.recomposed != 'true' && needs.resolve-hermes.outputs.cache-hit != 'true' }}
55137
needs: resolve-hermes
56138
runs-on: macos-15
57139
timeout-minutes: 30
@@ -92,7 +174,7 @@ jobs:
92174

93175
build-hermes-slice:
94176
name: "Hermes ${{ matrix.slice }}"
95-
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
177+
if: ${{ needs.resolve-hermes.outputs.recomposed != 'true' && needs.resolve-hermes.outputs.cache-hit != 'true' }}
96178
needs: [resolve-hermes, build-hermesc]
97179
runs-on: macos-15
98180
timeout-minutes: 45
@@ -160,7 +242,7 @@ jobs:
160242

161243
assemble-hermes:
162244
name: "Assemble Hermes xcframework"
163-
if: ${{ needs.resolve-hermes.outputs.cache-hit != 'true' }}
245+
if: ${{ needs.resolve-hermes.outputs.recomposed != 'true' && needs.resolve-hermes.outputs.cache-hit != 'true' }}
164246
needs: [resolve-hermes, build-hermes-slice]
165247
runs-on: macos-15
166248
timeout-minutes: 15

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,52 @@ async function getLatestStableVersionFromNPM() /*: Promise<string> */ {
184184
return json.version;
185185
}
186186

187+
/**
188+
* Downloads the upstream Hermes tarball from Maven for the mapped
189+
* upstream version. Returns the tarball path and version on success,
190+
* or null if no tarball is available.
191+
*
192+
* The caller is responsible for extracting and recomposing the
193+
* xcframework (e.g. adding the macOS slice to the universal).
194+
*/
195+
async function downloadUpstreamHermesTarball(
196+
buildType /*: string */ = 'Debug',
197+
) /*: Promise<?{| tarballPath: string, version: string |}> */ {
198+
const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json');
199+
const version = findMatchingHermesVersion(packageJsonPath);
200+
if (version == null) {
201+
macosLog('No upstream version found, cannot download tarball');
202+
return null;
203+
}
204+
205+
const mavenUrl = `https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-ios-${buildType.toLowerCase()}.tar.gz`;
206+
207+
macosLog(
208+
`Downloading upstream Hermes tarball (${version}) from ${mavenUrl}...`,
209+
);
210+
211+
try {
212+
const response /*: Response */ = await fetch(mavenUrl);
213+
if (!response.ok) {
214+
macosLog(
215+
`Tarball not found: ${response.status} ${response.statusText}`,
216+
);
217+
return null;
218+
}
219+
220+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-'));
221+
const tarballPath = path.join(tmpDir, 'hermes-ios.tar.gz');
222+
const buffer = await response.arrayBuffer();
223+
fs.writeFileSync(tarballPath, Buffer.from(buffer));
224+
225+
macosLog(`Downloaded upstream Hermes tarball to ${tarballPath}`);
226+
return {tarballPath, version};
227+
} catch (e) {
228+
macosLog(`Error downloading tarball: ${e.message}`);
229+
return null;
230+
}
231+
}
232+
187233
function abort(message /*: string */) {
188234
macosLog(message, 'error');
189235
throw new Error(message);
@@ -194,4 +240,5 @@ module.exports = {
194240
hermesCommitAtMergeBase,
195241
findVersionAtMergeBase,
196242
getLatestStableVersionFromNPM,
243+
downloadUpstreamHermesTarball,
197244
};

0 commit comments

Comments
 (0)