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/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 597c4b4370d1..09cd88363d75 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -13,10 +13,27 @@ ### 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. + # 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}" + 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 +467,96 @@ 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_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 + + # 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) + + xcconfig = Xcodeproj::Config.new(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 diff --git a/packages/react-native/scripts/ios-prebuild/__docs__/README.md b/packages/react-native/scripts/ios-prebuild/__docs__/README.md index 6933a47c175d..4d2786314714 100644 --- a/packages/react-native/scripts/ios-prebuild/__docs__/README.md +++ b/packages/react-native/scripts/ios-prebuild/__docs__/README.md @@ -121,6 +121,114 @@ 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 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..f5e8b1d7c13e --- /dev/null +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -0,0 +1,498 @@ +/** + * 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, +} | {disabled: true}>; +*/ + +const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { + 'ReactCommon/jsi/React-jsi.podspec': { + name: 'React-jsi', + headerPatterns: ['**/*.h'], + headerDir: 'jsi', + excludePatterns: ['**/test/*'], + }, + 'ReactCommon/hermes/React-hermes.podspec': { + name: 'React-hermes', + headerPatterns: [ + 'executor/*.h', + 'inspector-modern/chrome/*.h', + 'executor/HermesExecutorFactory.h', + ], + headerDir: 'reacthermes', + }, + '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', + }, + ], + }, + // 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). + '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'], + 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: [], + 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', + }, + ], + }, + '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'], + }, + ], + }, + 'React.podspec': {disabled: true}, + 'Libraries/PushNotificationIOS/React-RCTPushNotification.podspec': { + disabled: true, + }, +}; + +module.exports = {PodspecExceptions}; diff --git a/packages/react-native/scripts/ios-prebuild/headers.js b/packages/react-native/scripts/ios-prebuild/headers.js index 8ef1a23bbf0d..f04392b6030d 100644 --- a/packages/react-native/scripts/ios-prebuild/headers.js +++ b/packages/react-native/scripts/ios-prebuild/headers.js @@ -8,83 +8,287 @@ * @format */ -const fs = require('fs'); +const {PodspecExceptions} = require('./headers-config'); +const utils = require('./utils'); const path = require('path'); const {globSync} = require('tinyglobby'); -/** - * 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. - */ -const regex = - /podspec_sources\s*\(\s*((?:\[[^\]]*\]|"[^"]*"|'[^']*'|[^,])+)\s*,\s*((?:\[[^\]]*\]|"[^"]*"|'[^']*'|[^)])+)\s*\)/gs; +const {createLogger} = utils; +const headersLog = createLogger('headers'); + +/*:: +import type {PodSpecConfiguration} from './headers-config'; +type HeaderMap = { headerDir: string, specName: string, headers: {source: string, target: string}[]}; +*/ function getHeaderFilesFromPodspecs( rootFolder /*:string*/, -) /*: { [key: string]: string[] }*/ { - // Find podspec files - const podSpecFiles = globSync('**/*.podspec', { +) /*: { [key: string]: HeaderMap[] }*/ { + const result /*: { [key: string]: HeaderMap[] }*/ = {}; + + // 1. Find all podspec files in the rootFolder + const podspecFiles = globSync('**/*.podspec', { cwd: rootFolder, absolute: true, - onlyFiles: true, + ignore: ['**/node_modules/**', '**/Pods/**'], }); - 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; - } - // 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) + headersLog( + '🔍 Collecting header files from all podspec files in the project...', + ); + + // 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]; + + if (exception) { + // Check if the exception is disabled + if ('disabled' in exception && exception.disabled === true) { + headersLog(`⏭️ Skipping disabled podspec: ${relativeKey}`); + return; + } + + // Use getHeaderFilesFromPodspec for podspecs with exceptions + const headerMaps = getHeaderFilesFromPodspec( + exception, + path.dirname(podspecPath), + ); + if (headerMaps !== null) { + result[podspecPath] = headerMaps; + } + return; + } + + // 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. + // 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)]; + + // 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(item => item.trim()); + .map(s => s.trim().replace(/['"]/g, '')) + .filter(s => s.length > 0); } else { - globPatterns = [arg2]; + return [arg.replace(/['"]/g, '').trim()].filter(s => s.length > 0); } + }); + + // Add default excludes + const allExcludes = [...excludePatterns]; - // 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, + 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, - expandDirectories: false, }); }) .flat(); - if (!headers[podspec]) { - headers[podspec] = results; - } else { - headers[podspec].push(...results); - } + result[podspecPath] = [ + { + headerDir: inferredHeaderDir, + specName: path.basename(podspecPath, '.podspec'), + headers: foundHeaderFiles.map(headerFile => ({ + source: headerFile, + target: inferredHeaderDir + ? path.join(inferredHeaderDir, path.basename(headerFile)) + : path.basename(headerFile), + })), + }, + ]; } } }); - return headers; + 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[] */ = []; + + // 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; + } + } + } + + // 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.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(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 headerMaps; } module.exports = { 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..13e2cb233d5f --- /dev/null +++ b/packages/react-native/scripts/ios-prebuild/vfs.js @@ -0,0 +1,282 @@ +/** + * 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}'; + +/** + * 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 entries (files at root + top-level directories) + const rootDirs /*: Set */ = new Set(); + for (const dirPath of dirTree.keys()) { + const topLevel = dirPath.split('/')[0]; + if (topLevel) { + rootDirs.add(topLevel); + } + } + + 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': sourcePath, + }); + } + } + + 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': 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); + + 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 + 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 + // 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 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}/${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 1ff9f992ca2b..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'); @@ -114,39 +116,36 @@ function buildXCFrameworks( // Enumerate podspecs and copy headers, create umbrella headers and module map file Object.keys(podSpecsWithHeaderFiles).forEach(podspec => { - const headerFiles = podSpecsWithHeaderFiles[podspec]; - 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'; - } + 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) { // 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 +182,9 @@ function buildXCFrameworks( return; } - linkArchFolders( + // Copy header files and module map file to each platform slice in the XCFramework + copyHeaderFilesToSlices( + rootFolder, outputPath, moduleMapFile, umbrellaHeaders, @@ -196,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( @@ -253,13 +277,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 +335,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 +349,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', ); } @@ -345,6 +372,15 @@ function linkArchFolders( }); }); }); + + // 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)