Skip to content

Commit 22dee09

Browse files
aleqsiokosmydel
authored andcommitted
Implement privacy manifest aggregation (facebook#44214)
Summary: As of now, Apple does not respect privacy manifests added as cocoapods resource bundles. This forces react-native developers to manually copy `.xcprivacy` files content for each native dependency that accesses restricted reason APIs to the root file. This PR adds an aggregation step that crawls through pod dependencies to collect all reasons into the root privacy info file. ## Changelog: [IOS][ADDED] – Add privacy manifest aggregation. Pull Request resolved: facebook#44214 Test Plan: When run on RNTester, it appends aggregated entries (while keeping existing ones) to existing .xcprivacy file without modifing .pbxproj: ![image](https://github.com/facebook/react-native/assets/5597580/1d07a07d-bbec-4266-a599-a8d629078971) When run on RNTester with the xcprivacy file removed from xcode beforehand, it creates a new .xcprivacy file, and adds it to Compile Bundle Resources in the same way as in the new template: ![image](https://github.com/facebook/react-native/assets/5597580/f80a3b4e-e41a-4906-8e2f-06cca0bc225a) When run on RNTester with an empty .xcprivacy file, it appends aggregated entries from pods AND reasons for react-native core. When run with `privacy_file_aggregation_enabled: false` in `use_react_native`, it falls back to existing behavior: ![image](https://github.com/facebook/react-native/assets/5597580/4519bba1-c80e-4cd0-b19c-bbbebfa8493b) Reviewed By: cipolleschi Differential Revision: D56481045 Pulled By: philIip fbshipit-source-id: 1841bad821511c734d0cc0fcff5065ed92af76d8
1 parent c727527 commit 22dee09

File tree

3 files changed

+175
-40
lines changed

3 files changed

+175
-40
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
module PrivacyManifestUtils
7+
def self.add_aggregated_privacy_manifest(installer)
8+
user_project = get_user_project_from(installer)
9+
targets = get_application_targets(user_project)
10+
file_path = get_privacyinfo_file_path(user_project)
11+
12+
privacy_info = read_privacyinfo_file(file_path) || {
13+
"NSPrivacyCollectedDataTypes" => [],
14+
"NSPrivacyTracking" => false
15+
}
16+
17+
# Get all required reason APIs defined in current pods
18+
required_reason_apis = get_used_required_reason_apis(installer)
19+
20+
# Add the Required Reason APIs from React Native core
21+
get_core_accessed_apis.each do |accessed_api|
22+
api_type = accessed_api["NSPrivacyAccessedAPIType"]
23+
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"]
24+
required_reason_apis[api_type] ||= []
25+
required_reason_apis[api_type] += reasons
26+
end
27+
28+
# Merge the Required Reason APIs from pods with the ones from the existing PrivacyInfo file
29+
(privacy_info["NSPrivacyAccessedAPITypes"] || []).each do |accessed_api|
30+
api_type = accessed_api["NSPrivacyAccessedAPIType"]
31+
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"]
32+
# Add reasons from existing PrivacyInfo file to the ones from pods
33+
required_reason_apis[api_type] ||= []
34+
required_reason_apis[api_type] += reasons
35+
end
36+
37+
# Update the existing PrivacyInfo file with the new aggregated data
38+
privacy_info["NSPrivacyAccessedAPITypes"] = required_reason_apis.map { |api_type, reasons|
39+
{
40+
"NSPrivacyAccessedAPIType" => api_type,
41+
"NSPrivacyAccessedAPITypeReasons" => reasons.uniq
42+
}
43+
}
44+
45+
Xcodeproj::Plist.write_to_path(privacy_info, file_path)
46+
47+
targets.each do |target|
48+
ensure_reference(file_path, user_project, target)
49+
end
50+
end
51+
52+
def self.get_application_targets(user_project)
53+
return user_project.targets.filter { |t| t.symbol_type == :application }
54+
end
55+
56+
def self.read_privacyinfo_file(file_path)
57+
# Maybe add missing default NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, but this works without those keys
58+
source_data = nil
59+
# Try to read an existing PrivacyInfo.xcprivacy file
60+
begin
61+
source_data = Xcodeproj::Plist.read_from_path(file_path)
62+
Pod::UI.puts "[Privacy Manifest Aggregation] Appending aggregated reasons to existing PrivacyInfo.xcprivacy file."
63+
rescue => e
64+
Pod::UI.puts "[Privacy Manifest Aggregation] No existing PrivacyInfo.xcprivacy file found, creating a new one."
65+
end
66+
return source_data
67+
end
68+
69+
def self.ensure_reference(file_path, user_project, target)
70+
reference_exists = target.resources_build_phase.files_references.any? { |file_ref| file_ref.path.end_with? "PrivacyInfo.xcprivacy" }
71+
unless reference_exists
72+
# We try to find the main group, but if it doesn't exist, we default to adding the file to the project root – both work
73+
file_root = user_project.root_object.main_group.children.first { |group| group.name == target.name } || user_project
74+
file_ref = file_root.new_file(file_path)
75+
build_file = target.resources_build_phase.add_file_reference(file_ref, true)
76+
end
77+
end
78+
79+
def self.get_privacyinfo_file_path(user_project)
80+
# We try to find a file we know exists in the project to get the path to the main group directory
81+
info_plist_path = user_project.files.find { |file_ref| file_ref.name == "Info.plist" }
82+
if info_plist_path.nil?
83+
# return path that is sibling to .xcodeproj
84+
path = user_project.path
85+
return File.join(File.dirname(path), "PrivacyInfo.xcprivacy")
86+
end
87+
return File.join(File.dirname(info_plist_path.real_path),"PrivacyInfo.xcprivacy")
88+
end
89+
90+
def self.get_used_required_reason_apis(installer)
91+
# A dictionary with keys of type string (NSPrivacyAccessedAPIType) and values of type string[] (NSPrivacyAccessedAPITypeReasons[])
92+
used_apis = {}
93+
Pod::UI.puts "[Privacy Manifest Aggregation] Reading .xcprivacy files to aggregate all used Required Reason APIs."
94+
installer.pod_targets.each do |pod_target|
95+
# puts pod_target
96+
pod_target.file_accessors.each do |file_accessor|
97+
file_accessor.resource_bundles.each do |bundle_name, bundle_files|
98+
bundle_files.each do |file_path|
99+
# This needs to be named like that due to apple requirements
100+
if File.basename(file_path) == 'PrivacyInfo.xcprivacy'
101+
content = Xcodeproj::Plist.read_from_path(file_path)
102+
accessed_api_types = content["NSPrivacyAccessedAPITypes"]
103+
accessed_api_types.each do |accessed_api|
104+
api_type = accessed_api["NSPrivacyAccessedAPIType"]
105+
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"]
106+
used_apis[api_type] ||= []
107+
used_apis[api_type] += reasons
108+
end
109+
end
110+
end
111+
end
112+
end
113+
end
114+
return used_apis
115+
end
116+
117+
def self.get_privacy_manifest_paths_from(user_project)
118+
privacy_manifests = user_project
119+
.files
120+
.select { |p|
121+
p.path&.end_with?('PrivacyInfo.xcprivacy')
122+
}
123+
return privacy_manifests
124+
end
125+
126+
def self.get_core_accessed_apis()
127+
file_timestamp_accessed_api = {
128+
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryFileTimestamp",
129+
"NSPrivacyAccessedAPITypeReasons" => ["C617.1"],
130+
}
131+
user_defaults_accessed_api = {
132+
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryUserDefaults",
133+
"NSPrivacyAccessedAPITypeReasons" => ["CA92.1"],
134+
}
135+
boot_time_accessed_api = {
136+
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategorySystemBootTime",
137+
"NSPrivacyAccessedAPITypeReasons" => ["35F9.1"],
138+
}
139+
return [file_timestamp_accessed_api, user_defaults_accessed_api, boot_time_accessed_api]
140+
end
141+
142+
143+
def self.get_user_project_from(installer)
144+
user_project = installer.aggregate_targets
145+
.map{ |t| t.user_project }
146+
.first
147+
return user_project
148+
end
149+
150+
def self.add_privacy_manifest_if_needed(installer)
151+
user_project = get_user_project_from(installer)
152+
privacy_manifest = self.get_privacy_manifest_paths_from(user_project).first
153+
if privacy_manifest.nil?
154+
privacy_manifest = {
155+
"NSPrivacyCollectedDataTypes" => [],
156+
"NSPrivacyTracking" => false,
157+
"NSPrivacyAccessedAPITypes" => get_core_accessed_apis
158+
}
159+
path = File.join(user_project.path.parent, "PrivacyInfo.xcprivacy")
160+
Xcodeproj::Plist.write_to_path(privacy_manifest, path)
161+
Pod::UI.puts "Your app does not have a privacy manifest! A template has been generated containing Required Reasons API usage in the core React Native library. Please add the PrivacyInfo.xcprivacy file to your project and complete data use, tracking and any additional required reasons your app is using according to Apple's guidance: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files. Then, you will need to manually add this file to your project in Xcode.".red
162+
end
163+
end
164+
end

packages/react-native/scripts/cocoapods/utils.rb

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -592,44 +592,6 @@ def self.set_imagemanager_search_path(target_installation_result)
592592
ReactNativePodsUtils.update_header_paths_if_depends_on(target_installation_result, "React-ImageManager", header_search_paths)
593593
end
594594

595-
def self.get_privacy_manifest_paths_from(user_project)
596-
privacy_manifests = user_project
597-
.files
598-
.select { |p|
599-
p.path&.end_with?('PrivacyInfo.xcprivacy')
600-
}
601-
return privacy_manifests
602-
end
603-
604-
def self.add_privacy_manifest_if_needed(installer)
605-
user_project = installer.aggregate_targets
606-
.map{ |t| t.user_project }
607-
.first
608-
privacy_manifest = self.get_privacy_manifest_paths_from(user_project).first
609-
if privacy_manifest.nil?
610-
file_timestamp_reason = {
611-
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryFileTimestamp",
612-
"NSPrivacyAccessedAPITypeReasons" => ["C617.1"],
613-
}
614-
user_defaults_reason = {
615-
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryUserDefaults",
616-
"NSPrivacyAccessedAPITypeReasons" => ["CA92.1"],
617-
}
618-
boot_time_reason = {
619-
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategorySystemBootTime",
620-
"NSPrivacyAccessedAPITypeReasons" => ["35F9.1"],
621-
}
622-
privacy_manifest = {
623-
"NSPrivacyCollectedDataTypes" => [],
624-
"NSPrivacyTracking" => false,
625-
"NSPrivacyAccessedAPITypes" => [file_timestamp_reason, user_defaults_reason, boot_time_reason]
626-
}
627-
path = File.join(user_project.path.parent, "PrivacyInfo.xcprivacy")
628-
Xcodeproj::Plist.write_to_path(privacy_manifest, path)
629-
Pod::UI.puts "Your app does not have a privacy manifest! A template has been generated containing Required Reasons API usage in the core React Native library. Please add the PrivacyInfo.xcprivacy file to your project and complete data use, tracking and any additional required reasons your app is using according to Apple's guidance: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files. Then, you will need to manually add this file to your project in Xcode.".red
630-
end
631-
end
632-
633595
def self.react_native_pods
634596
return [
635597
"DoubleConversion",

packages/react-native/scripts/react_native_pods.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require_relative './cocoapods/local_podspec_patch.rb'
1717
require_relative './cocoapods/runtime.rb'
1818
require_relative './cocoapods/helpers.rb'
19+
require_relative './cocoapods/privacy_manifest_utils.rb'
1920
# Importing to expose use_native_modules!
2021
require_relative './cocoapods/autolinking.rb'
2122

@@ -63,7 +64,8 @@ def use_react_native! (
6364
production: false, # deprecated
6465
hermes_enabled: ENV['USE_HERMES'] && ENV['USE_HERMES'] == '0' ? false : true,
6566
app_path: '..',
66-
config_file_dir: ''
67+
config_file_dir: '',
68+
privacy_file_aggregation_enabled: true
6769
)
6870

6971
# Set the app_path as env variable so the podspecs can access it.
@@ -87,6 +89,7 @@ def use_react_native! (
8789

8890
ENV['RCT_FABRIC_ENABLED'] = fabric_enabled ? "1" : "0"
8991
ENV['USE_HERMES'] = hermes_enabled ? "1" : "0"
92+
ENV['RCT_AGGREGATE_PRIVACY_FILES'] = privacy_file_aggregation_enabled ? "1" : "0"
9093

9194
prefix = path
9295

@@ -273,6 +276,7 @@ def react_native_post_install(
273276

274277
fabric_enabled = ENV['RCT_FABRIC_ENABLED'] == '1'
275278
hermes_enabled = ENV['USE_HERMES'] == '1'
279+
privacy_file_aggregation_enabled = ENV['RCT_AGGREGATE_PRIVACY_FILES'] == '1'
276280

277281
if hermes_enabled
278282
ReactNativePodsUtils.set_gcc_preprocessor_definition_for_React_hermes(installer)
@@ -287,7 +291,12 @@ def react_native_post_install(
287291
ReactNativePodsUtils.updateOSDeploymentTarget(installer)
288292
ReactNativePodsUtils.set_dynamic_frameworks_flags(installer)
289293
ReactNativePodsUtils.add_ndebug_flag_to_pods_in_release(installer)
290-
ReactNativePodsUtils.add_privacy_manifest_if_needed(installer)
294+
295+
if privacy_file_aggregation_enabled
296+
PrivacyManifestUtils.add_aggregated_privacy_manifest(installer)
297+
else
298+
PrivacyManifestUtils.add_privacy_manifest_if_needed(installer)
299+
end
291300

292301
NewArchitectureHelper.set_clang_cxx_language_standard_if_needed(installer)
293302
NewArchitectureHelper.modify_flags_for_new_architecture(installer, NewArchitectureHelper.new_arch_enabled)

0 commit comments

Comments
 (0)