diff --git a/.github/workflows/prebuild-ios-core.yml b/.github/workflows/prebuild-ios-core.yml index 77148d99914d..9c6bad03a434 100644 --- a/.github/workflows/prebuild-ios-core.yml +++ b/.github/workflows/prebuild-ios-core.yml @@ -175,6 +175,27 @@ jobs: tar -xzf /tmp/third-party/ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz -C /tmp/third-party/ mkdir -p packages/react-native/third-party/ mv /tmp/third-party/packages/react-native/third-party/ReactNativeDependencies.xcframework packages/react-native/third-party/ReactNativeDependencies.xcframework + - name: Set Hermes version + if: steps.restore-ios-xcframework.outputs.cache-hit != 'true' + shell: bash + run: | + # Non-stable RN builds resolve Hermes from npm's latest-v1 dist-tag. + # TODO: rename 'latest-v1' to 'latest' once V1 is the only Hermes on npm. + # Stable builds use the version pinned in version.properties. + if [ "${{ inputs.use-hermes-prebuilt }}" == "true" ]; then + HERMES_VERSION="latest-v1" + else + HERMES_VERSION=$(sed -n 's/^HERMES_VERSION_NAME=//p' packages/react-native/sdks/hermes-engine/version.properties) + fi + echo "Using Hermes version: $HERMES_VERSION" + echo "HERMES_VERSION=$HERMES_VERSION" >> $GITHUB_ENV + - name: Stage Hermes headers + if: steps.restore-ios-xcframework.outputs.cache-hit != 'true' + working-directory: packages/react-native + env: + FLAVOR: ${{ matrix.flavor }} + run: | + node -e "require('./scripts/ios-prebuild/hermes').prepareHermesArtifactsAsync(require('./package.json').version, process.env.FLAVOR).then(()=>process.exit(0)).catch(e=>{console.error(e);process.exit(1)})" - name: Setup Keychain if: ${{ steps.restore-ios-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 @@ -185,12 +206,12 @@ jobs: if: ${{ steps.restore-ios-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 }}" + node scripts/ios-prebuild -c -f "${{ matrix.flavor }}" ${{ inputs.version-type != '' && '--require-hermes' || '' }} - name: Create and Sign XCFramework if: ${{ steps.restore-ios-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" + node scripts/ios-prebuild -c -f "${{ matrix.flavor }}" -i "React Org" ${{ inputs.version-type != '' && '--require-hermes' || '' }} - name: Verify composed headers if: steps.restore-ios-xcframework.outputs.cache-hit != 'true' run: | diff --git a/packages/react-native/scripts/ios-prebuild.js b/packages/react-native/scripts/ios-prebuild.js index 2f8499d31f41..30096fb909e9 100644 --- a/packages/react-native/scripts/ios-prebuild.js +++ b/packages/react-native/scripts/ios-prebuild.js @@ -77,6 +77,7 @@ async function main() { frameworkPaths, buildType, cli.identity, + cli.requireHermes, ); } diff --git a/packages/react-native/scripts/ios-prebuild/__tests__/xcframework-test.js b/packages/react-native/scripts/ios-prebuild/__tests__/xcframework-test.js new file mode 100644 index 000000000000..210c41f2e40c --- /dev/null +++ b/packages/react-native/scripts/ios-prebuild/__tests__/xcframework-test.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +const {resolveHermesHeaders} = require('../xcframework'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +describe('resolveHermesHeaders', () => { + let tmp /*: string */ = ''; + let buildFolder /*: string */ = ''; + + beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'xcframework-test-')); + buildFolder = path.join(tmp, '.build'); + }); + + afterEach(() => { + fs.rmSync(tmp, {recursive: true, force: true}); + }); + + test('returns the base include directory when hermes/hermes.h exists', () => { + const includeDir = path.join( + buildFolder, + 'artifacts', + 'hermes', + 'destroot', + 'include', + ); + fs.mkdirSync(path.join(includeDir, 'hermes'), {recursive: true}); + fs.writeFileSync(path.join(includeDir, 'hermes', 'hermes.h'), ''); + + expect(resolveHermesHeaders(buildFolder, true)).toBe(includeDir); + }); + + test('finds a non-standard nested include directory', () => { + const includeDir = path.join( + buildFolder, + 'artifacts', + 'hermes', + 'nested', + 'archive', + 'destroot', + 'include', + ); + fs.mkdirSync(path.join(includeDir, 'hermes'), {recursive: true}); + fs.writeFileSync(path.join(includeDir, 'hermes', 'hermes.h'), ''); + + expect(resolveHermesHeaders(buildFolder, true)).toBe(includeDir); + }); + + test('returns null when Hermes headers are absent and not required', () => { + expect(resolveHermesHeaders(buildFolder, false)).toBeNull(); + }); + + test('throws when Hermes headers are absent and required', () => { + expect(() => resolveHermesHeaders(buildFolder, true)).toThrow( + /ReactNativeHeaders[\s\S]*[\s\S]*destroot\/include/, + ); + }); +}); diff --git a/packages/react-native/scripts/ios-prebuild/cli.js b/packages/react-native/scripts/ios-prebuild/cli.js index 1dfb23c23f24..dca81927b960 100644 --- a/packages/react-native/scripts/ios-prebuild/cli.js +++ b/packages/react-native/scripts/ios-prebuild/cli.js @@ -66,6 +66,11 @@ const cli = yargs describe: 'Specify the code signing identity to use for signing the frameworks.', }) + .option('require-hermes', { + type: 'boolean', + describe: + 'Require Hermes headers when composing the ReactNativeHeaders XCFramework.', + }) .help(); /** @@ -80,6 +85,7 @@ async function getCLIConfiguration() /*: Promise, identity: ?string, + requireHermes: boolean, |}> */ { // Run input parsing const argv = await cli.argv; @@ -124,6 +130,7 @@ async function getCLIConfiguration() /*: Promise boolean */, + depth /*: number */, +) /*: string | null */ { + if (depth <= 0 || !fs.existsSync(dir)) { + return null; + } + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + // $FlowFixMe[incompatible-type] Dirent.name is string|Buffer in Flow but always string here + const full /*: string */ = path.join(dir, entry.name); + // $FlowFixMe[incompatible-type] Dirent.name is string|Buffer in Flow but always string here + if (predicate(entry.name)) { + return full; + } + if (entry.isDirectory()) { + const hit = findFirst(full, predicate, depth - 1); + if (hit != null) { + return hit; + } + } + } + return null; +} + function throwIfOnEden() { try { execSync('eden info', {stdio: 'ignore'}); @@ -104,6 +130,7 @@ async function computeNightlyTarballURL( module.exports = { createFolderIfNotExists, + findFirst, throwIfOnEden, createLogger, computeNightlyTarballURL, diff --git a/packages/react-native/scripts/ios-prebuild/xcframework.js b/packages/react-native/scripts/ios-prebuild/xcframework.js index 847a8a6ea38b..cf910dd7cb69 100644 --- a/packages/react-native/scripts/ios-prebuild/xcframework.js +++ b/packages/react-native/scripts/ios-prebuild/xcframework.js @@ -19,16 +19,45 @@ const fs = require('node:fs'); const path = require('node:path'); const {execSync, execFileSync} = childProcess; -const {createLogger} = utils; +const {createLogger, findFirst} = utils; const frameworkLog = createLogger('XCFramework'); +function resolveHermesHeaders( + buildFolder /*: string */, + required /*: boolean */, +) /*: ?string */ { + const hermesArtifacts = path.join(buildFolder, 'artifacts', 'hermes'); + const baseInclude = path.join(hermesArtifacts, 'destroot', 'include'); + if (fs.existsSync(path.join(baseInclude, 'hermes', 'hermes.h'))) { + return baseInclude; + } + + const includeDir = findFirst(hermesArtifacts, name => name === 'include', 8); + if ( + includeDir != null && + fs.existsSync(path.join(includeDir, 'hermes', 'hermes.h')) + ) { + return includeDir; + } + + if (required) { + throw new Error( + 'Cannot compose ReactNativeHeaders: will not resolve because ' + + "hermes/hermes.h is missing. Stage the hermes-ios tarball's " + + 'destroot/include into .build/artifacts/hermes before composing.', + ); + } + return null; +} + function buildXCFrameworks( rootFolder /*: string */, buildFolder /*: string */, frameworkFolders /*: Array */, buildType /*: BuildFlavor */, identity /*: ?string */, + requireHermes /*: boolean */ = false, ) { // Let's run codegen for FBReactNativeSpec otherwise some headers will be missing generateFBReactNativeSpecIOS('.'); @@ -103,19 +132,9 @@ function buildXCFrameworks( // ReactNativeHeaders so consumers resolve `` out of the box // (same fold ensureHeadersLayout does consumer-side). The hermes-ios tarball // is staged at .build/artifacts/hermes/destroot/include by the hermes prebuild - // step; pass it when its `hermes/` namespace is present, else null (then - // `` stays consumer-composed, as before). - const hermesInclude = path.resolve( - process.cwd(), - '.build', - 'artifacts', - 'hermes', - 'destroot', - 'include', - ); - const hermesHeaders = fs.existsSync(path.join(hermesInclude, 'hermes')) - ? hermesInclude - : null; + // step; pass it when its `hermes/` namespace is present, else null for local + // development. CI release composition requires it and fails closed. + const hermesHeaders = resolveHermesHeaders(buildFolder, requireHermes); const headersXcfw = buildReactNativeHeadersXcframework( path.dirname(outputPath), plan, @@ -271,4 +290,5 @@ function signXCFramework( module.exports = { buildXCFrameworks, + resolveHermesHeaders, };