Skip to content

Commit bc6a1ae

Browse files
chrfalchclaude
andcommitted
feat(spm): translate podspec preprocessor defines into scaffolded targets
Some community libs inject required preprocessor defines via their podspec's pod_target_xcconfig — worklets sets `-DWORKLETS_VERSION=#{package['version']}` (and a `-DWORKLETS_FEATURE_FLAGS="[...]"` string-literal macro) in OTHER_CFLAGS, plus `HERMES_ENABLE_DEBUGGER=1` in a Debug-only GCC_PREPROCESSOR_DEFINITIONS. Without them the build fails (`use of undeclared identifier WORKLETS_VERSION_STRING`). The scaffolder dropped these on the floor. Generic fix (no per-library knowledge — same shape as the existing HEADER_SEARCH_PATHS handling): `pod ipc` already resolves the Ruby, so read the resolved values and emit `.define(...)`. - read-podspec.js: lift defines from pod_target_xcconfig across all subspecs — `-D` tokens from OTHER_CFLAGS and NAME[=VALUE] entries from GCC_PREPROCESSOR_DEFINITIONS (incl. per-config `[config=*Debug*/Release*]`). A shell-aware tokenizer keeps quoted values intact (a naive whitespace split would shred `-DFOO="[a:b]"`). Safety filters: only `-D` flags (arbitrary compiler flags dropped), strip `$(inherited)`, skip unresolved `$(...)` tokens and non-identifier names. New `PreprocessorDefine` model field. - scaffold-package-swift.js: emit `.define("NAME", to: "VALUE", .when(...))` in cSettings + cxxSettings, escaping the value as a Swift string literal (WORKLETS_FEATURE_FLAGS's value contains embedded quotes) and honoring the per-config scope. SCAFFOLDER_VERSION 11 → 12. - spm-types.js: PreprocessorDefine type; preprocessorDefines on PodspecModel + SpmScaffoldSpec; podspecPath already on AutolinkingIosPlatform. - Tests: define parsing (worklets shape, quoted value, per-config, dropped non-define/unresolved tokens) and `.define(...)` emission. Verified: re-scaffolding worklets auto-emits `.define("WORKLETS_VERSION", to: "0.9.2")` + the escaped feature-flags macro + the Debug-scoped Hermes debugger define; the `WORKLETS_VERSION_STRING` build error is gone. The build now fails only on `<hermes/hermes.h>` (the Hermes-headers artifact gap, tracked separately). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 841e98e commit bc6a1ae

5 files changed

Lines changed: 242 additions & 5 deletions

File tree

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,61 @@ describe('flattenSubspecs', () => {
201201
expect(model.partial).toBe(false);
202202
});
203203

204+
it('lifts preprocessor defines from OTHER_CFLAGS + GCC_PREPROCESSOR_DEFINITIONS (worklets shape)', () => {
205+
const raw = {
206+
name: 'RNWorklets',
207+
version: '0.9.2',
208+
pod_target_xcconfig: {
209+
OTHER_CFLAGS:
210+
'$(inherited) -DWORKLETS_FEATURE_FLAGS="[A:false][B:true]" -DWORKLETS_VERSION=0.9.2 ',
211+
'GCC_PREPROCESSOR_DEFINITIONS[config=*Debug*]':
212+
'$(inherited) HERMES_ENABLE_DEBUGGER=1',
213+
'GCC_PREPROCESSOR_DEFINITIONS[config=*Release*]': '$(inherited)',
214+
},
215+
};
216+
const model = flattenSubspecs(raw);
217+
const byName = Object.fromEntries(
218+
model.preprocessorDefines.map(d => [d.name, d]),
219+
);
220+
// Quoted string-literal value kept intact (incl. its quotes).
221+
expect(byName.WORKLETS_FEATURE_FLAGS).toEqual({
222+
name: 'WORKLETS_FEATURE_FLAGS',
223+
value: '"[A:false][B:true]"',
224+
config: null,
225+
});
226+
expect(byName.WORKLETS_VERSION).toEqual({
227+
name: 'WORKLETS_VERSION',
228+
value: '0.9.2',
229+
config: null,
230+
});
231+
// Per-config define scoped to debug; $(inherited) dropped.
232+
expect(byName.HERMES_ENABLE_DEBUGGER).toEqual({
233+
name: 'HERMES_ENABLE_DEBUGGER',
234+
value: '1',
235+
config: 'debug',
236+
});
237+
expect(model.preprocessorDefines).toHaveLength(3);
238+
});
239+
240+
it('drops non-define flags and unresolved tokens from OTHER_CFLAGS', () => {
241+
const raw = {
242+
name: 'foo',
243+
version: '1',
244+
pod_target_xcconfig: {
245+
OTHER_CFLAGS:
246+
'-Wno-comma -gen-cdb-fragment-path build/cdb -DGOOD=1 -D$(BAD_TOKEN)=x -DALSO_GOOD',
247+
},
248+
};
249+
const model = flattenSubspecs(raw);
250+
const names = model.preprocessorDefines.map(d => d.name).sort();
251+
// Only the two valid -D defines survive; -W / -gen-cdb-fragment-path and
252+
// the unresolved $(...) token are dropped.
253+
expect(names).toEqual(['ALSO_GOOD', 'GOOD']);
254+
expect(model.preprocessorDefines.find(d => d.name === 'ALSO_GOOD').value).toBe(
255+
null,
256+
);
257+
});
258+
204259
it('unions source_files across selected subspecs', () => {
205260
const raw = {
206261
name: 'foo',

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function podspec(overrides /*: Object */ = {}) {
4141
dependencies: [],
4242
compilerFlags: [],
4343
headerSearchPaths: [],
44+
preprocessorDefines: [],
4445
resources: [],
4546
requiresArc: true,
4647
warnings: [],
@@ -348,6 +349,7 @@ describe('emitScaffoldedPackageSwift', () => {
348349
swiftName: 'foo',
349350
sources: [],
350351
headerSearchPaths: [],
352+
preprocessorDefines: [],
351353
coreReactNative: false,
352354
siblingNames: [],
353355
extraFrameworks: [],
@@ -453,6 +455,33 @@ describe('emitScaffoldedPackageSwift', () => {
453455
);
454456
});
455457

458+
it('emits preprocessor defines as .define(...) in c/cxx settings, escaping quoted values and honoring config', () => {
459+
const out = emitScaffoldedPackageSwift(
460+
baseSpec({
461+
preprocessorDefines: [
462+
{name: 'WORKLETS_VERSION', value: '0.9.2', config: null},
463+
{
464+
name: 'WORKLETS_FEATURE_FLAGS',
465+
value: '"[A:false][B:true]"',
466+
config: null,
467+
},
468+
{name: 'HERMES_ENABLE_DEBUGGER', value: '1', config: 'debug'},
469+
{name: 'NDEBUG', value: null, config: 'release'},
470+
],
471+
}),
472+
);
473+
expect(out).toContain('.define("WORKLETS_VERSION", to: "0.9.2")');
474+
// Embedded quotes escaped for the Swift string literal.
475+
expect(out).toContain(
476+
'.define("WORKLETS_FEATURE_FLAGS", to: "\\"[A:false][B:true]\\"")',
477+
);
478+
expect(out).toContain(
479+
'.define("HERMES_ENABLE_DEBUGGER", to: "1", .when(configuration: .debug))',
480+
);
481+
// Valueless define + release config.
482+
expect(out).toContain('.define("NDEBUG", .when(configuration: .release))');
483+
});
484+
456485
it('emits sources: array when podspec declared globs', () => {
457486
const out = emitScaffoldedPackageSwift(
458487
baseSpec({sources: ['ios/**/*.{h,m,mm}', 'common/cpp/**/*.{cpp,h}']}),
@@ -825,6 +854,7 @@ describe('SCAFFOLDER_VERSION', () => {
825854
swiftName: 'foo',
826855
sources: [],
827856
headerSearchPaths: [],
857+
preprocessorDefines: [],
828858
coreReactNative: false,
829859
siblingNames: [],
830860
extraFrameworks: [],

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

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
'use strict';
1212

13-
/*:: import type {PodspecModel} from './spm-types'; */
13+
/*:: import type {PodspecModel, PreprocessorDefine} from './spm-types'; */
1414

1515
/**
1616
* read-podspec.js — produces a flattened, SPM-friendly view of an iOS
@@ -315,6 +315,39 @@ function tokenizeFlags(value /*: string | null */) /*: Array<string> */ {
315315
return value.split(/\s+/).filter(Boolean);
316316
}
317317

318+
/**
319+
* Split a flag string on whitespace while keeping quoted spans intact, so a
320+
* define like `-DWORKLETS_FEATURE_FLAGS="[A:false][B:true]"` stays one token
321+
* (the quotes are part of the macro value). A naive whitespace split would
322+
* shred any define whose value contains spaces.
323+
*/
324+
function shellTokenize(value /*: string */) /*: Array<string> */ {
325+
const tokens /*: Array<string> */ = [];
326+
let cur = '';
327+
let quote /*: string | null */ = null;
328+
let has = false;
329+
for (let i = 0; i < value.length; i++) {
330+
const c = value[i];
331+
if (quote != null) {
332+
cur += c;
333+
if (c === quote) quote = null;
334+
} else if (c === '"' || c === "'") {
335+
cur += c;
336+
quote = c;
337+
has = true;
338+
} else if (/\s/.test(c)) {
339+
if (has) tokens.push(cur);
340+
cur = '';
341+
has = false;
342+
} else {
343+
cur += c;
344+
has = true;
345+
}
346+
}
347+
if (has) tokens.push(cur);
348+
return tokens;
349+
}
350+
318351
// ---------------------------------------------------------------------------
319352
// Subspec flattening
320353
// ---------------------------------------------------------------------------
@@ -413,6 +446,83 @@ function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
413446
return Array.from(new Set(out));
414447
}
415448

449+
// Lift preprocessor defines from pod_target_xcconfig across all layers:
450+
// `-D` tokens in OTHER_CFLAGS, and NAME[=VALUE] entries in
451+
// GCC_PREPROCESSOR_DEFINITIONS (incl. per-config `[config=*Debug*]` keys).
452+
// Non-define compiler flags in OTHER_CFLAGS are intentionally dropped — only
453+
// `-D`s are safe to forward; arbitrary flags may be machine- or
454+
// example-app-specific. $(inherited), unresolved $(...) tokens, and invalid
455+
// C identifiers are skipped.
456+
function mergePreprocessorDefines() /*: Array<PreprocessorDefine> */ {
457+
const out /*: Array<PreprocessorDefine> */ = [];
458+
const seen /*: Set<string> */ = new Set();
459+
const validName = /^[A-Za-z_]\w*$/;
460+
const add = (
461+
name /*: string */,
462+
value /*: ?string */,
463+
config /*: ?('debug' | 'release') */,
464+
) => {
465+
if (!validName.test(name)) return;
466+
if (/\$[({]/.test(name) || (value != null && /\$[({]/.test(value))) {
467+
return; // unresolved Xcode/Ruby token — don't emit a broken define
468+
}
469+
const key = `${name}|${config ?? ''}`;
470+
if (seen.has(key)) return;
471+
seen.add(key);
472+
out.push({name, value, config});
473+
};
474+
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(
485+
rawKey,
486+
);
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;
513+
}
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);
519+
}
520+
}
521+
}
522+
}
523+
return out;
524+
}
525+
416526
function mergeDependencies() /*: Array<string> */ {
417527
const out /*: Array<string> */ = [];
418528
for (const layer of layers) {
@@ -483,6 +593,7 @@ function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
483593
weakFrameworks: mergeArrayField('weak_frameworks'),
484594
libraries: mergeArrayField('libraries'),
485595
dependencies: mergeDependencies(),
596+
preprocessorDefines: mergePreprocessorDefines(),
486597
compilerFlags: mergeCompilerFlags(),
487598
headerSearchPaths: mergeHeaderSearchPaths(),
488599
resources: mergeArrayField('resources'),

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const {log} = makeLogger('scaffold-package-swift');
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
9292
// name (fixes "package ... doesn't exist" on resolve).
93-
const SCAFFOLDER_VERSION = 11;
93+
const SCAFFOLDER_VERSION = 12;
9494
const SCAFFOLDER_VERSION_LINE_RE = /^\/\/ AUTO-SCAFFOLDED-VERSION: (\d+)$/m;
9595

9696
const AUTOGEN_MARKER =
@@ -324,6 +324,7 @@ function translatePodspecToSpmTarget(
324324
swiftName,
325325
sources: expandedSources,
326326
headerSearchPaths,
327+
preprocessorDefines: model.preprocessorDefines,
327328
coreReactNative,
328329
siblingNames,
329330
extraFrameworks: model.frameworks,
@@ -383,10 +384,31 @@ function emitScaffoldedPackageSwift(
383384
const headerSearchPathDirectives = spec.headerSearchPaths
384385
.map(p => `.headerSearchPath("${p}")`)
385386
.join(', ');
387+
388+
// Preprocessor defines → `.define("NAME", to: "VALUE", .when(...))`. The
389+
// value is escaped as a Swift string literal because it may itself contain
390+
// quotes (e.g. a string-literal macro `-DFOO="[A:false]"`).
391+
const swiftStr = (s /*: string */) =>
392+
`"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
393+
const defineDirectives = (spec.preprocessorDefines ?? [])
394+
.map(d => {
395+
const toPart = d.value != null ? `, to: ${swiftStr(d.value)}` : '';
396+
const condPart =
397+
d.config === 'debug'
398+
? ', .when(configuration: .debug)'
399+
: d.config === 'release'
400+
? ', .when(configuration: .release)'
401+
: '';
402+
return `.define(${swiftStr(d.name)}${toPart}${condPart})`;
403+
})
404+
.join(', ');
405+
386406
const settingsEntries = (extra /*: Array<string> */) => {
387-
const parts = [headerSearchPathDirectives, ...extra].filter(
388-
e => e.length > 0,
389-
);
407+
const parts = [
408+
defineDirectives,
409+
headerSearchPathDirectives,
410+
...extra,
411+
].filter(e => e.length > 0);
390412
return `[${parts.join(', ')}]`;
391413
};
392414
const cSettings = settingsEntries([]);

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,17 @@ export type PbxprojEntry = {
326326
327327
export type PbxprojSections = {[string]: Array<PbxprojEntry>};
328328
329+
// A preprocessor define lifted from a podspec's pod_target_xcconfig
330+
// (OTHER_CFLAGS `-D...` tokens + GCC_PREPROCESSOR_DEFINITIONS entries). `value`
331+
// is null for a bare `-DNAME` (define with no value); `config` scopes the
332+
// define to a build configuration (from `[config=*Debug*]`/`[config=*Release*]`
333+
// xcconfig keys), null = unconditional. Emitted as SPM `.define(...)`.
334+
export type PreprocessorDefine = {
335+
name: string,
336+
value: ?string,
337+
config: ?('debug' | 'release'),
338+
};
339+
329340
// ---------------------------------------------------------------------------
330341
// Scaffold types — for the `npx react-native spm scaffold` command that
331342
// generates a `Package.swift` into `node_modules/<dep>/` for community RN
@@ -363,6 +374,11 @@ export type PodspecModel = {
363374
// Raw header-search-path entries from `pod_target_xcconfig['HEADER_SEARCH_PATHS']`.
364375
// May contain Xcode build setting placeholders like `$(PODS_TARGET_SRCROOT)`.
365376
headerSearchPaths: Array<string>,
377+
// Preprocessor defines lifted from pod_target_xcconfig OTHER_CFLAGS (`-D`
378+
// tokens) + GCC_PREPROCESSOR_DEFINITIONS (incl. per-config variants). Already
379+
// resolved by `pod ipc` (e.g. `-DWORKLETS_VERSION=#{package['version']}` →
380+
// `WORKLETS_VERSION=0.9.2`). Emitted as `.define(...)` on the SPM target.
381+
preprocessorDefines: Array<PreprocessorDefine>,
366382
// File paths or glob patterns the dep declares as bundled resources.
367383
resources: Array<string>,
368384
requiresArc: boolean,
@@ -389,6 +405,9 @@ export type SpmScaffoldSpec = {
389405
// Header search paths resolved to dep-root-relative form. Each entry
390406
// becomes `.headerSearchPath("<path>")` in cSettings + cxxSettings.
391407
headerSearchPaths: Array<string>,
408+
// Preprocessor defines (resolved by pod ipc) — emitted as `.define(...)` in
409+
// cSettings + cxxSettings, honoring any per-config scope.
410+
preprocessorDefines: Array<PreprocessorDefine>,
392411
// Bucketed dependency references — pre-computed by the translation layer.
393412
// `coreReactNative` is true when ANY React-* / RCT* / RCT-Folly / glog
394413
// dep is present (so we add a single `.product(name: "ReactNative", ...)`).

0 commit comments

Comments
 (0)