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 1c5b22f08e9f..9eed2b5e70a2 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 @@ -2386,4 +2386,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 8bce8bce0f21..4801581d568b 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, CXX_API_DEFINES, @@ -822,58 +822,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. @@ -890,6 +838,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 */, @@ -924,8 +881,6 @@ function autolinkingDepToSpmTarget( ? spmDeps.map(n => requireSwiftName(n, swiftNameByNpm.get(n))) : undefined; - const headerSearchPaths = extractPodspecHeaderSearchPaths(sourceDir); - return { name: targetName, path: relSourcePath, @@ -933,8 +888,6 @@ function autolinkingDepToSpmTarget( publicHeadersPath: inferPublicHeadersPath(sourceDir), resources, spmTargetDependencies, - headerSearchPaths: - headerSearchPaths.length > 0 ? headerSearchPaths : undefined, }; } @@ -1144,12 +1097,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; @@ -1233,25 +1180,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 = [ - ...reactCxxDefines(spec.cxxApiDefines), - ...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: [${reactCxxDefines(spec.cxxApiDefines).join(', ')}],`; return `// swift-tools-version: 6.0 // AUTO-GENERATED by scripts/generate-spm-autolinking.js – do not edit manually. @@ -1269,7 +1202,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")] ), ], @@ -1729,15 +1662,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, @@ -1749,7 +1673,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 2fc514907eb4..d9af84f39652 100644 --- a/packages/react-native/scripts/spm/spm-types.js +++ b/packages/react-native/scripts/spm/spm-types.js @@ -90,13 +90,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 @@ -377,13 +370,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, // React Native C++ stable API macros defined in the synth target. cxxApiDefines?: ReadonlyArray,