Skip to content

Commit 84e60bc

Browse files
chrfalcht
authored andcommitted
SwiftPM: declare ReactAppHeaders in the codegen placeholder package (#59293)
Summary: A SwiftPM app can get stuck when setup skips codegen. Setup skips codegen when you pass `--skipCodegen`, when codegen fails ("Continuing anyway"), or when the codegen script is missing. In each case `spm add` / `spm update` writes a placeholder `build/generated/ios/Package.swift`, which the "Sync SPM Autolinking" hook replaces during the build. The placeholder declared only `ReactCodegen` and `ReactAppDependencyProvider`. Every autolinked or scaffolded library manifest also depends on `ReactAppHeaders` from that package. So Xcode fails package resolution: ``` product 'ReactAppHeaders' required by package 'reactnativesafeareacontext' target 'ReactNativeSafeAreaContext' not found in package 'React-GeneratedCode'. ``` Xcode resolves packages before the scheme pre-action and the build phases, so neither hook runs, and every later build fails the same way. The only way out is to run setup again with codegen, and the error does not say so. This change makes the placeholder declare `ReactAppHeaders` (every entry of `REACT_CODEGEN_PRODUCTS`) too. `SPM_PRODUCT_PACKAGES` is unchanged, because it also decides which products the app target links, and the app must not link `ReactAppHeaders`. Known limit, not changed here: after this fix the first build still fails once, because the hook generates the library's codegen headers during that build, after the package graph is resolved (for example `'react/renderer/components/safeareacontext/Props.h' file not found`). The next build succeeds with no other change. ## Changelog: [IOS] [FIXED] - SwiftPM: a project set up with skipped codegen no longer fails package resolution on `ReactAppHeaders` Pull Request resolved: #59293 Test Plan: Unit tests: two new tests for `ensureStubPackages` in `generate-spm-xcodeproj-test.js`. One checks that the placeholder declares each codegen product exactly once. One checks that an existing `Package.swift` is not changed. ``` yarn jest --no-cache -i packages/react-native/scripts/spm/__tests__/generate-spm-xcodeproj-test.js ``` - Without the source change: 1 failed (`ReactAppHeaders` declared 0 times). - With the source change: 66 passed, 66 total. - ESLint (`--max-warnings 0`) and Prettier pass on both files. Fresh app (Xcode 27.1): `0.89.0-nightly-20261021-c7429b0e9` + `react-native-safe-area-context` 5.10.1, `npx react-native spm add --deintegrate --skipCodegen`, then `xcodebuild -resolvePackageDependencies` and `xcodebuild build`: - Without the fix: resolution fails with the error above; the build stops at resolution (exit 74) and no hook runs. - With the fix applied to the app's copy of the script: resolution passes; the first build runs the sync hook, which replaces the placeholder, and then fails on the missing library codegen headers (exit 65); the second build succeeds. Not run: full `yarn flow-check` (a scoped Flow check showed no errors in the changed files). Reviewed By: andrewdacenko Differential Revision: D126658850 Pulled By: cipolleschi fbshipit-source-id: 0ef10c5180f5932cfd761b16418e9a368401550b (cherry picked from commit e3aa9a1)
1 parent a25ec46 commit 84e60bc

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
buildEmbedFrameworksScript,
1616
buildSchemePreActionScript,
1717
buildSyncAutolinkingScript,
18+
ensureStubPackages,
1819
flavorForBuildConfiguration,
1920
frameworkConditionalSettings,
2021
generateReactNativeXcconfig,
@@ -895,3 +896,52 @@ describe('readScriptPhasesManifest', () => {
895896
]);
896897
});
897898
});
899+
900+
describe('ensureStubPackages', () => {
901+
const {REACT_CODEGEN_PRODUCTS} = require('../spm-utils');
902+
let appRoot;
903+
let logSpy;
904+
905+
beforeEach(() => {
906+
appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'spm-stub-packages-'));
907+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
908+
});
909+
afterEach(() => {
910+
logSpy.mockRestore();
911+
fs.rmSync(appRoot, {recursive: true, force: true});
912+
});
913+
914+
const codegenManifestPath = () =>
915+
path.join(appRoot, 'build', 'generated', 'ios', 'Package.swift');
916+
917+
it('declares every codegen product exactly once in the codegen stub', () => {
918+
ensureStubPackages(appRoot);
919+
const manifest = fs.readFileSync(codegenManifestPath(), 'utf8');
920+
921+
const products = [
922+
'ReactCodegen',
923+
'ReactAppDependencyProvider',
924+
...REACT_CODEGEN_PRODUCTS,
925+
];
926+
const declarationCounts = Object.fromEntries(
927+
products.map(product => [
928+
product,
929+
manifest.split(`.library(name: "${product}"`).length - 1,
930+
]),
931+
);
932+
933+
expect(declarationCounts).toEqual(
934+
Object.fromEntries(products.map(product => [product, 1])),
935+
);
936+
});
937+
938+
it('leaves an existing codegen Package.swift untouched', () => {
939+
const existing = '// swift-tools-version: 5.9\n// real codegen output\n';
940+
fs.mkdirSync(path.dirname(codegenManifestPath()), {recursive: true});
941+
fs.writeFileSync(codegenManifestPath(), existing, 'utf8');
942+
943+
ensureStubPackages(appRoot);
944+
945+
expect(fs.readFileSync(codegenManifestPath(), 'utf8')).toBe(existing);
946+
});
947+
});

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const {
5454
AUTOLINKED_PACKAGE_NAME,
5555
REACT_CODEGEN_APP_PRODUCTS,
5656
REACT_CODEGEN_PACKAGE_NAME,
57+
REACT_CODEGEN_PRODUCTS,
5758
REACT_NATIVE_PACKAGE_NAME,
5859
REACT_NATIVE_PRODUCTS,
5960
isValidScriptPhaseId,
@@ -1136,6 +1137,18 @@ function ensureStubPackages(appRoot /*: string */) /*: void */ {
11361137
}
11371138
}
11381139

1140+
// Library manifests depend on these products, but the app must not link
1141+
// them, so they are stubbed here instead of listed in SPM_PRODUCT_PACKAGES.
1142+
for (const def of byPath.values()) {
1143+
if (def.packageName === REACT_CODEGEN_PACKAGE_NAME) {
1144+
for (const product of REACT_CODEGEN_PRODUCTS) {
1145+
if (!def.products.includes(product)) {
1146+
def.products.push(product);
1147+
}
1148+
}
1149+
}
1150+
}
1151+
11391152
for (const [relPath, def] of byPath) {
11401153
const pkgDir = path.join(appRoot, relPath);
11411154
const pkgSwiftPath = path.join(pkgDir, 'Package.swift');

0 commit comments

Comments
 (0)