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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
resolveCacheSlotVersion,
resolveHermesArtifact,
resolveLatestV1Version,
resolveLocalHermesCompilerVersion,
resolveNightlyVersion,
resolveRNCoreArtifact,
resolveRNDepsArtifact,
Expand Down Expand Up @@ -95,6 +96,34 @@ function routerFetch(routes /*: {[string]: any} */) {
// artifact at the RN nightly version (which won't exist on Maven).
// ---------------------------------------------------------------------------

// Creates a scratch dir with (optionally) a `node_modules/hermes-compiler`
// package inside it, mimicking a real project root for
// resolveLocalHermesCompilerVersion()'s require.resolve({paths: [rnRoot]}).
function makeFakeRnRoot(hermesCompilerVersion /*: ?string */) /*: string */ {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-root-'));
if (hermesCompilerVersion != null) {
const pkgDir = path.join(root, 'node_modules', 'hermes-compiler');
fs.mkdirSync(pkgDir, {recursive: true});
fs.writeFileSync(
path.join(pkgDir, 'package.json'),
JSON.stringify({name: 'hermes-compiler', version: hermesCompilerVersion}),
);
}
return root;
}

describe('resolveLocalHermesCompilerVersion', () => {
it('reads the version from the locally installed hermes-compiler package', () => {
const root = makeFakeRnRoot('0.13.7');
expect(resolveLocalHermesCompilerVersion(root)).toBe('0.13.7');
});

it('returns null when hermes-compiler is not installed', () => {
const root = makeFakeRnRoot(null);
expect(resolveLocalHermesCompilerVersion(root)).toBeNull();
});
});

describe('resolveHermesArtifact', () => {
let origFetch;
let origHermesEnv;
Expand Down Expand Up @@ -122,57 +151,80 @@ describe('resolveHermesArtifact', () => {
}

describe('default behavior (no HERMES_VERSION set)', () => {
it('resolves to the latest-v1 hermes-compiler dist-tag, NOT the RN version', async () => {
it('uses the locally pinned hermes-compiler version, without hitting npm', async () => {
const rnRoot = makeFakeRnRoot('0.13.7');
mockFetch({
'hermes-compiler/latest-v1': {json: {version: '0.13.0'}},
// Pretend the release URL exists once we ask for 0.13.0.
'hermes-ios/0.13.0/hermes-ios-0.13.0': {ok: true},
'hermes-ios/0.13.7/hermes-ios-0.13.7': {ok: true},
});
const result = await resolveHermesArtifact(
'0.87.0-nightly-20260519-58cd1bf58',
'debug',
null,
rnRoot,
);
expect(result.version).toBe('0.13.7');
expect(result.url).toContain('/0.13.7/');
// Must resolve straight from node_modules — no npm registry round trip.
expect(globalThis.fetch).not.toHaveBeenCalledWith(
expect.stringContaining('registry.npmjs.org'),
expect.anything(),
);
expect(result.version).toBe('0.13.0');
expect(result.url).toContain('/0.13.0/');
// The RN nightly hash MUST NOT leak into the hermes URL.
expect(result.url).not.toContain('20260519');
});

it('ignores rawVersion (the RN --version arg) when HERMES_VERSION is unset', async () => {
const rnRoot = makeFakeRnRoot('0.13.7');
mockFetch({
'hermes-compiler/latest-v1': {json: {version: '0.13.0'}},
'hermes-ios/0.13.0/hermes-ios-0.13.0': {ok: true},
'hermes-ios/0.13.7/hermes-ios-0.13.7': {ok: true},
});
// Caller passes the original RN --version verbatim; hermes should
// still default to latest-v1 instead of using this.
// still use the locally pinned version instead of using this.
const result = await resolveHermesArtifact(
'0.87.0-nightly-20260519-58cd1bf58',
'debug',
'0.87.0-nightly-20260519-58cd1bf58',
rnRoot,
);
expect(result.version).toBe('0.13.0');
expect(result.version).toBe('0.13.7');
expect(result.url).not.toContain('20260519');
});

it('falls back to the latest-v1 npm dist-tag when hermes-compiler is not locally installed', async () => {
const rnRoot = makeFakeRnRoot(null);
mockFetch({
'hermes-compiler/latest-v1': {json: {version: '0.13.0'}},
'hermes-ios/0.13.0/hermes-ios-0.13.0': {ok: true},
});
const result = await resolveHermesArtifact(
'0.87.0-nightly-20260519-58cd1bf58',
'debug',
null,
rnRoot,
);
expect(result.version).toBe('0.13.0');
expect(result.url).toContain('/0.13.0/');
});
});

describe('HERMES_VERSION escape hatches', () => {
it('HERMES_VERSION=<literal-version> uses it verbatim', async () => {
it('HERMES_VERSION=<literal-version> uses it verbatim, even with a local package installed', async () => {
process.env.HERMES_VERSION = '0.13.5';
const rnRoot = makeFakeRnRoot('0.13.7');
mockFetch({
'hermes-ios/0.13.5/hermes-ios-0.13.5': {ok: true},
});
const result = await resolveHermesArtifact(
'0.87.0-nightly-anything',
'debug',
null,
rnRoot,
);
expect(result.version).toBe('0.13.5');
expect(result.url).toContain('/0.13.5/');
});

it('HERMES_VERSION=latest-v1 resolves via npm dist-tag', async () => {
it('HERMES_VERSION=latest-v1 resolves via npm dist-tag, even with a local package installed', async () => {
process.env.HERMES_VERSION = 'latest-v1';
const rnRoot = makeFakeRnRoot('0.13.7');
mockFetch({
'hermes-compiler/latest-v1': {json: {version: '0.13.0'}},
'hermes-ios/0.13.0/hermes-ios-0.13.0': {ok: true},
Expand All @@ -181,12 +233,14 @@ describe('resolveHermesArtifact', () => {
'0.87.0-nightly-anything',
'debug',
null,
rnRoot,
);
expect(result.version).toBe('0.13.0');
});

it('HERMES_VERSION=nightly resolves hermes-compiler@nightly from npm', async () => {
process.env.HERMES_VERSION = 'nightly';
const rnRoot = makeFakeRnRoot(null);
mockFetch({
'hermes-compiler/nightly': {json: {version: '0.14.0-nightly-abc'}},
'hermes-ios/0.14.0-nightly-abc/hermes-ios-0.14.0-nightly-abc': {
Expand All @@ -197,12 +251,14 @@ describe('resolveHermesArtifact', () => {
'0.87.0-nightly-anything',
'debug',
null,
rnRoot,
);
expect(result.version).toBe('0.14.0-nightly-abc');
});

it('falls back to the hermes snapshot URL when the release is missing', async () => {
process.env.HERMES_VERSION = '0.13.5';
const rnRoot = makeFakeRnRoot(null);
globalThis.fetch = jest.fn(async (url, opts) => {
if (opts && opts.method === 'HEAD') {
return {status: 404};
Expand All @@ -215,7 +271,12 @@ describe('resolveHermesArtifact', () => {
'<buildNumber>2</buildNumber></metadata>',
};
});
const result = await resolveHermesArtifact('0.87.0', 'debug', null);
const result = await resolveHermesArtifact(
'0.87.0',
'debug',
null,
rnRoot,
);
expect(result.url).toContain('maven-snapshots');
expect(result.url).toContain('hermes-ios-debug.tar.gz');
});
Expand Down
59 changes: 53 additions & 6 deletions packages/react-native/scripts/spm/download-spm-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,15 +452,52 @@ async function resolveRNDepsArtifact(
return {url: snapshotUrl, version};
}

/**
* Resolves the `hermes-compiler` npm package's version from THIS project's own
* node_modules — the exact same lookup generate-spm-xcodeproj.js's
* resolveHermesCliPathSetting() uses to find the hermesc binary that will
* compile the JS bundle, and the same one react-native-xcode.sh falls back to
* for SwiftPM builds. Returns null when the package isn't resolvable (e.g.
* USE_HERMES=false apps that never installed it) so the caller can fall back
* to the npm dist-tag lookup.
*/
function resolveLocalHermesCompilerVersion(
rnRoot /*: string */,
) /*: string | null */ {
try {
const pkgPath = require.resolve('hermes-compiler/package.json', {
paths: [rnRoot],
});
// $FlowFixMe[incompatible-type] JSON.parse returns any
const pkg /*: {version: string} */ = JSON.parse(
fs.readFileSync(pkgPath, 'utf8'),
);
assertSafeVersion(pkg.version, 'local hermes-compiler/package.json');
return pkg.version;
} catch {
return null;
}
}

/**
* Returns {url, version} for Hermes. Hermes uses its own version space
* decoupled from React Native's nightly cadence — RN's `hermes-compiler`
* npm package publishes a `latest-v1` dist-tag that always resolves to a
* binary that's been built and uploaded to Maven. Our default mirrors RN's
* CocoaPods prebuild path (see scripts/ios-prebuild/hermes.js):
* binary that's been built and uploaded to Maven.
*
* HERMES_VERSION unset → 'latest-v1' dist-tag
* HERMES_VERSION=latest-v1 → same (explicit)
* HERMES_VERSION unset → version pinned by the locally installed
* hermes-compiler package (node_modules).
* This is the SAME source
* resolveHermesCliPathSetting() reads for
* HERMES_CLI_PATH, so the downloaded VM and
* the hermesc that compiles the JS bundle
* always agree — a mismatched pair crashes at
* launch with "Wrong bytecode version" (#57917).
* Falls back to the 'latest-v1' npm dist-tag
* (RN's CocoaPods prebuild default; see
* scripts/ios-prebuild/hermes.js) only when
* hermes-compiler isn't locally resolvable.
* HERMES_VERSION=latest-v1 → 'latest-v1' dist-tag (explicit)
* HERMES_VERSION=nightly → hermes-compiler@nightly dist-tag
* HERMES_VERSION=<literal> → use that version verbatim
*
Expand All @@ -472,8 +509,17 @@ async function resolveHermesArtifact(
rnVersion /*: string */,
flavor /*: string */,
rawVersion /*: string | null */,
rnRoot /*: string */,
) /*: Promise<ResolvedArtifact> */ {
let version = process.env.HERMES_VERSION ?? 'latest-v1';
let version = process.env.HERMES_VERSION;

if (version == null) {
const localVersion = resolveLocalHermesCompilerVersion(rnRoot);
if (localVersion != null) {
log(` Using locally pinned hermes-compiler: ${localVersion}`);
}
version = localVersion ?? 'latest-v1';
}

if (version === 'nightly') {
version = await resolveNightlyVersion('hermes-compiler');
Expand Down Expand Up @@ -1178,7 +1224,7 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
label: 'hermes',
name: 'hermes-engine',
resolve: () =>
resolveHermesArtifact(resolvedRnVersion, flavor, rawVersion),
resolveHermesArtifact(resolvedRnVersion, flavor, rawVersion, rnRoot),
sharedName: (v /*: string */) => `hermes-ios-${v}-${flavor}.tar.gz`,
},
];
Expand Down Expand Up @@ -1449,6 +1495,7 @@ module.exports = {
main,
resolveCacheSlotVersion,
resolveHermesArtifact,
resolveLocalHermesCompilerVersion,
REQUIRED_ARTIFACTS,
validateArtifactsCache,
// Exposed for unit tests (pure / fetch-stubbable helpers).
Expand Down
Loading