Skip to content

Commit ce116a2

Browse files
chrfalchclaude
andcommitted
fix(iOS): gate prebuilt headers on Swift C++-interop importability
Add a compile gate to headers-verify.js that imports every composed ReactNativeHeaders module under -cxx-interoperability-mode=default and probes ClangImporter's copyability path for a list of value types, so a C++ type with an ill-formed-on-instantiation implicit copy constructor (invisible to C++ CI, breaks every Nitro-based Swift consumer) fails the prebuild instead of a downstream community nightly. Covers the two known offenders (TraceRecordingState, HostTracingProfile); future types are a one-line list edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e640278 commit ce116a2

1 file changed

Lines changed: 178 additions & 2 deletions

File tree

packages/react-native/scripts/ios-prebuild/headers-verify.js

Lines changed: 178 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
* every R9 textual Fabric header against ReactNativeHeaders.
3232
* c. A Swift TU: `import React` + `RCTBridge.moduleRegistry` — the Expo
3333
* Swift case, proving the R9 modular header is module-visible.
34+
* d. Swift C++-interop TUs that import React plus every importable module
35+
* declared by the composed ReactNativeHeaders module map, then the
36+
* shipped inspector umbrella as an isolated probe module, forcing
37+
* ClangImporter to instantiate imported C++ value-type special members.
3438
*
3539
* Usage:
3640
* node scripts/ios-prebuild/headers-verify.js [--flavor Debug|Release]
@@ -63,6 +67,18 @@ const FOLLY_DEFINES = [
6367
];
6468
const SIM_TARGET = 'arm64-apple-ios15.0-simulator';
6569

70+
// This Objective-C app-bootstrap module is not importable with Swift C++
71+
// interop: its __cplusplus-only factory conformances cross-import React and
72+
// ReactCommon module members, which ClangImporter rejects for hidden
73+
// declarations and for re-reading the unguarded RCTComponentViewProtocol.h.
74+
// Any new exclusion must have its own explicit justification comment.
75+
const SWIFT_CXX_INTEROP_EXCLUDED_MODULES = new Set(['React_RCTAppDelegate']);
76+
const SWIFT_CXX_INTEROP_PROBE_MODULE = 'ReactNativeHeaders_CxxInteropProbe';
77+
const SWIFT_CXX_INTEROP_PROBE_TYPES = [
78+
'facebook::react::jsinspector_modern::tracing::TraceRecordingState',
79+
'facebook::react::jsinspector_modern::tracing::HostTracingProfile',
80+
];
81+
6682
function log(msg /*: string */) {
6783
console.log(`[headers-verify] ${msg}`);
6884
}
@@ -303,12 +319,49 @@ func _headersVerifyProbe(_ bridge: RCTBridge) {
303319
`;
304320
}
305321

322+
/** Swift C++-interop fixture: every module shipped by ReactNativeHeaders. */
323+
function renderSwiftCxxInteropFixture(
324+
moduleNames /*: Array<string> */,
325+
) /*: string */ {
326+
return [
327+
'import React',
328+
...moduleNames.map(name => `import ${name}`),
329+
'',
330+
].join('\n');
331+
}
332+
333+
function renderSwiftCxxInteropProbeFixture() /*: string */ {
334+
return `import ${SWIFT_CXX_INTEROP_PROBE_MODULE}
335+
func _headersVerifyCxxValueProbe(
336+
_ state: borrowing facebook.react.jsinspector_modern.tracing.TraceRecordingState
337+
) {
338+
_ = state.mode
339+
}
340+
`;
341+
}
342+
343+
function parseModuleNames(moduleMapPath /*: string */) /*: Array<string> */ {
344+
const moduleMap = fs.readFileSync(moduleMapPath, 'utf8');
345+
// Dotted submodule names may not be standalone-importable if the composed
346+
// module map ever grows explicit submodules.
347+
return Array.from(
348+
moduleMap.matchAll(
349+
/^\s*(?:explicit\s+)?(?:framework\s+)?module\s+([A-Za-z_][A-Za-z0-9_.]*)\s*\{/gm,
350+
),
351+
match => match[1],
352+
);
353+
}
354+
306355
function xcrun(args /*: Array<string> */, what /*: string */) {
307356
try {
308-
execFileSync('xcrun', args, {stdio: ['ignore', 'pipe', 'pipe']});
357+
execFileSync('xcrun', args, {
358+
stdio: ['ignore', 'pipe', 'pipe'],
359+
maxBuffer: 64 * 1024 * 1024,
360+
});
309361
} catch (e) {
362+
const stdout = e.stdout != null ? String(e.stdout) : '';
310363
const stderr = e.stderr != null ? String(e.stderr) : String(e);
311-
throw new Error(`${what} FAILED:\n${stderr}`);
364+
throw new Error(`${what} FAILED:\n${stdout}${stderr}`);
312365
}
313366
}
314367

@@ -405,6 +458,129 @@ function runCompileGates(
405458
'Swift gate (import React + RCTBridge.moduleRegistry)',
406459
);
407460
log('compile: Swift moduleRegistry fixture OK.');
461+
462+
const moduleMap = path.join(rnhHeaders, 'module.modulemap');
463+
const moduleNames = parseModuleNames(moduleMap);
464+
if (moduleNames.length === 0) {
465+
throw new Error(
466+
`No modules declared in composed module map: ${moduleMap}`,
467+
);
468+
}
469+
const importableModuleNames = moduleNames.filter(
470+
name => !SWIFT_CXX_INTEROP_EXCLUDED_MODULES.has(name),
471+
);
472+
// React_RCTAppDelegate is the only composed module that reaches the
473+
// inspector C++ graph, but it cannot itself be imported in C++-interop
474+
// mode (see the exclusion comment above). Wrap the shipped inspector
475+
// umbrella in a temporary module so ClangImporter still checks that graph.
476+
const cxxInteropProbeHeaders = path.join(tmp, 'rnh-cxx-interop-probe');
477+
fs.cpSync(rnhHeaders, cxxInteropProbeHeaders, {
478+
recursive: true,
479+
filter: source => path.basename(source) !== 'module.modulemap',
480+
});
481+
const cxxInteropProbeHeader = path.join(tmp, 'cxx-interop-probe.h');
482+
const cxxInteropCopyProbes = SWIFT_CXX_INTEROP_PROBE_TYPES.map(
483+
(typeName, index) =>
484+
`inline void probeCopy${index}(const ${typeName} &value) {\n` +
485+
` instantiateCopy(value);\n` +
486+
`}\n`,
487+
).join('');
488+
fs.writeFileSync(
489+
cxxInteropProbeHeader,
490+
`#include <jsinspector-modern/ReactCdp.h>\n` +
491+
`#include <type_traits>\n` +
492+
`namespace rn_headers_verify {\n` +
493+
`template <typename T> void instantiateCopy(const T &value) {\n` +
494+
` if constexpr (std::is_copy_constructible_v<T>) {\n` +
495+
` T copy(value);\n` +
496+
` (void)copy;\n` +
497+
` }\n` +
498+
`}\n` +
499+
cxxInteropCopyProbes +
500+
`}\n`,
501+
);
502+
const cxxInteropProbeModuleMap = path.join(
503+
tmp,
504+
'module-cxx-interop-probe.modulemap',
505+
);
506+
fs.writeFileSync(
507+
cxxInteropProbeModuleMap,
508+
`module ${SWIFT_CXX_INTEROP_PROBE_MODULE} {\n` +
509+
` header ${JSON.stringify(cxxInteropProbeHeader)}\n` +
510+
` export *\n` +
511+
`}\n`,
512+
);
513+
const swiftCxxInterop = path.join(tmp, 'gate-swift-cxx-interop.swift');
514+
fs.writeFileSync(
515+
swiftCxxInterop,
516+
renderSwiftCxxInteropFixture(importableModuleNames),
517+
);
518+
xcrun(
519+
[
520+
'swiftc',
521+
'-typecheck',
522+
'-cxx-interoperability-mode=default',
523+
'-sdk',
524+
sdk,
525+
'-target',
526+
SIM_TARGET,
527+
'-module-cache-path',
528+
path.join(tmp, 'mc-swift-cxx-interop'),
529+
'-F',
530+
reactSlice,
531+
'-I',
532+
rnhHeaders,
533+
'-I',
534+
depsHeaders,
535+
'-Xcc',
536+
'-std=c++20',
537+
'-Xcc',
538+
'-w',
539+
'-Xcc',
540+
`-fmodule-map-file=${moduleMap}`,
541+
...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]),
542+
swiftCxxInterop,
543+
],
544+
'Swift C++-interop gate (React + importable ReactNativeHeaders modules)',
545+
);
546+
547+
const swiftCxxInteropProbe = path.join(
548+
tmp,
549+
'gate-swift-cxx-interop-probe.swift',
550+
);
551+
fs.writeFileSync(swiftCxxInteropProbe, renderSwiftCxxInteropProbeFixture());
552+
xcrun(
553+
[
554+
'swiftc',
555+
'-typecheck',
556+
'-cxx-interoperability-mode=default',
557+
'-sdk',
558+
sdk,
559+
'-target',
560+
SIM_TARGET,
561+
'-module-cache-path',
562+
path.join(tmp, 'mc-swift-cxx-interop-probe'),
563+
'-I',
564+
cxxInteropProbeHeaders,
565+
'-I',
566+
depsHeaders,
567+
'-Xcc',
568+
'-std=c++20',
569+
'-Xcc',
570+
'-w',
571+
'-Xcc',
572+
`-fmodule-map-file=${cxxInteropProbeModuleMap}`,
573+
...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]),
574+
swiftCxxInteropProbe,
575+
],
576+
'Swift C++-interop gate (inspector value types)',
577+
);
578+
log(
579+
`compile: Swift C++ interop React + ${importableModuleNames.length} ` +
580+
`ReactNativeHeaders modules + inspector probes ` +
581+
`[${SWIFT_CXX_INTEROP_PROBE_TYPES.join(', ')}] OK ` +
582+
`(${moduleNames.length - importableModuleNames.length} excluded).`,
583+
);
408584
} finally {
409585
fs.rmSync(tmp, {recursive: true, force: true});
410586
}

0 commit comments

Comments
 (0)