Repository navigation
Conversation
…51033) # Why The committed Xcode project of the `apps/minimal-swiftpm` tester links a precompiled ExpoFont framework. On a fresh checkout, nobody has precompiled expo-font, so the build fails at the link step: ``` clang: error: no such file or directory: '.../ios/build/xcframeworks/debug/plugins/expo-font.xcframework/ios-arm64_x86_64-simulator/ExpoFont.framework/ExpoFont' ``` The project was generated on a machine that had expo-font precompiled, and that state was committed. expo-font is pure Swift, so the Expo SwiftPM plugin can build it from source. Tracked in ENG-26916 ("row 2"). # How Remove every ExpoFont slot from `project.pbxproj` (embed phase, build settings, search path, linker flag, Debug and Release) and from the React Native marker `.spm-injected.json`. The removals are exactly the ExpoFont hunks that React Native's `setup-apple-spm.js update` produces for this precompiled set. The other changes `update` makes are not taken: machine-specific pnpm paths for `REACT_NATIVE_PATH`, `/bin/bash` shell paths, and the removal of the JSI macOS/tvOS slices. The tester now needs these modules precompiled: `expo-modules-core` (also produces ExpoModulesWorklets), `expo-modules-jsi` and `expo-file-system`. expo-file-system stays precompiled because it mixes Swift and Objective-C and has no `Package.swift`, so the plugin cannot build it from source, and `expo` depends on it. Note: with React Native's open PR react/react-native#58827, a machine that has expo-font precompiled will get a sync mismatch error for this project. Run `npx react-native spm` to regenerate it locally. # Test Plan Fresh worktree, `pnpm install --ignore-scripts`, `turbo run build --filter='minimal-swiftpm^...'`, then precompile only core, jsi and file-system (Debug and Release) with `node tools/bin/expotools.js prebuild`, React Native 0.88.0-rc.3. Then `setup-apple-spm.js sync`, `generate-spm-package.js`, and `xcodebuild` (Debug, iOS Simulator): - Before: `** BUILD FAILED **`, link error above. - After: `** BUILD SUCCEEDED **` twice. The first build compiles 32 `packages/expo-font/ios/*.swift` files from source, and the app's `Frameworks/` has no `ExpoFont.framework`. Release was not built. Its configuration has the same removal, identical to the `update` output. # Checklist - [ ] No CHANGELOG entry: this changes a test app only, not a published package. - [x] `plutil -lint` passes on the project; the marker JSON parses.
| # autolinker already printed an \`error:\` line per dep (so Xcode shows them | ||
| # and the fix). Fail the build — the developer must run | ||
| # \`npx react-native spm scaffold\` from a terminal to generate the manifest. | ||
| # Exit 2 = a problem only a terminal command fixes (e.g. a dependency |
There was a problem hiding this comment.
What happens if the only change is to project.pbxproj, e.g. after pulling a teammate's commit? As far as I can tell, the pbxproj isn't one of the stale-check inputs, so sync (and this check) wouldn't run and we'd still hit the original link error. The same might apply to a module precompiled after the last spm update, unless its output is under a plugin's watchPaths. Have we considered adding $PROJECT_FILE_PATH/project.pbxproj as a staleness input, or running this check outside the STALE gate?
There was a problem hiding this comment.
Good point. In 76972bd, spm add and spm update record the linked frameworks in .spm-injected.json, and that file is now a sync input. A teammate's pull that changes what the project links starts the sync and this check on the next build. I did not make project.pbxproj an input, because Xcode rewrites it on many IDE edits (your comment on #58828). For a module that is precompiled after the last sync: the check sees it only if the plugin lists the output folder in watchPaths. Before #58828, a watch path that did not exist at the last sync was dropped from the list. #58828 fixes that.
| let linkedPluginNames /*: ?Set<string> */ = null; | ||
| if ( | ||
| fs.existsSync( | ||
| path.join(appRoot, 'build', 'xcframeworks', FLAVORED_FRAMEWORKS_MANIFEST), |
There was a problem hiding this comment.
What happens if build/xcframeworks/flavored-frameworks.json is missing? It looks like linkedPluginNames stays null and the reverse check ("project links a framework no plugin provides") is skipped without a warning. Have we considered recording the linked {id, frameworkName} list in .spm-injected.json at inject time? That would avoid regex-parsing inputPaths/outputPaths and wouldn't depend on the staged manifest.
There was a problem hiding this comment.
Agreed. In 76972bd, the check reads the recorded {id, frameworkName} list from .spm-injected.json, with no regex parsing and no dependency on the staged manifest. For a marker from an older version, it still parses the embed phase. If the manifest is also missing in that case, it now prints a warning that the reverse check was skipped and that spm update records the list.
| for (const frameworkName of linkedPluginNames ?? []) { | ||
| if (!pairedNames.has(frameworkName)) { | ||
| problems.push( | ||
| `error: The Xcode project links ${frameworkName}.framework, but no autolinking plugin provides it on this machine. Precompile ${frameworkName}, or run \`npx react-native spm\` to update the project.`, |
There was a problem hiding this comment.
Have we considered how this works for teams that commit the pbxproj? If precompiling is per-machine, running npx react-native spm on one machine could remove a framework that another teammate's run then adds back. Would it make sense to suggest precompiling first, or is the expectation that every machine precompiles the same set?
There was a problem hiding this comment.
The expectation is that every machine precompiles the same set: the committed project is the contract. In 76972bd, the reverse message says to precompile the framework and then run npx react-native spm sync. It also warns that spm update instead removes the framework from the committed project for the whole team. The forward message says that spm update adds it for the whole team. Projects with a different set on each machine are a known gap, and this PR does not solve that.
| try { | ||
| deps.assertPluginFrameworksLinked(appRoot); | ||
| } catch (e) { | ||
| fs.rmSync(stampPath, {force: true}); |
There was a problem hiding this comment.
What happens to build time while the mismatch persists? If I read it correctly, the scheme pre-action runs sync, fails, and deletes the stamp. Xcode ignores the pre-action's exit code, so the build phase then runs the full sync (codegen + autolinking) again before failing. Is it expected that every failing build runs sync twice?
There was a problem hiding this comment.
You're right, every failing build ran the full sync twice. In 76972bd, a mismatch saves the error lines in build/generated/autolinking/.spm-plugin-mismatch and refreshes the stamp. When no sync input has changed, the next hook prints the saved errors and fails without a sync. So a failing build runs one sync, and later builds run none until an input changes. A passing check deletes the file. Shell tests run the generated script for each case.
| // The per-dep `error:` lines were already printed by the autolinker. | ||
| if ( | ||
| e instanceof MissingManifestError || | ||
| e instanceof PluginFrameworkMismatchError |
There was a problem hiding this comment.
Nit: should the require.main === module entry point in sync-spm-autolinking.js also map PluginFrameworkMismatchError to exit code 2, as it does for RemoteVersionError? Right now, running that script directly would print a stack trace and exit 1, which the build phase treats as a warning.
There was a problem hiding this comment.
Fixed in 76972bd: the direct entry point now maps PluginFrameworkMismatchError and MissingManifestError to exit code 2, with only the message. MissingManifestError had the same gap on main. The handler is now an exported function, so tests can call it.
An autolinking plugin can provide precompiled frameworks, but only `spm add` and `spm update` add them to the Xcode project's linker and embed settings. When the two disagree, the build failed late with an unclear link error or "no such module". The build-time sync now compares them, prints an `error:` line for each framework, and exits with code 2 so the build fails early. The fix is to run `npx react-native spm`, or to precompile the missing framework.
… the built project
…h errors, recovery step, exit codes
76972bd to
ef71d3d
Compare
Summary:
An autolinking plugin, such as Expo, can provide precompiled frameworks. The build-time
syncreads the list of these frameworks from the plugin.npx react-native spm addandspm updatewrite a second list: the frameworks that the Xcode project links. When the two lists differ, the build fails at the link step with "no such file … plugins/.xcframework" or "no such module". That error does not say what is wrong or what to run.This happens, for example, when a module is precompiled after the last
spm update, or when a teammate commits a project that links a module this machine does not provide.This PR makes
synccompare the two lists before the build continues. For each framework that differs, it prints an Xcodeerror:line that names the framework and what to do, and it exits with code 2. Existing projects already stop the build on exit code 2.How it works:
spm addandspm updaterecord the linked frameworks in.spm-injected.json, next to the project. The check reads them there. For a marker from an older version, the check reads the embed phase ofproject.pbxproj, as before..spm-injected.jsonis now a sync input. A pull that changes what the project links starts a sync on the next build.project.pbxprojis not a sync input, because Xcode rewrites it on many IDE edits.syncsaves the error lines inbuild/generated/autolinking/.spm-plugin-mismatchand refreshes its stamp. Later builds print the saved errors and stop without running the full sync again. When any sync input changes, the next build runs the sync and the check again. When the check passes,syncdeletes the file.npx react-native spm sync. Runningspm updateinstead removes it from the committed project for the whole team.npx react-native spm updateto add it for the whole team.npx react-native spmnow listssync, because the error tells users to run it.syncstill never writesproject.pbxproj. Onlyspm addandspm updatechange what the project links.PROJECT_FILE_PATHto find the project being built.This PR is independent of #58828. They can land in either order. Both change the generated sync check, so the second one to land needs a rebase.
Changelog:
[IOS] [ADDED] - SwiftPM: the build stops with a clear error when precompiled plugin frameworks and the Xcode project disagree, and the error says what to run.
Test Plan:
Result: 22 suites and 1046 tests pass. The new tests failed before the change. One exception: the test that a newer
project.pbxprojalone does not start a sync passed at once, because it guards against a regression.plugin-framework-mismatch-test.jsuses a real injected project. It runs each case twice: with the list recorded in the marker, and with an older marker that has no list. It covers a framework that is missing in either list, a renamed framework, matching lists, a name with a space, a project that is not injected or has no embed phase, a missing manifest, and two injected projects. It also checks that runningspm addorspm updatetwice gives the sameproject.pbxproj, and the warning when an older marker and a missing manifest make the check skip one direction.sync-spm-autolinking-test.jschecks that a mismatch saves the errors and refreshes the stamp, that a passing check deletes the saved errors, the recovery after a precompile, and the exit codes of the script's direct entry point.generate-spm-xcodeproj-test.jsruns the generated sync check under/bin/bash. It checks that a later build prints the saved errors without a sync, that a changed sync input runs the sync again, that a newer.spm-injected.jsonstarts a sync, and that a newerproject.pbxprojalone does not.Not tested:
setup-apple-spm.js.main()has no test seam. The change is oneinstanceofcheck.spm-scripts.md.