From 2f04a9fae17fd65e0cbda60e0b90a1f6ccf1109c Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 10 Dec 2025 14:28:50 +0100 Subject: [PATCH 01/12] [ios][precompile] refactor header files generator Replace the regex-based approach for parsing podspec files with a declarative configuration system for header file collection: Add headers-config.js with explicit podspec configurations defining header patterns, directories, and subspecs - Add vfs.js to generate VFS overlay YAML files for Clang virtual file system support - Refactor headers.js to use the new configuration-based approach with support for nested subspecs and path preservation - Update xcframework.js to handle the new header mapping structure with source/target paths - This provides more reliable and maintainable header file collection for XCFramework builds by avoiding fragile regex parsing of Ruby podspec files. --- .../scripts/ios-prebuild/headers-config.js | 888 ++++++++++++++++++ .../scripts/ios-prebuild/headers.js | 187 ++-- .../scripts/ios-prebuild/xcframework.js | 60 +- 3 files changed, 1050 insertions(+), 85 deletions(-) create mode 100644 packages/react-native/scripts/ios-prebuild/headers-config.js diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js new file mode 100644 index 000000000000..ff51d276b1dd --- /dev/null +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -0,0 +1,888 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +/*:: +export type PodSpecConfiguration = $ReadOnly<{ + name: string, + headerPatterns: Array, + headerDir?: string, + excludePatterns?: Array, + subSpecs?: $ReadOnlyArray, + preservePaths?: Array, +}>; +*/ + +// Remember that our GLOB library doesn't like {h} in its patterns, so we use **/*.h instead of **/*.{h} +const PodSpecConfigurations /*: {[key: string]: PodSpecConfiguration} */ = { + 'Libraries/ActionSheetIOS/React-RCTActionSheet.podspec': { + name: 'React-RCTActionSheet', + headerPatterns: [], + headerDir: 'RCTActionSheet', + }, + + 'Libraries/AppDelegate/React-RCTAppDelegate.podspec': { + name: 'React-RCTAppDelegate', + headerPatterns: ['**/*.h'], + headerDir: '', + }, + + 'Libraries/Blob/React-RCTBlob.podspec': { + name: 'React-RCTBlob', + headerPatterns: ['**/*.h'], + headerDir: 'RCTBlob', + }, + + 'Libraries/FBLazyVector/FBLazyVector.podspec': { + name: 'FBLazyVector', + headerPatterns: ['**/*.h'], + headerDir: 'FBLazyVector', + }, + + 'Libraries/Image/React-RCTImage.podspec': { + name: 'React-RCTImage', + headerPatterns: ['**/*.h'], + headerDir: 'RCTImage', + }, + + 'Libraries/LinkingIOS/React-RCTLinking.podspec': { + name: 'React-RCTLinking', + headerPatterns: [], + headerDir: 'RCTLinking', + }, + + 'Libraries/NativeAnimation/React-RCTAnimation.podspec': { + name: 'React-RCTAnimation', + headerPatterns: ['**/*.h'], + headerDir: 'RCTAnimation', + }, + + 'Libraries/Network/React-RCTNetwork.podspec': { + name: 'React-RCTNetwork', + headerPatterns: [], + headerDir: 'RCTNetwork', + }, + + 'Libraries/PushNotificationIOS/React-RCTPushNotification.podspec': { + name: '', + headerPatterns: [], + headerDir: '', + }, + + 'Libraries/Required/RCTRequired.podspec': { + name: 'RCTRequired', + headerPatterns: ['*.h'], + headerDir: 'RCTRequired', + }, + + 'Libraries/Settings/React-RCTSettings.podspec': { + name: 'React-RCTSettings', + headerPatterns: ['*.h'], + headerDir: 'RCTSettings', + }, + + 'Libraries/Text/React-RCTText.podspec': { + name: 'React-RCTText', + headerPatterns: ['**/*.h'], + headerDir: 'RCTText', + }, + + 'Libraries/TypeSafety/RCTTypeSafety.podspec': { + name: 'RCTTypeSafety', + headerPatterns: ['**/*.h'], + headerDir: 'RCTTypeSafety', + }, + + 'Libraries/Vibration/React-RCTVibration.podspec': { + name: 'React-RCTVibration', + headerPatterns: ['**/*.h'], + headerDir: 'RCTVibration', + }, + + 'React.podspec': {name: '', headerPatterns: [], headerDir: ''}, + + 'React/CoreModules/React-CoreModules.podspec': { + name: 'React-CoreModules', + headerPatterns: ['**/*.h'], + headerDir: 'CoreModules', + excludePatterns: ['PlatformStubs/**/*'], // TODO: Only for iOS! + }, + + 'React/React-RCTFabric.podspec': { + name: 'React-RCTFabric', + headerPatterns: ['Fabric/**/*.h'], + headerDir: 'React', + excludePatterns: ['**/tests/*', '**/android/*'], + }, + + 'React/React-RCTFBReactNativeSpec.podspec': { + name: 'React-RCTFBReactNativeSpec', + headerPatterns: ['FBReactNativeSpec/**/*.h'], + headerDir: 'FBReactNativeSpec', + excludePatterns: ['FBReactNativeSpec/react/renderer/components/**'], + subSpecs: [ + { + name: 'components', + headerPatterns: [ + 'FBReactNativeSpec/react/renderer/components/FBReactNativeSpec/**/*.h', + ], + headerDir: 'react/renderer/components/FBReactNativeSpec', + }, + ], + }, + + 'React/Runtime/React-RCTRuntime.podspec': { + name: 'React-RCTRuntime', + headerPatterns: ['*.h'], + headerDir: 'React', + }, + + 'ReactApple/Libraries/RCTFoundation/RCTDeprecation/RCTDeprecation.podspec': { + name: 'RCTDeprecation', + headerPatterns: ['Exported/*.h'], + headerDir: '', + }, + + 'ReactApple/RCTSwiftUI/RCTSwiftUI.podspec': { + name: 'RCTSwiftUI', + headerPatterns: ['*.h'], + headerDir: 'RCTSwiftUI', + }, + + 'ReactApple/RCTSwiftUIWrapper/RCTSwiftUIWrapper.podspec': { + name: 'RCTSwiftUIWrapper', + headerPatterns: ['*.h'], + headerDir: 'RCTSwiftUIWrapper', + }, + + 'ReactCommon/callinvoker/React-callinvoker.podspec': { + name: 'React-callinvoker', + headerPatterns: ['**/*.h'], + headerDir: 'ReactCommon', + }, + + 'ReactCommon/cxxreact/React-cxxreact.podspec': { + name: 'React-cxxreact', + headerPatterns: ['*.h'], + headerDir: 'cxxreact', + }, + + 'ReactCommon/hermes/executor/React-jsitracing.podspec': { + name: 'React-jsitracing', + headerPatterns: ['JSITracing.h'], + }, + + 'ReactCommon/hermes/React-hermes.podspec': { + name: 'React-hermes', + headerPatterns: [ + 'executor/*.h', + 'inspector-modern/chrome/*.h', + 'executor/HermesExecutorFactory.h', + ], + headerDir: 'reacthermes', + }, + + 'ReactCommon/jserrorhandler/React-jserrorhandler.podspec': { + name: 'React-jserrorhandler', + headerPatterns: ['JsErrorHandler.h', 'StackTraceParser.h'], + headerDir: 'jserrorhandler', + }, + + 'ReactCommon/jsi/React-jsi.podspec': { + name: 'React-jsi', + headerPatterns: ['**/*.h'], + headerDir: 'jsi', + excludePatterns: ['**/test/*'], + }, + + 'ReactCommon/jsiexecutor/React-jsiexecutor.podspec': { + name: 'React-jsiexecutor', + headerPatterns: ['jsireact/*.h'], + headerDir: 'jsireact', + }, + + 'ReactCommon/jsinspector-modern/cdp/React-jsinspectorcdp.podspec': { + name: 'React-jsinspectorcdp', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern/cdp', + }, + + 'ReactCommon/jsinspector-modern/network/React-jsinspectornetwork.podspec': { + name: 'React-jsinspectornetwork', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern/network', + }, + + 'ReactCommon/jsinspector-modern/React-jsinspector.podspec': { + name: 'React-jsinspector', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern', + }, + + 'ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec': { + name: 'React-jsinspectortracing', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern/tracing', + }, + + 'ReactCommon/jsitooling/React-jsitooling.podspec': { + name: 'React-jsitooling', + headerPatterns: ['react/runtime/*.h'], + headerDir: 'react/runtime', + }, + + 'ReactCommon/logger/React-logger.podspec': { + name: 'React-logger', + headerPatterns: ['*.h'], + headerDir: 'logger', + }, + + 'ReactCommon/oscompat/React-oscompat.podspec': { + name: 'React-oscompat', + headerPatterns: ['*.h'], + headerDir: 'oscompat', + }, + + 'ReactCommon/React-Fabric.podspec': { + name: 'React-Fabric', + headerPatterns: [], + headerDir: '', + subSpecs: [ + { + name: 'animated', + headerPatterns: ['react/renderer/animated/**/*.h'], + excludePatterns: ['react/renderer/animated/tests'], + headerDir: 'react/renderer/animated', + }, + + { + name: 'animations', + headerPatterns: ['react/renderer/animations/**/*.h'], + excludePatterns: ['react/renderer/animations/tests'], + headerDir: 'react/renderer/animations', + }, + + { + name: 'animationbackend', + headerPatterns: ['react/renderer/animationbackend/**/*.h'], + headerDir: 'react/renderer/animationbackend', + }, + + { + name: 'attributedstring', + headerPatterns: ['react/renderer/attributedstring/**/*.h'], + excludePatterns: ['react/renderer/attributedstring/tests'], + headerDir: 'react/renderer/attributedstring', + }, + + { + name: 'bridging', + headerPatterns: ['react/renderer/bridging/**/*.h'], + excludePatterns: ['react/renderer/bridging/tests'], + headerDir: 'react/renderer/bridging', + }, + + { + name: 'core', + headerPatterns: ['react/renderer/core/**/*.h'], + excludePatterns: ['react/renderer/core/tests'], + headerDir: 'react/renderer/core', + }, + + { + name: 'componentregistry', + headerPatterns: ['react/renderer/componentregistry/*.h'], + headerDir: 'react/renderer/componentregistry', + }, + + { + name: 'componentregistrynative', + headerPatterns: ['react/renderer/componentregistry/native/**/*.h'], + headerDir: 'react/renderer/componentregistry/native', + }, + + { + name: 'components', + headerPatterns: [], + headerDir: '', + subSpecs: [ + { + name: 'root', + headerPatterns: ['react/renderer/components/root/**/*.h'], + excludePatterns: ['react/renderer/components/root/tests'], + headerDir: 'react/renderer/components/root', + }, + { + name: 'view', + headerPatterns: [ + 'react/renderer/components/view/*.h', + 'react/renderer/components/view/platform/cxx/**/*.h', + ], + headerDir: 'react/renderer/components/view', + }, + + { + name: 'scrollview', + headerPatterns: ['react/renderer/components/scrollview/**/*.h'], + headerDir: 'react/renderer/components/scrollview', + excludePatterns: [ + 'react/renderer/components/scrollview/tests', + 'react/renderer/components/scrollview/platform/android', + ], + }, + + { + name: 'legacyviewmanagerinterop', + headerPatterns: [ + 'react/renderer/components/legacyviewmanagerinterop/**/*.h', + ], + excludePatterns: [ + 'react/renderer/components/legacyviewmanagerinterop/tests', + ], + headerDir: 'react/renderer/components/legacyviewmanagerinterop', + }, + ], + }, + + { + name: 'dom', + headerPatterns: ['react/renderer/dom/**/*.h'], + excludePatterns: ['react/renderer/dom/tests'], + headerDir: 'react/renderer/dom', + }, + + { + name: 'scheduler', + headerPatterns: ['react/renderer/scheduler/**/*.h'], + headerDir: 'react/renderer/scheduler', + }, + + { + name: 'imagemanager', + headerPatterns: ['react/renderer/imagemanager/*.h'], + headerDir: 'react/renderer/imagemanager', + }, + + { + name: 'mounting', + headerPatterns: ['react/renderer/mounting/**/*.h'], + excludePatterns: ['react/renderer/mounting/tests'], + headerDir: 'react/renderer/mounting', + }, + + { + name: 'observers', + headerPatterns: [], + subSpecs: [ + { + name: 'events', + headerPatterns: ['react/renderer/observers/events/**/*.h'], + excludePatterns: ['react/renderer/observers/events/tests'], + headerDir: 'react/renderer/observers/events', + }, + ], + }, + + { + name: 'templateprocessor', + headerPatterns: ['react/renderer/templateprocessor/**/*.h'], + excludePatterns: ['react/renderer/templateprocessor/tests'], + headerDir: 'react/renderer/templateprocessor', + }, + + { + name: 'telemetry', + headerPatterns: ['react/renderer/telemetry/**/*.h'], + excludePatterns: ['react/renderer/telemetry/tests'], + headerDir: 'react/renderer/telemetry', + }, + + { + name: 'consistency', + headerPatterns: ['react/renderer/consistency/**/*.h'], + headerDir: 'react/renderer/consistency', + }, + + { + name: 'uimanager', + subSpecs: [ + { + name: 'consistency', + headerPatterns: ['react/renderer/uimanager/consistency/*.h'], + headerDir: 'react/renderer/uimanager/consistency', + }, + ], + + headerPatterns: ['react/renderer/uimanager/*.h'], + headerDir: 'react/renderer/uimanager', + }, + + { + name: 'leakchecker', + headerPatterns: ['react/renderer/leakchecker/**/*.h'], + excludePatterns: ['react/renderer/leakchecker/tests'], + headerDir: 'react/renderer/leakchecker', + }, + ], + }, + + 'ReactCommon/React-FabricComponents.podspec': { + name: 'React-FabricComponents', + headerPatterns: [], + headerDir: '', + subSpecs: [ + { + name: 'components', + headerPatterns: [], + headerDir: '', + subSpecs: [ + { + name: 'inputaccessory', + headerPatterns: ['react/renderer/components/inputaccessory/**/*.h'], + excludePatterns: ['react/renderer/components/inputaccessory/tests'], + headerDir: 'react/renderer/components/inputaccessory', + }, + + { + name: 'modal', + headerPatterns: ['react/renderer/components/modal/*.h'], + excludePatterns: ['react/renderer/components/modal/tests'], + headerDir: 'react/renderer/components/modal', + }, + + { + name: 'safeareaview', + headerPatterns: ['react/renderer/components/safeareaview/**/*.h'], + excludePatterns: ['react/renderer/components/safeareaview/tests'], + headerDir: 'react/renderer/components/safeareaview', + }, + + { + name: 'scrollview', + headerPatterns: [ + 'react/renderer/components/scrollview/*.h', + 'react/renderer/components/scrollview/platform/cxx/**/*.h', + ], + excludePatterns: ['react/renderer/components/scrollview/tests'], + headerDir: 'react/renderer/components/scrollview', + }, + + { + name: 'text', + headerPatterns: [ + 'react/renderer/components/text/*.h', + 'react/renderer/components/text/platform/cxx/**/*.h', + ], + headerDir: 'react/renderer/components/text', + }, + + { + name: 'iostextinput', + headerPatterns: [ + 'react/renderer/components/textinput/*.h', + 'react/renderer/components/textinput/platform/ios/**/*.h', + ], + headerDir: 'react/renderer/components/iostextinput', + }, + + { + name: 'switch', + headerPatterns: [ + 'react/renderer/components/switch/iosswitch/**/*.h', + ], + excludePatterns: [ + 'react/renderer/components/switch/iosswitch/**/MacOS*.{m,mm,cpp,h}', + ], + headerDir: 'react/renderer/components/switch/', + }, + + { + name: 'textinput', + headerPatterns: ['react/renderer/components/textinput/**/*.h'], + headerDir: 'react/renderer/components/textinput', + }, + + { + name: 'unimplementedview', + headerPatterns: [ + 'react/renderer/components/unimplementedview/**/*.h', + ], + excludePatterns: [ + 'react/renderer/components/unimplementedview/tests', + ], + headerDir: 'react/renderer/components/unimplementedview', + }, + + { + name: 'virtualview', + headerPatterns: [ + 'react/renderer/components/virtualview/**/*.{m,mm,cpp,h}', + ], + excludePatterns: ['react/renderer/components/virtualview/tests'], + headerDir: 'react/renderer/components/virtualview', + }, + + { + name: 'virtualviewexperimental', + headerPatterns: [ + 'react/renderer/components/virtualviewexperimental/**/*.h', + ], + excludePatterns: [ + 'react/renderer/components/virtualviewexperimental/tests', + ], + headerDir: 'react/renderer/components/virtualviewexperimental', + }, + + { + name: 'rncore', + headerPatterns: ['react/renderer/components/rncore/**/*.h'], + headerDir: 'react/renderer/components/rncore', + }, + ], + }, + { + name: 'textlayoutmanager', + + headerPatterns: [ + 'react/renderer/textlayoutmanager/platform/ios/**/*.h', + 'react/renderer/textlayoutmanager/*.h', + ], + excludePatterns: [ + 'react/renderer/textlayoutmanager/tests', + 'react/renderer/textlayoutmanager/platform/android', + 'react/renderer/textlayoutmanager/platform/cxx', + ], + headerDir: 'react/renderer/textlayoutmanager', + }, + ], + }, + + 'ReactCommon/React-FabricImage.podspec': { + name: 'React-FabricImage', + headerPatterns: ['react/renderer/components/image/**/*.h'], + excludePatterns: ['react/renderer/components/image/tests'], + headerDir: 'react/renderer/components/image', + }, + + 'ReactCommon/React-Mapbuffer.podspec': { + name: 'React-Mapbuffer', + headerPatterns: ['react/renderer/mapbuffer/*.h'], + headerDir: 'react/renderer/mapbuffer', + }, + + 'ReactCommon/react/debug/React-debug.podspec': { + name: 'React-debug', + headerPatterns: ['**/*.h'], + headerDir: 'react/debug', + }, + + 'ReactCommon/react/featureflags/React-featureflags.podspec': { + name: 'React-featureflags', + headerPatterns: ['**/*.h'], + headerDir: 'react/featureflags', + }, + + 'ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec': + { + name: 'React-NativeModulesApple', + headerPatterns: ['ReactCommon/**/*.h'], + headerDir: 'ReactCommon', + }, + + 'ReactCommon/react/nativemodule/defaults/React-defaultsnativemodule.podspec': + { + name: 'React-defaultsnativemodule', + headerPatterns: ['*.h'], + headerDir: 'react/nativemodule/defaults', + }, + + 'ReactCommon/react/nativemodule/dom/React-domnativemodule.podspec': { + name: 'React-domnativemodule', + headerPatterns: ['*.h'], + headerDir: 'react/nativemodule/dom', + }, + + 'ReactCommon/react/nativemodule/featureflags/React-featureflagsnativemodule.podspec': + { + name: 'React-featureflagsnativemodule', + headerPatterns: ['*.h'], + headerDir: 'react/nativemodule/featureflags', + }, + + 'ReactCommon/react/nativemodule/idlecallbacks/React-idlecallbacksnativemodule.podspec': + { + name: 'React-idlecallbacksnativemodule', + headerPatterns: ['*.h'], + headerDir: 'react/nativemodule/idlecallbacks', + }, + + 'ReactCommon/react/nativemodule/microtasks/React-microtasksnativemodule.podspec': + { + name: 'React-microtasksnativemodule', + headerPatterns: ['*.h'], + headerDir: 'react/nativemodule/microtasks', + }, + // We don't need to include samples in our header file structure. + // 'ReactCommon/react/nativemodule/samples/ReactCommon-Samples.podspec': + // { + // name: 'ReactCommon-Samples', + // headerPatterns: ['**/*.h'], + // headerDir: 'react/nativemodule/samples', + // }, + + 'ReactCommon/react/nativemodule/webperformance/React-webperformancenativemodule.podspec': + { + name: 'React-webperformancenativemodule', + headerPatterns: ['*.h'], + headerDir: 'react/nativemodule/webperformance', + }, + + 'ReactCommon/react/networking/React-networking.podspec': { + name: 'React-networking', + headerPatterns: ['*.h'], + headerDir: 'react/networking', + }, + + 'ReactCommon/react/performance/cdpmetrics/React-performancecdpmetrics.podspec': + { + name: 'React-performancecdpmetrics', + headerPatterns: ['*.h'], + headerDir: 'react/performance/cdpmetrics', + }, + + 'ReactCommon/react/performance/timeline/React-performancetimeline.podspec': { + name: 'React-performancetimeline', + headerPatterns: ['*.h'], + headerDir: 'react/performance/timeline', + }, + + 'ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec': { + name: 'React-rendererconsistency', + headerPatterns: ['*.h'], + headerDir: 'react/renderer/consistency', + }, + + 'ReactCommon/react/renderer/css/React-renderercss.podspec': { + name: 'React-renderercss', + headerPatterns: ['*.h'], + headerDir: 'react/renderer/css', + }, + + 'ReactCommon/react/renderer/debug/React-rendererdebug.podspec': { + name: 'React-rendererdebug', + headerPatterns: ['*.h'], + headerDir: 'react/renderer/debug', + }, + + 'ReactCommon/react/renderer/graphics/React-graphics.podspec': { + name: 'React-graphics', + headerPatterns: ['*.h', 'platform/ios/**/*.h'], + headerDir: 'react/renderer/graphics', + }, + + 'ReactCommon/react/renderer/imagemanager/platform/ios/React-ImageManager.podspec': + { + name: 'React-ImageManager', + headerPatterns: ['**/*.h'], + headerDir: 'react/renderer/imagemanager', + }, + + 'ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec': + { + name: 'React-runtimescheduler', + headerPatterns: ['*.h'], + headerDir: 'react/renderer/runtimescheduler', + }, + + 'ReactCommon/react/runtime/platform/ios/React-RuntimeApple.podspec': { + name: 'React-RuntimeApple', + headerPatterns: ['ReactCommon/*.h'], + headerDir: 'ReactCommon', + excludePatterns: ['ReactCommon/RCTJscInstance.h'], + }, + + 'ReactCommon/react/runtime/React-RuntimeCore.podspec': { + name: 'React-RuntimeCore', + headerPatterns: ['*.h', 'nativeviewconfig/*.h'], + headerDir: 'react/runtime', + }, + + 'ReactCommon/react/runtime/React-RuntimeHermes.podspec': { + name: 'React-RuntimeHermes', + headerPatterns: ['hermes/*.h'], + headerDir: 'react/runtime/hermes', + }, + + 'ReactCommon/react/timing/React-timing.podspec': { + name: 'React-timing', + headerPatterns: ['**/*.h'], + headerDir: 'react/timing', + }, + + 'ReactCommon/react/utils/React-utils.podspec': { + name: 'React-utils', + headerPatterns: ['*.h', 'platform/ios/**/*.h'], + headerDir: 'react/utils', + excludePatterns: ['tests'], + }, + + 'ReactCommon/ReactCommon.podspec': { + name: 'ReactCommon', + headerPatterns: [], + headerDir: 'ReactCommon', + subSpecs: [ + { + name: 'turbomodule', + headerPatterns: [], + subSpecs: [ + { + name: 'bridging', + headerPatterns: ['react/bridging/**/*.h'], + headerDir: 'react/bridging', + excludePatterns: ['react/bridging/tests/**/*'], + }, + { + name: 'core', + headerPatterns: ['react/nativemodule/core/ReactCommon/**/*.h'], + }, + ], + }, + ], + }, + + 'ReactCommon/reactperflogger/React-perflogger.podspec': { + name: 'React-perflogger', + headerPatterns: ['reactperflogger/*.h', 'fusebox/*.h'], + headerDir: 'reactperflogger', + }, + + 'ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec': { + name: 'React-runtimeexecutor', + headerPatterns: ['ReactCommon/*.h', 'platform/ios/**/*.h'], + headerDir: 'ReactCommon', + }, + + 'ReactCommon/yoga/Yoga.podspec': { + name: 'Yoga', + headerPatterns: ['yoga/**/*.h'], + headerDir: 'yoga', + preservePaths: ['yoga/**/*.h'], + }, + // These should be distributed through the Hermes xcframework. + // 'sdks/hermes/hermes-engine.podspec': + // { + // name: 'hermes-engine', + // headerPatterns: [], + // headerDir: '', + // preservePaths: ['**/*.*'], + // subSpecs: [ + // { + // name: 'Hermes', + // headerPatterns: ['destroot/include/hermes/*.h'], + // headerDir: 'hermes', + // }, + + // { + // name: 'cdp', + // headerPatterns: ['destroot/include/hermes/cdp/*.h'], + // headerDir: 'hermes/cdp', + // }, + + // { + // name: 'inspector', + // headerPatterns: ['destroot/include/hermes/inspector/*.h'], + // headerDir: 'hermes/inspector', + // }, + + // { + // name: 'inspector_chrome', + // headerPatterns: ['destroot/include/hermes/inspector/chrome/*.h'], + // headerDir: 'hermes/inspector/chrome', + // }, + + // { + // name: 'jsi', + // headerPatterns: ['destroot/include/jsi/*.h'], + // headerDir: 'jsi', + // }, + + // { + // name: 'Public', + // headerPatterns: ['public/hermes/Public/*.h'], + // headerDir: 'hermes/Public', + // }, + // ], + // }, + + 'React-Core.podspec': { + name: 'React-Core', + headerPatterns: [], + headerDir: 'React', + subSpecs: [ + { + name: 'Default', + headerPatterns: ['React/**/*.h'], + excludePatterns: [ + 'React/CoreModules/**/*', + 'React/DevSupport/**/*', + 'React/Fabric/**/*', + 'React/FBReactNativeSpec/**/*', + 'React/Tests/**/*', + 'React/Inspector/**/*', + 'React/Runtime/**/*', + 'React/CxxBridge/JSCExecutorFactory.h', + ], + }, + { + name: 'DevSupport', + headerPatterns: ['React/DevSupport/*.h', 'React/Inspector/*.h'], + }, + {name: 'RCTWebSocket', headerPatterns: ['Libraries/WebSocket/*.h']}, + { + name: 'CoreModulesHeaders', + headerPatterns: ['React/CoreModules/**/*.h'], + }, + { + name: 'RCTActionSheetHeaders', + headerPatterns: ['Libraries/ActionSheetIOS/*.h'], + }, + { + name: 'RCTAnimationHeaders', + headerPatterns: ['Libraries/NativeAnimation/{Drivers/*,Nodes/*,*}.h'], + }, + { + name: 'RCTBlobHeaders', + headerPatterns: [ + 'Libraries/Blob/{RCTBlobManager,RCTFileReaderModule}.h', + ], + }, + {name: 'RCTImageHeaders', headerPatterns: ['Libraries/Image/*.h']}, + { + name: 'RCTLinkingHeaders', + headerPatterns: ['Libraries/LinkingIOS/*.h'], + }, + {name: 'RCTNetworkHeaders', headerPatterns: ['Libraries/Network/*.h']}, + { + name: 'RCTPushNotificationHeaders', + headerPatterns: ['Libraries/PushNotificationIOS/*.h'], + }, + { + name: 'RCTSettingsHeaders', + headerPatterns: ['Libraries/Settings/*.h'], + }, + {name: 'RCTTextHeaders', headerPatterns: ['Libraries/Text/**/*.h']}, + { + name: 'RCTVibrationHeaders', + headerPatterns: ['Libraries/Vibration/*.h'], + }, + ], + }, +}; + +module.exports = PodSpecConfigurations; diff --git a/packages/react-native/scripts/ios-prebuild/headers.js b/packages/react-native/scripts/ios-prebuild/headers.js index 8ef1a23bbf0d..563e6703689b 100644 --- a/packages/react-native/scripts/ios-prebuild/headers.js +++ b/packages/react-native/scripts/ios-prebuild/headers.js @@ -8,83 +8,146 @@ * @format */ -const fs = require('fs'); +const PodSpecConfigurations = require('./headers-config'); +const utils = require('./utils'); const path = require('path'); const {globSync} = require('tinyglobby'); +const {createLogger} = utils; +const headersLog = createLogger('headers'); + +/*:: +import type {PodSpecConfiguration} from './headers-config'; +type HeaderMap = { headerDir: string, specName: string, headers: {source: string, target: string}[]}; +*/ + /** - * This regular expression is designed to match function calls to `podspec_sources` within a podspec file. - * - * Example matches: - * 1. `podspec_sources("source1", "sourceForPrebuilds1")` - * - Captures: "source1" as the first argument, "sourceForPrebuilds1" as the second argument. - * - * 2. `podspec_sources(["source1", "source2"], ["sourceForPrebuilds1", "sourceForPrebuilds2"])` - * - Captures: ["source1", "source2"] as the first argument, ["sourceForPrebuilds1", "sourceForPrebuilds2"] as the second argument. - * - * 3. `podspec_sources('source1', ['sourceForPrebuilds1', 'sourceForPrebuilds2'])` - * - Captures: 'source1' as the first argument, ['sourceForPrebuilds1', 'sourceForPrebuilds2'] as the second argument. + * Enumerates all podspec files in the PodSpecConfigurations structure above and maps them to + * their header files based on the configuration. + * @param {*} rootFolder Root folder to search for podspec files + * @param {*} testHeadersFlag Flag to indicate whether to test headers against a test directory + * @param {*} targetTestFolder Target folder to test headers against + * @returns */ -const regex = - /podspec_sources\s*\(\s*((?:\[[^\]]*\]|"[^"]*"|'[^']*'|[^,])+)\s*,\s*((?:\[[^\]]*\]|"[^"]*"|'[^']*'|[^)])+)\s*\)/gs; - function getHeaderFilesFromPodspecs( rootFolder /*:string*/, -) /*: { [key: string]: string[] }*/ { - // Find podspec files - const podSpecFiles = globSync('**/*.podspec', { - cwd: rootFolder, - absolute: true, - onlyFiles: true, - }); +) /*: { [key: string]: HeaderMap[] }*/ { + // Get podspec files in the configuration mapped to configurations + const podSpecFiles = Object.keys(PodSpecConfigurations).map(podspecPath => + path.resolve(rootFolder, podspecPath), + ); + + headersLog('🔍 Collecting header files from podspec configurations...'); + + const headerMaps /*: { [key: string]: HeaderMap[] }*/ = {}; + + podSpecFiles.forEach(podspecPath => { + const key = path.relative(rootFolder, podspecPath); + const podSpecConfig = PodSpecConfigurations[key]; + if ( + !podSpecConfig || + 'name' in podSpecConfig === false || + podSpecConfig.name === '' + ) { + headersLog( + `⚠️ Skipping podspec at ${podspecPath} due to missing or invalid configuration.`, + ); + return; + } + + const podSpecDirectory = path.dirname(podspecPath); + + // Now we can start collecting header files + const processConfig = ( + config /*: PodSpecConfiguration */, + parents /*: Array*/, + ) => { + const {headerDir, headerPatterns, excludePatterns, subSpecs} = config; - const headers /*: { [key: string]: string[] }*/ = {}; - - podSpecFiles.forEach(podspec => { - const content = fs.readFileSync(podspec, 'utf8'); - // Find all podspec_sources calls - let match; - while ((match = regex.exec(content)) !== null) { - if (match) { - let globPatterns /*: string[] */; - let arg2 = match[2]?.trim().replace(/['"]/g, ''); - if (!arg2) { - // Skip - continue; + // Find header files for configuration + const foundHeaderFiles = headerPatterns + .map(pattern => + globSync(pattern, { + cwd: podSpecDirectory, + absolute: true, + ignore: excludePatterns || [], + }), + ) + .flat(); + + let resolvedHeaderDir /*:string */ = headerDir || ''; + + // If headerDir is not set, we need to resolve it against parent specs + if (parents.length > 0 && !headerDir) { + for (let i = parents.length - 1; i >= 0; i--) { + const parentHeaderDir = parents[i].headerDir; + if (parentHeaderDir) { + resolvedHeaderDir = parentHeaderDir; + break; + } } - // Check if arg2 is an array (e.g., ['a', 'b']) - if (arg2.startsWith('[') && arg2.endsWith(']')) { - // Remove the brackets and split by comma - globPatterns = arg2 - .slice(1, -1) - .split(',') - .map(item => item.trim()); - } else { - globPatterns = [arg2]; + } + + // If still not resolved, default to spec name + if (!resolvedHeaderDir) { + resolvedHeaderDir = ''; + } + + // Resolve preservePaths from parent specs too + let resolvedPreservePaths = config.preservePaths || []; + if (resolvedPreservePaths.length === 0 && parents.length > 0) { + for (let i = parents.length - 1; i >= 0; i--) { + const parentPreservePaths = parents[i].preservePaths; + if (parentPreservePaths && parentPreservePaths.length > 0) { + resolvedPreservePaths = parentPreservePaths; + break; + } } + } - // Do the glob! - const p = path.resolve(process.cwd(), path.dirname(podspec)); - const results = globPatterns - .map(g => { - return globSync(g.replace('{h}', 'h'), { - cwd: p, + headerMaps[podspecPath] = (headerMaps[podspecPath] || []).concat({ + headerDir: resolvedHeaderDir, + specName: podSpecConfig.name, + headers: foundHeaderFiles.map(headerFile => { + // Check if we have preservePath set for this file - then we need to get the subfolder structure too + // and not just copy to the root of headerDir - we should also ignore the headerDir part of the path + const isPreserved = resolvedPreservePaths.some(preservePattern => { + return globSync(preservePattern, { + cwd: podSpecDirectory, absolute: true, - expandDirectories: false, - }); - }) - .flat(); - - if (!headers[podspec]) { - headers[podspec] = results; - } else { - headers[podspec].push(...results); - } + ignore: excludePatterns || [], + }).includes(headerFile); + }); + + if (isPreserved) { + // Get the subfolder for the header file + const relativePath = path.dirname( + path.relative(podSpecDirectory, headerFile), + ); + return { + source: headerFile, + target: path.join(relativePath, path.basename(headerFile)), + }; + } + return { + source: headerFile, + target: path.join(resolvedHeaderDir, path.basename(headerFile)), + }; + }), + }); + + // Process subSpecs recursively + if (subSpecs && subSpecs.length > 0) { + subSpecs.forEach(subSpecConfig => { + processConfig(subSpecConfig, [config, ...parents]); + }); } - } + }; + + processConfig(podSpecConfig, []); }); - return headers; + return headerMaps; } module.exports = { diff --git a/packages/react-native/scripts/ios-prebuild/xcframework.js b/packages/react-native/scripts/ios-prebuild/xcframework.js index 1ff9f992ca2b..01d93de6454d 100644 --- a/packages/react-native/scripts/ios-prebuild/xcframework.js +++ b/packages/react-native/scripts/ios-prebuild/xcframework.js @@ -114,7 +114,16 @@ function buildXCFrameworks( // Enumerate podspecs and copy headers, create umbrella headers and module map file Object.keys(podSpecsWithHeaderFiles).forEach(podspec => { - const headerFiles = podSpecsWithHeaderFiles[podspec]; + const headerFiles = podSpecsWithHeaderFiles[podspec] + .map(h => h.headers) + .flat(); + + // Use the first podspec spec name as the podspec name (this is the root spec in the podspec file) + const podSpecName = podSpecsWithHeaderFiles[podspec][0].specName.replace( + '-', + '_', + ); + if (headerFiles.length > 0) { // Get podspec name without directory and extension and make sure it is a valid identifier // by replacing any non-alphanumeric characters with an underscore. @@ -129,24 +138,24 @@ function buildXCFrameworks( } // Create a folder for the podspec in the output headers path - const podSpecFolder = path.join(outputHeadersPath, podSpecName); - createFolderIfNotExists(podSpecFolder); + const podSpecTargetFolder = path.join(outputHeadersPath, podSpecName); // Copy each header file to the podspec folder copiedHeaderFilesWithPodspecNames[podSpecName] = headerFiles.map( headerFile => { - // Header files shall be flattened into the podSpecFoldder: - const targetFile = path.join( - podSpecFolder, - path.basename(headerFile), + const headerFileTargetPath = path.join( + podSpecTargetFolder, + headerFile.target, ); - fs.copyFileSync(headerFile, targetFile); - return targetFile; + createFolderIfNotExists(path.dirname(headerFileTargetPath)); + fs.copyFileSync(headerFile.source, headerFileTargetPath); + return headerFileTargetPath; }, ); + // Create umbrella header file for the podspec const umbrellaHeaderFilename = path.join( - podSpecFolder, + podSpecTargetFolder, podSpecName + '-umbrella.h', ); @@ -183,7 +192,9 @@ function buildXCFrameworks( return; } - linkArchFolders( + // Copy header files and module map file to each platform slice in the XCFramework + copyHeaderFilesToSlices( + rootFolder, outputPath, moduleMapFile, umbrellaHeaders, @@ -253,13 +264,15 @@ function copySymbols( }); } -function linkArchFolders( +// Copy header files and module map file to each platform slice in the XCFramework. +function copyHeaderFilesToSlices( + rootFolder /*:string*/, outputPath /*:string*/, moduleMapFile /*:string*/, umbrellaHeaderFiles /*:{[key: string]: string}*/, outputHeaderFiles /*: {[key: string]: string[]} */, ) { - frameworkLog('Linking modules and headers to platform folders...'); + frameworkLog('Linking modules and headers to platform folders for slice...'); // Enumerate all platform folders in the output path const platformFolders = fs @@ -309,7 +322,7 @@ function linkArchFolders( createFolderIfNotExists(targetPodSpecFolder); // Link the umbrella header file to the target folder try { - fs.linkSync( + fs.copyFileSync( umbrellaHeaderFile, path.join(targetPodSpecFolder, path.basename(umbrellaHeaderFile)), ); @@ -323,21 +336,22 @@ function linkArchFolders( Object.keys(outputHeaderFiles).forEach(podSpecName => { outputHeaderFiles[podSpecName].forEach(headerFile => { - // Create the target folder for the umbrella header file - const targetPodSpecFolder = path.join(targetHeadersFolder, podSpecName); - createFolderIfNotExists(targetPodSpecFolder); - // Link the header file to the target folder - here we might have a few files with the same name - // since we're flattening the imports. Yoga has two files - these can be ignored. + // Get the relative path from the root Headers folder to preserve directory structure + // headerFile is like /path/to/Headers/Yoga/yoga/style/Style.h + // We need to extract Yoga/yoga/style/Style.h and copy to the same structure in the slice + const rootHeadersFolder = path.join(outputPath, 'Headers'); + const relativeHeaderPath = path.relative(rootHeadersFolder, headerFile); const targetHeaderFile = path.join( - targetPodSpecFolder, - path.basename(headerFile), + targetHeadersFolder, + relativeHeaderPath, ); + createFolderIfNotExists(path.dirname(targetHeaderFile)); if (!fs.existsSync(targetHeaderFile)) { try { - fs.linkSync(headerFile, targetHeaderFile); + fs.copyFileSync(headerFile, targetHeaderFile); } catch (error) { frameworkLog( - `Error linking header file: ${error.message}. Check if the file exists.`, + `Error copying header file: ${error.message}. Check if the file exists.`, 'error', ); } From 9d9400102b983e4398ff8ff95e59ef67226446ce Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 10 Dec 2025 20:37:41 +0100 Subject: [PATCH 02/12] codereview: refactor header file generator even more Now it reads from podspec files, except for some special cases that we have in the config file. Updated RCTSwiftUIWrapper.podspec to use podspec_sources (which we use to detect source) I tested this against the header files I got with the previous iteration, and also with the ones installed by Cocoapods. --- .../RCTSwiftUIWrapper.podspec | 3 +- .../scripts/ios-prebuild/headers-config.js | 506 +----------------- .../scripts/ios-prebuild/headers.js | 329 ++++++++---- 3 files changed, 256 insertions(+), 582 deletions(-) diff --git a/packages/react-native/ReactApple/RCTSwiftUIWrapper/RCTSwiftUIWrapper.podspec b/packages/react-native/ReactApple/RCTSwiftUIWrapper/RCTSwiftUIWrapper.podspec index 93bc0e91bb85..135b2bb5ccb0 100644 --- a/packages/react-native/ReactApple/RCTSwiftUIWrapper/RCTSwiftUIWrapper.podspec +++ b/packages/react-native/ReactApple/RCTSwiftUIWrapper/RCTSwiftUIWrapper.podspec @@ -25,12 +25,11 @@ Pod::Spec.new do |s| s.author = "Meta Platforms, Inc. and its affiliates" s.platforms = min_supported_versions s.source = source - s.source_files = "*.{h,m}" + s.source_files = podspec_sources("*.{h,m}", "*.{h}") s.public_header_files = "*.h" s.module_name = "RCTSwiftUIWrapper" s.header_dir = "RCTSwiftUIWrapper" s.dependency "RCTSwiftUI" - s.pod_target_xcconfig = { "SWIFT_VERSION" => "5.0", } diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js index ff51d276b1dd..a445357b982c 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-config.js +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -16,168 +16,16 @@ export type PodSpecConfiguration = $ReadOnly<{ excludePatterns?: Array, subSpecs?: $ReadOnlyArray, preservePaths?: Array, -}>; +} | {disabled: true}>; */ -// Remember that our GLOB library doesn't like {h} in its patterns, so we use **/*.h instead of **/*.{h} -const PodSpecConfigurations /*: {[key: string]: PodSpecConfiguration} */ = { - 'Libraries/ActionSheetIOS/React-RCTActionSheet.podspec': { - name: 'React-RCTActionSheet', - headerPatterns: [], - headerDir: 'RCTActionSheet', - }, - - 'Libraries/AppDelegate/React-RCTAppDelegate.podspec': { - name: 'React-RCTAppDelegate', - headerPatterns: ['**/*.h'], - headerDir: '', - }, - - 'Libraries/Blob/React-RCTBlob.podspec': { - name: 'React-RCTBlob', - headerPatterns: ['**/*.h'], - headerDir: 'RCTBlob', - }, - - 'Libraries/FBLazyVector/FBLazyVector.podspec': { - name: 'FBLazyVector', - headerPatterns: ['**/*.h'], - headerDir: 'FBLazyVector', - }, - - 'Libraries/Image/React-RCTImage.podspec': { - name: 'React-RCTImage', - headerPatterns: ['**/*.h'], - headerDir: 'RCTImage', - }, - - 'Libraries/LinkingIOS/React-RCTLinking.podspec': { - name: 'React-RCTLinking', - headerPatterns: [], - headerDir: 'RCTLinking', - }, - - 'Libraries/NativeAnimation/React-RCTAnimation.podspec': { - name: 'React-RCTAnimation', - headerPatterns: ['**/*.h'], - headerDir: 'RCTAnimation', - }, - - 'Libraries/Network/React-RCTNetwork.podspec': { - name: 'React-RCTNetwork', - headerPatterns: [], - headerDir: 'RCTNetwork', - }, - - 'Libraries/PushNotificationIOS/React-RCTPushNotification.podspec': { - name: '', - headerPatterns: [], - headerDir: '', - }, - - 'Libraries/Required/RCTRequired.podspec': { - name: 'RCTRequired', - headerPatterns: ['*.h'], - headerDir: 'RCTRequired', - }, - - 'Libraries/Settings/React-RCTSettings.podspec': { - name: 'React-RCTSettings', - headerPatterns: ['*.h'], - headerDir: 'RCTSettings', - }, - - 'Libraries/Text/React-RCTText.podspec': { - name: 'React-RCTText', - headerPatterns: ['**/*.h'], - headerDir: 'RCTText', - }, - - 'Libraries/TypeSafety/RCTTypeSafety.podspec': { - name: 'RCTTypeSafety', - headerPatterns: ['**/*.h'], - headerDir: 'RCTTypeSafety', - }, - - 'Libraries/Vibration/React-RCTVibration.podspec': { - name: 'React-RCTVibration', - headerPatterns: ['**/*.h'], - headerDir: 'RCTVibration', - }, - - 'React.podspec': {name: '', headerPatterns: [], headerDir: ''}, - - 'React/CoreModules/React-CoreModules.podspec': { - name: 'React-CoreModules', - headerPatterns: ['**/*.h'], - headerDir: 'CoreModules', - excludePatterns: ['PlatformStubs/**/*'], // TODO: Only for iOS! - }, - - 'React/React-RCTFabric.podspec': { - name: 'React-RCTFabric', - headerPatterns: ['Fabric/**/*.h'], - headerDir: 'React', - excludePatterns: ['**/tests/*', '**/android/*'], - }, - - 'React/React-RCTFBReactNativeSpec.podspec': { - name: 'React-RCTFBReactNativeSpec', - headerPatterns: ['FBReactNativeSpec/**/*.h'], - headerDir: 'FBReactNativeSpec', - excludePatterns: ['FBReactNativeSpec/react/renderer/components/**'], - subSpecs: [ - { - name: 'components', - headerPatterns: [ - 'FBReactNativeSpec/react/renderer/components/FBReactNativeSpec/**/*.h', - ], - headerDir: 'react/renderer/components/FBReactNativeSpec', - }, - ], - }, - - 'React/Runtime/React-RCTRuntime.podspec': { - name: 'React-RCTRuntime', - headerPatterns: ['*.h'], - headerDir: 'React', - }, - - 'ReactApple/Libraries/RCTFoundation/RCTDeprecation/RCTDeprecation.podspec': { - name: 'RCTDeprecation', - headerPatterns: ['Exported/*.h'], - headerDir: '', - }, - - 'ReactApple/RCTSwiftUI/RCTSwiftUI.podspec': { - name: 'RCTSwiftUI', - headerPatterns: ['*.h'], - headerDir: 'RCTSwiftUI', - }, - - 'ReactApple/RCTSwiftUIWrapper/RCTSwiftUIWrapper.podspec': { - name: 'RCTSwiftUIWrapper', - headerPatterns: ['*.h'], - headerDir: 'RCTSwiftUIWrapper', - }, - - 'ReactCommon/callinvoker/React-callinvoker.podspec': { - name: 'React-callinvoker', +const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { + 'ReactCommon/jsi/React-jsi.podspec': { + name: 'React-jsi', headerPatterns: ['**/*.h'], - headerDir: 'ReactCommon', - }, - - 'ReactCommon/cxxreact/React-cxxreact.podspec': { - name: 'React-cxxreact', - headerPatterns: ['*.h'], - headerDir: 'cxxreact', - }, - - 'ReactCommon/hermes/executor/React-jsitracing.podspec': { - name: 'React-jsitracing', - headerPatterns: ['JSITracing.h'], + headerDir: 'jsi', + excludePatterns: ['**/test/*'], }, - 'ReactCommon/hermes/React-hermes.podspec': { name: 'React-hermes', headerPatterns: [ @@ -187,68 +35,6 @@ const PodSpecConfigurations /*: {[key: string]: PodSpecConfiguration} */ = { ], headerDir: 'reacthermes', }, - - 'ReactCommon/jserrorhandler/React-jserrorhandler.podspec': { - name: 'React-jserrorhandler', - headerPatterns: ['JsErrorHandler.h', 'StackTraceParser.h'], - headerDir: 'jserrorhandler', - }, - - 'ReactCommon/jsi/React-jsi.podspec': { - name: 'React-jsi', - headerPatterns: ['**/*.h'], - headerDir: 'jsi', - excludePatterns: ['**/test/*'], - }, - - 'ReactCommon/jsiexecutor/React-jsiexecutor.podspec': { - name: 'React-jsiexecutor', - headerPatterns: ['jsireact/*.h'], - headerDir: 'jsireact', - }, - - 'ReactCommon/jsinspector-modern/cdp/React-jsinspectorcdp.podspec': { - name: 'React-jsinspectorcdp', - headerPatterns: ['*.h'], - headerDir: 'jsinspector-modern/cdp', - }, - - 'ReactCommon/jsinspector-modern/network/React-jsinspectornetwork.podspec': { - name: 'React-jsinspectornetwork', - headerPatterns: ['*.h'], - headerDir: 'jsinspector-modern/network', - }, - - 'ReactCommon/jsinspector-modern/React-jsinspector.podspec': { - name: 'React-jsinspector', - headerPatterns: ['*.h'], - headerDir: 'jsinspector-modern', - }, - - 'ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec': { - name: 'React-jsinspectortracing', - headerPatterns: ['*.h'], - headerDir: 'jsinspector-modern/tracing', - }, - - 'ReactCommon/jsitooling/React-jsitooling.podspec': { - name: 'React-jsitooling', - headerPatterns: ['react/runtime/*.h'], - headerDir: 'react/runtime', - }, - - 'ReactCommon/logger/React-logger.podspec': { - name: 'React-logger', - headerPatterns: ['*.h'], - headerDir: 'logger', - }, - - 'ReactCommon/oscompat/React-oscompat.podspec': { - name: 'React-oscompat', - headerPatterns: ['*.h'], - headerDir: 'oscompat', - }, - 'ReactCommon/React-Fabric.podspec': { name: 'React-Fabric', headerPatterns: [], @@ -431,7 +217,21 @@ const PodSpecConfigurations /*: {[key: string]: PodSpecConfiguration} */ = { }, ], }, - + 'React/React-RCTFBReactNativeSpec.podspec': { + name: 'React-RCTFBReactNativeSpec', + headerPatterns: ['FBReactNativeSpec/**/*.h'], + headerDir: 'FBReactNativeSpec', + excludePatterns: ['FBReactNativeSpec/react/renderer/components/**'], + subSpecs: [ + { + name: 'components', + headerPatterns: [ + 'FBReactNativeSpec/react/renderer/components/FBReactNativeSpec/**/*.h', + ], + headerDir: 'react/renderer/components/FBReactNativeSpec', + }, + ], + }, 'ReactCommon/React-FabricComponents.podspec': { name: 'React-FabricComponents', headerPatterns: [], @@ -562,264 +362,6 @@ const PodSpecConfigurations /*: {[key: string]: PodSpecConfiguration} */ = { }, ], }, - - 'ReactCommon/React-FabricImage.podspec': { - name: 'React-FabricImage', - headerPatterns: ['react/renderer/components/image/**/*.h'], - excludePatterns: ['react/renderer/components/image/tests'], - headerDir: 'react/renderer/components/image', - }, - - 'ReactCommon/React-Mapbuffer.podspec': { - name: 'React-Mapbuffer', - headerPatterns: ['react/renderer/mapbuffer/*.h'], - headerDir: 'react/renderer/mapbuffer', - }, - - 'ReactCommon/react/debug/React-debug.podspec': { - name: 'React-debug', - headerPatterns: ['**/*.h'], - headerDir: 'react/debug', - }, - - 'ReactCommon/react/featureflags/React-featureflags.podspec': { - name: 'React-featureflags', - headerPatterns: ['**/*.h'], - headerDir: 'react/featureflags', - }, - - 'ReactCommon/react/nativemodule/core/platform/ios/React-NativeModulesApple.podspec': - { - name: 'React-NativeModulesApple', - headerPatterns: ['ReactCommon/**/*.h'], - headerDir: 'ReactCommon', - }, - - 'ReactCommon/react/nativemodule/defaults/React-defaultsnativemodule.podspec': - { - name: 'React-defaultsnativemodule', - headerPatterns: ['*.h'], - headerDir: 'react/nativemodule/defaults', - }, - - 'ReactCommon/react/nativemodule/dom/React-domnativemodule.podspec': { - name: 'React-domnativemodule', - headerPatterns: ['*.h'], - headerDir: 'react/nativemodule/dom', - }, - - 'ReactCommon/react/nativemodule/featureflags/React-featureflagsnativemodule.podspec': - { - name: 'React-featureflagsnativemodule', - headerPatterns: ['*.h'], - headerDir: 'react/nativemodule/featureflags', - }, - - 'ReactCommon/react/nativemodule/idlecallbacks/React-idlecallbacksnativemodule.podspec': - { - name: 'React-idlecallbacksnativemodule', - headerPatterns: ['*.h'], - headerDir: 'react/nativemodule/idlecallbacks', - }, - - 'ReactCommon/react/nativemodule/microtasks/React-microtasksnativemodule.podspec': - { - name: 'React-microtasksnativemodule', - headerPatterns: ['*.h'], - headerDir: 'react/nativemodule/microtasks', - }, - // We don't need to include samples in our header file structure. - // 'ReactCommon/react/nativemodule/samples/ReactCommon-Samples.podspec': - // { - // name: 'ReactCommon-Samples', - // headerPatterns: ['**/*.h'], - // headerDir: 'react/nativemodule/samples', - // }, - - 'ReactCommon/react/nativemodule/webperformance/React-webperformancenativemodule.podspec': - { - name: 'React-webperformancenativemodule', - headerPatterns: ['*.h'], - headerDir: 'react/nativemodule/webperformance', - }, - - 'ReactCommon/react/networking/React-networking.podspec': { - name: 'React-networking', - headerPatterns: ['*.h'], - headerDir: 'react/networking', - }, - - 'ReactCommon/react/performance/cdpmetrics/React-performancecdpmetrics.podspec': - { - name: 'React-performancecdpmetrics', - headerPatterns: ['*.h'], - headerDir: 'react/performance/cdpmetrics', - }, - - 'ReactCommon/react/performance/timeline/React-performancetimeline.podspec': { - name: 'React-performancetimeline', - headerPatterns: ['*.h'], - headerDir: 'react/performance/timeline', - }, - - 'ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec': { - name: 'React-rendererconsistency', - headerPatterns: ['*.h'], - headerDir: 'react/renderer/consistency', - }, - - 'ReactCommon/react/renderer/css/React-renderercss.podspec': { - name: 'React-renderercss', - headerPatterns: ['*.h'], - headerDir: 'react/renderer/css', - }, - - 'ReactCommon/react/renderer/debug/React-rendererdebug.podspec': { - name: 'React-rendererdebug', - headerPatterns: ['*.h'], - headerDir: 'react/renderer/debug', - }, - - 'ReactCommon/react/renderer/graphics/React-graphics.podspec': { - name: 'React-graphics', - headerPatterns: ['*.h', 'platform/ios/**/*.h'], - headerDir: 'react/renderer/graphics', - }, - - 'ReactCommon/react/renderer/imagemanager/platform/ios/React-ImageManager.podspec': - { - name: 'React-ImageManager', - headerPatterns: ['**/*.h'], - headerDir: 'react/renderer/imagemanager', - }, - - 'ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec': - { - name: 'React-runtimescheduler', - headerPatterns: ['*.h'], - headerDir: 'react/renderer/runtimescheduler', - }, - - 'ReactCommon/react/runtime/platform/ios/React-RuntimeApple.podspec': { - name: 'React-RuntimeApple', - headerPatterns: ['ReactCommon/*.h'], - headerDir: 'ReactCommon', - excludePatterns: ['ReactCommon/RCTJscInstance.h'], - }, - - 'ReactCommon/react/runtime/React-RuntimeCore.podspec': { - name: 'React-RuntimeCore', - headerPatterns: ['*.h', 'nativeviewconfig/*.h'], - headerDir: 'react/runtime', - }, - - 'ReactCommon/react/runtime/React-RuntimeHermes.podspec': { - name: 'React-RuntimeHermes', - headerPatterns: ['hermes/*.h'], - headerDir: 'react/runtime/hermes', - }, - - 'ReactCommon/react/timing/React-timing.podspec': { - name: 'React-timing', - headerPatterns: ['**/*.h'], - headerDir: 'react/timing', - }, - - 'ReactCommon/react/utils/React-utils.podspec': { - name: 'React-utils', - headerPatterns: ['*.h', 'platform/ios/**/*.h'], - headerDir: 'react/utils', - excludePatterns: ['tests'], - }, - - 'ReactCommon/ReactCommon.podspec': { - name: 'ReactCommon', - headerPatterns: [], - headerDir: 'ReactCommon', - subSpecs: [ - { - name: 'turbomodule', - headerPatterns: [], - subSpecs: [ - { - name: 'bridging', - headerPatterns: ['react/bridging/**/*.h'], - headerDir: 'react/bridging', - excludePatterns: ['react/bridging/tests/**/*'], - }, - { - name: 'core', - headerPatterns: ['react/nativemodule/core/ReactCommon/**/*.h'], - }, - ], - }, - ], - }, - - 'ReactCommon/reactperflogger/React-perflogger.podspec': { - name: 'React-perflogger', - headerPatterns: ['reactperflogger/*.h', 'fusebox/*.h'], - headerDir: 'reactperflogger', - }, - - 'ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec': { - name: 'React-runtimeexecutor', - headerPatterns: ['ReactCommon/*.h', 'platform/ios/**/*.h'], - headerDir: 'ReactCommon', - }, - - 'ReactCommon/yoga/Yoga.podspec': { - name: 'Yoga', - headerPatterns: ['yoga/**/*.h'], - headerDir: 'yoga', - preservePaths: ['yoga/**/*.h'], - }, - // These should be distributed through the Hermes xcframework. - // 'sdks/hermes/hermes-engine.podspec': - // { - // name: 'hermes-engine', - // headerPatterns: [], - // headerDir: '', - // preservePaths: ['**/*.*'], - // subSpecs: [ - // { - // name: 'Hermes', - // headerPatterns: ['destroot/include/hermes/*.h'], - // headerDir: 'hermes', - // }, - - // { - // name: 'cdp', - // headerPatterns: ['destroot/include/hermes/cdp/*.h'], - // headerDir: 'hermes/cdp', - // }, - - // { - // name: 'inspector', - // headerPatterns: ['destroot/include/hermes/inspector/*.h'], - // headerDir: 'hermes/inspector', - // }, - - // { - // name: 'inspector_chrome', - // headerPatterns: ['destroot/include/hermes/inspector/chrome/*.h'], - // headerDir: 'hermes/inspector/chrome', - // }, - - // { - // name: 'jsi', - // headerPatterns: ['destroot/include/jsi/*.h'], - // headerDir: 'jsi', - // }, - - // { - // name: 'Public', - // headerPatterns: ['public/hermes/Public/*.h'], - // headerDir: 'hermes/Public', - // }, - // ], - // }, - 'React-Core.podspec': { name: 'React-Core', headerPatterns: [], @@ -883,6 +425,10 @@ const PodSpecConfigurations /*: {[key: string]: PodSpecConfiguration} */ = { }, ], }, + 'React.podspec': {disabled: true}, + 'Libraries/PushNotificationIOS/React-RCTPushNotification.podspec': { + disabled: true, + }, }; -module.exports = PodSpecConfigurations; +module.exports = {PodspecExceptions}; diff --git a/packages/react-native/scripts/ios-prebuild/headers.js b/packages/react-native/scripts/ios-prebuild/headers.js index 563e6703689b..15a99989be29 100644 --- a/packages/react-native/scripts/ios-prebuild/headers.js +++ b/packages/react-native/scripts/ios-prebuild/headers.js @@ -8,7 +8,7 @@ * @format */ -const PodSpecConfigurations = require('./headers-config'); +const {PodspecExceptions} = require('./headers-config'); const utils = require('./utils'); const path = require('path'); const {globSync} = require('tinyglobby'); @@ -21,131 +21,260 @@ import type {PodSpecConfiguration} from './headers-config'; type HeaderMap = { headerDir: string, specName: string, headers: {source: string, target: string}[]}; */ -/** - * Enumerates all podspec files in the PodSpecConfigurations structure above and maps them to - * their header files based on the configuration. - * @param {*} rootFolder Root folder to search for podspec files - * @param {*} testHeadersFlag Flag to indicate whether to test headers against a test directory - * @param {*} targetTestFolder Target folder to test headers against - * @returns - */ function getHeaderFilesFromPodspecs( rootFolder /*:string*/, ) /*: { [key: string]: HeaderMap[] }*/ { - // Get podspec files in the configuration mapped to configurations - const podSpecFiles = Object.keys(PodSpecConfigurations).map(podspecPath => - path.resolve(rootFolder, podspecPath), + const result /*: { [key: string]: HeaderMap[] }*/ = {}; + + // 1. Find all podspec files in the rootFolder + const podspecFiles = globSync('**/*.podspec', { + cwd: rootFolder, + absolute: true, + ignore: ['**/node_modules/**', '**/Pods/**'], + }); + + headersLog( + '🔍 Collecting header files from all podspec files in the project...', ); - headersLog('🔍 Collecting header files from podspec configurations...'); + // 2. For each podspec file, we would need to parse it and extract header information. We should + // do this by checking if the file contains the text 'podspec_sources'. + podspecFiles.forEach(podspecPath => { + // Check if this podspec has an exception registered + const relativeKey = path.relative(rootFolder, podspecPath); + const exception = PodspecExceptions[relativeKey]; - const headerMaps /*: { [key: string]: HeaderMap[] }*/ = {}; + if (exception) { + // Check if the exception is disabled + if ('disabled' in exception && exception.disabled === true) { + headersLog(`⏭️ Skipping disabled podspec: ${relativeKey}`); + return; + } - podSpecFiles.forEach(podspecPath => { - const key = path.relative(rootFolder, podspecPath); - const podSpecConfig = PodSpecConfigurations[key]; - if ( - !podSpecConfig || - 'name' in podSpecConfig === false || - podSpecConfig.name === '' - ) { - headersLog( - `⚠️ Skipping podspec at ${podspecPath} due to missing or invalid configuration.`, + // Use getHeaderFilesFromPodspec for podspecs with exceptions + const headerMaps = getHeaderFilesFromPodspec( + exception, + path.dirname(podspecPath), ); + if (headerMaps !== null) { + result[podspecPath] = headerMaps; + } return; } - const podSpecDirectory = path.dirname(podspecPath); + // Open file and read content + const fileContent = require('fs').readFileSync(podspecPath, 'utf8'); - // Now we can start collecting header files - const processConfig = ( - config /*: PodSpecConfiguration */, - parents /*: Array*/, - ) => { - const {headerDir, headerPatterns, excludePatterns, subSpecs} = config; + // Check if it contains 'podspec_sources' + if (fileContent.includes('podspec_sources')) { + // Parse podspec_sources(source_files, header_patterns) - we want the SECOND argument. + // Examples: + // podspec_sources("*.{cpp,h}", "**/*.h") + // podspec_sources(["a.m", "b.h"], "*.h") + // podspec_sources(["a.m", "b.h"], ["c.h", "d.h"]) + // podspec_sources(source_files, ["*.h", "platform/ios/**/*.h"]) # first arg is a variable + // + // Regex explanation: + // podspec_sources\( - match "podspec_sources(" + // (?:\[[^\]]*\]|"[^"]*"|[\w]+) - first arg: either [...] or "..." or a variable name + // \s*,\s* - comma separator with optional whitespace + // (\[[^\]]*\]|"[^"]*") - second arg (captured): either [...] or "..." + // \) - closing paren + const headerPatternRegex = + /podspec_sources\((?:\[[^\]]*\]|"[^"]*"|\w+)\s*,\s*(\[[^\]]*\]|"[^"]*")\)/gm; + const matches = [...fileContent.matchAll(headerPatternRegex)]; - // Find header files for configuration - const foundHeaderFiles = headerPatterns - .map(pattern => - globSync(pattern, { - cwd: podSpecDirectory, - absolute: true, - ignore: excludePatterns || [], - }), - ) - .flat(); - - let resolvedHeaderDir /*:string */ = headerDir || ''; - - // If headerDir is not set, we need to resolve it against parent specs - if (parents.length > 0 && !headerDir) { - for (let i = parents.length - 1; i >= 0; i--) { - const parentHeaderDir = parents[i].headerDir; - if (parentHeaderDir) { - resolvedHeaderDir = parentHeaderDir; - break; - } + // Also extract exclude_files patterns from the podspec + // Examples: + // s.exclude_files = "tests/**/*.h" + // s.exclude_files = ["tests/**/*.h", "internal/**/*.h"] + // ss.exclude_files = "..." + const excludeFilesRegex = /\.exclude_files\s*=\s*(\[[^\]]*\]|"[^"]*")/gm; + const excludeMatches = [...fileContent.matchAll(excludeFilesRegex)]; + + // Parse exclude patterns + const excludePatterns = excludeMatches.flatMap(match => { + const arg = match[1].trim(); + if (arg.startsWith('[')) { + const arrayContent = arg.slice(1, arg.lastIndexOf(']')); + return arrayContent + .split(',') + .map(s => s.trim().replace(/['"]/g, '')) + .filter(s => s.length > 0); + } else { + return [arg.replace(/['"]/g, '').trim()].filter(s => s.length > 0); } + }); + + // Add default excludes + const allExcludes = [...excludePatterns]; + + if (matches.length > 0) { + // Extract header patterns (second argument) from all matches + const patterns = matches.flatMap(match => { + const secondArg = match[1].trim(); + + // Parse the second argument - it can be a string or an array + if (secondArg.startsWith('[')) { + // It's an array, extract the contents and split by comma + const arrayContent = secondArg.slice(1, secondArg.lastIndexOf(']')); + return arrayContent + .split(',') + .map(s => s.trim().replace(/['"]/g, '')) + .filter(s => s.length > 0); + } else { + // It's a single string + return [secondArg.replace(/['"]/g, '').trim()].filter( + s => s.length > 0, + ); + } + }); + + // Now we can find header files based on these patterns + const foundHeaderFiles = patterns + .map(pattern => { + // our GLOB library doesn't like {h} in its patterns, so we use **/*.h instead of **/*.{h} + if (pattern.includes('{h}')) { + pattern = pattern.replaceAll('{h}', 'h'); + } + return globSync(pattern, { + cwd: path.dirname(podspecPath), + ignore: allExcludes, + absolute: true, + }); + }) + .flat(); + + result[podspecPath] = [ + { + headerDir: '', // We don't have headerDir info here + specName: path.basename(podspecPath, '.podspec'), + headers: foundHeaderFiles.map(headerFile => ({ + source: headerFile, + target: path.basename(headerFile), + })), + }, + ]; } + } + }); + + return result; +} + +/** + * Extracts header files from a single podspec based on its configuration. + * @param {PodSpecConfiguration} podSpecConfig The podspec configuration object + * @param {string} podSpecDirectory Directory where the podspec is located + * @returns {HeaderMap[] | null} Array of header maps or null if configuration is invalid + */ +function getHeaderFilesFromPodspec( + podSpecConfig /*: PodSpecConfiguration*/, + podSpecDirectory /*:string*/, +) /*: HeaderMap[] | null*/ { + if ( + !podSpecConfig || + 'name' in podSpecConfig === false || + podSpecConfig.name === '' + ) { + headersLog(`⚠️ Skipping podspec due to missing or invalid configuration.`); + return null; + } + + const headerMaps /*: HeaderMap[] */ = []; - // If still not resolved, default to spec name - if (!resolvedHeaderDir) { - resolvedHeaderDir = ''; + // Now we can start collecting header files + const processConfig = ( + config /*: PodSpecConfiguration */, + parents /*: Array*/, + ) => { + if (config.disabled === true) { + return; + } + + const {headerDir, headerPatterns, excludePatterns, subSpecs} = config; + + // Find header files for configuration + const foundHeaderFiles = headerPatterns + .map(pattern => + globSync(pattern, { + cwd: podSpecDirectory, + absolute: true, + ignore: excludePatterns || [], + }), + ) + .flat(); + + let resolvedHeaderDir /*:string */ = headerDir || ''; + + // If headerDir is not set, we need to resolve it against parent specs + if (parents.length > 0 && !headerDir) { + for (let i = parents.length - 1; i >= 0; i--) { + const parentHeaderDir = parents[i].headerDir; + if (parentHeaderDir) { + resolvedHeaderDir = parentHeaderDir; + break; + } } + } - // Resolve preservePaths from parent specs too - let resolvedPreservePaths = config.preservePaths || []; - if (resolvedPreservePaths.length === 0 && parents.length > 0) { - for (let i = parents.length - 1; i >= 0; i--) { - const parentPreservePaths = parents[i].preservePaths; - if (parentPreservePaths && parentPreservePaths.length > 0) { - resolvedPreservePaths = parentPreservePaths; - break; - } + // If still not resolved, default to spec name + if (!resolvedHeaderDir) { + resolvedHeaderDir = ''; + } + + // Resolve preservePaths from parent specs too + let resolvedPreservePaths = config.preservePaths || []; + if (resolvedPreservePaths.length === 0 && parents.length > 0) { + for (let i = parents.length - 1; i >= 0; i--) { + const parentPreservePaths = parents[i].preservePaths; + if (parentPreservePaths && parentPreservePaths.length > 0) { + resolvedPreservePaths = parentPreservePaths; + break; } } + } - headerMaps[podspecPath] = (headerMaps[podspecPath] || []).concat({ - headerDir: resolvedHeaderDir, - specName: podSpecConfig.name, - headers: foundHeaderFiles.map(headerFile => { - // Check if we have preservePath set for this file - then we need to get the subfolder structure too - // and not just copy to the root of headerDir - we should also ignore the headerDir part of the path - const isPreserved = resolvedPreservePaths.some(preservePattern => { - return globSync(preservePattern, { - cwd: podSpecDirectory, - absolute: true, - ignore: excludePatterns || [], - }).includes(headerFile); - }); - - if (isPreserved) { - // Get the subfolder for the header file - const relativePath = path.dirname( - path.relative(podSpecDirectory, headerFile), - ); - return { - source: headerFile, - target: path.join(relativePath, path.basename(headerFile)), - }; - } + headerMaps.push({ + headerDir: resolvedHeaderDir, + specName: config.name, + headers: foundHeaderFiles.map(headerFile => { + // Check if we have preservePath set for this file - then we need to get the subfolder structure too + // and not just copy to the root of headerDir - we should also ignore the headerDir part of the path + const isPreserved = resolvedPreservePaths.some(preservePattern => { + return globSync(preservePattern, { + cwd: podSpecDirectory, + absolute: true, + ignore: excludePatterns || [], + }).includes(headerFile); + }); + + if (isPreserved) { + // Get the subfolder for the header file + const relativePath = path.dirname( + path.relative(podSpecDirectory, headerFile), + ); return { source: headerFile, - target: path.join(resolvedHeaderDir, path.basename(headerFile)), + target: path.join(relativePath, path.basename(headerFile)), }; - }), - }); + } + return { + source: headerFile, + target: path.join(resolvedHeaderDir, path.basename(headerFile)), + }; + }), + }); - // Process subSpecs recursively - if (subSpecs && subSpecs.length > 0) { - subSpecs.forEach(subSpecConfig => { - processConfig(subSpecConfig, [config, ...parents]); - }); - } - }; + // Process subSpecs recursively + if (subSpecs && subSpecs.length > 0) { + subSpecs.forEach(subSpecConfig => { + processConfig(subSpecConfig, [config, ...parents]); + }); + } + }; - processConfig(podSpecConfig, []); - }); + processConfig(podSpecConfig, []); return headerMaps; } From 9bb1f6a21482ccc15832c3bc71185a45469ce5e7 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 10 Dec 2025 14:41:15 +0100 Subject: [PATCH 03/12] [ios][precompile] add support for clang virtual file system To solve our problem with header files from the React.XCFramework not being consumable due to the legacy Cocoapods headers, we used to point all header search / resolving into the Pods/Headers folder when building even with the framework installed. This is a problem since an xcframework expects a modular header structure that can be consumed from within the headers folder of the framework. The headers are used for both Objective-c compiling and for creating clang modules that can be consumed by Swift in our setup. This commit fixes the above issues and makes a modular clang virtual file system from the header files we distribute. This vfs-overlay file will then be used by the `rncore.rb` script when installing the 'React-Core-prebuilt' pod and map the header files requested in the source code to the actual location on disk where the React.xcframework headers are installed. Here are the changes: - Added vfs.js with types for creating clang virtual file system overlays - Added resolving the paths in the vfs file to the actual install path - Added installing the vfs overlay with the switch `-ivfsoverlay ` to compiler settings for both obj-c and swift on pod installation - Add vfs targets to the app target on post install --- .../react-native/scripts/cocoapods/rncore.rb | 95 ++++++- .../scripts/ios-prebuild/types.js | 18 ++ .../react-native/scripts/ios-prebuild/vfs.js | 268 ++++++++++++++++++ .../scripts/ios-prebuild/xcframework.js | 46 ++- .../react-native/scripts/react_native_pods.rb | 7 + 5 files changed, 420 insertions(+), 14 deletions(-) create mode 100644 packages/react-native/scripts/ios-prebuild/vfs.js diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 597c4b4370d1..2ce3c3def4ee 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -13,10 +13,26 @@ ### building ReactNativeCore from source (then this function does nothing). def add_rncore_dependency(s) if !ReactNativeCoreUtils.build_rncore_from_source() + # Add the dependency + s.dependency "React-Core-prebuilt" + current_pod_target_xcconfig = s.to_hash["pod_target_xcconfig"] || {} current_pod_target_xcconfig = current_pod_target_xcconfig.to_h unless current_pod_target_xcconfig.is_a?(Hash) - s.dependency "React-Core-prebuilt" - current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] ||= [] << "$(PODS_ROOT)/React-Core-prebuilt/React.xcframework/Headers" + + # Add VFS overlay flags for both Objective-C and Swift + # The VFS overlay file is pre-resolved at pod install time for each platform slice. + # We reference it directly in the xcframework using the React-VFS.yaml file that + # is written to the React-Core-prebuilt folder during setup_vfs_overlay. + vfs_overlay_flag = "-ivfsoverlay $(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml" + current_pod_target_xcconfig["OTHER_CFLAGS"] ||= "$(inherited)" + current_pod_target_xcconfig["OTHER_CFLAGS"] += " #{vfs_overlay_flag}" + current_pod_target_xcconfig["OTHER_CPLUSPLUSFLAGS"] ||= "$(inherited)" + current_pod_target_xcconfig["OTHER_CPLUSPLUSFLAGS"] += " #{vfs_overlay_flag}" + # For Swift, we need to use -Xcc to pass flags to the underlying Clang compiler + # Both the flag and its argument need separate -Xcc prefixes + current_pod_target_xcconfig["OTHER_SWIFT_FLAGS"] ||= "$(inherited)" + current_pod_target_xcconfig["OTHER_SWIFT_FLAGS"] += " -Xcc -ivfsoverlay -Xcc $(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml" + s.pod_target_xcconfig = current_pod_target_xcconfig end end @@ -450,4 +466,79 @@ def self.get_nightly_npm_version() return latest_nightly end + # Processes the VFS overlay file from the React.xcframework to resolve the ${ROOT_PATH} placeholder. + # This method should be called from react_native_post_install after pod install completes. + # + # The VFS overlay file maps header import paths to their actual locations within the xcframework. + # Since the xcframework contains platform-specific slices, we generate a resolved VFS file for each + # slice and also create a default VFS file that can be used immediately (before script phases run). + def self.process_vfs_overlay() + return if @@build_from_source + + prebuilt_path = File.join(Pod::Config.instance.project_pods_root, "React-Core-prebuilt") + xcframework_path = File.join(prebuilt_path, "React.xcframework") + vfs_template_path = File.join(xcframework_path, "React-VFS-template.yaml") + + unless File.exist?(vfs_template_path) + rncore_log("VFS overlay template not found at #{vfs_template_path}", :error) + exit 1 + end + + rncore_log("Processing VFS overlay file...") + + # Read the template content + vfs_template_content = File.read(vfs_template_path) + + # Write the VFS file - use the top-level xcframework path + # so that ${ROOT_PATH}/Headers points to the xcframework's Headers folder + resolved_vfs_content = vfs_template_content.gsub('${ROOT_PATH}', xcframework_path) + resolved_vfs_path = File.join(prebuilt_path, "React-VFS.yaml") + File.write(resolved_vfs_path, resolved_vfs_content) + rncore_log(" Created VFS overlay at #{resolved_vfs_path}") + + rncore_log("VFS overlay setup complete") + end + + # Configures the xcconfig files for aggregate (main app) targets to enable VFS overlay for React Native Core. + # This is needed because the main app target does not go through podspec processing, + # so it won't get the VFS overlay flags from add_rncore_dependency. + # + # Parameters: + # - installer: The CocoaPods installer object + def self.configure_aggregate_xcconfig(installer) + return if @@build_from_source + + prebuilt_path = File.join(Pod::Config.instance.project_pods_root, "React-Core-prebuilt") + vfs_overlay_path = File.join(prebuilt_path, "React-VFS.yaml") + + unless File.exist?(vfs_overlay_path) + rncore_log("VFS overlay not found at #{vfs_overlay_path}, skipping prebuilt xcconfig configuration", :error) + exit 1 + end + + rncore_log("Configuring xcconfig for prebuilt React Native Core...") + + vfs_overlay_flag = " -ivfsoverlay \"#{vfs_overlay_path}\"" + swift_vfs_overlay_flag = " -Xcc -ivfsoverlay -Xcc \"#{vfs_overlay_path}\"" + + # Add flags to aggregate target xcconfigs (these are used by the main app target) + installer.aggregate_targets.each do |aggregate_target| + aggregate_target.xcconfigs.each do |config_name, config_file| + # Add VFS overlay to compiler flags (C/C++ and Swift) + ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_CFLAGS", vfs_overlay_flag) + ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_CPLUSPLUSFLAGS", vfs_overlay_flag) + + # For Swift, we need to use -Xcc to pass the flag to the underlying Clang compiler + ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_SWIFT_FLAGS", swift_vfs_overlay_flag) + + # Suppress incomplete umbrella warnings for the prebuilt frameworks (it is expected, as our umbrella headers do not include all headers) + ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_SWIFT_FLAGS", " -Xcc -Wno-incomplete-umbrella") + + xcconfig_path = aggregate_target.xcconfig_path(config_name) + config_file.save_as(xcconfig_path) + end + end + + rncore_log("Prebuilt xcconfig configuration complete") + end end diff --git a/packages/react-native/scripts/ios-prebuild/types.js b/packages/react-native/scripts/ios-prebuild/types.js index 663ebfa77a91..56cad1f9ab18 100644 --- a/packages/react-native/scripts/ios-prebuild/types.js +++ b/packages/react-native/scripts/ios-prebuild/types.js @@ -22,6 +22,24 @@ export type Destination = export type BuildFlavor = 'Debug' | 'Release'; export type MavenSubGroup = 'hermes' | 'react'; + +export type VFSEntry = { + name: string, + type: 'file' | 'directory', + 'external-contents'?: string, + contents?: Array, +}; + +export type VFSOverlay = { + version: number, + 'case-sensitive': boolean, + roots: Array, +}; + +export type HeaderMapping = { + key: string, + path: string, +}; */ module.exports = {}; diff --git a/packages/react-native/scripts/ios-prebuild/vfs.js b/packages/react-native/scripts/ios-prebuild/vfs.js new file mode 100644 index 000000000000..ea32ea73d99c --- /dev/null +++ b/packages/react-native/scripts/ios-prebuild/vfs.js @@ -0,0 +1,268 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +/*:: import type {HeaderMapping, VFSEntry, VFSOverlay} from './types'; */ + +const headers = require('./headers'); + +const {getHeaderFilesFromPodspecs} = headers; + +const ROOT_PATH_PLACEHOLDER = '${ROOT_PATH}'; + +/** + * Converts a path to use POSIX-style separators (forward slashes) + */ +function toPosix(value /*: string */) /*: string */ { + return value.split(/[\\/]/).join('/'); +} + +/** + * Builds a hierarchical VFS directory structure from a list of header mappings. + * Clang's VFS overlay requires a tree structure where directories contain their children. + */ +function buildVFSStructure( + mappings /*: Array */, +) /*: Array */ { + // Group files by their directory structure + const dirTree /*: Map> */ = new Map(); + + for (const mapping of mappings) { + const parts = mapping.key.split('/'); + const fileName = parts[parts.length - 1]; + const dirPath = parts.slice(0, -1).join('/'); + + if (!dirTree.has(dirPath)) { + dirTree.set(dirPath, new Map()); + } + const filesMap = dirTree.get(dirPath); + if (filesMap) { + filesMap.set(fileName, mapping.path); + } + } + + // Build the root-level directory entries + const rootDirs /*: Set */ = new Set(); + for (const dirPath of dirTree.keys()) { + const topLevel = dirPath.split('/')[0]; + if (topLevel) { + rootDirs.add(topLevel); + } + } + + const roots /*: Array */ = []; + + for (const rootDir of Array.from(rootDirs).sort()) { + const dirEntry = buildDirectoryEntry(rootDir, '', dirTree); + roots.push(dirEntry); + } + + return roots; +} + +/** + * Recursively builds a directory entry for the VFS + */ +function buildDirectoryEntry( + dirName /*: string */, + parentPath /*: string */, + dirTree /*: Map> */, +) /*: VFSEntry */ { + const currentPath = parentPath ? `${parentPath}/${dirName}` : dirName; + const contents /*: Array */ = []; + + // Add files in this directory + const filesInDir = dirTree.get(currentPath); + if (filesInDir) { + for (const [fileName, sourcePath] of Array.from( + filesInDir.entries(), + ).sort()) { + contents.push({ + name: fileName, + type: 'file', + 'external-contents': toPosix(sourcePath), + }); + } + } + + // Add subdirectories + const subdirs /*: Set */ = new Set(); + for (const dirPath of dirTree.keys()) { + if (dirPath.startsWith(currentPath + '/')) { + const remainder = dirPath.slice(currentPath.length + 1); + const nextDir = remainder.split('/')[0]; + if (nextDir) { + subdirs.add(nextDir); + } + } + } + + for (const subdir of Array.from(subdirs).sort()) { + contents.push(buildDirectoryEntry(subdir, currentPath, dirTree)); + } + + return { + name: dirName, + type: 'directory', + contents, + }; +} + +/** + * Simple YAML generator for VFS overlay structure (hierarchical format) + */ +function generateVFSOverlayYAML(overlay /*: VFSOverlay */) /*: string */ { + let yaml = ''; + + yaml += `version: ${String(overlay.version)}\n`; + yaml += `case-sensitive: ${String(overlay['case-sensitive'])}\n`; + yaml += `roots:\n`; + + for (const root of overlay.roots) { + yaml += generateEntryYAML(root, 1); + } + + return yaml; +} + +/** + * Recursively generates YAML for a VFS entry + */ +function generateEntryYAML( + entry /*: VFSEntry */, + indent /*: number */, +) /*: string */ { + const spaces = ' '.repeat(indent); + let yaml = ''; + + yaml += `${spaces}- name: '${entry.name}'\n`; + yaml += `${spaces} type: '${entry.type}'\n`; + + if (entry['external-contents']) { + yaml += `${spaces} external-contents: '${entry['external-contents']}'\n`; + } + + if (entry.contents && entry.contents.length > 0) { + yaml += `${spaces} contents:\n`; + for (const child of entry.contents) { + yaml += generateEntryYAML(child, indent + 2); + } + } + + return yaml; +} + +/** + * Creates a VFS overlay object from the header files in podspecs. + * The source paths use ${ROOT_PATH} as a placeholder for later replacement + * with the actual root path on the end user's machine. + * + * The VFS overlay wraps all header mappings under a single root at + * ${ROOT_PATH}/Headers, which matches the HEADER_SEARCH_PATHS configured + * in rncore.rb. This allows the compiler to find headers like + * by looking up ${ROOT_PATH}/Headers/yoga/style/Style.h + * which the VFS redirects to the flat location in the xcframework. + * + * @param rootFolder The root folder of the React Native package + * @returns A VFS overlay object that can be serialized to YAML + */ +function createVFSOverlayContents(rootFolder /*: string */) /*: VFSOverlay */ { + // Get header files from podspecs (disable testing since we just need the mappings) + const podSpecsWithHeaderFiles = getHeaderFilesFromPodspecs( + rootFolder, + false, // testHeadersFlag + ); + + const mappings /*: Array */ = []; + + // Process each podspec and its header files + Object.keys(podSpecsWithHeaderFiles).forEach(podspecPath => { + const headerMaps = podSpecsWithHeaderFiles[podspecPath]; + + // Use the first podspec spec name as the podspec name (this is the root spec) + const podSpecName = headerMaps[0].specName.replace('-', '_'); + + headerMaps.forEach(headerMap => { + headerMap.headers.forEach(header => { + // The key is just the target path (the import path) + // e.g., 'react/renderer/graphics/Size.h' for #import + const key = toPosix(header.target); + + // The external-contents path uses the full target path because headers are copied + // with their directory structure preserved in the XCFramework's root Headers folder + // (see xcframework.js where headerFile.target is used). + // The path is: ${ROOT_PATH}/Headers/{podSpecName}/{target} + const sourcePath = `${ROOT_PATH_PLACEHOLDER}/Headers/${podSpecName}/${toPosix(header.target)}`; + + mappings.push({ + key, + path: sourcePath, + }); + }); + }); + }); + + // Build the hierarchical VFS structure from mappings + const innerRoots = buildVFSStructure(mappings); + + // Wrap all roots under a single ${ROOT_PATH}/Headers root. + // This is required because Clang's VFS overlay needs absolute paths for root entries. + // The compiler will have -I${ROOT_PATH}/Headers in its include paths, so when it + // searches for , it looks for ${ROOT_PATH}/Headers/yoga/style/Style.h. + // The VFS overlay intercepts this and maps it to the actual flat location. + const wrappedRoot /*: VFSEntry */ = { + name: `${ROOT_PATH_PLACEHOLDER}/Headers`, + type: 'directory', + contents: innerRoots, + }; + + return { + version: 0, + 'case-sensitive': false, + roots: [wrappedRoot], + }; +} + +/** + * Creates a VFS overlay YAML file from the header files in podspecs. + * This is a convenience function that combines createVFSOverlayContents and + * generateVFSOverlayYAML into a single call. + * + * @param rootFolder The root folder of the React Native package + * @returns The VFS overlay as a YAML string ready to be written to a file + */ +function createVFSOverlay(rootFolder /*: string */) /*: string */ { + const overlay = createVFSOverlayContents(rootFolder); + return generateVFSOverlayYAML(overlay); +} + +/** + * Resolves a VFS overlay template by replacing the ${ROOT_PATH} placeholder + * with the actual root path. This is the equivalent of the Ruby create_vfs_overlay + * function in rncore.rb. + * + * The VFS overlay template contains ${ROOT_PATH} placeholders that need to be + * replaced with the actual path to the xcframework on the end user's machine + * (e.g., the path to React.xcframework in the Pods folder). + * + * @param vfsTemplate The VFS overlay template content (YAML string with ${ROOT_PATH} placeholders) + * @param rootPath The actual root path to substitute for ${ROOT_PATH} + * @returns The resolved VFS overlay YAML string with absolute paths + */ +function resolveVFSOverlay( + vfsTemplate /*: string */, + rootPath /*: string */, +) /*: string */ { + return vfsTemplate.split(ROOT_PATH_PLACEHOLDER).join(rootPath); +} + +module.exports = { + createVFSOverlay, + resolveVFSOverlay, +}; diff --git a/packages/react-native/scripts/ios-prebuild/xcframework.js b/packages/react-native/scripts/ios-prebuild/xcframework.js index 01d93de6454d..5ba426b3ae0a 100644 --- a/packages/react-native/scripts/ios-prebuild/xcframework.js +++ b/packages/react-native/scripts/ios-prebuild/xcframework.js @@ -15,6 +15,7 @@ const { } = require('../codegen/generate-artifacts-executor/generateFBReactNativeSpecIOS'); const headers = require('./headers'); const utils = require('./utils'); +const vfs = require('./vfs'); const childProcess = require('child_process'); const fs = require('fs'); const path = require('path'); @@ -22,6 +23,7 @@ const path = require('path'); const {execSync} = childProcess; const {getHeaderFilesFromPodspecs} = headers; const {createFolderIfNotExists, createLogger} = utils; +const {createVFSOverlay} = vfs; const frameworkLog = createLogger('XCFramework'); @@ -125,18 +127,6 @@ function buildXCFrameworks( ); if (headerFiles.length > 0) { - // Get podspec name without directory and extension and make sure it is a valid identifier - // by replacing any non-alphanumeric characters with an underscore. - let podSpecName = path - .basename(podspec, '.podspec') - .replace(/[^a-zA-Z0-9_]/g, '_'); - - // Fix for FBReactNativeSpec. RN expect FBReactNative spec headers - // To be in a folder named FBReactNativeSpec. - if (podSpecName === 'React_RCTFBReactNativeSpec') { - podSpecName = 'FBReactNativeSpec'; - } - // Create a folder for the podspec in the output headers path const podSpecTargetFolder = path.join(outputHeadersPath, podSpecName); @@ -207,6 +197,29 @@ function buildXCFrameworks( if (identity) { signXCFramework(identity, outputPath); } + + // Tar the output folder to a .tar.gz file + const tarFilePath = path.join( + buildFolder, + 'output', + 'xcframeworks', + buildType, + 'React.xcframework.tar.gz', + ); + frameworkLog('Creating tar file: ' + tarFilePath); + try { + execSync( + `tar -czf ${tarFilePath} -C ${path.dirname(outputPath)} React.xcframework`, + { + stdio: 'inherit', + }, + ); + } catch (error) { + frameworkLog( + `Error creating tar file: ${error.message}. Check if the tar command is available.`, + 'warning', + ); + } } function copySymbols( @@ -359,6 +372,15 @@ function copyHeaderFilesToSlices( }); }); }); + + // Create VFS overlay file at the XCFramework root (same for all platforms) + const vfsFilePath = path.join(outputPath, 'React-VFS-template.yaml'); + try { + fs.writeFileSync(vfsFilePath, createVFSOverlay(rootFolder), 'utf8'); + frameworkLog(`Created VFS overlay: ${path.basename(vfsFilePath)}`); + } catch (error) { + frameworkLog(`Error creating VFS overlay file: ${error.message}.`, 'error'); + } } function createModuleMapFile(outputPath /*: string */) { diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 756f039c8c37..d157fce21723 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -547,6 +547,13 @@ def react_native_post_install( # In XCode 26 we need to revert the new setting SWIFT_ENABLE_EXPLICIT_MODULES when building # with precompiled binaries. ReactNativePodsUtils.set_build_setting(installer, build_setting: "SWIFT_ENABLE_EXPLICIT_MODULES", value: "NO") + + # Process the VFS overlay for prebuilt React Native Core - this is done as part of the post install so + # that we can update paths based on the final location of the Pods installation. + ReactNativeCoreUtils.process_vfs_overlay() + + # Configure xcconfig for prebuilt usage (VFS overlay, header paths, cleanup redundant paths) + ReactNativeCoreUtils.configure_aggregate_xcconfig(installer) end SPM.apply_on_post_install(installer) From 6097ca6a5dd4e21338ec5386d796d6a7863a2bb4 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 10 Dec 2025 17:31:05 +0100 Subject: [PATCH 04/12] codereview: removed superflous parameter --- packages/react-native/scripts/ios-prebuild/vfs.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react-native/scripts/ios-prebuild/vfs.js b/packages/react-native/scripts/ios-prebuild/vfs.js index ea32ea73d99c..7ace66d48d9d 100644 --- a/packages/react-native/scripts/ios-prebuild/vfs.js +++ b/packages/react-native/scripts/ios-prebuild/vfs.js @@ -174,10 +174,7 @@ function generateEntryYAML( */ function createVFSOverlayContents(rootFolder /*: string */) /*: VFSOverlay */ { // Get header files from podspecs (disable testing since we just need the mappings) - const podSpecsWithHeaderFiles = getHeaderFilesFromPodspecs( - rootFolder, - false, // testHeadersFlag - ); + const podSpecsWithHeaderFiles = getHeaderFilesFromPodspecs(rootFolder); const mappings /*: Array */ = []; From f04b3381f8b9d769c42c1b4ef5702536752a4870 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Sat, 13 Dec 2025 08:53:10 +0100 Subject: [PATCH 05/12] [ios][precompiled] adjusted vfs generation Had some podspecs with header_dir set to a variable that wasn't emitted correctly in the VFS file. This commit fixes this by adding explicit reading of header-dir as well as exceptions for the specs that had variables. - Tested comparing the output from a regular pod install with the generated header files. - Tested with RNTester - Tested in BareExpo with prebuild Expo modules and full compile. --- .../scripts/ios-prebuild/headers-config.js | 57 +++++++++++++++++++ .../scripts/ios-prebuild/headers.js | 17 +++++- .../react-native/scripts/ios-prebuild/vfs.js | 39 +++++++++++-- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js index a445357b982c..4c6be3de77bf 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-config.js +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -217,6 +217,63 @@ const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { }, ], }, + + // ReactCommon.podspec has multiple subspecs with different header_dir values + // that the generic parser cannot handle (it only extracts the first header_dir). + 'ReactCommon/ReactCommon.podspec': { + name: 'ReactCommon', + headerPatterns: [], + headerDir: 'ReactCommon', + subSpecs: [ + { + name: 'bridging', + headerPatterns: ['react/bridging/**/*.h'], + excludePatterns: ['react/bridging/tests/**'], + headerDir: 'react/bridging', + }, + { + name: 'core', + headerPatterns: ['react/nativemodule/core/ReactCommon/**/*.h'], + headerDir: 'ReactCommon', + }, + ], + }, + + // these podspecs set `header_dir` via Ruby variables, which the generic + // podspec parser cannot infer. Add explicit exceptions so headers are emitted under + // the expected `jsinspector-modern/...` include paths. + 'React/Runtime/React-RCTRuntime.podspec': { + name: 'React-RCTRuntime', + headerPatterns: ['*.h'], + headerDir: 'React', + }, + + 'ReactCommon/jsinspector-modern/React-jsinspector.podspec': { + name: 'React-jsinspector', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern', + }, + 'ReactCommon/jsinspector-modern/cdp/React-jsinspectorcdp.podspec': { + name: 'React-jsinspectorcdp', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern/cdp', + }, + 'ReactCommon/jsinspector-modern/network/React-jsinspectornetwork.podspec': { + name: 'React-jsinspectornetwork', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern/network', + }, + 'ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec': { + name: 'React-jsinspectortracing', + headerPatterns: ['*.h'], + headerDir: 'jsinspector-modern/tracing', + }, + 'React/React-RCTFabric.podspec': { + name: 'React-RCTFabric', + headerPatterns: ['Fabric/**/*.h'], + headerDir: 'React', + }, + 'React/React-RCTFBReactNativeSpec.podspec': { name: 'React-RCTFBReactNativeSpec', headerPatterns: ['FBReactNativeSpec/**/*.h'], diff --git a/packages/react-native/scripts/ios-prebuild/headers.js b/packages/react-native/scripts/ios-prebuild/headers.js index 15a99989be29..26c17cca9326 100644 --- a/packages/react-native/scripts/ios-prebuild/headers.js +++ b/packages/react-native/scripts/ios-prebuild/headers.js @@ -47,7 +47,6 @@ function getHeaderFilesFromPodspecs( if (exception) { // Check if the exception is disabled if ('disabled' in exception && exception.disabled === true) { - headersLog(`⏭️ Skipping disabled podspec: ${relativeKey}`); return; } @@ -65,6 +64,16 @@ function getHeaderFilesFromPodspecs( // Open file and read content const fileContent = require('fs').readFileSync(podspecPath, 'utf8'); + // Try to infer header_dir when it's a string literal. + // We intentionally keep this simple and do not attempt to resolve Ruby variables. + // Examples supported: + // s.header_dir = "ReactCommon" + // ss.header_dir = 'jsinspector-modern/cdp' + const headerDirMatch = fileContent.match( + /\.header_dir\s*=\s*(['"])([^'"\n]+)\1/, + ); + const inferredHeaderDir = headerDirMatch ? headerDirMatch[2].trim() : ''; + // Check if it contains 'podspec_sources' if (fileContent.includes('podspec_sources')) { // Parse podspec_sources(source_files, header_patterns) - we want the SECOND argument. @@ -147,11 +156,13 @@ function getHeaderFilesFromPodspecs( result[podspecPath] = [ { - headerDir: '', // We don't have headerDir info here + headerDir: inferredHeaderDir, specName: path.basename(podspecPath, '.podspec'), headers: foundHeaderFiles.map(headerFile => ({ source: headerFile, - target: path.basename(headerFile), + target: inferredHeaderDir + ? path.join(inferredHeaderDir, path.basename(headerFile)) + : path.basename(headerFile), })), }, ]; diff --git a/packages/react-native/scripts/ios-prebuild/vfs.js b/packages/react-native/scripts/ios-prebuild/vfs.js index 7ace66d48d9d..a4250ded7cdb 100644 --- a/packages/react-native/scripts/ios-prebuild/vfs.js +++ b/packages/react-native/scripts/ios-prebuild/vfs.js @@ -47,7 +47,7 @@ function buildVFSStructure( } } - // Build the root-level directory entries + // Build the root-level entries (files at root + top-level directories) const rootDirs /*: Set */ = new Set(); for (const dirPath of dirTree.keys()) { const topLevel = dirPath.split('/')[0]; @@ -58,6 +58,20 @@ function buildVFSStructure( const roots /*: Array */ = []; + // Add files that live at the root (e.g. key === 'RCTAppDelegate.h') + const rootFiles = dirTree.get(''); + if (rootFiles) { + for (const [fileName, sourcePath] of Array.from( + rootFiles.entries(), + ).sort()) { + roots.push({ + name: fileName, + type: 'file', + 'external-contents': toPosix(sourcePath), + }); + } + } + for (const rootDir of Array.from(rootDirs).sort()) { const dirEntry = buildDirectoryEntry(rootDir, '', dirTree); roots.push(dirEntry); @@ -189,13 +203,30 @@ function createVFSOverlayContents(rootFolder /*: string */) /*: VFSOverlay */ { headerMap.headers.forEach(header => { // The key is just the target path (the import path) // e.g., 'react/renderer/graphics/Size.h' for #import - const key = toPosix(header.target); + let key = toPosix(header.target); + + // If the podspec doesn't specify a header_dir, CocoaPods exposes public headers under + // (and umbrella headers typically use quoted imports resolved relative + // to the pod's public headers directory). To mirror that layout and avoid collisions + // between pods, prefix root-level header targets with the pod spec name. + if ( + !key.includes('/') && + (!headerMap.headerDir || headerMap.headerDir === '') + ) { + key = `${podSpecName}/${key}`; + } // The external-contents path uses the full target path because headers are copied // with their directory structure preserved in the XCFramework's root Headers folder // (see xcframework.js where headerFile.target is used). - // The path is: ${ROOT_PATH}/Headers/{podSpecName}/{target} - const sourcePath = `${ROOT_PATH_PLACEHOLDER}/Headers/${podSpecName}/${toPosix(header.target)}`; + // Avoid double-nesting when target already starts with the pod name + // e.g., ReactCommon/AString.h should not become ReactCommon/ReactCommon/AString.h + const targetFirstDir = toPosix(header.target).split('/')[0]; + const targetPath = + targetFirstDir === podSpecName + ? toPosix(header.target) + : `${podSpecName}/${toPosix(header.target)}`; + const sourcePath = `${ROOT_PATH_PLACEHOLDER}/Headers/${targetPath}`; mappings.push({ key, From 961b199fb0e45228b7c59a0b2204525758a6020c Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Sat, 13 Dec 2025 12:18:33 +0100 Subject: [PATCH 06/12] Reverted unneeded change after adding exceptions Tried to build some smartness in last comitt - not working in a generic way so I reverted it. This is handled by the exception in header-config.js instead. --- packages/react-native/scripts/ios-prebuild/vfs.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/react-native/scripts/ios-prebuild/vfs.js b/packages/react-native/scripts/ios-prebuild/vfs.js index a4250ded7cdb..64f4c1ec6aeb 100644 --- a/packages/react-native/scripts/ios-prebuild/vfs.js +++ b/packages/react-native/scripts/ios-prebuild/vfs.js @@ -216,17 +216,10 @@ function createVFSOverlayContents(rootFolder /*: string */) /*: VFSOverlay */ { key = `${podSpecName}/${key}`; } - // The external-contents path uses the full target path because headers are copied - // with their directory structure preserved in the XCFramework's root Headers folder - // (see xcframework.js where headerFile.target is used). - // Avoid double-nesting when target already starts with the pod name - // e.g., ReactCommon/AString.h should not become ReactCommon/ReactCommon/AString.h - const targetFirstDir = toPosix(header.target).split('/')[0]; - const targetPath = - targetFirstDir === podSpecName - ? toPosix(header.target) - : `${podSpecName}/${toPosix(header.target)}`; - const sourcePath = `${ROOT_PATH_PLACEHOLDER}/Headers/${targetPath}`; + // The external-contents path is always podSpecName + header.target because + // xcframework.js copies headers to: outputHeadersPath/podSpecName/headerFile.target + // So the VFS must point to that same location. + const sourcePath = `${ROOT_PATH_PLACEHOLDER}/Headers/${podSpecName}/${toPosix(header.target)}`; mappings.push({ key, From 2943685ae7faa05df0119fe5ef794ea58ef4e10a Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Sun, 14 Dec 2025 12:31:18 +0100 Subject: [PATCH 07/12] Added exception for Yoga to preserve directories --- .../react-native/scripts/ios-prebuild/headers-config.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js index 4c6be3de77bf..f5e8b1d7c13e 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-config.js +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -217,6 +217,13 @@ const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { }, ], }, + // Yoga should preserve its directory structure + 'ReactCommon/yoga/Yoga.podspec': { + name: 'Yoga', + headerPatterns: ['yoga/**/*.h'], + headerDir: 'yoga', + preservePaths: ['yoga/**/*.h'], + }, // ReactCommon.podspec has multiple subspecs with different header_dir values // that the generic parser cannot handle (it only extracts the first header_dir). From 78a668d792c3293bd842ae542d3401fc2287631c Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 17 Dec 2025 14:23:05 +0100 Subject: [PATCH 08/12] codereview: removed toPosix function --- packages/react-native/scripts/ios-prebuild/vfs.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/react-native/scripts/ios-prebuild/vfs.js b/packages/react-native/scripts/ios-prebuild/vfs.js index 64f4c1ec6aeb..13e2cb233d5f 100644 --- a/packages/react-native/scripts/ios-prebuild/vfs.js +++ b/packages/react-native/scripts/ios-prebuild/vfs.js @@ -16,13 +16,6 @@ const {getHeaderFilesFromPodspecs} = headers; const ROOT_PATH_PLACEHOLDER = '${ROOT_PATH}'; -/** - * Converts a path to use POSIX-style separators (forward slashes) - */ -function toPosix(value /*: string */) /*: string */ { - return value.split(/[\\/]/).join('/'); -} - /** * Builds a hierarchical VFS directory structure from a list of header mappings. * Clang's VFS overlay requires a tree structure where directories contain their children. @@ -67,7 +60,7 @@ function buildVFSStructure( roots.push({ name: fileName, type: 'file', - 'external-contents': toPosix(sourcePath), + 'external-contents': sourcePath, }); } } @@ -100,7 +93,7 @@ function buildDirectoryEntry( contents.push({ name: fileName, type: 'file', - 'external-contents': toPosix(sourcePath), + 'external-contents': sourcePath, }); } } @@ -203,7 +196,7 @@ function createVFSOverlayContents(rootFolder /*: string */) /*: VFSOverlay */ { headerMap.headers.forEach(header => { // The key is just the target path (the import path) // e.g., 'react/renderer/graphics/Size.h' for #import - let key = toPosix(header.target); + let key = header.target; // If the podspec doesn't specify a header_dir, CocoaPods exposes public headers under // (and umbrella headers typically use quoted imports resolved relative @@ -219,7 +212,7 @@ function createVFSOverlayContents(rootFolder /*: string */) /*: VFSOverlay */ { // The external-contents path is always podSpecName + header.target because // xcframework.js copies headers to: outputHeadersPath/podSpecName/headerFile.target // So the VFS must point to that same location. - const sourcePath = `${ROOT_PATH_PLACEHOLDER}/Headers/${podSpecName}/${toPosix(header.target)}`; + const sourcePath = `${ROOT_PATH_PLACEHOLDER}/Headers/${podSpecName}/${header.target}`; mappings.push({ key, From 7fea6c583131709132354498813cb52ca885872f Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Thu, 18 Dec 2025 12:53:35 +0100 Subject: [PATCH 09/12] testing: we need to addd vfs overlay to all targets, not only aggregate targets when installing This one uses installer.pod_targets.each to do this. Added helper function to avoid code ducplication. --- .../react-native/scripts/cocoapods/rncore.rb | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 2ce3c3def4ee..c86866f99c98 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -524,21 +524,38 @@ def self.configure_aggregate_xcconfig(installer) # Add flags to aggregate target xcconfigs (these are used by the main app target) installer.aggregate_targets.each do |aggregate_target| aggregate_target.xcconfigs.each do |config_name, config_file| - # Add VFS overlay to compiler flags (C/C++ and Swift) - ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_CFLAGS", vfs_overlay_flag) - ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_CPLUSPLUSFLAGS", vfs_overlay_flag) + add_vfs_overlay_flags(config_file.attributes, vfs_overlay_flag, swift_vfs_overlay_flag) + xcconfig_path = aggregate_target.xcconfig_path(config_name) + config_file.save_as(xcconfig_path) + end + end - # For Swift, we need to use -Xcc to pass the flag to the underlying Clang compiler - ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_SWIFT_FLAGS", swift_vfs_overlay_flag) + # Add flags to ALL pod targets (for third-party pods that don't call add_rncore_dependency) + installer.pod_targets.each do |pod_target| + pod_target.build_settings.each do |config_name, build_settings| + xcconfig_path = pod_target.xcconfig_path(config_name) + next unless File.exist?(xcconfig_path) - # Suppress incomplete umbrella warnings for the prebuilt frameworks (it is expected, as our umbrella headers do not include all headers) - ReactNativePodsUtils.add_flag_to_map_with_inheritance(config_file.attributes, "OTHER_SWIFT_FLAGS", " -Xcc -Wno-incomplete-umbrella") + xcconfig = Xcodeproj::Config.new(xcconfig_path) - xcconfig_path = aggregate_target.xcconfig_path(config_name) - config_file.save_as(xcconfig_path) + # Check if VFS overlay is already present + other_cflags = xcconfig.attributes["OTHER_CFLAGS"] || "" + next if other_cflags.include?("ivfsoverlay") + + add_vfs_overlay_flags(xcconfig.attributes, vfs_overlay_flag, swift_vfs_overlay_flag) + xcconfig.save_as(xcconfig_path) end end rncore_log("Prebuilt xcconfig configuration complete") end + + # Helper method to add VFS overlay flags to an xcconfig attributes map + def self.add_vfs_overlay_flags(attributes, vfs_overlay_flag, swift_vfs_overlay_flag) + ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "OTHER_CFLAGS", vfs_overlay_flag) + ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "OTHER_CPLUSPLUSFLAGS", vfs_overlay_flag) + ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "OTHER_SWIFT_FLAGS", swift_vfs_overlay_flag) + # Suppress incomplete umbrella warnings for the prebuilt frameworks (it is expected, as our umbrella headers do not include all headers) + ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "OTHER_SWIFT_FLAGS", " -Xcc -Wno-incomplete-umbrella") + end end From ea1abc3d707aba39ebb32e1c856d6f5241cfe2cd Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Mon, 26 Jan 2026 20:29:53 +0100 Subject: [PATCH 10/12] codereivew: added missing log --- packages/react-native/scripts/ios-prebuild/headers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native/scripts/ios-prebuild/headers.js b/packages/react-native/scripts/ios-prebuild/headers.js index 26c17cca9326..f04392b6030d 100644 --- a/packages/react-native/scripts/ios-prebuild/headers.js +++ b/packages/react-native/scripts/ios-prebuild/headers.js @@ -47,6 +47,7 @@ function getHeaderFilesFromPodspecs( if (exception) { // Check if the exception is disabled if ('disabled' in exception && exception.disabled === true) { + headersLog(`⏭️ Skipping disabled podspec: ${relativeKey}`); return; } From ab9b5a56bca5d61fc5bad6986e17496fd098473f Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Mon, 26 Jan 2026 20:36:34 +0100 Subject: [PATCH 11/12] codereview: updated readme and added reference --- .../react-native/scripts/cocoapods/rncore.rb | 1 + .../scripts/ios-prebuild/__docs__/README.md | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index c86866f99c98..09cd88363d75 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -23,6 +23,7 @@ def add_rncore_dependency(s) # The VFS overlay file is pre-resolved at pod install time for each platform slice. # We reference it directly in the xcframework using the React-VFS.yaml file that # is written to the React-Core-prebuilt folder during setup_vfs_overlay. + # See scripts/ios-prebuild/__docs__/README.md for more details on VFS overlays. vfs_overlay_flag = "-ivfsoverlay $(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml" current_pod_target_xcconfig["OTHER_CFLAGS"] ||= "$(inherited)" current_pod_target_xcconfig["OTHER_CFLAGS"] += " #{vfs_overlay_flag}" diff --git a/packages/react-native/scripts/ios-prebuild/__docs__/README.md b/packages/react-native/scripts/ios-prebuild/__docs__/README.md index 6933a47c175d..fdf722b1eb54 100644 --- a/packages/react-native/scripts/ios-prebuild/__docs__/README.md +++ b/packages/react-native/scripts/ios-prebuild/__docs__/README.md @@ -121,6 +121,113 @@ issues when: - Building dependent frameworks that rely on proper module boundaries - Integrating with Swift Package Manager projects expecting modular headers +## VFS Overlay System + +The prebuilt XCFrameworks use Clang's Virtual File System (VFS) overlay +mechanism to enable header imports without modifying the actual header file +structure. This is necessary because React Native's headers are organized +differently than standard framework conventions. + +### Overview + +The VFS overlay creates a virtual mapping between the import paths used in code +(e.g., `#import `) and the actual physical +locations of headers within the XCFramework. This allows the prebuilt frameworks +to work seamlessly while maintaining the original import syntax. + +### Build-Time VFS Generation (`vfs.js`) + +The `vfs.js` script creates a VFS overlay template during the prebuild process: + +1. **Header Collection** (`headers.js`): Scans all podspec files in the React + Native package to discover header files and their target import paths. + +2. **VFS Structure Building**: The `buildVFSStructure()` function creates a + hierarchical directory tree representation from the header mappings. Clang's + VFS overlay requires directories to contain their children in a tree + structure. + +3. **YAML Generation**: The `generateVFSOverlayYAML()` function converts the VFS + structure into Clang's expected YAML format. + +4. **Template Creation**: The generated overlay uses `${ROOT_PATH}` as a + placeholder for the actual installation path. This template is included in + the XCFramework as `React-VFS-template.yaml`. + +#### Key Functions + +- `createVFSOverlay(rootFolder)`: Main entry point that generates the complete + VFS overlay YAML string +- `createVFSOverlayContents(rootFolder)`: Creates the VFS overlay object + structure +- `buildVFSStructure(mappings)`: Builds the hierarchical directory tree from + flat mappings +- `resolveVFSOverlay(vfsTemplate, rootPath)`: Replaces `${ROOT_PATH}` with the + actual path + +### Runtime VFS Processing (CocoaPods) + +When consuming prebuilt frameworks via CocoaPods, the VFS overlay is processed +at pod install time by `rncore.rb`: + +#### `process_vfs_overlay()` + +Called during `react_native_post_install`, this method: + +1. Reads the `React-VFS-template.yaml` from the XCFramework +2. Resolves the `${ROOT_PATH}` placeholder with the actual XCFramework path +3. Writes the resolved overlay to `$(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml` + +#### `add_rncore_dependency(s)` + +Adds VFS overlay compiler flags to podspecs that depend on React Native: + +```ruby +# For C/C++ compilation +OTHER_CFLAGS += "-ivfsoverlay $(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml" +OTHER_CPLUSPLUSFLAGS += "-ivfsoverlay $(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml" + +# For Swift compilation (flags passed to underlying Clang) +OTHER_SWIFT_FLAGS += "-Xcc -ivfsoverlay -Xcc $(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml" +``` + +#### `configure_aggregate_xcconfig(installer)` + +Configures VFS overlay flags for: + +- **Aggregate targets**: Main app targets that don't go through podspec + processing +- **All pod targets**: Third-party pods that don't explicitly call + `add_rncore_dependency` + +This ensures all compilation units in the project can resolve React Native +headers through the VFS overlay. + +### VFS Overlay Format + +The VFS overlay uses Clang's hierarchical YAML format: + +```yaml +version: 0 +case-sensitive: false +roots: + - name: '${ROOT_PATH}/Headers' + type: 'directory' + contents: + - name: 'react' + type: 'directory' + contents: + - name: 'renderer' + type: 'directory' + contents: + - name: 'Size.h' + type: 'file' + external-contents: '${ROOT_PATH}/Headers/React/react/renderer/Size.h' +``` + +The structure maps virtual paths (what the compiler sees) to physical paths +(where the files actually exist in the XCFramework). + ## Integrating in your project with Cocoapods For consuming, debugging or troubleshooting when using Cocoapods scripts, you From 4a30613e3026cf8be8851252098815a30ad4be51 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Tue, 27 Jan 2026 15:41:38 +0100 Subject: [PATCH 12/12] codereview: lint fix --- packages/react-native/scripts/ios-prebuild/__docs__/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native/scripts/ios-prebuild/__docs__/README.md b/packages/react-native/scripts/ios-prebuild/__docs__/README.md index fdf722b1eb54..4d2786314714 100644 --- a/packages/react-native/scripts/ios-prebuild/__docs__/README.md +++ b/packages/react-native/scripts/ios-prebuild/__docs__/README.md @@ -176,7 +176,8 @@ Called during `react_native_post_install`, this method: 1. Reads the `React-VFS-template.yaml` from the XCFramework 2. Resolves the `${ROOT_PATH}` placeholder with the actual XCFramework path -3. Writes the resolved overlay to `$(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml` +3. Writes the resolved overlay to + `$(PODS_ROOT)/React-Core-prebuilt/React-VFS.yaml` #### `add_rncore_dependency(s)`