mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
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: https://github.com/react-native-community/template/pull/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: https://github.com/react/react-native/pull/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
This commit is contained in:
committed by
meta-codesync[bot]
parent
a4733b1ca1
commit
4433cdbe62
@@ -209,6 +209,14 @@ configuration selects Release. Selection uses only generated build settings and
|
||||
standard macOS tools: builds do not run Node, mutate symlinks, regenerate the
|
||||
package graph, or require a second build.
|
||||
|
||||
Those same debug-flavored configurations also get
|
||||
`SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"` — the only thing
|
||||
that makes Swift's `#if DEBUG` true (`GCC_PREPROCESSOR_DEFINITIONS` reaches
|
||||
C/ObjC/C++ only), and what `AppDelegate.swift`'s `bundleURL()` branches on to
|
||||
load from Metro instead of a bundled `main.jsbundle`. CocoaPods injects it at
|
||||
`pod install` time, so this keeps SwiftPM apps at parity. An existing value is
|
||||
left alone.
|
||||
|
||||
## What to commit
|
||||
|
||||
| Path | Commit? | Why |
|
||||
|
||||
@@ -31,6 +31,32 @@ const PODS = PLAIN.replace(
|
||||
'AA0000000000000000000901 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = BB0000000000000000000001 /* Pods-MyApp.debug.xcconfig */;\n\t\t\tbuildSettings = {',
|
||||
);
|
||||
|
||||
// The app target's two XCBuildConfiguration UUIDs in the fixture.
|
||||
const APP_DEBUG_CONFIG = 'AA0000000000000000000901';
|
||||
const APP_RELEASE_CONFIG = 'AA00000000000000000000A2';
|
||||
|
||||
const DEBUG_CONFIG_HEAD =
|
||||
'AA0000000000000000000901 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {';
|
||||
|
||||
// Seed the app target's Debug config with a SWIFT_ACTIVE_COMPILATION_CONDITIONS
|
||||
// the user already had, in the scalar form Xcode and the app template write.
|
||||
function withDebugCondition(text, value) {
|
||||
return text.replace(
|
||||
DEBUG_CONFIG_HEAD,
|
||||
`${DEBUG_CONFIG_HEAD}\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = ${value};`,
|
||||
);
|
||||
}
|
||||
|
||||
// One XCBuildConfiguration's buildSettings dict, by config UUID. Build settings
|
||||
// hold only scalars and `( … )` arrays, so the first `};` closes the dict.
|
||||
function buildSettingsOf(text, configUuid) {
|
||||
const open = text.indexOf(
|
||||
'buildSettings = {',
|
||||
text.indexOf(`${configUuid} /*`),
|
||||
);
|
||||
return text.slice(open, text.indexOf('};', open));
|
||||
}
|
||||
|
||||
const RN_PATH = '../node_modules/react-native';
|
||||
|
||||
// Absolute, mirroring resolveHermesCliPathSetting (a `..`-relative path through
|
||||
@@ -218,6 +244,40 @@ describe('injectSpmIntoPbxproj — Tier 2 (build settings + phase)', () => {
|
||||
expect(text).not.toContain('HERMES_CLI_PATH');
|
||||
});
|
||||
|
||||
// Swift's `#if DEBUG` — which AppDelegate.swift's bundleURL() uses to pick the
|
||||
// Metro URL — is gated by this setting alone. CocoaPods injects it at `pod
|
||||
// install`; an SPM app has to get it here or a Debug build looks for a
|
||||
// main.jsbundle it never built.
|
||||
it('sets SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG on the debug config only', () => {
|
||||
const {text} = inject(PLAIN);
|
||||
const debugSettings = buildSettingsOf(text, APP_DEBUG_CONFIG);
|
||||
expect(debugSettings).toMatch(
|
||||
/SWIFT_ACTIVE_COMPILATION_CONDITIONS = \(\s*"\$\(inherited\)",\s*DEBUG,\s*\)/,
|
||||
);
|
||||
expect(buildSettingsOf(text, APP_RELEASE_CONFIG)).not.toContain(
|
||||
'SWIFT_ACTIVE_COMPILATION_CONDITIONS',
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves a config that already sets DEBUG (scalar form) untouched', () => {
|
||||
const {text} = inject(withDebugCondition(PLAIN, '"$(inherited) DEBUG"'));
|
||||
// Not promoted to an array, not re-appended — DEBUG is already there.
|
||||
expect(buildSettingsOf(text, APP_DEBUG_CONFIG)).toContain(
|
||||
'SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";',
|
||||
);
|
||||
expect(text.match(/\bDEBUG\b/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("adds DEBUG alongside the user's own compilation conditions", () => {
|
||||
const {text} = inject(
|
||||
withDebugCondition(PLAIN, '"$(inherited) MY_DEBUG_UI"'),
|
||||
);
|
||||
// MY_DEBUG_UI must not be mistaken for DEBUG by a substring check.
|
||||
const debugSettings = buildSettingsOf(text, APP_DEBUG_CONFIG);
|
||||
expect(debugSettings).toContain('"$(inherited) MY_DEBUG_UI"');
|
||||
expect(debugSettings).toMatch(/^\s*DEBUG,$/m);
|
||||
});
|
||||
|
||||
it('prepends the Sync SPM Autolinking build phase', () => {
|
||||
const {text} = inject(PLAIN);
|
||||
expect(text).toContain('Sync SPM Autolinking');
|
||||
|
||||
@@ -190,6 +190,38 @@ describe('removeSpmInjection — the surgical inverse of add', () => {
|
||||
expect(fs.existsSync(schemePath)).toBe(false);
|
||||
});
|
||||
|
||||
// A Debug config that already carries DEBUG gets no edit at all, so there is
|
||||
// nothing for the marker to record — and nothing left behind. Injecting into
|
||||
// the scalar form regardless (addArrayStringValues dedupes by exact array
|
||||
// member, which the scalar never matches) would promote it to an array the
|
||||
// marker has no record of, and deinit would strand it.
|
||||
it('leaves a Debug config that already sets DEBUG alone, add through deinit', () => {
|
||||
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
|
||||
const head =
|
||||
'AA0000000000000000000901 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {';
|
||||
fs.writeFileSync(
|
||||
path.join(xcodeprojPath, 'project.pbxproj'),
|
||||
PLAIN.replace(
|
||||
head,
|
||||
`${head}\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";`,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const before = pbxprojOf(xcodeprojPath);
|
||||
|
||||
injectSpmIntoExistingXcodeproj({
|
||||
appRoot,
|
||||
reactNativeRoot: rnRoot,
|
||||
xcodeprojPath,
|
||||
});
|
||||
expect(pbxprojOf(xcodeprojPath)).toContain(
|
||||
'SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";',
|
||||
);
|
||||
|
||||
expect(removeSpmInjection({appRoot, xcodeprojPath}).status).toBe('removed');
|
||||
expect(pbxprojOf(xcodeprojPath)).toBe(before);
|
||||
});
|
||||
|
||||
it('preserves an unrelated edit made to the pbxproj after add', () => {
|
||||
const {appRoot, xcodeprojPath, rnRoot} = scaffoldApp();
|
||||
|
||||
|
||||
+54
-3
@@ -1093,6 +1093,25 @@ const INJECTED_ARRAY_SETTINGS = [
|
||||
},
|
||||
];
|
||||
|
||||
// Array build settings injected only into debug-flavored configurations.
|
||||
//
|
||||
// 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 the setting: CocoaPods injects it at `pod install`
|
||||
// time (react_native_post_install → set_build_setting
|
||||
// SWIFT_ACTIVE_COMPILATION_CONDITIONS = ["$(inherited)", "DEBUG"] on Debug).
|
||||
// An SPM app never runs CocoaPods, so without this `#if DEBUG` is false even
|
||||
// in a Debug build — AppDelegate.swift's `bundleURL()` skips the Metro URL,
|
||||
// falls back to a main.jsbundle that a Debug build never produced, and the app
|
||||
// dies at launch with "No script url provided … unsanitizedScriptURLString =
|
||||
// (null)" while Metro is running right there.
|
||||
//
|
||||
// Paired with RN_SPM_FLAVOR via flavorForBuildConfiguration, so a config that
|
||||
// links the debug xcframeworks also compiles its Swift with DEBUG.
|
||||
const DEBUG_ARRAY_SETTINGS = [
|
||||
{key: 'SWIFT_ACTIVE_COMPILATION_CONDITIONS', values: ['DEBUG']},
|
||||
];
|
||||
|
||||
/** The XCBuildConfiguration UUIDs of a target (via its buildConfigurationList). */
|
||||
function targetBuildConfigUuids(
|
||||
text /*: string */,
|
||||
@@ -1687,6 +1706,28 @@ function resolveHermesCliPathSetting(
|
||||
}
|
||||
}
|
||||
|
||||
/** Strip the surrounding plist quotes from a build-setting token, if any. */
|
||||
function unquotePlist(s /*: string */) /*: string */ {
|
||||
return s.replace(/^"/, '').replace(/"$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* The individual values a build setting already carries, unquoted — for both
|
||||
* shapes a pbxproj uses: the array form Xcode writes for a multi-value setting
|
||||
* (`("$(inherited)", DEBUG)`) and the scalar form the app template and
|
||||
* hand-edits use (`"$(inherited) DEBUG"`). Membership, not substring: the
|
||||
* latter would read `MY_DEBUG_FLAG` as `DEBUG` already being set and silently
|
||||
* skip the injection.
|
||||
*/
|
||||
function buildSettingValueTokens(value /*: string */) /*: Set<string> */ {
|
||||
return new Set(
|
||||
value
|
||||
.split(/[\s,()]+/)
|
||||
.filter(Boolean)
|
||||
.map(unquotePlist),
|
||||
);
|
||||
}
|
||||
|
||||
function mergeReactBuildSettings(
|
||||
input /*: string */,
|
||||
configUuid /*: string */,
|
||||
@@ -1732,6 +1773,9 @@ function mergeReactBuildSettings(
|
||||
const createdScalars /*: Array<string> */ = [];
|
||||
const arraySettings = [
|
||||
...INJECTED_ARRAY_SETTINGS,
|
||||
...(flavorForBuildConfiguration(configurationName) === 'debug'
|
||||
? DEBUG_ARRAY_SETTINGS
|
||||
: []),
|
||||
...frameworkArrayBuildSettings(flavoredFrameworks),
|
||||
];
|
||||
for (const {key, values} of arraySettings) {
|
||||
@@ -1743,10 +1787,17 @@ function mergeReactBuildSettings(
|
||||
if (existing == null) {
|
||||
createdArrayKeys.push(key);
|
||||
} else {
|
||||
const fresh = values.filter(v => !existing.value.includes(v));
|
||||
if (fresh.length > 0) {
|
||||
appendedArrayValues[key] = fresh;
|
||||
const present = buildSettingValueTokens(existing.value);
|
||||
const fresh = values.filter(v => !present.has(unquotePlist(v)));
|
||||
if (fresh.length === 0) {
|
||||
// Nothing to add. Skip addArrayStringValues entirely: its dedupe is by
|
||||
// EXACT array member, so a value the user carries in the scalar form
|
||||
// (`SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"`) would
|
||||
// otherwise be promoted to an array and re-appended — an edit `deinit`
|
||||
// has no record of and so could never reverse.
|
||||
continue;
|
||||
}
|
||||
appendedArrayValues[key] = fresh;
|
||||
}
|
||||
text = addArrayStringValues(text, d, key, values);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user