Skip to content

Commit 4433cdb

Browse files
radoslawrolkameta-codesync[bot]
authored andcommitted
Set SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG when injecting SPM (#57914)
Summary: Swift's `#if DEBUG` is gated by SWIFT_ACTIVE_COMPILATION_CONDITIONS, not by GCC_PREPROCESSOR_DEFINITIONS (which only reaches C/ObjC/C++). The app template does not commit that setting; CocoaPods injects it at `pod install` time (react_native_post_install -> set_build_setting SWIFT_ACTIVE_COMPILATION_CONDITIONS = ["$(inherited)", "DEBUG"] on Debug). An app set up with the experimental SwiftPM support never runs CocoaPods, so `#if DEBUG` is false even in a Debug build: AppDelegate.swift's `bundleURL()` skips the Metro URL and falls back to a main.jsbundle a Debug build never produced, and the app dies at launch with "No script url provided ... unsanitizedScriptURLString = (null)" while Metro is running. Info: react-native-community/template#244 ## Changelog: Inject the setting from `spm add`/`update` alongside the other React build settings, into debug-flavored configurations only (the same flavorForBuildConfiguration test that selects the debug xcframeworks), so a config linking the debug binaries also compiles its Swift with DEBUG. <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [IOS] [FIXED] - Set SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG when injecting SPM For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests Pull Request resolved: #57914 Test Plan: 1. Build spm-only rn app in debug - App will fail to connect to the Metro 2. Apply changes and run `react-native spm` 3. Build again - App will work as expected in debug and Metro will be connected Reviewed By: Abbondanzo Differential Revision: D115736551 Pulled By: cipolleschi fbshipit-source-id: 1e42ca26ac1992bfa827ce85a63cf4564d313e15
1 parent a4733b1 commit 4433cdb

4 files changed

Lines changed: 154 additions & 3 deletions

File tree

packages/react-native/scripts/spm/__doc__/spm-scripts.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,14 @@ configuration selects Release. Selection uses only generated build settings and
209209
standard macOS tools: builds do not run Node, mutate symlinks, regenerate the
210210
package graph, or require a second build.
211211

212+
Those same debug-flavored configurations also get
213+
`SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"` — the only thing
214+
that makes Swift's `#if DEBUG` true (`GCC_PREPROCESSOR_DEFINITIONS` reaches
215+
C/ObjC/C++ only), and what `AppDelegate.swift`'s `bundleURL()` branches on to
216+
load from Metro instead of a bundled `main.jsbundle`. CocoaPods injects it at
217+
`pod install` time, so this keeps SwiftPM apps at parity. An existing value is
218+
left alone.
219+
212220
## What to commit
213221

214222
| Path | Commit? | Why |

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,32 @@ const PODS = PLAIN.replace(
3131
'AA0000000000000000000901 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = BB0000000000000000000001 /* Pods-MyApp.debug.xcconfig */;\n\t\t\tbuildSettings = {',
3232
);
3333

34+
// The app target's two XCBuildConfiguration UUIDs in the fixture.
35+
const APP_DEBUG_CONFIG = 'AA0000000000000000000901';
36+
const APP_RELEASE_CONFIG = 'AA00000000000000000000A2';
37+
38+
const DEBUG_CONFIG_HEAD =
39+
'AA0000000000000000000901 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {';
40+
41+
// Seed the app target's Debug config with a SWIFT_ACTIVE_COMPILATION_CONDITIONS
42+
// the user already had, in the scalar form Xcode and the app template write.
43+
function withDebugCondition(text, value) {
44+
return text.replace(
45+
DEBUG_CONFIG_HEAD,
46+
`${DEBUG_CONFIG_HEAD}\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = ${value};`,
47+
);
48+
}
49+
50+
// One XCBuildConfiguration's buildSettings dict, by config UUID. Build settings
51+
// hold only scalars and `( … )` arrays, so the first `};` closes the dict.
52+
function buildSettingsOf(text, configUuid) {
53+
const open = text.indexOf(
54+
'buildSettings = {',
55+
text.indexOf(`${configUuid} /*`),
56+
);
57+
return text.slice(open, text.indexOf('};', open));
58+
}
59+
3460
const RN_PATH = '../node_modules/react-native';
3561

3662
// Absolute, mirroring resolveHermesCliPathSetting (a `..`-relative path through
@@ -218,6 +244,40 @@ describe('injectSpmIntoPbxproj — Tier 2 (build settings + phase)', () => {
218244
expect(text).not.toContain('HERMES_CLI_PATH');
219245
});
220246

247+
// Swift's `#if DEBUG` — which AppDelegate.swift's bundleURL() uses to pick the
248+
// Metro URL — is gated by this setting alone. CocoaPods injects it at `pod
249+
// install`; an SPM app has to get it here or a Debug build looks for a
250+
// main.jsbundle it never built.
251+
it('sets SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG on the debug config only', () => {
252+
const {text} = inject(PLAIN);
253+
const debugSettings = buildSettingsOf(text, APP_DEBUG_CONFIG);
254+
expect(debugSettings).toMatch(
255+
/SWIFT_ACTIVE_COMPILATION_CONDITIONS = \(\s*"\$\(inherited\)",\s*DEBUG,\s*\)/,
256+
);
257+
expect(buildSettingsOf(text, APP_RELEASE_CONFIG)).not.toContain(
258+
'SWIFT_ACTIVE_COMPILATION_CONDITIONS',
259+
);
260+
});
261+
262+
it('leaves a config that already sets DEBUG (scalar form) untouched', () => {
263+
const {text} = inject(withDebugCondition(PLAIN, '"$(inherited) DEBUG"'));
264+
// Not promoted to an array, not re-appended — DEBUG is already there.
265+
expect(buildSettingsOf(text, APP_DEBUG_CONFIG)).toContain(
266+
'SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";',
267+
);
268+
expect(text.match(/\bDEBUG\b/g)).toHaveLength(1);
269+
});
270+
271+
it("adds DEBUG alongside the user's own compilation conditions", () => {
272+
const {text} = inject(
273+
withDebugCondition(PLAIN, '"$(inherited) MY_DEBUG_UI"'),
274+
);
275+
// MY_DEBUG_UI must not be mistaken for DEBUG by a substring check.
276+
const debugSettings = buildSettingsOf(text, APP_DEBUG_CONFIG);
277+
expect(debugSettings).toContain('"$(inherited) MY_DEBUG_UI"');
278+
expect(debugSettings).toMatch(/^\s*DEBUG,$/m);
279+
});
280+
221281
it('prepends the Sync SPM Autolinking build phase', () => {
222282
const {text} = inject(PLAIN);
223283
expect(text).toContain('Sync SPM Autolinking');

packages/react-native/scripts/spm/__tests__/remove-spm-injection-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,38 @@ describe('removeSpmInjection — the surgical inverse of add', () => {
190190
expect(fs.existsSync(schemePath)).toBe(false);
191191
});
192192

193+
// A Debug config that already carries DEBUG gets no edit at all, so there is
194+
// nothing for the marker to record — and nothing left behind. Injecting into
195+
// the scalar form regardless (addArrayStringValues dedupes by exact array
196+
// member, which the scalar never matches) would promote it to an array the
197+
// marker has no record of, and deinit would strand it.
198+
it('leaves a Debug config that already sets DEBUG alone, add through deinit', () => {
199+
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
200+
const head =
201+
'AA0000000000000000000901 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {';
202+
fs.writeFileSync(
203+
path.join(xcodeprojPath, 'project.pbxproj'),
204+
PLAIN.replace(
205+
head,
206+
`${head}\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";`,
207+
),
208+
'utf8',
209+
);
210+
const before = pbxprojOf(xcodeprojPath);
211+
212+
injectSpmIntoExistingXcodeproj({
213+
appRoot,
214+
reactNativeRoot: rnRoot,
215+
xcodeprojPath,
216+
});
217+
expect(pbxprojOf(xcodeprojPath)).toContain(
218+
'SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";',
219+
);
220+
221+
expect(removeSpmInjection({appRoot, xcodeprojPath}).status).toBe('removed');
222+
expect(pbxprojOf(xcodeprojPath)).toBe(before);
223+
});
224+
193225
it('preserves an unrelated edit made to the pbxproj after add', () => {
194226
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
195227

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,25 @@ const INJECTED_ARRAY_SETTINGS = [
10931093
},
10941094
];
10951095

1096+
// Array build settings injected only into debug-flavored configurations.
1097+
//
1098+
// Swift's `#if DEBUG` is gated by SWIFT_ACTIVE_COMPILATION_CONDITIONS, NOT by
1099+
// GCC_PREPROCESSOR_DEFINITIONS (which only reaches C/ObjC/C++). The app
1100+
// template does not commit the setting: CocoaPods injects it at `pod install`
1101+
// time (react_native_post_install → set_build_setting
1102+
// SWIFT_ACTIVE_COMPILATION_CONDITIONS = ["$(inherited)", "DEBUG"] on Debug).
1103+
// An SPM app never runs CocoaPods, so without this `#if DEBUG` is false even
1104+
// in a Debug build — AppDelegate.swift's `bundleURL()` skips the Metro URL,
1105+
// falls back to a main.jsbundle that a Debug build never produced, and the app
1106+
// dies at launch with "No script url provided … unsanitizedScriptURLString =
1107+
// (null)" while Metro is running right there.
1108+
//
1109+
// Paired with RN_SPM_FLAVOR via flavorForBuildConfiguration, so a config that
1110+
// links the debug xcframeworks also compiles its Swift with DEBUG.
1111+
const DEBUG_ARRAY_SETTINGS = [
1112+
{key: 'SWIFT_ACTIVE_COMPILATION_CONDITIONS', values: ['DEBUG']},
1113+
];
1114+
10961115
/** The XCBuildConfiguration UUIDs of a target (via its buildConfigurationList). */
10971116
function targetBuildConfigUuids(
10981117
text /*: string */,
@@ -1687,6 +1706,28 @@ function resolveHermesCliPathSetting(
16871706
}
16881707
}
16891708

1709+
/** Strip the surrounding plist quotes from a build-setting token, if any. */
1710+
function unquotePlist(s /*: string */) /*: string */ {
1711+
return s.replace(/^"/, '').replace(/"$/, '');
1712+
}
1713+
1714+
/**
1715+
* The individual values a build setting already carries, unquoted — for both
1716+
* shapes a pbxproj uses: the array form Xcode writes for a multi-value setting
1717+
* (`("$(inherited)", DEBUG)`) and the scalar form the app template and
1718+
* hand-edits use (`"$(inherited) DEBUG"`). Membership, not substring: the
1719+
* latter would read `MY_DEBUG_FLAG` as `DEBUG` already being set and silently
1720+
* skip the injection.
1721+
*/
1722+
function buildSettingValueTokens(value /*: string */) /*: Set<string> */ {
1723+
return new Set(
1724+
value
1725+
.split(/[\s,()]+/)
1726+
.filter(Boolean)
1727+
.map(unquotePlist),
1728+
);
1729+
}
1730+
16901731
function mergeReactBuildSettings(
16911732
input /*: string */,
16921733
configUuid /*: string */,
@@ -1732,6 +1773,9 @@ function mergeReactBuildSettings(
17321773
const createdScalars /*: Array<string> */ = [];
17331774
const arraySettings = [
17341775
...INJECTED_ARRAY_SETTINGS,
1776+
...(flavorForBuildConfiguration(configurationName) === 'debug'
1777+
? DEBUG_ARRAY_SETTINGS
1778+
: []),
17351779
...frameworkArrayBuildSettings(flavoredFrameworks),
17361780
];
17371781
for (const {key, values} of arraySettings) {
@@ -1743,10 +1787,17 @@ function mergeReactBuildSettings(
17431787
if (existing == null) {
17441788
createdArrayKeys.push(key);
17451789
} else {
1746-
const fresh = values.filter(v => !existing.value.includes(v));
1747-
if (fresh.length > 0) {
1748-
appendedArrayValues[key] = fresh;
1790+
const present = buildSettingValueTokens(existing.value);
1791+
const fresh = values.filter(v => !present.has(unquotePlist(v)));
1792+
if (fresh.length === 0) {
1793+
// Nothing to add. Skip addArrayStringValues entirely: its dedupe is by
1794+
// EXACT array member, so a value the user carries in the scalar form
1795+
// (`SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"`) would
1796+
// otherwise be promoted to an array and re-appended — an edit `deinit`
1797+
// has no record of and so could never reverse.
1798+
continue;
17491799
}
1800+
appendedArrayValues[key] = fresh;
17501801
}
17511802
text = addArrayStringValues(text, d, key, values);
17521803
}

0 commit comments

Comments
 (0)