Repository navigation
Conversation
…t.pbxproj `spm add` and `spm update` now write React Native's build settings to ReactNativeSPM/ReactNativeSPM.xcconfig and the two build-script bodies to ReactNativeSPM/Scripts/. The project's build configurations use the xcconfig as their base, and the build phases only run the script files. A version bump then leaves project.pbxproj unchanged (local-path mode). REACT_NATIVE_PATH is now $(SRCROOT)-relative through node_modules/react-native, not a resolved pnpm store path.
rachel11026725
left a comment
There was a problem hiding this comment.
yarn jest --no-cache -i packages/react-native/scripts/spm
cipolleschi
left a comment
There was a problem hiding this comment.
Thanks for this, moving these values out of project.pbxproj should help a lot with merge conflicts! Given the size of the change, have we considered validating it with a real Xcode build, e.g. a CI job that builds the helloworld app in SwiftPM mode? The PR mentions that a full Xcode build with the generated xcconfig wasn't tested.
| 'supports SPM-only targets', | ||
| }; | ||
| } | ||
| const foreign = foreignBaseConfiguration( |
There was a problem hiding this comment.
What happens on the next spm update after the user follows this advice and #includes the generated xcconfig from their own file? As far as I can tell, foreignBaseConfiguration still finds a base config that isn't ours and refuses again, since it doesn't check for the #include. Have we considered detecting the include (or only refusing when it's missing) so users have a way forward? A test for this case would also help.
There was a problem hiding this comment.
Good catch. The advice led nowhere. Fixed in b797547: spm now follows the #include and #include? lines of the configuration's base xcconfig. It resolves each path from the folder of the file that contains the line, follows nested includes, and stops on a cycle. When the chain reaches ReactNativeSPM.xcconfig, spm accepts the configuration and leaves its base unchanged. The $(inherited) check still applies. A file that cannot be read, or that does not include ours, is still refused. New tests cover the accepted chain, #include?, a cycle, a commented-out include, and a full add → sync → deinit cycle.
| reason: | ||
| `the ${foreign.config} configuration is already based on ` + | ||
| `${foreign.file}, and React Native does not edit xcconfig files it ` + | ||
| `does not own — #include ${GENERATED_XCCONFIG_PATH} (relative to the ` + |
There was a problem hiding this comment.
Could "relative to the project directory" be misleading here? Xcode resolves an xcconfig #include from the folder of the file that contains it, so a file in Config/ would need ../ReactNativeSPM/ReactNativeSPM.xcconfig. Have we considered computing the exact relative path from foreign.file and printing the line to paste?
There was a problem hiding this comment.
You're right. Xcode resolves #include from the folder of the including file. Fixed in b797547: when the file is on disk, the message now prints the exact line to add. For example, for App.xcconfig in Config/: add #include "../ReactNativeSPM/ReactNativeSPM.xcconfig" to App.xcconfig instead. When the file cannot be found, the message says that the path is relative to the folder of that xcconfig.
| configNames.flatMap(name => { | ||
| const flavor = flavorForBuildConfiguration(name); | ||
| return [ | ||
| `RN_SPM_FLAVOR[config=${name}] = ${flavor}`, |
There was a problem hiding this comment.
What happens if a user duplicates a configuration in Xcode, e.g. Release → "Staging", without re-running spm update? The copy keeps the base config reference, but [config=Release] doesn't match "Staging", so RN_SPM_FLAVOR would be unset and the framework paths would resolve empty. Before this change, duplicating a configuration also copied the injected settings. Have we considered adding an unconditioned default (e.g. RN_SPM_FLAVOR = release) before the conditional lines? It might also help with names that contain spaces.
There was a problem hiding this comment.
Fixed in b797547: the xcconfig now has RN_SPM_FLAVOR = release before the [config=…] lines. A configuration that is duplicated in Xcode, such as "Staging", now links the release frameworks and does not get empty paths. A duplicate of Debug also gets release until the next spm update adds its own [config=…] line. I did not add an unconditioned DEBUG condition, because a wrong DEBUG is worse than a release fallback.
| `project directory) from ${foreign.file} instead`, | ||
| }; | ||
| } | ||
| const hiding = listSettingsHidingXcconfig(text, configUuids); |
There was a problem hiding this comment.
Have we considered how this affects existing apps on their next spm update? Apps that base configurations on their own xcconfig (common for environment config), or that set a list setting without $(inherited), worked before and would now be refused. Should the changelog call this out as [BREAKING], or should these cases warn and fall back instead of refusing?
There was a problem hiding this comment.
SwiftPM support is still a Preview feature, so I kept this as [CHANGED], not [BREAKING]. I added the effect to the changelog entry instead. Both cases now stop with a message that gives the exact fix. For a configuration based on its own xcconfig, the message gives the #include line to add, and spm accepts the configuration after that (fixed in b797547 for your first comment). For a setting without $(inherited), the message names the setting. I chose a refusal over a silent fallback so that we do not keep two ways of writing the settings. If you prefer the [BREAKING] tag anyway, I can change it.
| 'EmbedFlavoredFrameworks', | ||
| ); | ||
| const embedScript = buildEmbedFrameworksScript(flavoredFrameworks); | ||
| const embedInputs = [ |
There was a problem hiding this comment.
Should $(SRCROOT)/ReactNativeSPM/Scripts/embed-flavored-frameworks.sh be listed in embedInputs? Now that the phase text in the pbxproj never changes, Xcode's incremental build may not notice when only the script file changes, so the phase could be skipped until a clean build.
There was a problem hiding this comment.
Yes. Fixed in b797547: $(SRCROOT)/ReactNativeSPM/Scripts/embed-flavored-frameworks.sh is now an input of the embed phase. The path has no version in it, so project.pbxproj still stays the same across a version bump, and the existing test checks this. The sync phase has no inputs or outputs, so Xcode runs it on every build, and it does not need this change.
| SYNC_AUTOLINKING_SCRIPT_PATH, | ||
| EMBED_FRAMEWORKS_SCRIPT_PATH, | ||
| ]) { | ||
| fs.rmSync(path.join(appRoot, relPath), {force: true}); |
There was a problem hiding this comment.
Nit: what happens if Debug is re-based on the user's own xcconfig, which #includes ours, while Release is still based on ours? ownsXcconfig would be true and deinit would delete the file that the user's xcconfig still includes. Is that case worth handling, e.g. by keeping the file whenever any configuration has been re-based?
There was a problem hiding this comment.
Fixed in b797547: deinit now deletes ReactNativeSPM.xcconfig only when no configuration of the target uses a different xcconfig, and when none was recorded as including ours. Otherwise it keeps the file and logs which configurations can still include it. The scripts are still deleted.
…lavor, embed script input, safe deinit
Summary:
npx react-native spm addandspm updateedit the app'sproject.pbxproj. Today they write React Native's build settings and the full text of two build scripts into that file. Some of these values change with every React Native version. So a routinespm updategives aproject.pbxprojdiff, and teams get merge conflicts in a file that is hard to read.This PR moves those values out of
project.pbxprojinto files that React Native generates next to it:ReactNativeSPM/ReactNativeSPM.xcconfigholds the build settings. Each app build configuration uses it as its base configuration.ReactNativeSPM/Scripts/sync-autolinking.shandReactNativeSPM/Scripts/embed-flavored-frameworks.shhold the script bodies. The build phases inproject.pbxprojonly run these files.The result: when a version bump does not change the set of packages or plugin frameworks,
spm updategives noproject.pbxprojdiff. The changes go to the generated files.Commit the
ReactNativeSPM/directory with the project. Xcode reads these files before the build starts, so they must exist on a fresh clone. They are not in the ignoredbuild/directory for that reason.project.pbxprojstill holds the package references, the product dependencies, and the build phases. Xcode has no other place for them.REACT_NATIVE_PATHalso changes. It is now$(SRCROOT)/<path>throughnode_modules/react-native. Before, with pnpm, it could be the resolved.pnpm/react-native@<version>_<hash>path. That path is specific to one machine, and it was committed.Limits
project.pbxprojstill changes on each bump.project.pbxprojstill change. Xcode requires these paths in the project file.spm addandspm updatenow refuse a target in two new cases, with a message that tells you what to do:#includethe generated one. React Native does not edit xcconfig files that it does not own. The message gives the exact#includeline to add to your file. After you add it,spmaccepts the configuration and leaves its base unchanged.$(inherited). The target value would hide the React Native value. The message names the settings to fix.]are not escaped in the[config=<name>]conditions.spm update(for example, a copy of Release named "Staging") gets the release flavor until the nextspm updateadds its own condition.Existing apps
No manual migration is necessary. On the next
spm update, React Native removes the build settings that it wrote before (it records them in its marker file), then writes the xcconfig file.Details for reviewers
generate-spm-xcodeproj.js:generateReactNativeXcconfig()writes the settings. Flavor-specific settings (RN_SPM_FLAVOR,SWIFT_ACTIVE_COMPILATION_CONDITIONS) use Xcode's[config=<name>]conditions, with the real configuration names from the project. One file serves all configurations.mergeReactBuildSettings()is removed.RN_SPM_FLAVOR = releasecomes before the[config=<name>]lines, as the fallback for configurations that the file does not list.generatedScriptWrapper()writes the stable build-phase text. If the script file is missing, the build fails with a message to runnpx react-native spm update. The embed script is an input path of its phase, so Xcode runs the phase again when the script changes.planInjection()follows the#includechain of a configuration's base xcconfig. Each path resolves from the folder of the file that contains it, and a cycle stops the walk. The configurations that include the generated file are recorded in the marker asxcconfig.includedByConfigUuids.REACT_NATIVE_PATHrelative to${PODS_ROOT}. A target value outranks an xcconfig value, so injection removes that one value. Deinit does not put it back, because the value resolves to an empty path when CocoaPods is not used.linkedReactNativeRoot()finds thenode_modules/react-nativelink that resolves to the real React Native directory, to build the stable path.injectSpmIntoPbxproj()now takes{fromAppRoot, fromSrcRoot}as its third argument.xcconfig-files.js(new): xcconfig path resolution and#includeparsing, moved out ofios-deployment-target.jsso both files share it.ios-deployment-target.jsbehaves as before.spm-pbxproj.js:ensureScalarField()takes an{after}option, to putbaseConfigurationReferenceat the correct position.Changelog:
[IOS] [CHANGED] - SwiftPM:
spm addandspm updatewrite React Native's build settings toReactNativeSPM/ReactNativeSPM.xcconfigand its build scripts toReactNativeSPM/Scripts/, so a version bump no longer changesproject.pbxproj. A configuration that uses its own xcconfig must now#includethe generated file, and a target setting that hides React Native's value must add$(inherited).spmprints the exact fix in both cases.Test Plan:
Result: 21 suites and 1055 tests pass. The new tests failed before the change.
New tests:
inject-spm-xcodeproj-test.js: a version bump (new React Native path and new framework slice data) gives a byte-identicalproject.pbxproj. Other tests check that each configuration uses the generated xcconfig, that a${PODS_ROOT}-basedREACT_NATIVE_PATHis removed, that a user xcconfig is accepted when its#includechain reaches the generated file and refused otherwise, that the refusal gives the exact#includeline, and that the embed script is an input of its phase.remove-spm-injection-test.js: a list setting without$(inherited)is refused. Settings from an older injection move into the xcconfig on the next update. Deinit removes the xcconfig reference and the generated files, keeps a base configuration that the user changed, and keeps the generated xcconfig while a user xcconfig may include it. A user xcconfig that includes the generated one survives add → sync → deinit unchanged. pnpm layouts give$(SRCROOT)/…/node_modules/react-native. A nearernode_modules/react-nativethat is a different install is skipped.generate-spm-xcodeproj-test.jsandspm-pbxproj-test.js: the xcconfig content and the{after}option.Also run:
flow focus-checkongenerate-spm-xcodeproj.jsandspm-pbxproj.js: 0 errors.prettier --checkandeslint --max-warnings 0on the changed files: no problems.Not tested:
--xcodeproj). The generated files are written relative to the app root. This case was also not supported before this change.