Skip to content

Commit 810fff4

Browse files
chrfalchclaude
andcommitted
feat(spm): scaffold ObjC prefix header + read defines from s.xcconfig
Two fixes that, together with the prior scaffolder work, make react-native-reanimated + react-native-worklets build end-to-end under SPM. 1. ObjC prefix header (replaces CocoaPods' generated prefix.pch). worklets' ObjC++ sources (IOSUIScheduler.mm, AssertJavaScriptQueue.h) use NSThread / dispatch_* / UIKit with NO explicit import — they rely on the prefix header CocoaPods auto-generates for every pod (imports Foundation + UIKit). SPM has no prefix-header mechanism, so the build failed with "use of undeclared identifier 'NSThread'". Fix: when a scaffolded target has ObjC(++) sources (.m/.mm), emit a `react-native-spm-prefix.h` at the dep root and `-include` it on the target (cSettings + cxxSettings). The `__OBJC__` guard makes it a no-op for plain C/C++; UIKit is `__has_include`-guarded. `-include` resolves the bare name via the dep-root header search path (".", ensured in translate). 2. Read defines from `s.xcconfig`, not just `pod_target_xcconfig`. worklets declares `-DWORKLETS_VERSION` in pod_target_xcconfig; reanimated declares `-DREANIMATED_VERSION` in `s.xcconfig`. The defines reader only scanned pod_target_xcconfig, so REANIMATED_VERSION was dropped → "use of undeclared identifier 'REANIMATED_VERSION_STRING'". Now scans pod_target_xcconfig, xcconfig, and user_target_xcconfig (deduped). - SCAFFOLDER_VERSION 12 → 14 (output changed → regenerate existing scaffolds). - spm-types.js: `needsObjCPrefix` on SpmScaffoldSpec. - Tests: prefix emission + ObjC-source detection; defines lifted from s.xcconfig. Verified end-to-end: `xcodebuild` of the ReactNativeReanimated scheme (which builds worklets too) **BUILD SUCCEEDED** on a real RN 0.87 app — the complete reanimated/worklets graph compiles and links via SPM. 336 spm tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0caee15 commit 810fff4

5 files changed

Lines changed: 192 additions & 47 deletions

File tree

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,32 @@ describe('flattenSubspecs', () => {
237237
expect(model.preprocessorDefines).toHaveLength(3);
238238
});
239239

240+
it('lifts defines from s.xcconfig too, not just pod_target_xcconfig (reanimated shape)', () => {
241+
const raw = {
242+
name: 'RNReanimated',
243+
version: '4.4.1',
244+
// reanimated declares its version define in `s.xcconfig`, not
245+
// pod_target_xcconfig (where worklets puts it).
246+
xcconfig: {
247+
OTHER_CFLAGS: '$(inherited) -DREANIMATED_VERSION=4.4.1',
248+
},
249+
pod_target_xcconfig: {
250+
'GCC_PREPROCESSOR_DEFINITIONS[config=*Debug*]':
251+
'$(inherited) HERMES_ENABLE_DEBUGGER=1',
252+
},
253+
};
254+
const model = flattenSubspecs(raw);
255+
const byName = Object.fromEntries(
256+
model.preprocessorDefines.map(d => [d.name, d]),
257+
);
258+
expect(byName.REANIMATED_VERSION).toEqual({
259+
name: 'REANIMATED_VERSION',
260+
value: '4.4.1',
261+
config: null,
262+
});
263+
expect(byName.HERMES_ENABLE_DEBUGGER.config).toBe('debug');
264+
});
265+
240266
it('drops non-define flags and unresolved tokens from OTHER_CFLAGS', () => {
241267
const raw = {
242268
name: 'foo',

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,37 @@ describe('translatePodspecToSpmTarget', () => {
172172
}
173173
});
174174

175+
it('flags needsObjCPrefix (and adds "." to the search path) when the target has ObjC(++) sources', () => {
176+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'objc-scaffold-'));
177+
try {
178+
fs.writeFileSync(path.join(root, 'A.mm'), '');
179+
const model = podspec({sourceFiles: ['A.mm', 'B.cpp']});
180+
const spec = translatePodspecToSpmTarget(
181+
model,
182+
autolinkedDep({name: 'react-native-foo', root}),
183+
);
184+
expect(spec.needsObjCPrefix).toBe(true);
185+
expect(spec.headerSearchPaths).toContain('.');
186+
} finally {
187+
fs.rmSync(root, {recursive: true, force: true});
188+
}
189+
});
190+
191+
it('does not flag needsObjCPrefix for a C++-only target', () => {
192+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cpp-scaffold-'));
193+
try {
194+
fs.writeFileSync(path.join(root, 'A.cpp'), '');
195+
const model = podspec({sourceFiles: ['A.cpp']});
196+
const spec = translatePodspecToSpmTarget(
197+
model,
198+
autolinkedDep({name: 'react-native-foo', root}),
199+
);
200+
expect(spec.needsObjCPrefix).toBe(false);
201+
} finally {
202+
fs.rmSync(root, {recursive: true, force: true});
203+
}
204+
});
205+
175206
it('does not add "." for a single-segment header_mappings_dir', () => {
176207
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-scaffold-'));
177208
try {
@@ -350,6 +381,7 @@ describe('emitScaffoldedPackageSwift', () => {
350381
sources: [],
351382
headerSearchPaths: [],
352383
preprocessorDefines: [],
384+
needsObjCPrefix: false,
353385
coreReactNative: false,
354386
siblingNames: [],
355387
extraFrameworks: [],
@@ -455,6 +487,24 @@ describe('emitScaffoldedPackageSwift', () => {
455487
);
456488
});
457489

490+
it('-includes the ObjC prefix header in c/cxx settings when needsObjCPrefix is set', () => {
491+
const withPrefix = emitScaffoldedPackageSwift(
492+
baseSpec({needsObjCPrefix: true}),
493+
);
494+
expect(
495+
(
496+
withPrefix.match(
497+
/\.unsafeFlags\(\["-include", "react-native-spm-prefix\.h"\]\)/g,
498+
) ?? []
499+
).length,
500+
).toBe(2); // cSettings + cxxSettings
501+
// Not emitted for a C/C++-only target.
502+
const noPrefix = emitScaffoldedPackageSwift(
503+
baseSpec({needsObjCPrefix: false}),
504+
);
505+
expect(noPrefix).not.toContain('-include');
506+
});
507+
458508
it('emits preprocessor defines as .define(...) in c/cxx settings, escaping quoted values and honoring config', () => {
459509
const out = emitScaffoldedPackageSwift(
460510
baseSpec({
@@ -855,6 +905,7 @@ describe('SCAFFOLDER_VERSION', () => {
855905
sources: [],
856906
headerSearchPaths: [],
857907
preprocessorDefines: [],
908+
needsObjCPrefix: false,
858909
coreReactNative: false,
859910
siblingNames: [],
860911
extraFrameworks: [],

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

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -471,51 +471,64 @@ function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
471471
seen.add(key);
472472
out.push({name, value, config});
473473
};
474+
// Defines can live in the target xcconfig (pod_target_xcconfig) OR the
475+
// aggregate/user xcconfig (`s.xcconfig` / user_target_xcconfig). worklets
476+
// puts its version define in pod_target_xcconfig; reanimated puts its in
477+
// `s.xcconfig` — scan all three.
478+
const XCCONFIG_KEYS = [
479+
'pod_target_xcconfig',
480+
'xcconfig',
481+
'user_target_xcconfig',
482+
];
474483
for (const layer of layers) {
475-
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`
476-
const xc =
477-
layer != null && typeof layer === 'object'
478-
? layer.pod_target_xcconfig
479-
: null;
480-
if (xc == null || typeof xc !== 'object') continue;
481-
for (const rawKey of Object.keys(xc)) {
482-
const cflags = /^OTHER_CFLAGS(?:\[config=\*(\w+)\*\])?$/i.exec(rawKey);
483-
const ppDefs =
484-
/^GCC_PREPROCESSOR_DEFINITIONS(?:\[config=\*(\w+)\*\])?$/i.exec(
484+
if (layer == null || typeof layer !== 'object') continue;
485+
for (const xcKey of XCCONFIG_KEYS) {
486+
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`
487+
const xc = layer[xcKey];
488+
if (xc == null || typeof xc !== 'object') continue;
489+
for (const rawKey of Object.keys(xc)) {
490+
const cflags = /^OTHER_CFLAGS(?:\[config=\*(\w+)\*\])?$/i.exec(
485491
rawKey,
486492
);
487-
if (cflags == null && ppDefs == null) continue;
488-
const cfgRaw = ((cflags?.[1] ?? ppDefs?.[1] ?? '') + '').toLowerCase();
489-
const config =
490-
cfgRaw === 'debug'
491-
? 'debug'
492-
: cfgRaw === 'release'
493-
? 'release'
494-
: null;
495-
// $FlowFixMe[incompatible-use] xc value access is intentional
496-
const val = xc[rawKey];
497-
const strs =
498-
typeof val === 'string'
499-
? [val]
500-
: Array.isArray(val)
501-
? val.filter(v => typeof v === 'string')
502-
: [];
503-
for (const s of strs) {
504-
for (const tok of shellTokenize(s)) {
505-
if (tok === '$(inherited)') continue;
506-
// OTHER_CFLAGS: only `-D` tokens are defines; others are flags.
507-
// GCC_PREPROCESSOR_DEFINITIONS: every token is `NAME[=VALUE]`.
508-
let body /*: ?string */ = null;
509-
if (cflags != null) {
510-
if (tok.startsWith('-D')) body = tok.slice(2);
511-
} else {
512-
body = tok;
493+
const ppDefs =
494+
/^GCC_PREPROCESSOR_DEFINITIONS(?:\[config=\*(\w+)\*\])?$/i.exec(
495+
rawKey,
496+
);
497+
if (cflags == null && ppDefs == null) continue;
498+
const cfgRaw = (
499+
(cflags?.[1] ?? ppDefs?.[1] ?? '') + ''
500+
).toLowerCase();
501+
const config =
502+
cfgRaw === 'debug'
503+
? 'debug'
504+
: cfgRaw === 'release'
505+
? 'release'
506+
: null;
507+
// $FlowFixMe[incompatible-use] xc value access is intentional
508+
const val = xc[rawKey];
509+
const strs =
510+
typeof val === 'string'
511+
? [val]
512+
: Array.isArray(val)
513+
? val.filter(v => typeof v === 'string')
514+
: [];
515+
for (const s of strs) {
516+
for (const tok of shellTokenize(s)) {
517+
if (tok === '$(inherited)') continue;
518+
// OTHER_CFLAGS: only `-D` tokens are defines; others are flags.
519+
// GCC_PREPROCESSOR_DEFINITIONS: every token is `NAME[=VALUE]`.
520+
let body /*: ?string */ = null;
521+
if (cflags != null) {
522+
if (tok.startsWith('-D')) body = tok.slice(2);
523+
} else {
524+
body = tok;
525+
}
526+
if (body == null || body.length === 0) continue;
527+
const eq = body.indexOf('=');
528+
const name = eq >= 0 ? body.slice(0, eq) : body;
529+
const value = eq >= 0 ? body.slice(eq + 1) : null;
530+
add(name, value, config);
513531
}
514-
if (body == null || body.length === 0) continue;
515-
const eq = body.indexOf('=');
516-
const name = eq >= 0 ? body.slice(0, eq) : body;
517-
const value = eq >= 0 ? body.slice(eq + 1) : null;
518-
add(name, value, config);
519532
}
520533
}
521534
}

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

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,36 @@ const {log} = makeLogger('scaffold-package-swift');
8989
// package exposes `<namespace/...>` to dependents; pod-style sibling deps
9090
// (reanimated's `s.dependency "RNWorklets"`) wired to their npm package. v11:
9191
// sibling .package path uses the libs/<SwiftName> symlink name, not the npm
92-
// name (fixes "package ... doesn't exist" on resolve).
93-
const SCAFFOLDER_VERSION = 12;
92+
// name (fixes "package ... doesn't exist" on resolve). v12: preprocessor
93+
// defines from pod_target_xcconfig emitted as `.define(...)`. v13: ObjC(++)
94+
// targets get an ambient-import prefix header (Foundation/UIKit) `-include`d,
95+
// replacing CocoaPods' generated prefix.pch.
96+
const SCAFFOLDER_VERSION = 14;
9497
const SCAFFOLDER_VERSION_LINE_RE = /^\/\/ AUTO-SCAFFOLDED-VERSION: (\d+)$/m;
9598

9699
const AUTOGEN_MARKER =
97100
'// AUTO-GENERATED by scripts/generate-spm-autolinking.js';
98101

102+
// CocoaPods auto-generates a `<Target>-prefix.pch` that imports Foundation +
103+
// UIKit into every ObjC translation unit, so pod sources can use NSThread /
104+
// dispatch / UIKit symbols without an explicit import. SPM has no prefix-header
105+
// mechanism, so we emit this file at the dep root and `-include` it on the
106+
// target (cSettings + cxxSettings) to reproduce that ambient import. The
107+
// `__OBJC__` guard makes it inert for plain C/C++ sources, and UIKit is
108+
// `__has_include`-guarded for platforms that lack it.
109+
const SCAFFOLD_PREFIX_HEADER = 'react-native-spm-prefix.h';
110+
const SCAFFOLD_PREFIX_HEADER_CONTENTS = `// AUTO-SCAFFOLDED by react-native spm scaffold — mirrors CocoaPods' default
111+
// prefix header so ObjC sources that rely on an implicit Foundation/UIKit
112+
// import compile under SPM (which has no prefix-header mechanism). Safe to
113+
// delete + regenerate via \`npx react-native spm scaffold\`.
114+
#ifdef __OBJC__
115+
#import <Foundation/Foundation.h>
116+
#if __has_include(<UIKit/UIKit.h>)
117+
#import <UIKit/UIKit.h>
118+
#endif
119+
#endif
120+
`;
121+
99122
// Names of deps the scaffolder always refuses to touch — `react-native`
100123
// itself is handled by the xcframework subpackage, never as an autolinked
101124
// target.
@@ -276,6 +299,15 @@ function translatePodspecToSpmTarget(
276299
}
277300
}
278301

302+
// ObjC(++) sources may rely on CocoaPods' implicit prefix-header import of
303+
// Foundation/UIKit. When present, emit + `-include` a prefix header (below).
304+
// The prefix is `-include`d by bare name, so the dep root (".") must be on
305+
// the header search path for clang to find it.
306+
const needsObjCPrefix = expandedSources.some(f => /\.mm?$/.test(f));
307+
if (needsObjCPrefix && !headerSearchPaths.includes('.')) {
308+
headerSearchPaths.push('.');
309+
}
310+
279311
// SPM REQUIRES publicHeadersPath to be a real directory inside the target
280312
// when the target compiles C-family sources (default is "include" which
281313
// typically doesn't exist in a podspec-shaped dep). Pick the first
@@ -325,6 +357,7 @@ function translatePodspecToSpmTarget(
325357
sources: expandedSources,
326358
headerSearchPaths,
327359
preprocessorDefines: model.preprocessorDefines,
360+
needsObjCPrefix,
328361
coreReactNative,
329362
siblingNames,
330363
extraFrameworks: model.frameworks,
@@ -411,11 +444,20 @@ function emitScaffoldedPackageSwift(
411444
].filter(e => e.length > 0);
412445
return `[${parts.join(', ')}]`;
413446
};
414-
const cSettings = settingsEntries([]);
447+
// Force-include the ambient-import prefix header on ObjC(++) sources (the
448+
// `__OBJC__` guard makes it a no-op for plain C/C++). `-include` resolves the
449+
// bare name via the dep-root header search path (".", ensured in translate).
450+
const prefixFlag = spec.needsObjCPrefix
451+
? `.unsafeFlags(["-include", "${SCAFFOLD_PREFIX_HEADER}"])`
452+
: '';
453+
const cSettings = settingsEntries([prefixFlag].filter(e => e.length > 0));
415454
const cxxSettings = settingsEntries(
416-
customFlagsCxx.length > 0
417-
? [`.unsafeFlags([${customFlagsCxx.join(', ')}])`]
418-
: [],
455+
[
456+
prefixFlag,
457+
...(customFlagsCxx.length > 0
458+
? [`.unsafeFlags([${customFlagsCxx.join(', ')}])`]
459+
: []),
460+
].filter(e => e.length > 0),
419461
);
420462

421463
// Linker frameworks: defaults + podspec-declared extras + weak frameworks.
@@ -768,6 +810,15 @@ function scaffoldPackageSwiftForDep(
768810

769811
if (!ctx.dryRun) {
770812
fs.writeFileSync(pkgSwiftPath, content, 'utf8');
813+
// Emit the ambient-import prefix header next to the manifest when the
814+
// target has ObjC(++) sources (the manifest `-include`s it by bare name).
815+
if (spec.needsObjCPrefix) {
816+
fs.writeFileSync(
817+
path.join(dep.root, SCAFFOLD_PREFIX_HEADER),
818+
SCAFFOLD_PREFIX_HEADER_CONTENTS,
819+
'utf8',
820+
);
821+
}
771822
}
772823

773824
return {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ export type SpmScaffoldSpec = {
408408
// Preprocessor defines (resolved by pod ipc) — emitted as `.define(...)` in
409409
// cSettings + cxxSettings, honoring any per-config scope.
410410
preprocessorDefines: Array<PreprocessorDefine>,
411+
// True when the target has ObjC(++) sources (.m/.mm). Drives emitting +
412+
// `-include`ing a prefix header that ambient-imports Foundation/UIKit (which
413+
// CocoaPods provides via a generated prefix.pch and SPM does not).
414+
needsObjCPrefix: boolean,
411415
// Bucketed dependency references — pre-computed by the translation layer.
412416
// `coreReactNative` is true when ANY React-* / RCT* / RCT-Folly / glog
413417
// dep is present (so we add a single `.product(name: "ReactNative", ...)`).

0 commit comments

Comments
 (0)