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 @@ -67,11 +67,6 @@ function withHeaderSearchPaths(value) {
}

const RN_PATH = '../node_modules/react-native';

// Absolute, mirroring resolveHermesCliPathSetting (a `..`-relative path through
// a symlinked react-native would resolve to the wrong dir at build time).
const TEST_HERMES_CLI_PATH =
'/abs/node_modules/hermes-compiler/hermesc/osx-bin/hermesc';
const TEST_FRAMEWORKS = [
{
id: 'react',
Expand Down Expand Up @@ -104,7 +99,10 @@ const TEST_FRAMEWORKS = [
function inject(
text,
remote = null,
hermesCliPath = TEST_HERMES_CLI_PATH,
// Dead positional slot: injectSpmIntoPbxproj no longer takes a hermesCliPath
// (see the HERMES_CLI_PATH test below). Kept so the call sites below, which
// pass it as `null`, stay untouched.
_hermesCliPath = null,
generatedSources = [],
scriptPhases = [],
) {
Expand All @@ -121,7 +119,6 @@ function inject(
},
RN_PATH,
remote,
hermesCliPath,
generatedSources,
TEST_FRAMEWORKS,
scriptPhases,
Expand Down Expand Up @@ -237,20 +234,22 @@ describe('injectSpmIntoPbxproj — Tier 2 (build settings + phase)', () => {
expect(text.match(/CLANG_CXX_LANGUAGE_STANDARD = "c\+\+20"/g)).toHaveLength(
2,
);
// HERMES_CLI_PATH points react-native-xcode.sh at the hermes-compiler npm
// package (no hermes-engine pod under SPM), injected into both configs.
expect(text.match(/HERMES_CLI_PATH = /g)).toHaveLength(2);
expect(text).toContain(TEST_HERMES_CLI_PATH);
expect(text).toContain('RN_SPM_FLAVOR = debug');
expect(text).toContain('RN_SPM_FLAVOR = release');
expect(text).toContain('RN_SPM_REACT_BINARY[sdk=iphoneos*]');
expect(text).toContain('RN_SPM_REACT_BINARY[sdk=iphonesimulator*]');
expect(text).toContain('$(RN_SPM_REACT_BINARY)');
});

it('omits HERMES_CLI_PATH when hermesc could not be resolved', () => {
const {text} = inject(PLAIN, null, null);
// An absolute hermesc path is machine-specific, and the app commits its
// project.pbxproj. react-native-xcode.sh resolves hermesc through
// react-native's own dependency graph at build time instead.
it('never writes HERMES_CLI_PATH into either configuration', () => {
const {text, buildSettingChanges} = inject(PLAIN);
expect(text).not.toContain('HERMES_CLI_PATH');
expect(
buildSettingChanges.flatMap(change => change.createdScalars ?? []),
).not.toContain('HERMES_CLI_PATH');
});

// Swift's `#if DEBUG` — which AppDelegate.swift's bundleURL() uses to pick the
Expand Down Expand Up @@ -419,7 +418,6 @@ describe('injectSpmIntoPbxproj — Tier 3 (plugin generated sources)', () => {
},
RN_PATH,
null,
null,
[PROVIDER_SOURCE],
TEST_FRAMEWORKS,
).text;
Expand Down Expand Up @@ -459,7 +457,6 @@ describe('injectSpmIntoPbxproj — Tier 3 (plugin generated sources)', () => {
},
RN_PATH,
null,
null,
[PROVIDER_SOURCE],
TEST_FRAMEWORKS,
);
Expand Down Expand Up @@ -1104,7 +1101,6 @@ describe('injectSpmIntoPbxproj — invariants', () => {
},
RN_PATH,
null,
null,
[],
TEST_FRAMEWORKS,
).text;
Expand Down Expand Up @@ -1145,7 +1141,6 @@ describe('injectSpmIntoPbxproj — invariants', () => {
},
RN_PATH,
null,
null,
[],
TEST_FRAMEWORKS,
).text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ function scaffoldApp(pbxproj /*: string */ = PLAIN) {
return {appRoot, xcodeprojPath, rnRoot};
}

// The hoisted `hermes-compiler` layout a real installed app has: the package
// sits in the app's node_modules, NOT next to react-native, and carries the
// host hermesc binary. Returns its absolute path — the value the injector used
// to bake into the project.
function withHermesCompiler(appRoot /*: string */) {
const pkgRoot = path.join(appRoot, 'node_modules', 'hermes-compiler');
const binDir = path.join(pkgRoot, 'hermesc', 'osx-bin');
fs.mkdirSync(binDir, {recursive: true});
fs.writeFileSync(
path.join(pkgRoot, 'package.json'),
JSON.stringify({name: 'hermes-compiler', version: '1.0.0'}),
'utf8',
);
const hermesc = path.join(binDir, 'hermesc');
fs.writeFileSync(hermesc, '#!/bin/sh\n', 'utf8');
// os.tmpdir() is a symlink on macOS; require.resolve reports the real path,
// which is what the injector would have written.
return fs.realpathSync(hermesc);
}

function pbxprojOf(xcodeprojPath) {
return fs.readFileSync(path.join(xcodeprojPath, 'project.pbxproj'), 'utf8');
}
Expand Down Expand Up @@ -156,6 +176,42 @@ function schemePathOf(xcodeprojPath) {
);
}

// An absolute hermesc path is machine-specific and the app commits its
// project.pbxproj, so `spm add` must never write one. react-native-xcode.sh
// resolves hermesc through react-native's own dependency graph at build time.
describe('injectSpmIntoExistingXcodeproj — HERMES_CLI_PATH', () => {
it('writes no HERMES_CLI_PATH, in any configuration or the marker', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
const hermesc = withHermesCompiler(appRoot);
// Guard against a vacuous pass: hermesc must be resolvable from rnRoot,
// which is the only condition under which a path could be written at all.
expect(
require.resolve('hermes-compiler/package.json', {paths: [rnRoot]}),
).toBe(
fs.realpathSync(
path.join(appRoot, 'node_modules', 'hermes-compiler', 'package.json'),
),
);
expect(fs.existsSync(hermesc)).toBe(true);

expect(
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
xcodeprojPath,
}).status,
).toBe('injected');

expect(pbxprojOf(xcodeprojPath)).not.toContain('HERMES_CLI_PATH');
expect(pbxprojOf(xcodeprojPath)).not.toContain(hermesc);
expect(
readMarker(xcodeprojPath).buildSettingChanges.flatMap(
change => change.createdScalars ?? [],
),
).not.toContain('HERMES_CLI_PATH');
});
});

describe('removeSpmInjection — the surgical inverse of add', () => {
it('round-trips: add then deinit restores the pbxproj byte-for-byte', () => {
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
Expand Down
11 changes: 5 additions & 6 deletions packages/react-native/scripts/spm/download-spm-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,10 +454,9 @@ async function resolveRNDepsArtifact(

/**
* 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.
* node_modules — the same lookup react-native-xcode.sh falls back to for
* SwiftPM builds to find the hermesc binary that compiles the JS bundle.
* 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.
*/
Expand Down Expand Up @@ -499,8 +498,8 @@ function resolveLocalHermesCompilerVersion(
* 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
* react-native-xcode.sh resolves hermesc
* from, 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).
Expand Down
65 changes: 7 additions & 58 deletions packages/react-native/scripts/spm/generate-spm-xcodeproj.js
Original file line number Diff line number Diff line change
Expand Up @@ -1250,7 +1250,6 @@ function injectSpmIntoPbxproj(
plan /*: {rootUuid: string, targetUuid: string, configUuids: Array<string>, frameworksPhaseUuid: string, sourcesPhaseUuid?: ?string} */,
reactNativePath /*: string */,
remote /*: ?RemoteCfg */,
hermesCliPath /*: ?string */ = null,
generatedSources /*: ReadonlyArray<GeneratedSource> */ = [],
flavoredFrameworks /*: ReadonlyArray<FlavoredFrameworkManifestEntry> */ = [],
scriptPhases /*: ReadonlyArray<PluginScriptPhase> */ = [],
Expand Down Expand Up @@ -1350,7 +1349,6 @@ function injectSpmIntoPbxproj(
configUuid,
buildConfigurationName(text, configUuid),
reactNativePath,
hermesCliPath,
flavoredFrameworks,
);
text = merged.text;
Expand Down Expand Up @@ -1659,48 +1657,6 @@ function findApplicationTargetByUuid(
return obj;
}

/**
* Merge the React build settings into one XCBuildConfiguration's dict. Returns
* the modified text plus a precise record of what was actually added — so
* `deinit` (removeSpmInjection) can reverse exactly these edits, never touching
* a value the user already had (key insight: ensureScalarField/
* addArrayStringValues are no-ops / dedupe when a value is already present).
*/
/**
* Resolves the host `hermesc` from the `hermes-compiler` npm package and returns
* its ABSOLUTE path as the HERMES_CLI_PATH value, or null when it can't be found
* (e.g. USE_HERMES=false apps without the package). require.resolve (anchored at
* reactNativeRoot) follows Node's lookup, so a hoisted monorepo layout — where
* hermes-compiler sits in the workspace-root node_modules, NOT next to
* react-native — resolves correctly.
*
* The value is intentionally ABSOLUTE, not `$(REACT_NATIVE_PATH)/../...`: when
* react-native is a symlink (the monorepo default, and common in real apps), a
* `..` after it resolves — kernel-side — to the symlink TARGET's parent, not the
* node_modules dir, so the relative form points at a non-existent
* `<rn-target>/../hermes-compiler`. An absolute path sidesteps that entirely
* (and matches how the CocoaPods hermes-engine pod sets HERMES_CLI_PATH). It is
* regenerated on every `spm add`, so machine-specificity is a non-issue.
*/
function resolveHermesCliPathSetting(
reactNativeRoot /*: string */,
) /*: ?string */ {
try {
const pkg = require.resolve('hermes-compiler/package.json', {
paths: [reactNativeRoot],
});
const hermesc = path.join(
path.dirname(pkg),
'hermesc',
'osx-bin',
'hermesc',
);
return fs.existsSync(hermesc) ? hermesc : null;
} catch {
return null;
}
}

/** Strip the surrounding plist quotes from a build-setting token, if any. */
function unquotePlist(s /*: string */) /*: string */ {
return s.replace(/^"/, '').replace(/"$/, '');
Expand All @@ -1723,29 +1679,24 @@ function buildSettingValueTokens(value /*: string */) /*: Set<string> */ {
);
}

/**
* Merge the React build settings into one XCBuildConfiguration's dict. Returns
* the modified text plus a precise record of what was actually added — so
* `deinit` (removeSpmInjection) can reverse exactly these edits, never touching
* a value the user already had (key insight: ensureScalarField/
* addArrayStringValues are no-ops / dedupe when a value is already present).
*/
function mergeReactBuildSettings(
input /*: string */,
configUuid /*: string */,
configurationName /*: string */,
reactNativePath /*: string */,
hermesCliPath /*: ?string */ = null,
flavoredFrameworks /*: ReadonlyArray<FlavoredFrameworkManifestEntry> */ = [],
) /*: {text: string, change: BuildSettingChange} */ {
let text = input;
const scalars = [
{key: 'CLANG_CXX_LANGUAGE_STANDARD', value: '"c++20"'},
{key: 'REACT_NATIVE_PATH', value: quoteIfNeeded(reactNativePath)},
// Under SwiftPM there is no hermes-engine pod, so react-native-xcode.sh's
// fallback ($PODS_ROOT/hermes-engine/destroot/bin/hermesc) resolves to a
// non-existent "/hermes-engine/..." and the Release JS→Hermes bundling
// fails. Point HERMES_CLI_PATH at the hermes-compiler npm package's host
// hermesc (an ABSOLUTE path resolved by the caller — see
// resolveHermesCliPathSetting). react-native-xcode.sh honors an already-set
// HERMES_CLI_PATH before its pod fallback; ensureScalarField leaves any
// user-provided value untouched.
...(hermesCliPath != null
? [{key: 'HERMES_CLI_PATH', value: quoteIfNeeded(hermesCliPath)}]
: []),
];
// Re-locate the buildSettings dict before each edit (offsets shift).
const dict = () => {
Expand Down Expand Up @@ -2368,7 +2319,6 @@ function injectSpmIntoExistingXcodeproj(
}
const reactNativePath = path.relative(appRoot, reactNativeRoot);
const remote = remotePackageConfig(appRoot);
const hermesCliPath = resolveHermesCliPathSetting(reactNativeRoot);
const generatedSources = readGeneratedSourcesManifest(appRoot);
const scriptPhases = readScriptPhasesManifest(appRoot);
const flavoredFrameworks = readFlavoredFrameworksManifest(appRoot).frameworks;
Expand Down Expand Up @@ -2441,7 +2391,6 @@ function injectSpmIntoExistingXcodeproj(
},
reactNativePath,
remote,
hermesCliPath,
generatedSources,
flavoredFrameworks,
scriptPhases,
Expand Down
Loading