From fbcfdaceadef50edd962c54fc81f7e76e0422b7f Mon Sep 17 00:00:00 2001 From: Dawid Malecki Date: Mon, 28 Sep 2026 11:07:37 -0700 Subject: [PATCH 1/2] Fix dynamic-framework header resolution for stable C++ umbrella headers (#58679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Fix dynamic-framework header resolution for stable C++ umbrella headers. Header paths are now added only to targets that depend on the owning framework, including transitively, without exposing a framework’s built headers to its own compilation. ## Changelog: [Internal] Test Plan: - Focused CocoaPods tests: 16 tests, 27 assertions, all passing. - Dynamic-framework CocoaPods generation: passing. - Verified React-RuntimeApple → React-RCTFabric → React-Fabric receives the React-Fabric umbrella path. - Verified React-Fabric does not receive its own built-framework path. - Verified the dynamic React-Fabric pod target resolves `$(PODS_TARGET_SRCROOT)/react/renderer/components/view` and `$(PODS_TARGET_SRCROOT)/react/renderer/core` before the framework exists. - `ruby --disable-gems -c packages/react-native/ReactCommon/React-Fabric.podspec` - `yarn format-check` - `arc lint xplat/js/react-native-github/packages/react-native/ReactCommon/React-Fabric.podspec` - Agent validators: arc-format, arc-lint, and arc-lint-testquality passed. - React-Fabric.podspec change reduced to one added search path in the `core` subspec (`react/renderer/components/view`); the `components/root` umbrella deps moved to D120532145. Evaluated the podspec with a stub CocoaPods DSL (root + subspec `pod_target_xcconfig` merge): without frameworks the effective HEADER_SEARCH_PATHS and deps are identical to the parent; with `USE_FRAMEWORKS=dynamic` the only difference is the added `components/view` path. The podspec at the stack tip resolves to the same paths and deps as before the change. The full CocoaPods Ruby test process could not start on this host because its RubyGems/JSON installation is broken; the existing focused CocoaPods coverage remains unchanged. Reviewed By: cipolleschi Differential Revision: D121777587 Pulled By: coado --- .../ReactCommon/React-Fabric.podspec | 1 + .../scripts/cocoapods/__tests__/utils-test.rb | 90 ++++++++++++++++++- .../react-native/scripts/cocoapods/utils.rb | 65 ++++++++++++-- 3 files changed, 146 insertions(+), 10 deletions(-) diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 7a13eb84ef3..fa08462a354 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -104,6 +104,7 @@ Pod::Spec.new do |s| "\"$(PODS_TARGET_SRCROOT)/react/renderer/components/text/platform/cxx\"", "\"$(PODS_TARGET_SRCROOT)/react/renderer/components/textinput/platform/ios\"", "\"$(PODS_TARGET_SRCROOT)/react/renderer/components/view/platform/cxx\"", + "\"$(PODS_TARGET_SRCROOT)/react/renderer/components/view\"", "\"$(PODS_TARGET_SRCROOT)/react/renderer/core\"", "\"$(PODS_TARGET_SRCROOT)/react/renderer/debug\"", ] diff --git a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb index 3946f72e4dc..8432512de07 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb @@ -766,7 +766,12 @@ def test_updateSearchPaths_whenUseFrameworks_addsSearchPaths second_target ] ) - pods_projects_mock = PodsProjectMock.new([third_target], {"hermes-engine" => {}}) + pods_projects_mock = PodsProjectMock.new( + [third_target], + {"hermes-engine" => {}}, + "test/path-pod.xcodeproj", + [prepare_config("Debug"), prepare_config("Release")] + ) installer = InstallerMock.new(pods_projects_mock, [ AggregatedProjectMock.new(user_project_mock) ]) @@ -777,10 +782,15 @@ def test_updateSearchPaths_whenUseFrameworks_addsSearchPaths # Assert user_project_mock.build_configurations.each do |config| received_search_path = config.build_settings["HEADER_SEARCH_PATHS"] - expected_search_path = "$(inherited) ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core ${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-timing/React_timing.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/React-featureflags/React_featureflags.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-renderercss/React_renderercss.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-cxxstableapi/React_cxxstableapi.framework/Headers" + expected_search_path = "$(inherited) ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core ${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/scrollview/platform/cxx ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/scrollview/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-debug/React_debug.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-rendererdebug/React_rendererdebug.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-timing/React_timing.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-utils/React_utils.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-FabricComponents/React_FabricComponents.framework/Headers/react/renderer/textlayoutmanager/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/React-FabricComponents/React_FabricComponents.framework/Headers/react/renderer/components/text/platform/cxx ${PODS_CONFIGURATION_BUILD_DIR}/React-FabricComponents/React_FabricComponents.framework/Headers/react/renderer/components/textinput/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/React-FabricComponents/React_FabricComponents.framework/Headers/react/renderer/components/switch/iosswitch ${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-bridging/React_bridging.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios ${PODS_CONFIGURATION_BUILD_DIR}/React-featureflags/React_featureflags.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-renderercss/React_renderercss.framework/Headers ${PODS_CONFIGURATION_BUILD_DIR}/React-cxxstableapi/React_cxxstableapi.framework/Headers" assert_equal(expected_search_path, received_search_path) end + pods_projects_mock.build_configurations.each do |config| + received_search_paths = Shellwords.shellsplit(config.build_settings["HEADER_SEARCH_PATHS"]) + assert_false(received_search_paths.include?("${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers")) + end + installer.target_installation_results.pod_target_installation_results.each do |pod_name, target_installation_result| if pod_name == "SecondTarget" target_installation_result.native_target.build_configurations.each do |config| @@ -842,6 +852,20 @@ def test_add_search_path_if_not_included_does_not_add_existing_path_to_string assert_equal("/path/to/headers /another/path", result) end + def test_add_search_path_if_not_included_adds_path_that_is_prefix_of_existing_path + current_paths = "/path/to/headers/platform/ios" + new_path = "/path/to/headers" + result = ReactNativePodsUtils.add_search_path_if_not_included(current_paths, new_path) + assert_equal("/path/to/headers/platform/ios /path/to/headers", result) + end + + def test_add_search_path_if_not_included_does_not_duplicate_quoted_path_with_spaces + current_paths = '"/path/with spaces/headers" /another/path' + new_path = '"/path/with spaces/headers"' + result = ReactNativePodsUtils.add_search_path_if_not_included(current_paths, new_path) + assert_equal(current_paths, result) + end + def test_add_search_path_if_not_included_does_not_add_existing_path_with_leading_space_to_string current_paths = " /path/with/leading/space /another/path" new_path = "/path/with/leading/space" @@ -977,6 +1001,68 @@ def test_createHeaderSearchPathsForStableUmbrellas_whenUsingPrebuiltCore_addsNot assert_equal([], result) end + def test_setStableUmbrellaSearchPaths_addsOwningFrameworkPathToDependentTarget + target = prepare_target("Consumer", nil, [DependencyMock.new("React-Fabric")]) + target_installation_result = TargetInstallationResultMock.new(target, target) + + ReactNativePodsUtils.set_stable_umbrella_search_paths(target_installation_result) + + target.build_configurations.each do |config| + assert_equal( + '$(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers"', + config.build_settings["HEADER_SEARCH_PATHS"] + ) + end + end + + def test_setStableUmbrellaSearchPaths_addsOwningFrameworkPathToTransitivelyDependentTarget + dependency = Struct.new(:name, :target) + fabric_target = prepare_target("React-Fabric") + intermediate_target = prepare_target( + "Intermediate", + nil, + [dependency.new("React-Fabric", fabric_target)] + ) + consumer_target = prepare_target( + "Consumer", + nil, + [dependency.new("Intermediate", intermediate_target)] + ) + target_installation_result = TargetInstallationResultMock.new(consumer_target, consumer_target) + + ReactNativePodsUtils.set_stable_umbrella_search_paths(target_installation_result) + + consumer_target.build_configurations.each do |config| + assert_equal( + '$(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers"', + config.build_settings["HEADER_SEARCH_PATHS"] + ) + end + end + + def test_setStableUmbrellaSearchPaths_doesNotAddOwningFrameworkPathToUnrelatedTarget + target = prepare_target("React-Fabric") + target_installation_result = TargetInstallationResultMock.new(target, target) + + ReactNativePodsUtils.set_stable_umbrella_search_paths(target_installation_result) + + target.build_configurations.each do |config| + assert_nil(config.build_settings["HEADER_SEARCH_PATHS"]) + end + end + + def test_setStableUmbrellaSearchPaths_whenUsingPrebuiltCore_addsNothing + ReactNativeCoreUtils.class_variable_set(:@@build_from_source, false) + target = prepare_target("Consumer", nil, [DependencyMock.new("React-Fabric")]) + target_installation_result = TargetInstallationResultMock.new(target, target) + + ReactNativePodsUtils.set_stable_umbrella_search_paths(target_installation_result) + + target.build_configurations.each do |config| + assert_nil(config.build_settings["HEADER_SEARCH_PATHS"]) + end + end + # ================================= # # TEST - Add RN_BUILDING definition # # ================================= # diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 13a5630b0b7..119ce41c24b 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -358,15 +358,19 @@ def self.create_header_search_path_for_frameworks(base_folder, pod_name, framewo def self.create_header_search_paths_for_stable_umbrellas(base_folder) return [] unless ReactNativeCoreUtils.build_rncore_from_source() + self.stable_umbrella_frameworks.flat_map { |pod_name, framework_name| + self.create_header_search_path_for_frameworks(base_folder, pod_name, framework_name, []) + } + end + + def self.stable_umbrella_frameworks [ ["React-Fabric", "React_Fabric"], ["React-debug", "React_debug"], ["React-rendererdebug", "React_rendererdebug"], ["React-timing", "React_timing"], ["React-utils", "React_utils"], - ].flat_map { |pod_name, framework_name| - self.create_header_search_path_for_frameworks(base_folder, pod_name, framework_name, []) - } + ] end # Add a new dependency to an existing spec, configuring also the headers search paths @@ -399,6 +403,9 @@ def self.update_search_paths(installer) projects = self.extract_projects(installer) projects.each do |project| + stable_umbrella_search_paths = project == installer.pods_project ? + [] : ReactNativePodsUtils.create_header_search_paths_for_stable_umbrellas("PODS_CONFIGURATION_BUILD_DIR") + project.build_configurations.each do |config| header_search_paths = config.build_settings["HEADER_SEARCH_PATHS"] ||= "$(inherited)" @@ -411,7 +418,7 @@ def self.update_search_paths(installer) "react/renderer/components/scrollview/platform/cxx", "react/renderer/components/scrollview/platform/ios", ], false)) - .concat(ReactNativePodsUtils.create_header_search_paths_for_stable_umbrellas("PODS_CONFIGURATION_BUILD_DIR")) + .concat(stable_umbrella_search_paths) .concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-FabricComponents", "React_FabricComponents", [ "react/renderer/textlayoutmanager/platform/ios", "react/renderer/components/text/platform/cxx", @@ -436,6 +443,8 @@ def self.update_search_paths(installer) end installer.target_installation_results.pod_target_installation_results.each do |pod_name, target_installation_result| + self.set_stable_umbrella_search_paths(target_installation_result) + if self.react_native_pods.include?(pod_name) || pod_name.include?("Pod") || pod_name.include?("Tests") next end @@ -652,7 +661,9 @@ def self.add_search_path_if_not_included(current_search_paths, new_search_path) if current_search_paths.is_a?(String) current_search_paths = current_search_paths.strip - return "#{current_search_paths} #{new_search_path}" unless current_search_paths.include?(new_search_path) + current_paths = Shellwords.shellsplit(current_search_paths) + normalized_new_path = Shellwords.shellsplit(new_search_path).first + return "#{current_search_paths} #{new_search_path}" unless current_paths.include?(normalized_new_path) end if current_search_paths.is_a?(Array) @@ -663,10 +674,29 @@ def self.add_search_path_if_not_included(current_search_paths, new_search_path) current_search_paths end - def self.update_header_paths_if_depends_on(target_installation_result, dependency_name, header_paths) - depends_on_framework = target_installation_result.native_target.dependencies.any? { |d| d.name == dependency_name } + def self.target_depends_on?(target, dependency_name, visited_targets = {}) + return false if target == nil + + target_identifier = target.respond_to?(:uuid) ? target.uuid : target.object_id + target_identifier ||= target.object_id + return false if visited_targets[target_identifier] + + visited_targets[target_identifier] = true + target.dependencies.any? do |dependency| + dependency.name == dependency_name || + (dependency.respond_to?(:target) && self.target_depends_on?(dependency.target, dependency_name, visited_targets)) + end + end + + def self.update_header_paths_if_depends_on(target_installation_result, dependency_name, header_paths, include_transitive_dependencies: false) + native_target = target_installation_result.native_target + depends_on_framework = if include_transitive_dependencies + self.target_depends_on?(native_target, dependency_name) + else + native_target.dependencies.any? { |d| d.name == dependency_name } + end if depends_on_framework - target_installation_result.native_target.build_configurations.each do |config| + native_target.build_configurations.each do |config| header_search_path = config.build_settings["HEADER_SEARCH_PATHS"] != nil ? config.build_settings["HEADER_SEARCH_PATHS"] : "$(inherited)" header_paths.each { |header| header_search_path = ReactNativePodsUtils.add_search_path_if_not_included(header_search_path, header) } config.build_settings["HEADER_SEARCH_PATHS"] = header_search_path @@ -674,6 +704,25 @@ def self.update_header_paths_if_depends_on(target_installation_result, dependenc end end + def self.set_stable_umbrella_search_paths(target_installation_result) + return unless ReactNativeCoreUtils.build_rncore_from_source() + + ReactNativePodsUtils.stable_umbrella_frameworks.each do |pod_name, framework_name| + header_search_paths = ReactNativePodsUtils.create_header_search_path_for_frameworks( + "PODS_CONFIGURATION_BUILD_DIR", + pod_name, + framework_name, + [] + ).map { |search_path| "\"#{search_path}\"" } + ReactNativePodsUtils.update_header_paths_if_depends_on( + target_installation_result, + pod_name, + header_search_paths, + include_transitive_dependencies: true + ) + end + end + def self.set_rctfolly_search_paths(target_installation_result) ReactNativePodsUtils.update_header_paths_if_depends_on(target_installation_result, "RCT-Folly", [ "\"$(PODS_ROOT)/RCT-Folly\"", From 56e918b415fbbd22f7a718da6bb76df3c2793707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Mon, 28 Sep 2026 11:07:37 -0700 Subject: [PATCH 2/2] Use public umbrellas in root headers (#58575) Summary: Route public C++ dependencies through their supported umbrella entry points so framework consumers can use the root module with strict API enforcement enabled. Stage the iOS ImageRequestParams header under its canonical imagemanager include path in the prebuilt header inventory. The public ImageManager umbrella exposes this platform-specific type transitively, so framework builds need it alongside the root imagemanager headers. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D120532145 --- packages/react-native/ReactCommon/React-Fabric.podspec | 2 ++ .../renderer/components/root/RootComponentDescriptor.h | 2 +- .../react/renderer/components/root/RootProps.h | 8 ++------ .../react/renderer/components/root/RootShadowNode.h | 5 ++--- .../react-native/scripts/ios-prebuild/headers-config.js | 5 ++++- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index fa08462a354..92be6414056 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -142,6 +142,8 @@ Pod::Spec.new do |s| s.subspec "components" do |ss| ss.subspec "root" do |sss| + sss.dependency "React-Fabric/coreUmbrella" + sss.dependency "React-Fabric/components/viewUmbrella" sss.source_files = podspec_sources("react/renderer/components/root/**/*.{m,mm,cpp,h}", "react/renderer/components/root/**/*.{h}") sss.exclude_files = "react/renderer/components/root/tests" sss.header_dir = "react/renderer/components/root" diff --git a/packages/react-native/ReactCommon/react/renderer/components/root/RootComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/root/RootComponentDescriptor.h index 07309a87af5..91868c76ce7 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/root/RootComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/components/root/RootComponentDescriptor.h @@ -9,8 +9,8 @@ #include +#include #include -#include namespace facebook::react { diff --git a/packages/react-native/ReactCommon/react/renderer/components/root/RootProps.h b/packages/react-native/ReactCommon/react/renderer/components/root/RootProps.h index 5a43e19ac60..9914e3b9839 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/root/RootProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/root/RootProps.h @@ -9,12 +9,8 @@ #include -#include - -#include -#include -#include -#include +#include +#include namespace facebook::react { diff --git a/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h index fa980b713d2..e3dc6d0b7ea 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/root/RootShadowNode.h @@ -11,10 +11,9 @@ #include +#include +#include #include -#include -#include -#include namespace facebook::react { diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js index de50c993878..bd4f5fa955f 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-config.js +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -185,7 +185,10 @@ const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { { name: 'imagemanager', - headerPatterns: ['react/renderer/imagemanager/*.h'], + headerPatterns: [ + 'react/renderer/imagemanager/*.h', + 'react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageRequestParams.h', + ], headerDir: 'react/renderer/imagemanager', },