Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
});
});
109 changes: 16 additions & 93 deletions packages/react-native/scripts/spm/generate-spm-autolinking.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
* `<react/renderer/components/safeareacontext/X.h>` (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<string> */ {
const podspecPath /*: ?string */ = findPodspecs(sourceDir)[0];
if (podspecPath == null) return [];

let model;
try {
model = readPodspecCached(podspecPath);
} catch {
return [];
}

const out /*: Array<string> */ = [];
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.
Expand All @@ -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 */,
Expand Down Expand Up @@ -924,17 +881,13 @@ function autolinkingDepToSpmTarget(
? spmDeps.map(n => requireSwiftName(n, swiftNameByNpm.get(n)))
: undefined;

const headerSearchPaths = extractPodspecHeaderSearchPaths(sourceDir);

return {
name: targetName,
path: relSourcePath,
exclude: [],
publicHeadersPath: inferPublicHeadersPath(sourceDir),
resources,
spmTargetDependencies,
headerSearchPaths:
headerSearchPaths.length > 0 ? headerSearchPaths : undefined,
};
}

Expand Down Expand Up @@ -1144,12 +1097,6 @@ function generateSynthPackageSwift(spec /*: SynthPackageSpec */) /*: string */ {
const exclude /*: Array<string> */ = spec.exclude ?? [];
const sources /*: ?Array<string> */ = 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<string> */ = spec.headerSearchPaths ?? [];
const spmDependencies /*: Array<{swiftName: string}> */ =
spec.spmDependencies ?? [];
const hasReactDep /*: boolean */ = spec.hasReactDep !== false;
Expand Down Expand Up @@ -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.
Expand All @@ -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")]
),
],
Expand Down Expand Up @@ -1729,15 +1662,6 @@ function main(argv /*:: ?: Array<string> */) /*: void */ {
const prefixedSources /*: ?Array<string> */ =
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<string> */ =
target.headerSearchPaths != null && target.headerSearchPaths.length > 0
? target.headerSearchPaths.map(withRoot)
: null;

const synthContent = generateSynthPackageSwift({
swiftName: target.name,
iosDeploymentTarget: args.iosDeploymentTarget,
Expand All @@ -1749,7 +1673,6 @@ function main(argv /*:: ?: Array<string> */) /*: void */ {
// header farm at <outputDir>/headers is folded into it).
publicHeadersPath: 'include',
resources: prefixedResources,
headerSearchPaths: prefixedHeaderSearchPaths,
spmDependencies: (target.spmTargetDependencies ?? []).map(swiftName => ({
swiftName,
})),
Expand Down
14 changes: 0 additions & 14 deletions packages/react-native/scripts/spm/spm-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
// 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
// `<react/renderer/components/safeareacontext/X.h>` resolve through these.
// Emitted as `.headerSearchPath(...)` directives in the target's
// cSettings / cxxSettings.
headerSearchPaths?: ?Array<string>,
};

// Routing metadata kept alongside an SpmTarget in main(). Lives in a wrapper
Expand Down Expand Up @@ -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
// `<react/renderer/components/safeareacontext/X.h>` resolve through the
// dep's own `common/cpp/` subtree.
headerSearchPaths?: ?Array<string>,
iosDeploymentTarget?: ?string,
// React Native C++ stable API macros defined in the synth target.
cxxApiDefines?: ReadonlyArray<string>,
Expand Down
Loading