From abe1c5f0411ea1f360ca8705d16870366a84cf27 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Fri, 23 Oct 2026 02:36:57 -0700 Subject: [PATCH] Stop reading podspec header search paths during SwiftPM sync (#59365) Summary: In the SwiftPM pipeline, every sync reads the podspec of each npm dependency to collect its `HEADER_SEARCH_PATHS`. (That can even start a `pod ipc spec` process for each dependency), and the result is never used. This change removes the read and the code that carried its result: - `extractPodspecHeaderSearchPaths` and its call in `autolinkingDepToSpmTarget` (`generate-spm-autolinking.js`) - the `headerSearchPaths` field on `SpmTarget` and `SynthPackageSpec` (`spm-types.js`) - the `root/` prefix step in `main()` and the `.headerSearchPath(...)` output in `generateSynthPackageSwift` The podspec reader (`read-podspec.js`) and the scaffolder (`scaffold-package-swift.js`) still use header search paths. They do not change. This PR does not depend on other open PRs. ## Changelog: [INTERNAL] [REMOVED] - Stop reading unused podspec header search paths during SwiftPM sync Pull Request resolved: https://github.com/react/react-native/pull/59365 Test Plan: - New test in `generate-spm-autolinking-test.js`: an npm dependency whose podspec sets `HEADER_SEARCH_PATHS` must not cause a call to the podspec reader. Before the change it failed with `Expected number of calls: 0, Received number of calls: 1`. After the change it passes. - `node_modules/.bin/jest --no-cache -i packages/react-native/scripts/spm`: 23 of 23 suites and 1129 of 1129 tests pass. - Generated output for RNTester, before and after, with `node packages/react-native/scripts/spm/generate-spm-autolinking.js --app-root packages/rn-tester --react-native-root packages/react-native --autolinking-json --output `: - With RNTester's npm dependencies, both runs stop with the same missing-manifest error and write no files. - With only RNTester's app-local modules, both runs write the same 7 `Package.swift` files. `diff -r` finds no differences. - ESLint and Prettier pass on the changed files. Flow finds no errors in them. I did not run a full `yarn flow-check`, because it fails locally on unrelated vendored files. Reviewed By: andrewdacenko Differential Revision: D126936104 Pulled By: cipolleschi fbshipit-source-id: 178520a0fe5037aa646c20d2b9ffafd178b4888f (cherry picked from commit 4bba13d4197ff9eba231391a1bf2a9c544f6d702) Conflict resolution for 0.88-stable: kept 0.88's REACT_CXX_CONFIG_DEFINES for cxxSettings instead of main's reactCxxDefines(spec.cxxApiDefines), which is not on 0.88. --- .../generate-spm-autolinking-test.js | 43 +++++++ .../scripts/spm/generate-spm-autolinking.js | 106 +++--------------- .../react-native/scripts/spm/spm-types.js | 14 --- 3 files changed, 59 insertions(+), 104 deletions(-) diff --git a/packages/react-native/scripts/spm/__tests__/generate-spm-autolinking-test.js b/packages/react-native/scripts/spm/__tests__/generate-spm-autolinking-test.js index aa2d2170ec73..82c308284c71 100644 --- a/packages/react-native/scripts/spm/__tests__/generate-spm-autolinking-test.js +++ b/packages/react-native/scripts/spm/__tests__/generate-spm-autolinking-test.js @@ -2287,4 +2287,47 @@ describe('autolinkingDepToSpmTarget', () => { ), ).toThrow(/expandSpmDependencies/); }); + + // Every npm dep is self-managed or reported missing, so nothing would + // consume header search paths read from its podspec — and the read can + // spawn `pod ipc spec`. + it('does not read the podspec of a dep that declares HEADER_SEARCH_PATHS', () => { + const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spm-hsp-')); + try { + fs.writeFileSync( + path.join(sourceDir, 'Consumer.podspec'), + "Pod::Spec.new do |s|\n s.name = 'Consumer'\n" + + " s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '\"$(PODS_TARGET_SRCROOT)/common/cpp\"' }\nend\n", + ); + const readPodspecCached = jest.fn(() => ({ + headerSearchPaths: ['$(PODS_TARGET_SRCROOT)/common/cpp'], + })); + let isolated; + jest.isolateModules(() => { + jest.doMock('../read-podspec', () => ({ + ...jest.requireActual('../read-podspec'), + readPodspecCached, + })); + isolated = require('../generate-spm-autolinking'); + }); + + const target = isolated.autolinkingDepToSpmTarget( + 'react-native-consumer', + { + name: 'react-native-consumer', + root: sourceDir, + platforms: {ios: {sourceDir}}, + swiftName: 'ReactNativeConsumer', + }, + '/out', + new Map(), + ); + + expect(readPodspecCached).not.toHaveBeenCalled(); + expect(target).not.toHaveProperty('headerSearchPaths'); + } finally { + jest.dontMock('../read-podspec'); + fs.rmSync(sourceDir, {recursive: true, force: true}); + } + }); }); diff --git a/packages/react-native/scripts/spm/generate-spm-autolinking.js b/packages/react-native/scripts/spm/generate-spm-autolinking.js index 17ecbbe7bef9..5d425e3e247d 100644 --- a/packages/react-native/scripts/spm/generate-spm-autolinking.js +++ b/packages/react-native/scripts/spm/generate-spm-autolinking.js @@ -71,7 +71,7 @@ const { MIN_IOS_VERSION_SUPPORTED, sanitizeIosDeploymentTarget, } = require('./ios-deployment-target'); -const {findPodspecs, readPodspecCached} = require('./read-podspec'); +const {findPodspecs} = require('./read-podspec'); const { AUTOLINKED_PACKAGE_NAME, REACT_CODEGEN_PACKAGE_NAME, @@ -801,58 +801,6 @@ function expandSpmSourceGlobs( return micromatch(walkSourceFiles(sourcePath), patterns).sort(); } -/** - * Converts an autolinking.json dependency to an SPM target spec. - * Returns null if the dependency doesn't have iOS support. - * - * `swiftNameByNpm` maps each autolinked dep's npm name to its resolved Swift - * name (populated by expandSpmDependencies from the dep's podspec and - * `spm.name`). Every name this function emits comes from there — see - * requireSwiftName. - */ -/** - * Read the dep's podspec (if any) and extract its declared - * `pod_target_xcconfig` HEADER_SEARCH_PATHS, substituted relative to the dep - * source dir. Returns paths suitable for an SPM `.headerSearchPath()` - * directive whose target.path is the dep root (entries are NOT yet prefixed - * with the synth wrapper's `root/` — the emission site adds that prefix - * because the wrapper's target.path is `.`, not the source dir). - * - * Without these, path-style angle includes like - * `` (used by - * react-native-safe-area-context, reanimated, screens, etc.) fail to resolve - * because the headers live under the dep's `common/cpp/` rather than under - * the framework-imported xcframework headers. - */ -function extractPodspecHeaderSearchPaths( - sourceDir /*: string */, -) /*: Array */ { - const podspecPath /*: ?string */ = findPodspecs(sourceDir)[0]; - if (podspecPath == null) return []; - - let model; - try { - model = readPodspecCached(podspecPath); - } catch { - return []; - } - - const out /*: Array */ = []; - for (const raw of model.headerSearchPaths) { - const substituted = raw - .replace(/\$\(PODS_TARGET_SRCROOT\)/g, '.') - .replace(/\$\{PODS_TARGET_SRCROOT\}/g, '.'); - // Drop entries still containing unresolved Xcode tokens — emitting them - // verbatim would surface as clang "no such file" failures. - if (/\$[({]/.test(substituted)) continue; - const cleaned = substituted.replace(/^\.\//, '').replace(/^\//, ''); - if (cleaned.length > 0 && !out.includes(cleaned)) { - out.push(cleaned); - } - } - return out; -} - /** * The Swift name expandSpmDependencies resolved for `npmName`, or a hard error: * re-deriving one here would emit a reference nothing in the graph matches. @@ -869,6 +817,15 @@ function requireSwiftName( return resolved; } +/** + * Converts an autolinking.json dependency to an SPM target spec. + * Returns null if the dependency doesn't have iOS support. + * + * `swiftNameByNpm` maps each autolinked dep's npm name to its resolved Swift + * name (populated by expandSpmDependencies from the dep's podspec and + * `spm.name`). Every name this function emits comes from there — see + * requireSwiftName. + */ function autolinkingDepToSpmTarget( depName /*: string */, dep /*: AutolinkedDep */, @@ -903,8 +860,6 @@ function autolinkingDepToSpmTarget( ? spmDeps.map(n => requireSwiftName(n, swiftNameByNpm.get(n))) : undefined; - const headerSearchPaths = extractPodspecHeaderSearchPaths(sourceDir); - return { name: targetName, path: relSourcePath, @@ -912,8 +867,6 @@ function autolinkingDepToSpmTarget( publicHeadersPath: inferPublicHeadersPath(sourceDir), resources, spmTargetDependencies, - headerSearchPaths: - headerSearchPaths.length > 0 ? headerSearchPaths : undefined, }; } @@ -1123,12 +1076,6 @@ function generateSynthPackageSwift(spec /*: SynthPackageSpec */) /*: string */ { const exclude /*: Array */ = spec.exclude ?? []; const sources /*: ?Array */ = spec.sources; const publicHeadersPath /*: ?string */ = spec.publicHeadersPath ?? null; - // Per-dep header search paths from the podspec's - // pod_target_xcconfig HEADER_SEARCH_PATHS — already prefixed by the caller - // with the synth wrapper's `root/` so they resolve relative to target.path. - // Emitted as `.headerSearchPath()` directives, which SPM accepts on - // cSettings / cxxSettings without needing absolute paths. - const headerSearchPaths /*: Array */ = spec.headerSearchPaths ?? []; const spmDependencies /*: Array<{swiftName: string}> */ = spec.spmDependencies ?? []; const hasReactDep /*: boolean */ = spec.hasReactDep !== false; @@ -1212,22 +1159,11 @@ function generateSynthPackageSwift(spec /*: SynthPackageSpec */) /*: string */ { packageDeps.length > 0 ? ` dependencies: [\n ${packageDeps.join(',\n ')},\n ],\n` : ''; - // `.headerSearchPath(...)` entries from the podspec — first-class - // directives keep SPM's diagnostics meaningful (clang reports the - // dep-relative path on miss). React headers need no paths at all. - const headerSearchPathList = headerSearchPaths.map( - p => `.headerSearchPath("${p}")`, - ); - const cSettingsLine = - headerSearchPathList.length > 0 - ? `\n cSettings: [${headerSearchPathList.join(', ')}],` - : ''; - // ALWAYS emit the config-gated C++ defines (even with no header paths): a - // Fabric C++ target must compile with the same NDEBUG state as the prebuilt - // React.framework, else DebugStringConvertible's vtable / ShadowNode layout - // diverges and a Release build fails to link. Mirrors Package.swift. - const cxxEntries = [...REACT_CXX_CONFIG_DEFINES, ...headerSearchPathList]; - const cxxSettingsLine = `\n cxxSettings: [${cxxEntries.join(', ')}],`; + // ALWAYS emit the config-gated C++ defines: a Fabric C++ target must + // compile with the same NDEBUG state as the prebuilt React.framework, else + // DebugStringConvertible's vtable / ShadowNode layout diverges and a + // Release build fails to link. Mirrors Package.swift. + const cxxSettingsLine = `\n cxxSettings: [${REACT_CXX_CONFIG_DEFINES.join(', ')}],`; return `// swift-tools-version: 6.0 // AUTO-GENERATED by scripts/generate-spm-autolinking.js – do not edit manually. @@ -1245,7 +1181,7 @@ ${packageDepsBlock} targets: [ .target( name: "${swiftName}", dependencies: [${targetDeps.join(', ')}], - path: "${targetPath}",${excludeLine}${sourcesLine}${publicHeadersLine}${resourcesLine}${cSettingsLine}${cxxSettingsLine} + path: "${targetPath}",${excludeLine}${sourcesLine}${publicHeadersLine}${resourcesLine}${cxxSettingsLine} linkerSettings: [.linkedFramework("UIKit"), .linkedFramework("Foundation"), .linkedFramework("CoreGraphics")] ), ], @@ -1705,15 +1641,6 @@ function main(argv /*:: ?: Array */) /*: void */ { const prefixedSources /*: ?Array */ = rawSources.length > 0 ? rawSources.map(withRoot) : null; - // Podspec HEADER_SEARCH_PATHS were captured relative to the dep's source - // dir. The wrapper exposes the source dir under `root/` (target.path is - // `.`, the wrapper dir), so each entry must be prefixed with `root/` so - // clang sees the real subtree. - const prefixedHeaderSearchPaths /*: ?Array */ = - target.headerSearchPaths != null && target.headerSearchPaths.length > 0 - ? target.headerSearchPaths.map(withRoot) - : null; - const synthContent = generateSynthPackageSwift({ swiftName: target.name, iosDeploymentTarget: args.iosDeploymentTarget, @@ -1725,7 +1652,6 @@ function main(argv /*:: ?: Array */) /*: void */ { // header farm at /headers is folded into it). publicHeadersPath: 'include', resources: prefixedResources, - headerSearchPaths: prefixedHeaderSearchPaths, spmDependencies: (target.spmTargetDependencies ?? []).map(swiftName => ({ swiftName, })), diff --git a/packages/react-native/scripts/spm/spm-types.js b/packages/react-native/scripts/spm/spm-types.js index a89142f6ada0..01c65e9b3af8 100644 --- a/packages/react-native/scripts/spm/spm-types.js +++ b/packages/react-native/scripts/spm/spm-types.js @@ -87,13 +87,6 @@ export type SpmTarget = { // .js/.podspec/.md siblings, etc. can never sneak in. Null/empty means // fall back to SPM's default scan of target.path. sources?: ?Array, - // Header search paths declared by the dep's podspec - // (pod_target_xcconfig HEADER_SEARCH_PATHS), already substituted relative - // to the dep's source dir. Path-style includes like - // `` resolve through these. - // Emitted as `.headerSearchPath(...)` directives in the target's - // cSettings / cxxSettings. - headerSearchPaths?: ?Array, }; // Routing metadata kept alongside an SpmTarget in main(). Lives in a wrapper @@ -373,13 +366,6 @@ export type SynthPackageSpec = { // supply absolute paths (tests). Production uses siblingSynthAbsolutePaths. siblingPackageBaseRelative?: string, siblingSynthAbsolutePaths?: {[swiftName: string]: string}, - // Header search paths from the dep's podspec `pod_target_xcconfig` - // HEADER_SEARCH_PATHS, with `$(PODS_TARGET_SRCROOT)` substituted and the - // synth wrapper's `root/` prefix applied. Emitted as `.headerSearchPath()` - // directives on cSettings / cxxSettings so path-style includes like - // `` resolve through the - // dep's own `common/cpp/` subtree. - headerSearchPaths?: ?Array, iosDeploymentTarget?: ?string, };