Skip to content

Commit fed41f8

Browse files
chrfalchclaude
andcommitted
feat(spm): scaffold header search paths from header_mappings_dir
Community libs that lay headers out under a namespaced subdir and consume them via that namespace — reanimated/worklets ship `apple/reanimated/...` and `Common/cpp/reanimated/...` and `#import <reanimated/...>` — declare it with a per-subspec `header_mappings_dir`. CocoaPods makes those includes resolve by copying the headers into `Pods/Headers` preserving structure relative to the mappings dir; SPM has no equivalent copy step, so `<reanimated/apple/sensor/X.h>` (physically at `apple/reanimated/apple/sensor/X.h`) was unresolved and the scaffolded target failed with "file not found". Fix: for every subspec's `header_mappings_dir`, add `dirname(mappings_dir)` as a header search path on the scaffolded target. That exposes the `<basename(mappings_dir)>/...` namespace from the physical source tree (e.g. `-I apple` makes `<reanimated/apple/sensor/X.h>` resolve). The podspec's literal HEADER_SEARCH_PATHS never list these because CocoaPods relies on the copy step. - read-podspec.js: collect ALL subspecs' header_mappings_dir into `headerMappingsDirs` (the existing singular `headerMappingsDir` is the merged value; the array is the union). - scaffold-package-swift.js: add `dirname()` of each (skipping "." and non-existent dirs); bump SCAFFOLDER_VERSION 8 → 9 so already-scaffolded deps regenerate with the new search paths. - spm-types.js: `headerMappingsDirs: Array<string>` on PodspecModel. - Tests for the reanimated/worklets pattern, the non-existent-dir skip, and the single-segment ("." → skip) case. Verified: re-scaffolding react-native-reanimated now auto-emits `.headerSearchPath("Common/cpp")` + `.headerSearchPath("apple")`, and its own `<reanimated/...>` headers resolve (build advances past them). 323 spm tests pass. NOTE: a full reanimated build still needs the reanimated→worklets cross-package dep and Hermes public headers in the artifacts (separate work). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c6c48ea commit fed41f8

4 files changed

Lines changed: 88 additions & 1 deletion

File tree

packages/react-native/scripts/spm/__tests__/scaffold-package-swift-test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function podspec(overrides /*: Object */ = {}) {
3333
privateHeaderFiles: [],
3434
excludeFiles: [],
3535
headerMappingsDir: null,
36+
headerMappingsDirs: [],
3637
headerDir: null,
3738
frameworks: [],
3839
weakFrameworks: [],
@@ -83,6 +84,60 @@ describe('translatePodspecToSpmTarget', () => {
8384
expect(spec.swiftName).toBe('ReactNativeSafeAreaContext');
8485
});
8586

87+
it('adds dirname(header_mappings_dir) as a header search path so namespaced includes resolve (reanimated/worklets pattern)', () => {
88+
// reanimated/worklets ship headers at `apple/reanimated/...` and
89+
// `Common/cpp/reanimated/...` with per-subspec header_mappings_dir, and
90+
// include them as `<reanimated/...>`. SPM has no header_mappings_dir copy
91+
// step, so the parent of each mappings dir must be on the search path.
92+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-scaffold-'));
93+
try {
94+
fs.mkdirSync(path.join(root, 'apple', 'reanimated'), {recursive: true});
95+
fs.mkdirSync(path.join(root, 'Common', 'cpp', 'reanimated'), {
96+
recursive: true,
97+
});
98+
const model = podspec({
99+
headerMappingsDirs: ['Common/cpp/reanimated', 'apple/reanimated'],
100+
});
101+
const spec = translatePodspecToSpmTarget(
102+
model,
103+
autolinkedDep({name: 'react-native-reanimated', root}),
104+
);
105+
expect(spec.headerSearchPaths).toContain('apple');
106+
expect(spec.headerSearchPaths).toContain('Common/cpp');
107+
} finally {
108+
fs.rmSync(root, {recursive: true, force: true});
109+
}
110+
});
111+
112+
it('skips a header_mappings_dir whose parent dir does not exist on disk', () => {
113+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-scaffold-'));
114+
try {
115+
const model = podspec({headerMappingsDirs: ['nope/reanimated']});
116+
const spec = translatePodspecToSpmTarget(
117+
model,
118+
autolinkedDep({name: 'react-native-foo', root}),
119+
);
120+
expect(spec.headerSearchPaths).not.toContain('nope');
121+
} finally {
122+
fs.rmSync(root, {recursive: true, force: true});
123+
}
124+
});
125+
126+
it('does not add "." for a single-segment header_mappings_dir', () => {
127+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-scaffold-'));
128+
try {
129+
fs.mkdirSync(path.join(root, 'ios'), {recursive: true});
130+
const model = podspec({headerMappingsDirs: ['ios']});
131+
const spec = translatePodspecToSpmTarget(
132+
model,
133+
autolinkedDep({name: 'react-native-foo', root}),
134+
);
135+
expect(spec.headerSearchPaths).not.toContain('.');
136+
} finally {
137+
fs.rmSync(root, {recursive: true, force: true});
138+
}
139+
});
140+
86141
it('still uses toSwiftName(npm-name) even when header_dir is a plain identifier (matches autolinker registration)', () => {
87142
const model = podspec({headerDir: 'reanimated'});
88143
const spec = translatePodspecToSpmTarget(

packages/react-native/scripts/spm/read-podspec.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,13 @@ function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
471471
privateHeaderFiles: mergeArrayField('private_header_files'),
472472
excludeFiles: mergeArrayField('exclude_files'),
473473
headerMappingsDir: mergeStringField('header_mappings_dir'),
474+
// ALL subspecs' header_mappings_dir values (not just the merged one). Each
475+
// implies a header search path of its parent dir so namespaced includes
476+
// (`<reanimated/apple/sensor/X.h>` for a header physically at
477+
// `apple/reanimated/apple/sensor/X.h` with mappings dir `apple/reanimated`)
478+
// resolve from the physical tree — CocoaPods does this via the
479+
// header_mappings_dir copy step, which SPM has no equivalent for.
480+
headerMappingsDirs: mergeArrayField('header_mappings_dir'),
474481
headerDir: mergeStringField('header_dir'),
475482
frameworks: mergeArrayField('frameworks'),
476483
weakFrameworks: mergeArrayField('weak_frameworks'),

packages/react-native/scripts/spm/scaffold-package-swift.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const {log} = makeLogger('scaffold-package-swift');
8383
// v8: relative app paths (codegen / xcframeworks) are now computed from the
8484
// autolinker's libs/<SwiftName> symlink location instead of the real dep.root,
8585
// fixing a doubled-path resolution failure on fresh SwiftPM resolves.
86-
const SCAFFOLDER_VERSION = 8;
86+
const SCAFFOLDER_VERSION = 9;
8787
const SCAFFOLDER_VERSION_LINE_RE = /^\/\/ AUTO-SCAFFOLDED-VERSION: (\d+)$/m;
8888

8989
const AUTOGEN_MARKER =
@@ -173,6 +173,27 @@ function translatePodspecToSpmTarget(
173173
}
174174
}
175175

176+
// header_mappings_dir → search path. CocoaPods exposes a subspec's headers
177+
// under `<basename(mappings_dir)>/...` by copying them into Pods/Headers
178+
// preserving structure relative to the mappings dir. SPM has no such copy
179+
// step, so namespaced includes like `<reanimated/apple/sensor/X.h>` (header
180+
// physically at `apple/reanimated/apple/sensor/X.h`, mappings dir
181+
// `apple/reanimated`) only resolve if the mappings dir's PARENT (`apple`) is
182+
// on the search path. Add dirname() of every subspec's mappings dir.
183+
for (const mappingsDir of model.headerMappingsDirs) {
184+
const parent = path.posix.dirname(mappingsDir.replace(/^\.\//, ''));
185+
// dirname of a single-segment dir is "." (root) — already implicitly
186+
// searched; skip it and anything that doesn't exist on disk.
187+
if (
188+
parent.length > 0 &&
189+
parent !== '.' &&
190+
!headerSearchPaths.includes(parent) &&
191+
fs.existsSync(path.join(dep.root, parent))
192+
) {
193+
headerSearchPaths.push(parent);
194+
}
195+
}
196+
176197
// Bucket dependencies. React-Core / React-jsi / RCT-Folly / glog etc. ALL
177198
// collapse to a single `.product(name: "ReactNative", package: "ReactNative")`
178199
// reference because they're bundled in the prebuilt React.xcframework.

packages/react-native/scripts/spm/spm-types.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ export type PodspecModel = {
346346
privateHeaderFiles: Array<string>,
347347
excludeFiles: Array<string>,
348348
headerMappingsDir: ?string,
349+
// Every subspec's header_mappings_dir (union). dirname() of each is added as
350+
// a header search path so `<namespace/...>` includes resolve from the
351+
// physical source tree (SPM has no header_mappings_dir copy step).
352+
headerMappingsDirs: Array<string>,
349353
headerDir: ?string,
350354
frameworks: Array<string>,
351355
weakFrameworks: Array<string>,

0 commit comments

Comments
 (0)