Skip to content

Commit

Permalink
Implement privacy manifest aggregation (#44214)
Browse files Browse the repository at this point in the history
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.

[IOS][ADDED] – Add privacy manifest aggregation.

Pull Request resolved: #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
  • Loading branch information
aleqsio authored and gabrieldonadel committed Jul 1, 2024
1 parent 9db3eb6 commit f4b9d09
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 38 deletions.
164 changes: 164 additions & 0 deletions packages/react-native/scripts/cocoapods/privacy_manifest_utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# 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.

module PrivacyManifestUtils
def self.add_aggregated_privacy_manifest(installer)
user_project = get_user_project_from(installer)
targets = get_application_targets(user_project)
file_path = get_privacyinfo_file_path(user_project)

privacy_info = read_privacyinfo_file(file_path) || {
"NSPrivacyCollectedDataTypes" => [],
"NSPrivacyTracking" => false
}

# Get all required reason APIs defined in current pods
required_reason_apis = get_used_required_reason_apis(installer)

# Add the Required Reason APIs from React Native core
get_core_accessed_apis.each do |accessed_api|
api_type = accessed_api["NSPrivacyAccessedAPIType"]
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"]
required_reason_apis[api_type] ||= []
required_reason_apis[api_type] += reasons
end

# Merge the Required Reason APIs from pods with the ones from the existing PrivacyInfo file
(privacy_info["NSPrivacyAccessedAPITypes"] || []).each do |accessed_api|
api_type = accessed_api["NSPrivacyAccessedAPIType"]
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"]
# Add reasons from existing PrivacyInfo file to the ones from pods
required_reason_apis[api_type] ||= []
required_reason_apis[api_type] += reasons
end

# Update the existing PrivacyInfo file with the new aggregated data
privacy_info["NSPrivacyAccessedAPITypes"] = required_reason_apis.map { |api_type, reasons|
{
"NSPrivacyAccessedAPIType" => api_type,
"NSPrivacyAccessedAPITypeReasons" => reasons.uniq
}
}

Xcodeproj::Plist.write_to_path(privacy_info, file_path)

targets.each do |target|
ensure_reference(file_path, user_project, target)
end
end

def self.get_application_targets(user_project)
return user_project.targets.filter { |t| t.symbol_type == :application }
end

def self.read_privacyinfo_file(file_path)
# Maybe add missing default NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, but this works without those keys
source_data = nil
# Try to read an existing PrivacyInfo.xcprivacy file
begin
source_data = Xcodeproj::Plist.read_from_path(file_path)
Pod::UI.puts "[Privacy Manifest Aggregation] Appending aggregated reasons to existing PrivacyInfo.xcprivacy file."
rescue => e
Pod::UI.puts "[Privacy Manifest Aggregation] No existing PrivacyInfo.xcprivacy file found, creating a new one."
end
return source_data
end

def self.ensure_reference(file_path, user_project, target)
reference_exists = target.resources_build_phase.files_references.any? { |file_ref| file_ref.path.end_with? "PrivacyInfo.xcprivacy" }
unless reference_exists
# 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
file_root = user_project.root_object.main_group.children.first { |group| group.name == target.name } || user_project
file_ref = file_root.new_file(file_path)
build_file = target.resources_build_phase.add_file_reference(file_ref, true)
end
end

def self.get_privacyinfo_file_path(user_project)
# We try to find a file we know exists in the project to get the path to the main group directory
info_plist_path = user_project.files.find { |file_ref| file_ref.name == "Info.plist" }
if info_plist_path.nil?
# return path that is sibling to .xcodeproj
path = user_project.path
return File.join(File.dirname(path), "PrivacyInfo.xcprivacy")
end
return File.join(File.dirname(info_plist_path.real_path),"PrivacyInfo.xcprivacy")
end

def self.get_used_required_reason_apis(installer)
# A dictionary with keys of type string (NSPrivacyAccessedAPIType) and values of type string[] (NSPrivacyAccessedAPITypeReasons[])
used_apis = {}
Pod::UI.puts "[Privacy Manifest Aggregation] Reading .xcprivacy files to aggregate all used Required Reason APIs."
installer.pod_targets.each do |pod_target|
# puts pod_target
pod_target.file_accessors.each do |file_accessor|
file_accessor.resource_bundles.each do |bundle_name, bundle_files|
bundle_files.each do |file_path|
# This needs to be named like that due to apple requirements
if File.basename(file_path) == 'PrivacyInfo.xcprivacy'
content = Xcodeproj::Plist.read_from_path(file_path)
accessed_api_types = content["NSPrivacyAccessedAPITypes"]
accessed_api_types.each do |accessed_api|
api_type = accessed_api["NSPrivacyAccessedAPIType"]
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"]
used_apis[api_type] ||= []
used_apis[api_type] += reasons
end
end
end
end
end
end
return used_apis
end

def self.get_privacy_manifest_paths_from(user_project)
privacy_manifests = user_project
.files
.select { |p|
p.path&.end_with?('PrivacyInfo.xcprivacy')
}
return privacy_manifests
end

def self.get_core_accessed_apis()
file_timestamp_accessed_api = {
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryFileTimestamp",
"NSPrivacyAccessedAPITypeReasons" => ["C617.1"],
}
user_defaults_accessed_api = {
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryUserDefaults",
"NSPrivacyAccessedAPITypeReasons" => ["CA92.1"],
}
boot_time_accessed_api = {
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategorySystemBootTime",
"NSPrivacyAccessedAPITypeReasons" => ["35F9.1"],
}
return [file_timestamp_accessed_api, user_defaults_accessed_api, boot_time_accessed_api]
end


def self.get_user_project_from(installer)
user_project = installer.aggregate_targets
.map{ |t| t.user_project }
.first
return user_project
end

def self.add_privacy_manifest_if_needed(installer)
user_project = get_user_project_from(installer)
privacy_manifest = self.get_privacy_manifest_paths_from(user_project).first
if privacy_manifest.nil?
privacy_manifest = {
"NSPrivacyCollectedDataTypes" => [],
"NSPrivacyTracking" => false,
"NSPrivacyAccessedAPITypes" => get_core_accessed_apis
}
path = File.join(user_project.path.parent, "PrivacyInfo.xcprivacy")
Xcodeproj::Plist.write_to_path(privacy_manifest, path)
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
end
end
end
38 changes: 0 additions & 38 deletions packages/react-native/scripts/cocoapods/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -561,44 +561,6 @@ def self.set_imagemanager_search_path(target_installation_result)
ReactNativePodsUtils.update_header_paths_if_depends_on(target_installation_result, "React-ImageManager", header_search_paths)
end

def self.get_privacy_manifest_paths_from(user_project)
privacy_manifests = user_project
.files
.select { |p|
p.path&.end_with?('PrivacyInfo.xcprivacy')
}
return privacy_manifests
end

def self.add_privacy_manifest_if_needed(installer)
user_project = installer.aggregate_targets
.map{ |t| t.user_project }
.first
privacy_manifest = self.get_privacy_manifest_paths_from(user_project).first
if privacy_manifest.nil?
file_timestamp_reason = {
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryFileTimestamp",
"NSPrivacyAccessedAPITypeReasons" => ["C617.1"],
}
user_defaults_reason = {
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryUserDefaults",
"NSPrivacyAccessedAPITypeReasons" => ["CA92.1"],
}
boot_time_reason = {
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategorySystemBootTime",
"NSPrivacyAccessedAPITypeReasons" => ["35F9.1"],
}
privacy_manifest = {
"NSPrivacyCollectedDataTypes" => [],
"NSPrivacyTracking" => false,
"NSPrivacyAccessedAPITypes" => [file_timestamp_reason, user_defaults_reason, boot_time_reason]
}
path = File.join(user_project.path.parent, "PrivacyInfo.xcprivacy")
Xcodeproj::Plist.write_to_path(privacy_manifest, path)
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/.../privacy_manifest_files. Then, you will need to manually add this file to your project in Xcode.".red
end
end

def self.react_native_pods
return [
"DoubleConversion",
Expand Down
10 changes: 10 additions & 0 deletions packages/react-native/scripts/react_native_pods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
require_relative './cocoapods/local_podspec_patch.rb'
require_relative './cocoapods/runtime.rb'
require_relative './cocoapods/helpers.rb'
require_relative './cocoapods/privacy_manifest_utils.rb'

$CODEGEN_OUTPUT_DIR = 'build/generated/ios'
$CODEGEN_COMPONENT_DIR = 'react/renderer/components'
Expand Down Expand Up @@ -80,6 +81,7 @@ def use_react_native! (
app_path: '..',
config_file_dir: '',
ios_folder: 'ios'
privacy_file_aggregation_enabled: true
)

# Set the app_path as env variable so the podspecs can access it.
Expand All @@ -101,6 +103,7 @@ def use_react_native! (
fabric_enabled = fabric_enabled || NewArchitectureHelper.new_arch_enabled
ENV['RCT_FABRIC_ENABLED'] = fabric_enabled ? "1" : "0"
ENV['USE_HERMES'] = hermes_enabled ? "1" : "0"
ENV['RCT_AGGREGATE_PRIVACY_FILES'] = privacy_file_aggregation_enabled ? "1" : "0"

prefix = path

Expand Down Expand Up @@ -295,6 +298,7 @@ def react_native_post_install(

fabric_enabled = ENV['RCT_FABRIC_ENABLED'] == '1'
hermes_enabled = ENV['USE_HERMES'] == '1'
privacy_file_aggregation_enabled = ENV['RCT_AGGREGATE_PRIVACY_FILES'] == '1'

if hermes_enabled
ReactNativePodsUtils.set_gcc_preprocessor_definition_for_React_hermes(installer)
Expand All @@ -311,6 +315,12 @@ def react_native_post_install(
ReactNativePodsUtils.fix_flipper_for_xcode_15_3(installer)
ReactNativePodsUtils.add_privacy_manifest_if_needed(installer)

if privacy_file_aggregation_enabled
PrivacyManifestUtils.add_aggregated_privacy_manifest(installer)
else
PrivacyManifestUtils.add_privacy_manifest_if_needed(installer)
end

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

Expand Down

0 comments on commit f4b9d09

Please sign in to comment.