Skip to content

Commit ec393ad

Browse files
chrfalchclaude
andcommitted
fix(spm): complete SwiftPM Release fixes surfaced by the build matrix
Follow-up to the prior Release-support commit — two gaps the ios-build-matrix Release run exposed that the prior commit missed. - (A, completion) The codegen ReactCodegen / ReactAppDependencyProvider targets were NOT getting NDEBUG. That is where the app's codegen'd Fabric C++ lives (RNTMyNativeViewProps), so rn-tester still failed to link with an undefined "vtable for DebugStringConvertible". Add the DEBUG/NDEBUG config defines to scripts/codegen/templates/Package.swift.spm-template (the SPM codegen manifest installed by spm sync). Verified: rn-tester SPM Release now BUILD SUCCEEDED. - (B, fix) HERMES_CLI_PATH must be ABSOLUTE, not `$(REACT_NATIVE_PATH)/../…`. node_modules/react-native is commonly a SYMLINK (monorepo default + many real apps); a `..` after it resolves — kernel-side — to the symlink TARGET's parent (packages/), so the relative path pointed at a non-existent packages/hermes-compiler and Release JS bundling failed with "No such file". resolveHermesCliPathSetting now returns the require.resolve'd absolute hermesc path (regenerated per `spm add`; matches how the hermes-engine pod sets it). Verified: HelloWorld SPM Release now BUILD SUCCEEDED. Matrix: helloworld + rn-tester SPM Release both PASS (newapp pending a Verdaccio re-publish of these scripts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cdb2a2b commit ec393ad

3 files changed

Lines changed: 83 additions & 15 deletions

File tree

packages/react-native/scripts/codegen/templates/Package.swift.spm-template

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ let package = Package(
5353
exclude: ["ReactCodegen.podspec"],
5454
publicHeadersPath: ".",
5555
cSettings: [.headerSearchPath("headers")],
56-
cxxSettings: [.headerSearchPath("headers")],
56+
// DEBUG/NDEBUG match the prebuilt React.framework's config-gated C++
57+
// ABI: NDEBUG (Release) strips DebugStringConvertible's vtable and
58+
// shifts the ShadowNode layout that codegen'd Props inherit, so this
59+
// target must compile with the same NDEBUG state or the Release link
60+
// fails (undefined "vtable for DebugStringConvertible"). Mirrors
61+
// packages/react-native/Package.swift.
62+
cxxSettings: [
63+
.headerSearchPath("headers"),
64+
.define("DEBUG", .when(configuration: .debug)),
65+
.define("NDEBUG", .when(configuration: .release)),
66+
],
5767
linkerSettings: [
5868
.linkedFramework("Foundation")
5969
]
@@ -65,7 +75,13 @@ let package = Package(
6575
exclude: ["ReactAppDependencyProvider.podspec"],
6676
publicHeadersPath: ".",
6777
cSettings: [.headerSearchPath(".."), .headerSearchPath("headers")],
68-
cxxSettings: [.headerSearchPath(".."), .headerSearchPath("headers")],
78+
cxxSettings: [
79+
.headerSearchPath(".."),
80+
.headerSearchPath("headers"),
81+
// Match the prebuilt React.framework NDEBUG-gated C++ ABI (see above).
82+
.define("DEBUG", .when(configuration: .debug)),
83+
.define("NDEBUG", .when(configuration: .release)),
84+
],
6985
linkerSettings: [
7086
.linkedFramework("Foundation")
7187
]

packages/react-native/scripts/spm/__tests__/inject-spm-xcodeproj-test.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ const PODS = PLAIN.replace(
3131

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

34-
function inject(text, remote = null) {
34+
// Absolute, mirroring resolveHermesCliPathSetting (a `..`-relative path through
35+
// a symlinked react-native would resolve to the wrong dir at build time).
36+
const TEST_HERMES_CLI_PATH =
37+
'/abs/node_modules/hermes-compiler/hermesc/osx-bin/hermesc';
38+
39+
function inject(text, remote = null, hermesCliPath = TEST_HERMES_CLI_PATH) {
3540
const plan = planInjection(text, {});
3641
expect(plan.ok).toBe(true);
3742
return injectSpmIntoPbxproj(
@@ -44,6 +49,7 @@ function inject(text, remote = null) {
4449
},
4550
RN_PATH,
4651
remote,
52+
hermesCliPath,
4753
);
4854
}
4955

@@ -150,9 +156,12 @@ describe('injectSpmIntoPbxproj — Tier 2 (build settings + phase)', () => {
150156
// HERMES_CLI_PATH points react-native-xcode.sh at the hermes-compiler npm
151157
// package (no hermes-engine pod under SPM), injected into both configs.
152158
expect(text.match(/HERMES_CLI_PATH = /g)).toHaveLength(2);
153-
expect(text).toContain(
154-
'$(REACT_NATIVE_PATH)/../hermes-compiler/hermesc/osx-bin/hermesc',
155-
);
159+
expect(text).toContain(TEST_HERMES_CLI_PATH);
160+
});
161+
162+
it('omits HERMES_CLI_PATH when hermesc could not be resolved', () => {
163+
const {text} = inject(PLAIN, null, null);
164+
expect(text).not.toContain('HERMES_CLI_PATH');
156165
});
157166

158167
it('prepends the Sync SPM Autolinking build phase', () => {

packages/react-native/scripts/spm/generate-spm-xcodeproj.js

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ function injectSpmIntoPbxproj(
759759
plan /*: {rootUuid: string, targetUuid: string, configUuids: Array<string>, frameworksPhaseUuid: string} */,
760760
reactNativePath /*: string */,
761761
remote /*: ?RemoteCfg */,
762+
hermesCliPath /*: ?string */ = null,
762763
) /*: {text: string, injectedUuids: Array<string>, createdArrayFields: Array<{container: 'project' | 'target', key: string}>, buildSettingChanges: Array<BuildSettingChange>} */ {
763764
let text = input;
764765
const mkUuid = (section /*: string */, id /*: string */) =>
@@ -853,7 +854,12 @@ function injectSpmIntoPbxproj(
853854
// 5. React build settings into every build config (Debug + Release).
854855
const buildSettingChanges /*: Array<BuildSettingChange> */ = [];
855856
for (const configUuid of plan.configUuids) {
856-
const merged = mergeReactBuildSettings(text, configUuid, reactNativePath);
857+
const merged = mergeReactBuildSettings(
858+
text,
859+
configUuid,
860+
reactNativePath,
861+
hermesCliPath,
862+
);
857863
text = merged.text;
858864
buildSettingChanges.push(merged.change);
859865
}
@@ -904,10 +910,46 @@ function findApplicationTargetByUuid(
904910
* a value the user already had (key insight: ensureScalarField/
905911
* addArrayStringValues are no-ops / dedupe when a value is already present).
906912
*/
913+
/**
914+
* Resolves the host `hermesc` from the `hermes-compiler` npm package and returns
915+
* its ABSOLUTE path as the HERMES_CLI_PATH value, or null when it can't be found
916+
* (e.g. USE_HERMES=false apps without the package). require.resolve (anchored at
917+
* reactNativeRoot) follows Node's lookup, so a hoisted monorepo layout — where
918+
* hermes-compiler sits in the workspace-root node_modules, NOT next to
919+
* react-native — resolves correctly.
920+
*
921+
* The value is intentionally ABSOLUTE, not `$(REACT_NATIVE_PATH)/../...`: when
922+
* react-native is a symlink (the monorepo default, and common in real apps), a
923+
* `..` after it resolves — kernel-side — to the symlink TARGET's parent, not the
924+
* node_modules dir, so the relative form points at a non-existent
925+
* `<rn-target>/../hermes-compiler`. An absolute path sidesteps that entirely
926+
* (and matches how the CocoaPods hermes-engine pod sets HERMES_CLI_PATH). It is
927+
* regenerated on every `spm add`, so machine-specificity is a non-issue.
928+
*/
929+
function resolveHermesCliPathSetting(
930+
reactNativeRoot /*: string */,
931+
) /*: ?string */ {
932+
try {
933+
const pkg = require.resolve('hermes-compiler/package.json', {
934+
paths: [reactNativeRoot],
935+
});
936+
const hermesc = path.join(
937+
path.dirname(pkg),
938+
'hermesc',
939+
'osx-bin',
940+
'hermesc',
941+
);
942+
return fs.existsSync(hermesc) ? hermesc : null;
943+
} catch {
944+
return null;
945+
}
946+
}
947+
907948
function mergeReactBuildSettings(
908949
input /*: string */,
909950
configUuid /*: string */,
910951
reactNativePath /*: string */,
952+
hermesCliPath /*: ?string */ = null,
911953
) /*: {text: string, change: BuildSettingChange} */ {
912954
let text = input;
913955
const scalars = [
@@ -917,14 +959,13 @@ function mergeReactBuildSettings(
917959
// fallback ($PODS_ROOT/hermes-engine/destroot/bin/hermesc) resolves to a
918960
// non-existent "/hermes-engine/..." and the Release JS→Hermes bundling
919961
// fails. Point HERMES_CLI_PATH at the hermes-compiler npm package's host
920-
// hermesc (sibling of react-native in node_modules). react-native-xcode.sh
921-
// honors an already-set HERMES_CLI_PATH before its pod fallback; the
922-
// ensureScalarField below leaves any user-provided value untouched.
923-
{
924-
key: 'HERMES_CLI_PATH',
925-
value:
926-
'"$(REACT_NATIVE_PATH)/../hermes-compiler/hermesc/osx-bin/hermesc"',
927-
},
962+
// hermesc (an ABSOLUTE path resolved by the caller — see
963+
// resolveHermesCliPathSetting). react-native-xcode.sh honors an already-set
964+
// HERMES_CLI_PATH before its pod fallback; ensureScalarField leaves any
965+
// user-provided value untouched.
966+
...(hermesCliPath != null
967+
? [{key: 'HERMES_CLI_PATH', value: quoteIfNeeded(hermesCliPath)}]
968+
: []),
928969
];
929970
// Re-locate the buildSettings dict before each edit (offsets shift).
930971
const dict = () => {
@@ -1161,6 +1202,7 @@ function injectSpmIntoExistingXcodeproj(
11611202
}
11621203
const reactNativePath = path.relative(appRoot, reactNativeRoot);
11631204
const remote = remotePackageConfig(appRoot);
1205+
const hermesCliPath = resolveHermesCliPathSetting(reactNativeRoot);
11641206
const {text, injectedUuids, createdArrayFields, buildSettingChanges} =
11651207
injectSpmIntoPbxproj(
11661208
original,
@@ -1172,6 +1214,7 @@ function injectSpmIntoExistingXcodeproj(
11721214
},
11731215
reactNativePath,
11741216
remote,
1217+
hermesCliPath,
11751218
);
11761219

11771220
const changed = writeIfChanged(pbxprojPath, text);

0 commit comments

Comments
 (0)