Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions .github/workflows/prebuild-ios-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/scripts/ios-prebuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async function main() {
frameworkPaths,
buildType,
cli.identity,
cli.requireHermes,
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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]*<hermes\/\.\.\.>[\s\S]*destroot\/include/,
);
});
});
7 changes: 7 additions & 0 deletions packages/react-native/scripts/ios-prebuild/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand All @@ -80,6 +85,7 @@ async function getCLIConfiguration() /*: Promise<?{|
flavor: BuildFlavor,
destinations: ReadonlyArray<Destination>,
identity: ?string,
requireHermes: boolean,
|}> */ {
// Run input parsing
const argv = await cli.argv;
Expand Down Expand Up @@ -124,6 +130,7 @@ async function getCLIConfiguration() /*: Promise<?{|
flavor: flavor,
destinations: resolvedPlatforms,
identity: argv.identity,
requireHermes: argv.requireHermes ?? false,
};
}

Expand Down
27 changes: 27 additions & 0 deletions packages/react-native/scripts/ios-prebuild/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

const {execSync} = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');

/**
* Creates a folder if it does not exist
Expand All @@ -28,6 +29,31 @@ function createFolderIfNotExists(folderPath /*:string*/) /*: string */ {
return folderPath;
}

function findFirst(
dir /*: string */,
predicate /*: (name: string) => 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'});
Expand Down Expand Up @@ -104,6 +130,7 @@ async function computeNightlyTarballURL(

module.exports = {
createFolderIfNotExists,
findFirst,
throwIfOnEden,
createLogger,
computeNightlyTarballURL,
Expand Down
48 changes: 34 additions & 14 deletions packages/react-native/scripts/ios-prebuild/xcframework.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: <hermes/...> 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<string> */,
buildType /*: BuildFlavor */,
identity /*: ?string */,
requireHermes /*: boolean */ = false,
) {
// Let's run codegen for FBReactNativeSpec otherwise some headers will be missing
generateFBReactNativeSpecIOS('.');
Expand Down Expand Up @@ -103,19 +132,9 @@ function buildXCFrameworks(
// ReactNativeHeaders so consumers resolve `<hermes/hermes.h>` 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
// `<hermes/...>` 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,
Expand Down Expand Up @@ -271,4 +290,5 @@ function signXCFramework(

module.exports = {
buildXCFrameworks,
resolveHermesHeaders,
};
Loading