Skip to content
Open
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
180 changes: 178 additions & 2 deletions packages/react-native/scripts/ios-prebuild/headers-verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
* every R9 textual Fabric header against ReactNativeHeaders.
* c. A Swift TU: `import React` + `RCTBridge.moduleRegistry` — the Expo
* Swift case, proving the R9 modular header is module-visible.
* d. Swift C++-interop TUs that import React plus every importable module
* declared by the composed ReactNativeHeaders module map, then the
* shipped inspector umbrella as an isolated probe module, forcing
* ClangImporter to instantiate imported C++ value-type special members.
*
* Usage:
* node scripts/ios-prebuild/headers-verify.js [--flavor Debug|Release]
Expand Down Expand Up @@ -63,6 +67,18 @@ const FOLLY_DEFINES = [
];
const SIM_TARGET = 'arm64-apple-ios15.0-simulator';

// This Objective-C app-bootstrap module is not importable with Swift C++
// interop: its __cplusplus-only factory conformances cross-import React and
// ReactCommon module members, which ClangImporter rejects for hidden
// declarations and for re-reading the unguarded RCTComponentViewProtocol.h.
// Any new exclusion must have its own explicit justification comment.
const SWIFT_CXX_INTEROP_EXCLUDED_MODULES = new Set(['React_RCTAppDelegate']);
const SWIFT_CXX_INTEROP_PROBE_MODULE = 'ReactNativeHeaders_CxxInteropProbe';
const SWIFT_CXX_INTEROP_PROBE_TYPES = [
'facebook::react::jsinspector_modern::tracing::TraceRecordingState',
'facebook::react::jsinspector_modern::tracing::HostTracingProfile',
];

function log(msg /*: string */) {
console.log(`[headers-verify] ${msg}`);
}
Expand Down Expand Up @@ -303,12 +319,49 @@ func _headersVerifyProbe(_ bridge: RCTBridge) {
`;
}

/** Swift C++-interop fixture: every module shipped by ReactNativeHeaders. */
function renderSwiftCxxInteropFixture(
moduleNames /*: Array<string> */,
) /*: string */ {
return [
'import React',
...moduleNames.map(name => `import ${name}`),
'',
].join('\n');
}

function renderSwiftCxxInteropProbeFixture() /*: string */ {
return `import ${SWIFT_CXX_INTEROP_PROBE_MODULE}
func _headersVerifyCxxValueProbe(
_ state: borrowing facebook.react.jsinspector_modern.tracing.TraceRecordingState
) {
_ = state.mode
}
`;
}

function parseModuleNames(moduleMapPath /*: string */) /*: Array<string> */ {
const moduleMap = fs.readFileSync(moduleMapPath, 'utf8');
// Dotted submodule names may not be standalone-importable if the composed
// module map ever grows explicit submodules.
return Array.from(
moduleMap.matchAll(
/^\s*(?:explicit\s+)?(?:framework\s+)?module\s+([A-Za-z_][A-Za-z0-9_.]*)\s*\{/gm,
),
match => match[1],
);
}

function xcrun(args /*: Array<string> */, what /*: string */) {
try {
execFileSync('xcrun', args, {stdio: ['ignore', 'pipe', 'pipe']});
execFileSync('xcrun', args, {
stdio: ['ignore', 'pipe', 'pipe'],
maxBuffer: 64 * 1024 * 1024,
});
} catch (e) {
const stdout = e.stdout != null ? String(e.stdout) : '';
const stderr = e.stderr != null ? String(e.stderr) : String(e);
throw new Error(`${what} FAILED:\n${stderr}`);
throw new Error(`${what} FAILED:\n${stdout}${stderr}`);
}
}

Expand Down Expand Up @@ -405,6 +458,129 @@ function runCompileGates(
'Swift gate (import React + RCTBridge.moduleRegistry)',
);
log('compile: Swift moduleRegistry fixture OK.');

const moduleMap = path.join(rnhHeaders, 'module.modulemap');
const moduleNames = parseModuleNames(moduleMap);
if (moduleNames.length === 0) {
throw new Error(
`No modules declared in composed module map: ${moduleMap}`,
);
}
const importableModuleNames = moduleNames.filter(
name => !SWIFT_CXX_INTEROP_EXCLUDED_MODULES.has(name),
);
// React_RCTAppDelegate is the only composed module that reaches the
// inspector C++ graph, but it cannot itself be imported in C++-interop
// mode (see the exclusion comment above). Wrap the shipped inspector
// umbrella in a temporary module so ClangImporter still checks that graph.
const cxxInteropProbeHeaders = path.join(tmp, 'rnh-cxx-interop-probe');
fs.cpSync(rnhHeaders, cxxInteropProbeHeaders, {
recursive: true,
filter: source => path.basename(source) !== 'module.modulemap',
});
const cxxInteropProbeHeader = path.join(tmp, 'cxx-interop-probe.h');
const cxxInteropCopyProbes = SWIFT_CXX_INTEROP_PROBE_TYPES.map(
(typeName, index) =>
`inline void probeCopy${index}(const ${typeName} &value) {\n` +
` instantiateCopy(value);\n` +
`}\n`,
).join('');
fs.writeFileSync(
cxxInteropProbeHeader,
`#include <jsinspector-modern/ReactCdp.h>\n` +
`#include <type_traits>\n` +
`namespace rn_headers_verify {\n` +
`template <typename T> void instantiateCopy(const T &value) {\n` +
` if constexpr (std::is_copy_constructible_v<T>) {\n` +
` T copy(value);\n` +
` (void)copy;\n` +
` }\n` +
`}\n` +
cxxInteropCopyProbes +
`}\n`,
);
const cxxInteropProbeModuleMap = path.join(
tmp,
'module-cxx-interop-probe.modulemap',
);
fs.writeFileSync(
cxxInteropProbeModuleMap,
`module ${SWIFT_CXX_INTEROP_PROBE_MODULE} {\n` +
` header ${JSON.stringify(cxxInteropProbeHeader)}\n` +
` export *\n` +
`}\n`,
);
const swiftCxxInterop = path.join(tmp, 'gate-swift-cxx-interop.swift');
fs.writeFileSync(
swiftCxxInterop,
renderSwiftCxxInteropFixture(importableModuleNames),
);
xcrun(
[
'swiftc',
'-typecheck',
'-cxx-interoperability-mode=default',
'-sdk',
sdk,
'-target',
SIM_TARGET,
'-module-cache-path',
path.join(tmp, 'mc-swift-cxx-interop'),
'-F',
reactSlice,
'-I',
rnhHeaders,
'-I',
depsHeaders,
'-Xcc',
'-std=c++20',
'-Xcc',
'-w',
'-Xcc',
`-fmodule-map-file=${moduleMap}`,
...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]),
swiftCxxInterop,
],
'Swift C++-interop gate (React + importable ReactNativeHeaders modules)',
);

const swiftCxxInteropProbe = path.join(
tmp,
'gate-swift-cxx-interop-probe.swift',
);
fs.writeFileSync(swiftCxxInteropProbe, renderSwiftCxxInteropProbeFixture());
xcrun(
[
'swiftc',
'-typecheck',
'-cxx-interoperability-mode=default',
'-sdk',
sdk,
'-target',
SIM_TARGET,
'-module-cache-path',
path.join(tmp, 'mc-swift-cxx-interop-probe'),
'-I',
cxxInteropProbeHeaders,
'-I',
depsHeaders,
'-Xcc',
'-std=c++20',
'-Xcc',
'-w',
'-Xcc',
`-fmodule-map-file=${cxxInteropProbeModuleMap}`,
...FOLLY_DEFINES.flatMap(flag => ['-Xcc', flag]),
swiftCxxInteropProbe,
],
'Swift C++-interop gate (inspector value types)',
);
log(
`compile: Swift C++ interop React + ${importableModuleNames.length} ` +
`ReactNativeHeaders modules + inspector probes ` +
`[${SWIFT_CXX_INTEROP_PROBE_TYPES.join(', ')}] OK ` +
`(${moduleNames.length - importableModuleNames.length} excluded).`,
);
} finally {
fs.rmSync(tmp, {recursive: true, force: true});
}
Expand Down
Loading