21 changed files with 2197 additions and 2 deletions
@ -0,0 +1,5 @@ |
|||||
|
# macOS |
||||
|
.DS_Store |
||||
|
|
||||
|
# Build |
||||
|
/Build/ |
@ -0,0 +1,58 @@ |
|||||
|
## 1.4.1 |
||||
|
- Fix macOS app re-signing issue. |
||||
|
- Automatically enable Hardened Runtime in macOS codesign. |
||||
|
- Add clean script. |
||||
|
|
||||
|
## 1.4.0 |
||||
|
- Support for macOS app ([#9](https://github.com/crasowas/app_privacy_manifest_fixer/issues/9)). |
||||
|
|
||||
|
## 1.3.11 |
||||
|
- Fix install issue by skipping `PBXAggregateTarget` ([#4](https://github.com/crasowas/app_privacy_manifest_fixer/issues/4)). |
||||
|
|
||||
|
## 1.3.10 |
||||
|
- Fix app re-signing issue. |
||||
|
- Enhance Build Phases script robustness. |
||||
|
|
||||
|
## 1.3.9 |
||||
|
- Add log file output. |
||||
|
|
||||
|
## 1.3.8 |
||||
|
- Add version info to privacy access report. |
||||
|
- Remove empty tables from privacy access report. |
||||
|
|
||||
|
## 1.3.7 |
||||
|
- Enhance API symbols analysis with strings tool. |
||||
|
- Improve performance of API usage analysis. |
||||
|
|
||||
|
## 1.3.5 |
||||
|
- Fix issue with inaccurate privacy manifest search. |
||||
|
- Disable dependency analysis to force the script to run on every build. |
||||
|
- Add placeholder for privacy access report. |
||||
|
- Update build output directory naming convention. |
||||
|
- Add examples for privacy access report. |
||||
|
|
||||
|
## 1.3.0 |
||||
|
- Add privacy access report generation. |
||||
|
|
||||
|
## 1.2.3 |
||||
|
- Fix issue with relative path parameter. |
||||
|
- Add support for all application targets. |
||||
|
|
||||
|
## 1.2.1 |
||||
|
- Fix backup issue with empty user templates directory. |
||||
|
|
||||
|
## 1.2.0 |
||||
|
- Add uninstall script. |
||||
|
|
||||
|
## 1.1.2 |
||||
|
- Remove `Templates/.gitignore` to track `UserTemplates`. |
||||
|
- Fix incorrect use of `App.xcprivacy` template in `App.framework`. |
||||
|
|
||||
|
## 1.1.0 |
||||
|
- Add logs for latest release fetch failure. |
||||
|
- Fix issue with converting published time to local time. |
||||
|
- Disable showing environment variables in the build log. |
||||
|
- Add `--install-builds-only` command line option. |
||||
|
|
||||
|
## 1.0.0 |
||||
|
- Initial version. |
@ -0,0 +1,80 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2025, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Prevent duplicate loading |
||||
|
if [ -n "$CONSTANTS_SH_LOADED" ]; then |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
readonly CONSTANTS_SH_LOADED=1 |
||||
|
|
||||
|
# File name of the privacy manifest |
||||
|
readonly PRIVACY_MANIFEST_FILE_NAME="PrivacyInfo.xcprivacy" |
||||
|
|
||||
|
# Common privacy manifest template file names |
||||
|
readonly APP_TEMPLATE_FILE_NAME="AppTemplate.xcprivacy" |
||||
|
readonly FRAMEWORK_TEMPLATE_FILE_NAME="FrameworkTemplate.xcprivacy" |
||||
|
|
||||
|
# Universal delimiter |
||||
|
readonly DELIMITER=":" |
||||
|
|
||||
|
# Space escape symbol for handling space in path |
||||
|
readonly SPACE_ESCAPE="\u0020" |
||||
|
|
||||
|
# Default value when the version cannot be retrieved |
||||
|
readonly UNKNOWN_VERSION="unknown" |
||||
|
|
||||
|
# Categories of required reason APIs |
||||
|
readonly API_CATEGORIES=( |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp" |
||||
|
"NSPrivacyAccessedAPICategorySystemBootTime" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace" |
||||
|
"NSPrivacyAccessedAPICategoryActiveKeyboards" |
||||
|
"NSPrivacyAccessedAPICategoryUserDefaults" |
||||
|
) |
||||
|
|
||||
|
# Symbol of the required reason APIs and their categories |
||||
|
# |
||||
|
# See also: |
||||
|
# * https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api |
||||
|
# * https://github.com/Wooder/ios_17_required_reason_api_scanner/blob/main/required_reason_api_binary_scanner.sh |
||||
|
readonly API_SYMBOLS=( |
||||
|
# NSPrivacyAccessedAPICategoryFileTimestamp |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlist" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistbulk" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fgetattrlist" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}stat" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstat" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstatat" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}lstat" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistat" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileCreationDate" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileModificationDate" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLContentModificationDateKey" |
||||
|
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLCreationDateKey" |
||||
|
# NSPrivacyAccessedAPICategorySystemBootTime |
||||
|
"NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}systemUptime" |
||||
|
"NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}mach_absolute_time" |
||||
|
# NSPrivacyAccessedAPICategoryDiskSpace |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statfs" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statvfs" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatfs" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatvfs" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemFreeSize" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemSize" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityKey" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForImportantUsageKey" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForOpportunisticUsageKey" |
||||
|
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeTotalCapacityKey" |
||||
|
# NSPrivacyAccessedAPICategoryActiveKeyboards |
||||
|
"NSPrivacyAccessedAPICategoryActiveKeyboards${DELIMITER}activeInputModes" |
||||
|
# NSPrivacyAccessedAPICategoryUserDefaults |
||||
|
"NSPrivacyAccessedAPICategoryUserDefaults${DELIMITER}NSUserDefaults" |
||||
|
) |
@ -0,0 +1,125 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2025, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Prevent duplicate loading |
||||
|
if [ -n "$UTILS_SH_LOADED" ]; then |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
readonly UTILS_SH_LOADED=1 |
||||
|
|
||||
|
# Absolute path of the script and the tool's root directory |
||||
|
script_path="$(realpath "${BASH_SOURCE[0]}")" |
||||
|
tool_root_path="$(dirname "$(dirname "$script_path")")" |
||||
|
|
||||
|
# Load common constants |
||||
|
source "$tool_root_path/Common/constants.sh" |
||||
|
|
||||
|
# Print the elements of an array along with their indices |
||||
|
function print_array() { |
||||
|
local -a array=("$@") |
||||
|
|
||||
|
for ((i=0; i<${#array[@]}; i++)); do |
||||
|
echo "[$i] $(decode_path "${array[i]}")" |
||||
|
done |
||||
|
} |
||||
|
|
||||
|
# Split a string into substrings using a specified delimiter |
||||
|
function split_string_by_delimiter() { |
||||
|
local string="$1" |
||||
|
local -a substrings=() |
||||
|
|
||||
|
IFS="$DELIMITER" read -ra substrings <<< "$string" |
||||
|
|
||||
|
echo "${substrings[@]}" |
||||
|
} |
||||
|
|
||||
|
# Encode a path string by replacing space with an escape character |
||||
|
function encode_path() { |
||||
|
echo "$1" | sed "s/ /$SPACE_ESCAPE/g" |
||||
|
} |
||||
|
|
||||
|
# Decode a path string by replacing encoded character with space |
||||
|
function decode_path() { |
||||
|
echo "$1" | sed "s/$SPACE_ESCAPE/ /g" |
||||
|
} |
||||
|
|
||||
|
# Get the dependency name by removing common suffixes |
||||
|
function get_dependency_name() { |
||||
|
local path="$1" |
||||
|
|
||||
|
local dir_name="$(basename "$path")" |
||||
|
# Remove `.app`, `.framework`, and `.xcframework` suffixes |
||||
|
local dep_name="${dir_name%.*}" |
||||
|
|
||||
|
echo "$dep_name" |
||||
|
} |
||||
|
|
||||
|
# Get the executable name from the specified `Info.plist` file |
||||
|
function get_plist_executable() { |
||||
|
local plist_file="$1" |
||||
|
|
||||
|
if [ ! -f "$plist_file" ]; then |
||||
|
echo "" |
||||
|
else |
||||
|
/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist_file" 2>/dev/null || echo "" |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# Get the version from the specified `Info.plist` file |
||||
|
function get_plist_version() { |
||||
|
local plist_file="$1" |
||||
|
|
||||
|
if [ ! -f "$plist_file" ]; then |
||||
|
echo "$UNKNOWN_VERSION" |
||||
|
else |
||||
|
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$plist_file" 2>/dev/null || echo "$UNKNOWN_VERSION" |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# Get the path of the specified framework version |
||||
|
function get_framework_path() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
|
||||
|
if [ -z "$version_path" ]; then |
||||
|
echo "$path" |
||||
|
else |
||||
|
echo "$path/$version_path" |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# Search for privacy manifest files in the specified directory |
||||
|
function search_privacy_manifest_files() { |
||||
|
local path="$1" |
||||
|
local -a privacy_manifest_files=() |
||||
|
|
||||
|
# Create a temporary file to store search results |
||||
|
local temp_file="$(mktemp)" |
||||
|
|
||||
|
# Ensure the temporary file is deleted on script exit |
||||
|
trap "rm -f $temp_file" EXIT |
||||
|
|
||||
|
# Find privacy manifest files within the specified directory and store the results in the temporary file |
||||
|
find "$path" -type f -name "$PRIVACY_MANIFEST_FILE_NAME" -print0 2>/dev/null > "$temp_file" |
||||
|
|
||||
|
while IFS= read -r -d '' file; do |
||||
|
privacy_manifest_files+=($(encode_path "$file")) |
||||
|
done < "$temp_file" |
||||
|
|
||||
|
echo "${privacy_manifest_files[@]}" |
||||
|
} |
||||
|
|
||||
|
# Get the privacy manifest file with the shortest path |
||||
|
function get_privacy_manifest_file() { |
||||
|
local privacy_manifest_file="$(printf "%s\n" "$@" | awk '{print length, $0}' | sort -n | head -n1 | cut -d ' ' -f2-)" |
||||
|
|
||||
|
echo "$(decode_path "$privacy_manifest_file")" |
||||
|
} |
@ -0,0 +1,80 @@ |
|||||
|
# Copyright (c) 2024, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
require 'xcodeproj' |
||||
|
|
||||
|
RUN_SCRIPT_PHASE_NAME = 'Fix Privacy Manifest' |
||||
|
|
||||
|
if ARGV.length < 2 |
||||
|
puts "Usage: ruby xcode_install_helper.rb <project_path> <script_content> [install_builds_only (true|false)]" |
||||
|
exit 1 |
||||
|
end |
||||
|
|
||||
|
project_path = ARGV[0] |
||||
|
run_script_content = ARGV[1] |
||||
|
install_builds_only = ARGV[2] == 'true' |
||||
|
|
||||
|
# Find the first .xcodeproj file in the project directory |
||||
|
xcodeproj_path = Dir.glob(File.join(project_path, "*.xcodeproj")).first |
||||
|
|
||||
|
# Validate the .xcodeproj file existence |
||||
|
unless xcodeproj_path |
||||
|
puts "Error: No .xcodeproj file found in the specified directory." |
||||
|
exit 1 |
||||
|
end |
||||
|
|
||||
|
# Open the Xcode project file |
||||
|
begin |
||||
|
project = Xcodeproj::Project.open(xcodeproj_path) |
||||
|
rescue StandardError => e |
||||
|
puts "Error: Unable to open the project file - #{e.message}" |
||||
|
exit 1 |
||||
|
end |
||||
|
|
||||
|
# Process all targets in the project |
||||
|
project.targets.each do |target| |
||||
|
# Skip PBXAggregateTarget |
||||
|
if target.is_a?(Xcodeproj::Project::Object::PBXAggregateTarget) |
||||
|
puts "Skipping aggregate target: #{target.name}." |
||||
|
next |
||||
|
end |
||||
|
|
||||
|
# Check if the target is a native application target |
||||
|
if target.product_type == 'com.apple.product-type.application' |
||||
|
puts "Processing target: #{target.name}..." |
||||
|
|
||||
|
# Check for an existing Run Script phase with the specified name |
||||
|
existing_phase = target.shell_script_build_phases.find { |phase| phase.name == RUN_SCRIPT_PHASE_NAME } |
||||
|
|
||||
|
# Remove the existing Run Script phase if found |
||||
|
if existing_phase |
||||
|
puts " - Removing existing Run Script." |
||||
|
target.build_phases.delete(existing_phase) |
||||
|
end |
||||
|
|
||||
|
# Add the new Run Script phase at the end |
||||
|
puts " - Adding new Run Script." |
||||
|
new_phase = target.new_shell_script_build_phase(RUN_SCRIPT_PHASE_NAME) |
||||
|
new_phase.shell_script = run_script_content |
||||
|
# Disable showing environment variables in the build log |
||||
|
new_phase.show_env_vars_in_log = '0' |
||||
|
# Run only for deployment post-processing if install_builds_only is true |
||||
|
new_phase.run_only_for_deployment_postprocessing = install_builds_only ? '1' : '0' |
||||
|
# Disable dependency analysis to force the script to run on every build, unless restricted to deployment builds by post-processing setting |
||||
|
new_phase.always_out_of_date = '1' |
||||
|
else |
||||
|
puts "Skipping non-application target: #{target.name}." |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Save the project file |
||||
|
begin |
||||
|
project.save |
||||
|
puts "Successfully added the Run Script phase: '#{RUN_SCRIPT_PHASE_NAME}'." |
||||
|
rescue StandardError => e |
||||
|
puts "Error: Unable to save the project file - #{e.message}" |
||||
|
exit 1 |
||||
|
end |
@ -0,0 +1,63 @@ |
|||||
|
# Copyright (c) 2024, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
require 'xcodeproj' |
||||
|
|
||||
|
RUN_SCRIPT_PHASE_NAME = 'Fix Privacy Manifest' |
||||
|
|
||||
|
if ARGV.length < 1 |
||||
|
puts "Usage: ruby xcode_uninstall_helper.rb <project_path>" |
||||
|
exit 1 |
||||
|
end |
||||
|
|
||||
|
project_path = ARGV[0] |
||||
|
|
||||
|
# Find the first .xcodeproj file in the project directory |
||||
|
xcodeproj_path = Dir.glob(File.join(project_path, "*.xcodeproj")).first |
||||
|
|
||||
|
# Validate the .xcodeproj file existence |
||||
|
unless xcodeproj_path |
||||
|
puts "Error: No .xcodeproj file found in the specified directory." |
||||
|
exit 1 |
||||
|
end |
||||
|
|
||||
|
# Open the Xcode project file |
||||
|
begin |
||||
|
project = Xcodeproj::Project.open(xcodeproj_path) |
||||
|
rescue StandardError => e |
||||
|
puts "Error: Unable to open the project file - #{e.message}" |
||||
|
exit 1 |
||||
|
end |
||||
|
|
||||
|
# Process all targets in the project |
||||
|
project.targets.each do |target| |
||||
|
# Check if the target is an application target |
||||
|
if target.product_type == 'com.apple.product-type.application' |
||||
|
puts "Processing target: #{target.name}..." |
||||
|
|
||||
|
# Check for an existing Run Script phase with the specified name |
||||
|
existing_phase = target.shell_script_build_phases.find { |phase| phase.name == RUN_SCRIPT_PHASE_NAME } |
||||
|
|
||||
|
# Remove the existing Run Script phase if found |
||||
|
if existing_phase |
||||
|
puts " - Removing existing Run Script." |
||||
|
target.build_phases.delete(existing_phase) |
||||
|
else |
||||
|
puts " - No existing Run Script found." |
||||
|
end |
||||
|
else |
||||
|
puts "Skipping non-application target: #{target.name}." |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# Save the project file |
||||
|
begin |
||||
|
project.save |
||||
|
puts "Successfully removed the Run Script phase: '#{RUN_SCRIPT_PHASE_NAME}'." |
||||
|
rescue StandardError => e |
||||
|
puts "Error: Unable to save the project file - #{e.message}" |
||||
|
exit 1 |
||||
|
end |
@ -0,0 +1,21 @@ |
|||||
|
MIT License |
||||
|
|
||||
|
Copyright (c) 2024 crasowas |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
@ -0,0 +1,240 @@ |
|||||
|
# App Privacy Manifest Fixer |
||||
|
|
||||
|
[](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest) |
||||
|
 |
||||
|
[](https://opensource.org/licenses/MIT) |
||||
|
|
||||
|
**English | [简体中文](./README.zh-CN.md)** |
||||
|
|
||||
|
This tool is an automation solution based on Shell scripts, designed to analyze and fix the privacy manifest of iOS/macOS apps to ensure compliance with App Store requirements. It leverages the [App Store Privacy Manifest Analyzer](https://github.com/crasowas/app_store_required_privacy_manifest_analyser) to analyze API usage within the app and its dependencies, and generate or fix the `PrivacyInfo.xcprivacy` file. |
||||
|
|
||||
|
## ✨ Features |
||||
|
|
||||
|
- **Non-Intrusive Integration**: No need to modify the source code or adjust the project structure. |
||||
|
- **Fast Installation and Uninstallation**: Quickly install or uninstall the tool with a single command. |
||||
|
- **Automatic Analysis and Fixes**: Automatically analyzes API usage and fixes privacy manifest issues during the project build. |
||||
|
- **Flexible Template Customization**: Supports custom privacy manifest templates for apps and frameworks, accommodating various usage scenarios. |
||||
|
- **Privacy Access Report**: Automatically generates a report displaying the `NSPrivacyAccessedAPITypes` declarations for the app and SDKs. |
||||
|
- **Effortless Version Upgrades**: Provides an upgrade script for quick updates to the latest version. |
||||
|
|
||||
|
## 📥 Installation |
||||
|
|
||||
|
### Download the Tool |
||||
|
|
||||
|
1. Download the [latest release](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest). |
||||
|
2. Extract the downloaded file: |
||||
|
- The extracted directory is usually named `app_privacy_manifest_fixer-xxx` (where `xxx` is the version number). |
||||
|
- It is recommended to rename it to `app_privacy_manifest_fixer` or use the full directory name in subsequent paths. |
||||
|
- **It is advised to move the directory to your iOS/macOS project to avoid path-related issues on different devices, and to easily customize the privacy manifest template for each project**. |
||||
|
|
||||
|
### ⚡ Automatic Installation (Recommended) |
||||
|
|
||||
|
1. **Navigate to the tool's directory**: |
||||
|
|
||||
|
```shell |
||||
|
cd /path/to/app_privacy_manifest_fixer |
||||
|
``` |
||||
|
|
||||
|
2. **Run the installation script**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> |
||||
|
``` |
||||
|
|
||||
|
- For Flutter projects, `project_path` should be the path to the `ios/macos` directory within the Flutter project. |
||||
|
- If the installation command is run again, the tool will first remove any existing installation (if present). To modify command-line options, simply rerun the installation command without the need to uninstall first. |
||||
|
|
||||
|
### Manual Installation |
||||
|
|
||||
|
If you prefer not to use the installation script, you can manually add the `Fix Privacy Manifest` task to the Xcode **Build Phases**. Follow these steps: |
||||
|
|
||||
|
#### 1. Add the Script in Xcode |
||||
|
|
||||
|
- Open your iOS/macOS project in Xcode, go to the **TARGETS** tab, and select your app target. |
||||
|
- Navigate to **Build Phases**, click the **+** button, and select **New Run Script Phase**. |
||||
|
- Rename the newly created **Run Script** to `Fix Privacy Manifest`. |
||||
|
- In the Shell script box, add the following code: |
||||
|
|
||||
|
```shell |
||||
|
# Use relative path (recommended): if `app_privacy_manifest_fixer` is within the project directory |
||||
|
"$PROJECT_DIR/path/to/app_privacy_manifest_fixer/fixer.sh" |
||||
|
|
||||
|
# Use absolute path: if `app_privacy_manifest_fixer` is outside the project directory |
||||
|
# "/absolute/path/to/app_privacy_manifest_fixer/fixer.sh" |
||||
|
``` |
||||
|
|
||||
|
**Modify `path/to` or `absolute/path/to` as needed, and ensure the paths are correct. Remove or comment out the unused lines accordingly.** |
||||
|
|
||||
|
#### 2. Adjust the Script Execution Order |
||||
|
|
||||
|
**Move this script after all other Build Phases to ensure the privacy manifest is fixed after all resource copying and build tasks are completed**. |
||||
|
|
||||
|
### Build Phases Screenshot |
||||
|
|
||||
|
Below is a screenshot of the Xcode Build Phases configuration after successful automatic/manual installation (with no command-line options enabled): |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 🚀 Getting Started |
||||
|
|
||||
|
After installation, the tool will automatically run with each project build, and the resulting application bundle will include the fixes. |
||||
|
|
||||
|
If the `--install-builds-only` command-line option is enabled during installation, the tool will only run during the installation of the build. |
||||
|
|
||||
|
### Xcode Build Log Screenshot |
||||
|
|
||||
|
Below is a screenshot of the log output from the tool during the project build (by default, it will be saved to the `app_privacy_manifest_fixer/Build` directory, unless the `-s` command-line option is enabled): |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 📖 Usage |
||||
|
|
||||
|
### Command Line Options |
||||
|
|
||||
|
- **Force overwrite existing privacy manifest (Not recommended)**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> -f |
||||
|
``` |
||||
|
|
||||
|
Enabling the `-f` option will force the tool to generate a new privacy manifest based on the API usage analysis and privacy manifest template, overwriting the existing privacy manifest. By default (without `-f`), the tool only fixes missing privacy manifests. |
||||
|
|
||||
|
- **Silent mode**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> -s |
||||
|
``` |
||||
|
|
||||
|
Enabling the `-s` option disables output during the fix process. The tool will no longer copy the generated `*.app`, automatically generate the privacy access report, or output the fix logs. By default (without `-s`), these outputs are stored in the `app_privacy_manifest_fixer/Build` directory. |
||||
|
|
||||
|
- **Run only during installation builds (Recommended)**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> --install-builds-only |
||||
|
``` |
||||
|
|
||||
|
Enabling the `--install-builds-only` option makes the tool run only during installation builds (such as the **Archive** operation), optimizing build performance for daily development. If you manually installed, this option is ineffective, and you need to manually check the **"For install builds only"** option. |
||||
|
|
||||
|
**Note**: If the iOS/macOS project is built in a development environment (where the generated app contains `*.debug.dylib` files), the tool's API usage analysis results may be inaccurate. |
||||
|
|
||||
|
### Upgrade the Tool |
||||
|
|
||||
|
To update to the latest version, run the following command: |
||||
|
|
||||
|
```shell |
||||
|
sh upgrade.sh |
||||
|
``` |
||||
|
|
||||
|
### Uninstall the Tool |
||||
|
|
||||
|
To quickly uninstall the tool, run the following command: |
||||
|
|
||||
|
```shell |
||||
|
sh uninstall.sh <project_path> |
||||
|
``` |
||||
|
|
||||
|
### Clean the Tool-Generated Files |
||||
|
|
||||
|
To remove files generated by the tool, run the following command: |
||||
|
|
||||
|
```shell |
||||
|
sh clean.sh |
||||
|
``` |
||||
|
|
||||
|
## 🔥 Privacy Manifest Templates |
||||
|
|
||||
|
The privacy manifest templates are stored in the [`Templates`](https://github.com/crasowas/app_privacy_manifest_fixer/tree/main/Templates) directory, with the default templates already included in the root directory. |
||||
|
|
||||
|
**How can you customize the privacy manifests for apps or SDKs? Simply use [custom templates](#custom-templates)!** |
||||
|
|
||||
|
### Template Types |
||||
|
|
||||
|
The templates are categorized as follows: |
||||
|
- **AppTemplate.xcprivacy**: A privacy manifest template for the app. |
||||
|
- **FrameworkTemplate.xcprivacy**: A generic privacy manifest template for frameworks. |
||||
|
- **FrameworkName.xcprivacy**: A privacy manifest template for a specific framework, available only in the `Templates/UserTemplates` directory. |
||||
|
|
||||
|
### Template Priority |
||||
|
|
||||
|
For an app, the priority of privacy manifest templates is as follows: |
||||
|
- `Templates/UserTemplates/AppTemplate.xcprivacy` > `Templates/AppTemplate.xcprivacy` |
||||
|
|
||||
|
For a specific framework, the priority of privacy manifest templates is as follows: |
||||
|
- `Templates/UserTemplates/FrameworkName.xcprivacy` > `Templates/UserTemplates/FrameworkTemplate.xcprivacy` > `Templates/FrameworkTemplate.xcprivacy` |
||||
|
|
||||
|
### Default Templates |
||||
|
|
||||
|
The default templates are located in the `Templates` root directory and currently include the following templates: |
||||
|
- `Templates/AppTemplate.xcprivacy` |
||||
|
- `Templates/FrameworkTemplate.xcprivacy` |
||||
|
|
||||
|
These templates will be modified based on the API usage analysis results, especially the `NSPrivacyAccessedAPIType` entries, to generate new privacy manifests for fixes, ensuring compliance with App Store requirements. |
||||
|
|
||||
|
**If adjustments to the privacy manifest template are needed, such as in the following scenarios, avoid directly modifying the default templates. Instead, use a custom template. If a custom template with the same name exists, it will take precedence over the default template for fixes.** |
||||
|
- Generating a non-compliant privacy manifest due to inaccurate API usage analysis. |
||||
|
- Modifying the reason declared in the template. |
||||
|
- Adding declarations for collected data. |
||||
|
|
||||
|
The privacy access API categories and their associated declared reasons in `AppTemplate.xcprivacy` are listed below: |
||||
|
|
||||
|
| [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | |
||||
|
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
||||
|
| NSPrivacyAccessedAPICategoryFileTimestamp | C617.1: Inside app or group container | |
||||
|
| NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | |
||||
|
| NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | |
||||
|
| NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | |
||||
|
| NSPrivacyAccessedAPICategoryUserDefaults | CA92.1: Access info from same app | |
||||
|
|
||||
|
The privacy access API categories and their associated declared reasons in `FrameworkTemplate.xcprivacy` are listed below: |
||||
|
|
||||
|
| [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | |
||||
|
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
||||
|
| NSPrivacyAccessedAPICategoryFileTimestamp | 0A2A.1: 3rd-party SDK wrapper on-device | |
||||
|
| NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | |
||||
|
| NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | |
||||
|
| NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | |
||||
|
| NSPrivacyAccessedAPICategoryUserDefaults | C56D.1: 3rd-party SDK wrapper on-device | |
||||
|
|
||||
|
### Custom Templates |
||||
|
|
||||
|
To create custom templates, place them in the `Templates/UserTemplates` directory with the following structure: |
||||
|
- `Templates/UserTemplates/AppTemplate.xcprivacy` |
||||
|
- `Templates/UserTemplates/FrameworkTemplate.xcprivacy` |
||||
|
- `Templates/UserTemplates/FrameworkName.xcprivacy` |
||||
|
|
||||
|
Among these templates, only `FrameworkTemplate.xcprivacy` will be modified based on the API usage analysis results to adjust the `NSPrivacyAccessedAPIType` entries, thereby generating a new privacy manifest for framework fixes. The other templates will remain unchanged and will be directly used for fixes. |
||||
|
|
||||
|
**Important Notes:** |
||||
|
- The template for a specific framework must follow the naming convention `FrameworkName.xcprivacy`, where `FrameworkName` should match the name of the framework. For example, the template for `Flutter.framework` should be named `Flutter.xcprivacy`. |
||||
|
- For macOS frameworks, the naming convention should be `FrameworkName.Version.xcprivacy`, where the version name is added to distinguish different versions. For a single version macOS framework, the `Version` is typically `A`. |
||||
|
- The name of an SDK may not exactly match the name of the framework. To determine the correct framework name, check the `Frameworks` directory in the application bundle after building the project. |
||||
|
|
||||
|
## 📑 Privacy Access Report |
||||
|
|
||||
|
By default, the tool automatically generates privacy access reports for both the original and fixed versions of the app during each project build, and stores the reports in the `app_privacy_manifest_fixer/Build` directory. |
||||
|
|
||||
|
If you need to manually generate a privacy access report for a specific app, run the following command: |
||||
|
|
||||
|
```shell |
||||
|
sh Report/report.sh <app_path> <report_output_path> |
||||
|
# <app_path>: Path to the app (e.g., /path/to/App.app) |
||||
|
# <report_output_path>: Path to save the report file (e.g., /path/to/report.html) |
||||
|
``` |
||||
|
|
||||
|
**Note**: The report generated by the tool currently only includes the privacy access section (`NSPrivacyAccessedAPITypes`). To view the data collection section (`NSPrivacyCollectedDataTypes`), please use Xcode to generate the `PrivacyReport`. |
||||
|
|
||||
|
### Sample Report Screenshots |
||||
|
|
||||
|
| Original App Report (report-original.html) | Fixed App Report (report.html) | |
||||
|
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| |
||||
|
|  |  | |
||||
|
|
||||
|
## 💡 Important Considerations |
||||
|
|
||||
|
- If the latest version of the SDK supports a privacy manifest, please upgrade as soon as possible to avoid unnecessary risks. |
||||
|
- This tool is a temporary solution and should not replace proper SDK management practices. |
||||
|
- Before submitting your app for review, ensure that the privacy manifest fix complies with the latest App Store requirements. |
||||
|
|
||||
|
## 🙌 Contributing |
||||
|
|
||||
|
Contributions in any form are welcome, including code optimizations, bug fixes, documentation improvements, and more. Please follow the project's guidelines and maintain a consistent coding style. Thank you for your support! |
@ -0,0 +1,240 @@ |
|||||
|
# App Privacy Manifest Fixer |
||||
|
|
||||
|
[](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest) |
||||
|
 |
||||
|
[](https://opensource.org/licenses/MIT) |
||||
|
|
||||
|
**[English](./README.md) | 简体中文** |
||||
|
|
||||
|
本工具是一个基于 Shell 脚本的自动化解决方案,旨在分析和修复 iOS/macOS App 的隐私清单,确保 App 符合 App Store 的要求。它利用 [App Store Privacy Manifest Analyzer](https://github.com/crasowas/app_store_required_privacy_manifest_analyser) 对 App 及其依赖项进行 API 使用分析,并生成或修复`PrivacyInfo.xcprivacy`文件。 |
||||
|
|
||||
|
## ✨ 功能特点 |
||||
|
|
||||
|
- **非侵入式集成**:无需修改源码或调整项目结构。 |
||||
|
- **极速安装与卸载**:一行命令即可快速完成工具的安装或卸载。 |
||||
|
- **自动分析与修复**:项目构建时自动分析 API 使用情况并修复隐私清单问题。 |
||||
|
- **灵活定制模板**:支持自定义 App 和 Framework 的隐私清单模板,满足多种使用场景。 |
||||
|
- **隐私访问报告**:自动生成报告用于查看 App 和 SDK 的`NSPrivacyAccessedAPITypes`声明情况。 |
||||
|
- **版本轻松升级**:提供升级脚本快速更新至最新版本。 |
||||
|
|
||||
|
## 📥 安装 |
||||
|
|
||||
|
### 下载工具 |
||||
|
|
||||
|
1. 下载[最新发布版本](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest)。 |
||||
|
2. 解压下载的文件: |
||||
|
- 解压后的目录通常为`app_privacy_manifest_fixer-xxx`(其中`xxx`是版本号)。 |
||||
|
- 建议重命名为`app_privacy_manifest_fixer`,或在后续路径中使用完整目录名。 |
||||
|
- **建议将该目录移动至 iOS/macOS 项目中,以避免因路径问题在不同设备上运行时出现错误,同时便于为每个项目单独自定义隐私清单模板**。 |
||||
|
|
||||
|
### ⚡ 自动安装(推荐) |
||||
|
|
||||
|
1. **切换到工具所在目录**: |
||||
|
|
||||
|
```shell |
||||
|
cd /path/to/app_privacy_manifest_fixer |
||||
|
``` |
||||
|
|
||||
|
2. **运行以下安装脚本**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> |
||||
|
``` |
||||
|
|
||||
|
- 如果是 Flutter 项目,`project_path`应为 Flutter 项目中的`ios/macos`目录路径。 |
||||
|
- 重复运行安装命令时,工具会先移除现有安装(如果有)。若需修改命令行选项,只需重新运行安装命令,无需先卸载。 |
||||
|
|
||||
|
### 手动安装 |
||||
|
|
||||
|
如果不使用安装脚本,可以手动添加`Fix Privacy Manifest`任务到 Xcode 的 **Build Phases** 完成安装。安装步骤如下: |
||||
|
|
||||
|
#### 1. 在 Xcode 中添加脚本 |
||||
|
|
||||
|
- 用 Xcode 打开你的 iOS/macOS 项目,进入 **TARGETS** 选项卡,选择你的 App 目标。 |
||||
|
- 进入 **Build Phases**,点击 **+** 按钮,选择 **New Run Script Phase**。 |
||||
|
- 将新建的 **Run Script** 重命名为`Fix Privacy Manifest`。 |
||||
|
- 在 Shell 脚本框中添加以下代码: |
||||
|
|
||||
|
```shell |
||||
|
# 使用相对路径(推荐):如果`app_privacy_manifest_fixer`在项目目录内 |
||||
|
"$PROJECT_DIR/path/to/app_privacy_manifest_fixer/fixer.sh" |
||||
|
|
||||
|
# 使用绝对路径:如果`app_privacy_manifest_fixer`不在项目目录内 |
||||
|
# "/absolute/path/to/app_privacy_manifest_fixer/fixer.sh" |
||||
|
``` |
||||
|
|
||||
|
**请根据实际情况修改`path/to`或`absolute/path/to`,并确保路径正确。同时,删除或注释掉不适用的行**。 |
||||
|
|
||||
|
#### 2. 调整脚本执行顺序 |
||||
|
|
||||
|
**将该脚本移动到所有其他 Build Phases 之后,确保隐私清单在所有资源拷贝和编译任务完成后再进行修复**。 |
||||
|
|
||||
|
### Build Phases 截图 |
||||
|
|
||||
|
下面是自动/手动安装成功后的 Xcode Build Phases 配置截图(未启用任何命令行选项): |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 🚀 快速开始 |
||||
|
|
||||
|
安装后,工具将在每次构建项目时自动运行,构建完成后得到的 App 包已经是修复后的结果。 |
||||
|
|
||||
|
如果启用`--install-builds-only`命令行选项安装,工具将仅在安装构建时运行。 |
||||
|
|
||||
|
### Xcode Build Log 截图 |
||||
|
|
||||
|
下面是项目构建时工具输出的日志截图(默认会存储到`app_privacy_manifest_fixer/Build`目录,除非启用`-s`命令行选项): |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 📖 使用方法 |
||||
|
|
||||
|
### 命令行选项 |
||||
|
|
||||
|
- **强制覆盖现有隐私清单(不推荐)**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> -f |
||||
|
``` |
||||
|
|
||||
|
启用`-f`选项后,工具会根据 API 使用分析结果和隐私清单模板生成新的隐私清单,并强制覆盖现有隐私清单。默认情况下(未启用`-f`),工具仅修复缺失的隐私清单。 |
||||
|
|
||||
|
- **静默模式**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> -s |
||||
|
``` |
||||
|
|
||||
|
启用`-s`选项后,工具将禁用修复时的输出,不再复制构建生成的`*.app`、自动生成隐私访问报告或输出修复日志。默认情况下(未启用`-s`),这些输出存储在`app_privacy_manifest_fixer/Build`目录。 |
||||
|
|
||||
|
- **仅在安装构建时运行(推荐)**: |
||||
|
|
||||
|
```shell |
||||
|
sh install.sh <project_path> --install-builds-only |
||||
|
``` |
||||
|
|
||||
|
启用`--install-builds-only`选项后,工具仅在执行安装构建(如 **Archive** 操作)时运行,以优化日常开发时的构建性能。如果你是手动安装的,该命令行选项无效,需要手动勾选 **"For install builds only"** 选项。 |
||||
|
|
||||
|
**注意**:如果 iOS/macOS 项目在开发环境构建(生成的 App 包含`*.debug.dylib`文件),工具的 API 使用分析结果可能不准确。 |
||||
|
|
||||
|
### 升级工具 |
||||
|
|
||||
|
要更新至最新版本,请运行以下命令: |
||||
|
|
||||
|
```shell |
||||
|
sh upgrade.sh |
||||
|
``` |
||||
|
|
||||
|
### 卸载工具 |
||||
|
|
||||
|
要快速卸载本工具,请运行以下命令: |
||||
|
|
||||
|
```shell |
||||
|
sh uninstall.sh <project_path> |
||||
|
``` |
||||
|
|
||||
|
### 清理工具生成的文件 |
||||
|
|
||||
|
要删除工具生成的文件,请运行以下命令: |
||||
|
|
||||
|
```shell |
||||
|
sh clean.sh |
||||
|
``` |
||||
|
|
||||
|
## 🔥 隐私清单模板 |
||||
|
|
||||
|
隐私清单模板存储在[`Templates`](https://github.com/crasowas/app_privacy_manifest_fixer/tree/main/Templates)目录,其中根目录已经包含默认模板。 |
||||
|
|
||||
|
**如何为 App 或 SDK 自定义隐私清单?只需使用[自定义模板](#自定义模板)!** |
||||
|
|
||||
|
### 模板类型 |
||||
|
|
||||
|
模板分为以下几类: |
||||
|
- **AppTemplate.xcprivacy**:App 的隐私清单模板。 |
||||
|
- **FrameworkTemplate.xcprivacy**:通用的 Framework 隐私清单模板。 |
||||
|
- **FrameworkName.xcprivacy**:特定的 Framework 隐私清单模板,仅在`Templates/UserTemplates`目录有效。 |
||||
|
|
||||
|
### 模板优先级 |
||||
|
|
||||
|
对于 App,隐私清单模板的优先级如下: |
||||
|
- `Templates/UserTemplates/AppTemplate.xcprivacy` > `Templates/AppTemplate.xcprivacy` |
||||
|
|
||||
|
对于特定的 Framework,隐私清单模板的优先级如下: |
||||
|
- `Templates/UserTemplates/FrameworkName.xcprivacy` > `Templates/UserTemplates/FrameworkTemplate.xcprivacy` > `Templates/FrameworkTemplate.xcprivacy` |
||||
|
|
||||
|
### 默认模板 |
||||
|
|
||||
|
默认模板位于`Templates`根目录,目前包括以下模板: |
||||
|
- `Templates/AppTemplate.xcprivacy` |
||||
|
- `Templates/FrameworkTemplate.xcprivacy` |
||||
|
|
||||
|
这些模板将根据 API 使用分析结果进行修改,特别是`NSPrivacyAccessedAPIType`条目将被调整,以生成新的隐私清单用于修复,确保符合 App Store 要求。 |
||||
|
|
||||
|
**如果需要调整隐私清单模板,例如以下场景,请避免直接修改默认模板,而是使用自定义模板。如果存在相同名称的自定义模板,它将优先于默认模板用于修复。** |
||||
|
- 由于 API 使用分析结果不准确,生成了不合规的隐私清单。 |
||||
|
- 需要修改模板中声明的理由。 |
||||
|
- 需要声明收集的数据。 |
||||
|
|
||||
|
`AppTemplate.xcprivacy`中隐私访问 API 类别及其对应声明的理由如下: |
||||
|
|
||||
|
| [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | |
||||
|
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
||||
|
| NSPrivacyAccessedAPICategoryFileTimestamp | C617.1: Inside app or group container | |
||||
|
| NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | |
||||
|
| NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | |
||||
|
| NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | |
||||
|
| NSPrivacyAccessedAPICategoryUserDefaults | CA92.1: Access info from same app | |
||||
|
|
||||
|
`FrameworkTemplate.xcprivacy`中隐私访问 API 类别及其对应声明的理由如下: |
||||
|
|
||||
|
| [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | |
||||
|
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
||||
|
| NSPrivacyAccessedAPICategoryFileTimestamp | 0A2A.1: 3rd-party SDK wrapper on-device | |
||||
|
| NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | |
||||
|
| NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | |
||||
|
| NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | |
||||
|
| NSPrivacyAccessedAPICategoryUserDefaults | C56D.1: 3rd-party SDK wrapper on-device | |
||||
|
|
||||
|
### 自定义模板 |
||||
|
|
||||
|
要创建自定义模板,请将其放在`Templates/UserTemplates`目录,结构如下: |
||||
|
- `Templates/UserTemplates/AppTemplate.xcprivacy` |
||||
|
- `Templates/UserTemplates/FrameworkTemplate.xcprivacy` |
||||
|
- `Templates/UserTemplates/FrameworkName.xcprivacy` |
||||
|
|
||||
|
在这些模板中,只有`FrameworkTemplate.xcprivacy`会根据 API 使用分析结果对`NSPrivacyAccessedAPIType`条目进行调整,以生成新的隐私清单用于 Framework 修复。其他模板保持不变,将直接用于修复。 |
||||
|
|
||||
|
**重要说明:** |
||||
|
- 特定的 Framework 模板必须遵循命名规范`FrameworkName.xcprivacy`,其中`FrameworkName`需与 Framework 的名称匹配。例如`Flutter.framework`的模板应命名为`Flutter.xcprivacy`。 |
||||
|
- 对于 macOS Framework,应遵循命名规范`FrameworkName.Version.xcprivacy`,额外增加版本名称用于区分不同的版本。对于单一版本的 macOS Framework,`Version`通常为`A`。 |
||||
|
- SDK 的名称可能与 Framework 的名称不完全一致。要确定正确的 Framework 名称,请在构建项目后检查 App 包中的`Frameworks`目录。 |
||||
|
|
||||
|
## 📑 隐私访问报告 |
||||
|
|
||||
|
默认情况下,工具会自动在每次构建时为原始 App 和修复后的 App 生成隐私访问报告,并存储到`app_privacy_manifest_fixer/Build`目录。 |
||||
|
|
||||
|
如果需要手动为特定 App 生成隐私访问报告,请运行以下命令: |
||||
|
|
||||
|
```shell |
||||
|
sh Report/report.sh <app_path> <report_output_path> |
||||
|
# <app_path>: App路径(例如:/path/to/App.app) |
||||
|
# <report_output_path>: 报告文件保存路径(例如:/path/to/report.html) |
||||
|
``` |
||||
|
|
||||
|
**注意**:工具生成的报告目前仅包含隐私访问部分(`NSPrivacyAccessedAPITypes`),如果想看数据收集部分(`NSPrivacyCollectedDataTypes`)请使用 Xcode 生成`PrivacyReport`。 |
||||
|
|
||||
|
### 报告示例截图 |
||||
|
|
||||
|
| 原始 App 报告(report-original.html) | 修复后 App 报告(report.html) | |
||||
|
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| |
||||
|
|  |  | |
||||
|
|
||||
|
## 💡 重要考量 |
||||
|
|
||||
|
- 如果最新版本的 SDK 支持隐私清单,请尽可能升级,以避免不必要的风险。 |
||||
|
- 此工具仅为临时解决方案,不应替代正确的 SDK 管理实践。 |
||||
|
- 在提交 App 审核之前,请检查隐私清单修复后是否符合最新的 App Store 要求。 |
||||
|
|
||||
|
## 🙌 贡献 |
||||
|
|
||||
|
欢迎任何形式的贡献,包括代码优化、Bug 修复、文档改进等。请确保遵循项目规范,并保持代码风格一致。感谢你的支持! |
@ -0,0 +1,124 @@ |
|||||
|
<!-- |
||||
|
Copyright (c) 2024, crasowas. |
||||
|
|
||||
|
Use of this source code is governed by a MIT-style license |
||||
|
that can be found in the LICENSE file or at |
||||
|
https://opensource.org/licenses/MIT. |
||||
|
--> |
||||
|
|
||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
|
||||
|
<title>Privacy Access Report</title> |
||||
|
|
||||
|
<style> |
||||
|
body { |
||||
|
font-family: Arial, sans-serif; |
||||
|
margin: 20px; |
||||
|
color: #333; |
||||
|
background-color: #f9f9f9; |
||||
|
line-height: 1.6; |
||||
|
} |
||||
|
|
||||
|
.card { |
||||
|
background-color: #fff; |
||||
|
border-radius: 10px; |
||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
||||
|
margin-bottom: 20px; |
||||
|
padding: 20px; |
||||
|
min-width: 735px; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 1.2em; |
||||
|
margin: 0 0 15px; |
||||
|
padding: 12px 20px; |
||||
|
color: #fff; |
||||
|
background-color: #5a9e6d; |
||||
|
border-radius: 8px; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
h2 .version { |
||||
|
font-size: 0.7em; |
||||
|
color: #5a9e6d; |
||||
|
background: #f1f1f1; |
||||
|
padding: 2px 6px; |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
|
||||
|
a { |
||||
|
text-decoration: none; |
||||
|
color: #5a9e6d; |
||||
|
background-color: #fcfcfc; |
||||
|
padding: 8px 16px; |
||||
|
border: 1px solid #5a9e6d; |
||||
|
border-radius: 5px; |
||||
|
font-size: 0.9em; |
||||
|
margin-right: 16px; |
||||
|
transition: background-color 0.3s ease, color 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
a:hover { |
||||
|
color: #fff; |
||||
|
background-color: #5a9e6d; |
||||
|
} |
||||
|
|
||||
|
a.warning { |
||||
|
color: #e0b73c; |
||||
|
background-color: #fcfcfc; |
||||
|
border: 1px solid #e0b73c; |
||||
|
} |
||||
|
|
||||
|
a.warning:hover { |
||||
|
color: #fff; |
||||
|
background-color: #e0b73c; |
||||
|
} |
||||
|
|
||||
|
table { |
||||
|
width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
background-color: #fff; |
||||
|
border-radius: 8px; |
||||
|
overflow: hidden; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
th, |
||||
|
td { |
||||
|
border: 1px solid #ddd; |
||||
|
padding: 12px 20px; |
||||
|
text-align: left; |
||||
|
} |
||||
|
|
||||
|
th { |
||||
|
color: #fff; |
||||
|
background-color: #b0b8b1; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
tbody tr:nth-child(odd) { |
||||
|
background-color: #f9f9f9; |
||||
|
} |
||||
|
|
||||
|
tbody tr:hover { |
||||
|
background-color: #f0f0f0; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="card" style="display: flex; justify-content: space-between; align-items: center;"> |
||||
|
<span> |
||||
|
This report was generated using version <strong>{{TOOL_VERSION}}</strong>. |
||||
|
</span> |
||||
|
<a href="https://github.com/crasowas/app_privacy_manifest_fixer" target="_blank">Like this |
||||
|
project? 🌟Star it on GitHub!</a> |
||||
|
</div> |
||||
|
{{REPORT_CONTENT}} |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,285 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2024, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Absolute path of the script and the tool's root directory |
||||
|
script_path="$(realpath "$0")" |
||||
|
tool_root_path="$(dirname "$(dirname "$script_path")")" |
||||
|
|
||||
|
# Load common constants and utils |
||||
|
source "$tool_root_path/Common/constants.sh" |
||||
|
source "$tool_root_path/Common/utils.sh" |
||||
|
|
||||
|
# Path to the app |
||||
|
app_path="$1" |
||||
|
|
||||
|
# Check if the app exists |
||||
|
if [ ! -d "$app_path" ] || [[ "$app_path" != *.app ]]; then |
||||
|
echo "Unable to find the app: $app_path" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Check if the app is iOS or macOS |
||||
|
is_ios_app=true |
||||
|
frameworks_dir="$app_path/Frameworks" |
||||
|
if [ -d "$app_path/Contents/MacOS" ]; then |
||||
|
is_ios_app=false |
||||
|
frameworks_dir="$app_path/Contents/Frameworks" |
||||
|
fi |
||||
|
|
||||
|
report_output_file="$2" |
||||
|
# Additional arguments as template usage records |
||||
|
template_usage_records=("${@:2}") |
||||
|
|
||||
|
# Copy report template to output file |
||||
|
report_template_file="$tool_root_path/Report/report-template.html" |
||||
|
if ! rsync -a "$report_template_file" "$report_output_file"; then |
||||
|
echo "Failed to copy the report template to $report_output_file" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Read the current tool's version from the VERSION file |
||||
|
tool_version_file="$tool_root_path/VERSION" |
||||
|
tool_version="N/A" |
||||
|
if [ -f "$tool_version_file" ]; then |
||||
|
tool_version="$(cat "$tool_version_file")" |
||||
|
fi |
||||
|
|
||||
|
# Initialize report content |
||||
|
report_content="" |
||||
|
|
||||
|
# Get the template file used for fixing based on the app or framework name |
||||
|
function get_used_template_file() { |
||||
|
local name="$1" |
||||
|
|
||||
|
for template_usage_record in "${template_usage_records[@]}"; do |
||||
|
if [[ "$template_usage_record" == "$name$DELIMITER"* ]]; then |
||||
|
echo "${template_usage_record#*$DELIMITER}" |
||||
|
return |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
echo "" |
||||
|
} |
||||
|
|
||||
|
# Analyze accessed API types and their corresponding reasons |
||||
|
function analyze_privacy_accessed_api() { |
||||
|
local privacy_manifest_file="$1" |
||||
|
local -a results=() |
||||
|
|
||||
|
if [ -f "$privacy_manifest_file" ]; then |
||||
|
local api_count=$(xmllint --xpath 'count(//dict/key[text()="NSPrivacyAccessedAPIType"])' "$privacy_manifest_file") |
||||
|
|
||||
|
for ((i=1; i<=api_count; i++)); do |
||||
|
local api_type=$(xmllint --xpath "(//dict/key[text()='NSPrivacyAccessedAPIType']/following-sibling::string[1])[$i]/text()" "$privacy_manifest_file" 2>/dev/null) |
||||
|
local api_reasons=$(xmllint --xpath "(//dict/key[text()='NSPrivacyAccessedAPITypeReasons']/following-sibling::array[1])[position()=$i]/string/text()" "$privacy_manifest_file" 2>/dev/null | paste -sd "/" -) |
||||
|
|
||||
|
if [ -z "$api_type" ]; then |
||||
|
api_type="N/A" |
||||
|
fi |
||||
|
|
||||
|
if [ -z "$api_reasons" ]; then |
||||
|
api_reasons="N/A" |
||||
|
fi |
||||
|
|
||||
|
results+=("$api_type$DELIMITER$api_reasons") |
||||
|
done |
||||
|
fi |
||||
|
|
||||
|
echo "${results[@]}" |
||||
|
} |
||||
|
|
||||
|
# Get the path to the `Info.plist` file for the specified app or framework |
||||
|
function get_plist_file() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
local plist_file="" |
||||
|
|
||||
|
if [[ "$path" == *.app ]]; then |
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
plist_file="$path/Info.plist" |
||||
|
else |
||||
|
plist_file="$path/Contents/Info.plist" |
||||
|
fi |
||||
|
elif [[ "$path" == *.framework ]]; then |
||||
|
local framework_path="$(get_framework_path "$path" "$version_path")" |
||||
|
|
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
plist_file="$framework_path/Info.plist" |
||||
|
else |
||||
|
plist_file="$framework_path/Resources/Info.plist" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
echo "$plist_file" |
||||
|
} |
||||
|
|
||||
|
# Add an HTML <div> element with the `card` class |
||||
|
function add_html_card_container() { |
||||
|
local card="$1" |
||||
|
|
||||
|
report_content="$report_content<div class=\"card\">$card</div>" |
||||
|
} |
||||
|
|
||||
|
# Generate an HTML <h2> element |
||||
|
function generate_html_header() { |
||||
|
local title="$1" |
||||
|
local version="$2" |
||||
|
|
||||
|
echo "<h2>$title<span class=\"version\">Version $version</span></h2>" |
||||
|
} |
||||
|
|
||||
|
# Generate an HTML <a> element with optional `warning` class |
||||
|
function generate_html_anchor() { |
||||
|
local text="$1" |
||||
|
local href="$2" |
||||
|
local warning="$3" |
||||
|
|
||||
|
if [ "$warning" == true ]; then |
||||
|
echo "<a class=\"warning\" href=\"$href\">$text</a>" |
||||
|
else |
||||
|
echo "<a href=\"$href\">$text</a>" |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# Generate an HTML <table> element |
||||
|
function generate_html_table() { |
||||
|
local thead="$1" |
||||
|
local tbody="$2" |
||||
|
|
||||
|
echo "<table>$thead$tbody</table>" |
||||
|
} |
||||
|
|
||||
|
# Generate an HTML <thead> element |
||||
|
function generate_html_thead() { |
||||
|
local ths=("$@") |
||||
|
local tr="" |
||||
|
|
||||
|
for th in "${ths[@]}"; do |
||||
|
tr="$tr<th>$th</th>" |
||||
|
done |
||||
|
|
||||
|
echo "<thead><tr>$tr</tr></thead>" |
||||
|
} |
||||
|
|
||||
|
# Generate an HTML <tbody> element |
||||
|
function generate_html_tbody() { |
||||
|
local trs=("$@") |
||||
|
local tbody="" |
||||
|
|
||||
|
for tr in "${trs[@]}"; do |
||||
|
tbody="$tbody<tr>" |
||||
|
local tds=($(split_string_by_delimiter "$tr")) |
||||
|
|
||||
|
for td in "${tds[@]}"; do |
||||
|
tbody="$tbody<td>$td</td>" |
||||
|
done |
||||
|
|
||||
|
tbody="$tbody</tr>" |
||||
|
done |
||||
|
|
||||
|
echo "<tbody>$tbody</tbody>" |
||||
|
} |
||||
|
|
||||
|
# Generate the report content for the specified directory |
||||
|
function generate_report_content() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
local privacy_manifest_file="" |
||||
|
|
||||
|
if [[ "$path" == *.app ]]; then |
||||
|
# Per the documentation, the privacy manifest should be placed at the root of the app’s bundle for iOS, while for macOS, it should be located in `Contents/Resources/` within the app’s bundle |
||||
|
# Reference: https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk#Add-a-privacy-manifest-to-your-app |
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
privacy_manifest_file="$path/$PRIVACY_MANIFEST_FILE_NAME" |
||||
|
else |
||||
|
privacy_manifest_file="$path/Contents/Resources/$PRIVACY_MANIFEST_FILE_NAME" |
||||
|
fi |
||||
|
else |
||||
|
# Per the documentation, the privacy manifest should be placed at the root of the iOS framework, while for a macOS framework with multiple versions, it should be located in the `Resources` directory within the corresponding version |
||||
|
# Some SDKs don’t follow the guideline, so we use a search-based approach for now |
||||
|
# Reference: https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk#Add-a-privacy-manifest-to-your-framework |
||||
|
local framework_path="$(get_framework_path "$path" "$version_path")" |
||||
|
local privacy_manifest_files=($(search_privacy_manifest_files "$framework_path")) |
||||
|
privacy_manifest_file="$(get_privacy_manifest_file "${privacy_manifest_files[@]}")" |
||||
|
fi |
||||
|
|
||||
|
local name="$(basename "$path")" |
||||
|
local title="$name" |
||||
|
if [ -n "$version_path" ]; then |
||||
|
title="$name ($version_path)" |
||||
|
fi |
||||
|
|
||||
|
local plist_file="$(get_plist_file "$path" "$version_path")" |
||||
|
local version="$(get_plist_version "$plist_file")" |
||||
|
local card="$(generate_html_header "$title" "$version")" |
||||
|
|
||||
|
if [ -f "$privacy_manifest_file" ]; then |
||||
|
card="$card$(generate_html_anchor "$PRIVACY_MANIFEST_FILE_NAME" "$privacy_manifest_file" false)" |
||||
|
|
||||
|
local used_template_file="$(get_used_template_file "$name$version_path")" |
||||
|
|
||||
|
if [ -f "$used_template_file" ]; then |
||||
|
card="$card$(generate_html_anchor "Template Used: $(basename "$used_template_file")" "$used_template_file" false)" |
||||
|
fi |
||||
|
|
||||
|
local trs=($(analyze_privacy_accessed_api "$privacy_manifest_file")) |
||||
|
|
||||
|
# Generate table only if the accessed privacy API types array is not empty |
||||
|
if [[ ${#trs[@]} -gt 0 ]]; then |
||||
|
local thead="$(generate_html_thead "NSPrivacyAccessedAPIType" "NSPrivacyAccessedAPITypeReasons")" |
||||
|
local tbody="$(generate_html_tbody "${trs[@]}")" |
||||
|
|
||||
|
card="$card$(generate_html_table "$thead" "$tbody")" |
||||
|
fi |
||||
|
else |
||||
|
card="$card$(generate_html_anchor "Missing Privacy Manifest" "$path" true)" |
||||
|
fi |
||||
|
|
||||
|
add_html_card_container "$card" |
||||
|
} |
||||
|
|
||||
|
# Generate the report content for app |
||||
|
function generate_app_report_content() { |
||||
|
generate_report_content "$app_path" "" |
||||
|
} |
||||
|
|
||||
|
# Generate the report content for frameworks |
||||
|
function generate_frameworks_report_content() { |
||||
|
if ! [ -d "$frameworks_dir" ]; then |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
for path in "$frameworks_dir"/*; do |
||||
|
if [ -d "$path" ]; then |
||||
|
local versions_dir="$path/Versions" |
||||
|
|
||||
|
if [ -d "$versions_dir" ]; then |
||||
|
for version in $(ls -1 "$versions_dir" | grep -vE '^Current$'); do |
||||
|
local version_path="Versions/$version" |
||||
|
generate_report_content "$path" "$version_path" |
||||
|
done |
||||
|
else |
||||
|
generate_report_content "$path" "" |
||||
|
fi |
||||
|
fi |
||||
|
done |
||||
|
} |
||||
|
|
||||
|
# Generate the final report with all content |
||||
|
function generate_final_report() { |
||||
|
# Replace placeholders in the template with the tool's version and report content |
||||
|
sed -i "" -e "s|{{TOOL_VERSION}}|$tool_version|g" -e "s|{{REPORT_CONTENT}}|${report_content}|g" "$report_output_file" |
||||
|
|
||||
|
echo "Privacy Access Report has been generated: $report_output_file" |
||||
|
} |
||||
|
|
||||
|
generate_app_report_content |
||||
|
generate_frameworks_report_content |
||||
|
generate_final_report |
@ -0,0 +1,55 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>NSPrivacyTracking</key> |
||||
|
<false/> |
||||
|
<key>NSPrivacyTrackingDomains</key> |
||||
|
<array/> |
||||
|
<key>NSPrivacyCollectedDataTypes</key> |
||||
|
<array/> |
||||
|
<key>NSPrivacyAccessedAPITypes</key> |
||||
|
<array> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>C617.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>35F9.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>E174.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>54BD.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>CA92.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
</array> |
||||
|
</dict> |
||||
|
</plist> |
@ -0,0 +1,55 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>NSPrivacyTracking</key> |
||||
|
<false/> |
||||
|
<key>NSPrivacyTrackingDomains</key> |
||||
|
<array/> |
||||
|
<key>NSPrivacyCollectedDataTypes</key> |
||||
|
<array/> |
||||
|
<key>NSPrivacyAccessedAPITypes</key> |
||||
|
<array> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>0A2A.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>35F9.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>E174.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>54BD.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
<dict> |
||||
|
<key>NSPrivacyAccessedAPIType</key> |
||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string> |
||||
|
<key>NSPrivacyAccessedAPITypeReasons</key> |
||||
|
<array> |
||||
|
<string>C56D.1</string> |
||||
|
</array> |
||||
|
</dict> |
||||
|
</array> |
||||
|
</dict> |
||||
|
</plist> |
@ -0,0 +1 @@ |
|||||
|
v1.4.1 |
@ -0,0 +1,29 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2025, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
target_paths=("Build") |
||||
|
|
||||
|
echo "Cleaning..." |
||||
|
|
||||
|
deleted_anything=false |
||||
|
|
||||
|
for path in "${target_paths[@]}"; do |
||||
|
if [ -e "$path" ]; then |
||||
|
echo "Removing $path..." |
||||
|
rm -rf "./$path" |
||||
|
deleted_anything=true |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
if [ "$deleted_anything" == true ]; then |
||||
|
echo "Cleanup completed." |
||||
|
else |
||||
|
echo "Nothing to clean." |
||||
|
fi |
@ -0,0 +1,490 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2024, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Absolute path of the script and the tool's root directory |
||||
|
script_path="$(realpath "$0")" |
||||
|
tool_root_path="$(dirname "$script_path")" |
||||
|
|
||||
|
# Load common constants and utils |
||||
|
source "$tool_root_path/Common/constants.sh" |
||||
|
source "$tool_root_path/Common/utils.sh" |
||||
|
|
||||
|
# Force replace the existing privacy manifest when the `-f` option is enabled |
||||
|
force=false |
||||
|
|
||||
|
# Enable silent mode to disable build output when the `-s` option is enabled |
||||
|
silent=false |
||||
|
|
||||
|
# Parse command-line options |
||||
|
while getopts ":fs" opt; do |
||||
|
case $opt in |
||||
|
f) |
||||
|
force=true |
||||
|
;; |
||||
|
s) |
||||
|
silent=true |
||||
|
;; |
||||
|
\?) |
||||
|
echo "Invalid option: -$OPTARG" >&2 |
||||
|
exit 1 |
||||
|
;; |
||||
|
esac |
||||
|
done |
||||
|
|
||||
|
shift $((OPTIND - 1)) |
||||
|
|
||||
|
# Path of the app produced by the build process |
||||
|
app_path="${TARGET_BUILD_DIR}/${WRAPPER_NAME}" |
||||
|
|
||||
|
# Check if the app exists |
||||
|
if [ ! -d "$app_path" ] || [[ "$app_path" != *.app ]]; then |
||||
|
echo "Unable to find the app: $app_path" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Check if the app is iOS or macOS |
||||
|
is_ios_app=true |
||||
|
frameworks_dir="$app_path/Frameworks" |
||||
|
if [ -d "$app_path/Contents/MacOS" ]; then |
||||
|
is_ios_app=false |
||||
|
frameworks_dir="$app_path/Contents/Frameworks" |
||||
|
fi |
||||
|
|
||||
|
# Default template directories |
||||
|
templates_dir="$tool_root_path/Templates" |
||||
|
user_templates_dir="$tool_root_path/Templates/UserTemplates" |
||||
|
|
||||
|
# Use user-defined app privacy manifest template if it exists, otherwise fallback to default |
||||
|
app_template_file="$user_templates_dir/$APP_TEMPLATE_FILE_NAME" |
||||
|
if [ ! -f "$app_template_file" ]; then |
||||
|
app_template_file="$templates_dir/$APP_TEMPLATE_FILE_NAME" |
||||
|
fi |
||||
|
|
||||
|
# Use user-defined framework privacy manifest template if it exists, otherwise fallback to default |
||||
|
framework_template_file="$user_templates_dir/$FRAMEWORK_TEMPLATE_FILE_NAME" |
||||
|
if [ ! -f "$framework_template_file" ]; then |
||||
|
framework_template_file="$templates_dir/$FRAMEWORK_TEMPLATE_FILE_NAME" |
||||
|
fi |
||||
|
|
||||
|
# Disable build output in silent mode |
||||
|
if [ "$silent" == false ]; then |
||||
|
# Script used to generate the privacy access report |
||||
|
report_script="$tool_root_path/Report/report.sh" |
||||
|
# An array to record template usage for generating the privacy access report |
||||
|
template_usage_records=() |
||||
|
|
||||
|
# Build output directory |
||||
|
build_dir="$tool_root_path/Build/${PRODUCT_NAME}-${CONFIGURATION}_${MARKETING_VERSION}_${CURRENT_PROJECT_VERSION}_$(date +%Y%m%d%H%M%S)" |
||||
|
# Ensure the build directory exists |
||||
|
mkdir -p "$build_dir" |
||||
|
|
||||
|
# Redirect both stdout and stderr to a log file while keeping console output |
||||
|
exec > >(tee "$build_dir/fix.log") 2>&1 |
||||
|
fi |
||||
|
|
||||
|
# Get the path to the `Info.plist` file for the specified app or framework |
||||
|
function get_plist_file() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
local plist_file="" |
||||
|
|
||||
|
if [[ "$path" == *.app ]]; then |
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
plist_file="$path/Info.plist" |
||||
|
else |
||||
|
plist_file="$path/Contents/Info.plist" |
||||
|
fi |
||||
|
elif [[ "$path" == *.framework ]]; then |
||||
|
local framework_path="$(get_framework_path "$path" "$version_path")" |
||||
|
|
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
plist_file="$framework_path/Info.plist" |
||||
|
else |
||||
|
plist_file="$framework_path/Resources/Info.plist" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
echo "$plist_file" |
||||
|
} |
||||
|
|
||||
|
# Get the path to the executable for the specified app or framework |
||||
|
function get_executable_path() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
local executable_path="" |
||||
|
|
||||
|
local plist_file="$(get_plist_file "$path" "$version_path")" |
||||
|
local executable_name="$(get_plist_executable "$plist_file")" |
||||
|
|
||||
|
if [[ "$path" == *.app ]]; then |
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
executable_path="$path/$executable_name" |
||||
|
else |
||||
|
executable_path="$path/Contents/MacOS/$executable_name" |
||||
|
fi |
||||
|
elif [[ "$path" == *.framework ]]; then |
||||
|
local framework_path="$(get_framework_path "$path" "$version_path")" |
||||
|
executable_path="$framework_path/$executable_name" |
||||
|
fi |
||||
|
|
||||
|
echo "$executable_path" |
||||
|
} |
||||
|
|
||||
|
# Analyze the specified binary file for API symbols and their categories |
||||
|
function analyze_binary_file() { |
||||
|
local path="$1" |
||||
|
local -a results=() |
||||
|
|
||||
|
# Check if the API symbol exists in the binary file using `nm` and `strings` |
||||
|
local nm_output=$(nm "$path" 2>/dev/null | xcrun swift-demangle) |
||||
|
local strings_output=$(strings "$path") |
||||
|
local combined_output="$nm_output"$'\n'"$strings_output" |
||||
|
|
||||
|
for api_symbol in "${API_SYMBOLS[@]}"; do |
||||
|
local substrings=($(split_string_by_delimiter "$api_symbol")) |
||||
|
local category=${substrings[0]} |
||||
|
local api=${substrings[1]} |
||||
|
|
||||
|
if echo "$combined_output" | grep -E "$api\$" >/dev/null; then |
||||
|
local index=-1 |
||||
|
for ((i=0; i < ${#results[@]}; i++)); do |
||||
|
local result="${results[i]}" |
||||
|
local result_substrings=($(split_string_by_delimiter "$result")) |
||||
|
# If the category matches an existing result, update it |
||||
|
if [ "$category" == "${result_substrings[0]}" ]; then |
||||
|
index=i |
||||
|
results[i]="${result_substrings[0]}$DELIMITER${result_substrings[1]},$api$DELIMITER${result_substrings[2]}" |
||||
|
break |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
# If no matching category found, add a new result |
||||
|
if [[ $index -eq -1 ]]; then |
||||
|
results+=("$category$DELIMITER$api$DELIMITER$(encode_path "$path")") |
||||
|
fi |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
echo "${results[@]}" |
||||
|
} |
||||
|
|
||||
|
# Analyze API usage in a binary file |
||||
|
function analyze_api_usage() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
local -a results=() |
||||
|
|
||||
|
local binary_file="$(get_executable_path "$path" "$version_path")" |
||||
|
|
||||
|
if [ -f "$binary_file" ]; then |
||||
|
results+=($(analyze_binary_file "$binary_file")) |
||||
|
fi |
||||
|
|
||||
|
echo "${results[@]}" |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
# Get unique categories from analysis results |
||||
|
function get_categories() { |
||||
|
local results=("$@") |
||||
|
local -a categories=() |
||||
|
|
||||
|
for result in "${results[@]}"; do |
||||
|
local substrings=($(split_string_by_delimiter "$result")) |
||||
|
local category=${substrings[0]} |
||||
|
if [[ ! "${categories[@]}" =~ "$category" ]]; then |
||||
|
categories+=("$category") |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
echo "${categories[@]}" |
||||
|
} |
||||
|
|
||||
|
# Get template file for the specified app or framework |
||||
|
function get_template_file() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
local template_file="" |
||||
|
|
||||
|
if [[ "$path" == *.app ]]; then |
||||
|
template_file="$app_template_file" |
||||
|
else |
||||
|
# Give priority to the user-defined framework privacy manifest template |
||||
|
local dep_name="$(get_dependency_name "$path")" |
||||
|
if [ -n "$version_path" ]; then |
||||
|
dep_name="$dep_name.$(basename "$version_path")" |
||||
|
fi |
||||
|
|
||||
|
local dep_template_file="$user_templates_dir/${dep_name}.xcprivacy" |
||||
|
if [ -f "$dep_template_file" ]; then |
||||
|
template_file="$dep_template_file" |
||||
|
else |
||||
|
template_file="$framework_template_file" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
echo "$template_file" |
||||
|
} |
||||
|
|
||||
|
# Check if the specified template file should be modified |
||||
|
# |
||||
|
# The following template files will be modified based on analysis: |
||||
|
# * Templates/AppTemplate.xcprivacy |
||||
|
# * Templates/FrameworkTemplate.xcprivacy |
||||
|
# * Templates/UserTemplates/FrameworkTemplate.xcprivacy |
||||
|
function is_template_modifiable() { |
||||
|
local template_file="$1" |
||||
|
|
||||
|
local template_file_name="$(basename "$template_file")" |
||||
|
|
||||
|
if [[ "$template_file" != "$user_templates_dir"* ]] || [ "$template_file_name" == "$FRAMEWORK_TEMPLATE_FILE_NAME" ]; then |
||||
|
return 0 |
||||
|
else |
||||
|
return 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# Check if `Hardened Runtime` is enabled for the specified path |
||||
|
function is_hardened_runtime_enabled() { |
||||
|
local path="$1" |
||||
|
|
||||
|
# Check environment variable first |
||||
|
if [ "${ENABLE_HARDENED_RUNTIME:-}" == "YES" ]; then |
||||
|
return 0 |
||||
|
fi |
||||
|
|
||||
|
# Check the code signature flags if environment variable is not set |
||||
|
local flags=$(codesign -dvvv "$path" 2>&1 | grep "flags=") |
||||
|
if echo "$flags" | grep -q "runtime"; then |
||||
|
return 0 |
||||
|
else |
||||
|
return 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# Re-sign the target app or framework if code signing is enabled |
||||
|
function resign() { |
||||
|
local path="$1" |
||||
|
|
||||
|
if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ] && [ "${CODE_SIGNING_REQUIRED:-}" != "NO" ] && [ "${CODE_SIGNING_ALLOWED:-}" != "NO" ]; then |
||||
|
echo "Re-signing $path with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME:-$EXPANDED_CODE_SIGN_IDENTITY}" |
||||
|
|
||||
|
local codesign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements" |
||||
|
|
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
$codesign_cmd "$path" |
||||
|
else |
||||
|
if is_hardened_runtime_enabled "$path"; then |
||||
|
codesign_cmd="$codesign_cmd -o runtime" |
||||
|
fi |
||||
|
|
||||
|
if [ -d "$path/Contents/MacOS" ]; then |
||||
|
find "$path/Contents/MacOS" -type f -name "*.dylib" | while read -r dylib_file; do |
||||
|
$codesign_cmd "$dylib_file" |
||||
|
done |
||||
|
fi |
||||
|
|
||||
|
$codesign_cmd "$path" |
||||
|
fi |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# Fix the privacy manifest for the app or specified framework |
||||
|
# To accelerate the build, existing privacy manifests will be left unchanged unless the `-f` option is enabled |
||||
|
# After fixing, the app or framework will be automatically re-signed |
||||
|
function fix() { |
||||
|
local path="$1" |
||||
|
local version_path="$2" |
||||
|
local force_resign="$3" |
||||
|
local privacy_manifest_file="" |
||||
|
|
||||
|
if [[ "$path" == *.app ]]; then |
||||
|
# Per the documentation, the privacy manifest should be placed at the root of the app’s bundle for iOS, while for macOS, it should be located in `Contents/Resources/` within the app’s bundle |
||||
|
# Reference: https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk#Add-a-privacy-manifest-to-your-app |
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
privacy_manifest_file="$path/$PRIVACY_MANIFEST_FILE_NAME" |
||||
|
else |
||||
|
privacy_manifest_file="$path/Contents/Resources/$PRIVACY_MANIFEST_FILE_NAME" |
||||
|
fi |
||||
|
else |
||||
|
# Per the documentation, the privacy manifest should be placed at the root of the iOS framework, while for a macOS framework with multiple versions, it should be located in the `Resources` directory within the corresponding version |
||||
|
# Some SDKs don’t follow the guideline, so we use a search-based approach for now |
||||
|
# Reference: https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk#Add-a-privacy-manifest-to-your-framework |
||||
|
local framework_path="$(get_framework_path "$path" "$version_path")" |
||||
|
local privacy_manifest_files=($(search_privacy_manifest_files "$framework_path")) |
||||
|
privacy_manifest_file="$(get_privacy_manifest_file "${privacy_manifest_files[@]}")" |
||||
|
|
||||
|
if [ -z "$privacy_manifest_file" ]; then |
||||
|
if [ "$is_ios_app" == true ]; then |
||||
|
privacy_manifest_file="$framework_path/$PRIVACY_MANIFEST_FILE_NAME" |
||||
|
else |
||||
|
privacy_manifest_file="$framework_path/Resources/$PRIVACY_MANIFEST_FILE_NAME" |
||||
|
fi |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# Check if the privacy manifest file exists |
||||
|
if [ -f "$privacy_manifest_file" ]; then |
||||
|
echo "💡 Found privacy manifest file: $privacy_manifest_file" |
||||
|
|
||||
|
if [ "$force" == false ]; then |
||||
|
if [ "$force_resign" == true ]; then |
||||
|
resign "$path" |
||||
|
fi |
||||
|
echo "✅ Privacy manifest file already exists, skipping fix." |
||||
|
return |
||||
|
fi |
||||
|
else |
||||
|
echo "⚠️ Missing privacy manifest file!" |
||||
|
fi |
||||
|
|
||||
|
local results=($(analyze_api_usage "$path" "$version_path")) |
||||
|
echo "API usage analysis result(s): ${#results[@]}" |
||||
|
print_array "${results[@]}" |
||||
|
|
||||
|
local template_file="$(get_template_file "$path" "$version_path")" |
||||
|
template_usage_records+=("$(basename "$path")$version_path$DELIMITER$template_file") |
||||
|
|
||||
|
# Copy the template file to the privacy manifest location, overwriting if it exists |
||||
|
cp "$template_file" "$privacy_manifest_file" |
||||
|
|
||||
|
if is_template_modifiable "$template_file"; then |
||||
|
local categories=($(get_categories "${results[@]}")) |
||||
|
local remove_categories=() |
||||
|
|
||||
|
# Check if categories is non-empty |
||||
|
if [[ ${#categories[@]} -gt 0 ]]; then |
||||
|
# Convert categories to a single space-separated string for easy matching |
||||
|
local categories_set=" ${categories[*]} " |
||||
|
|
||||
|
# Iterate through each element in `API_CATEGORIES` |
||||
|
for element in "${API_CATEGORIES[@]}"; do |
||||
|
# If element is not found in `categories_set`, add it to `remove_categories` |
||||
|
if [[ ! $categories_set =~ " $element " ]]; then |
||||
|
remove_categories+=("$element") |
||||
|
fi |
||||
|
done |
||||
|
else |
||||
|
# If categories is empty, add all of `API_CATEGORIES` to `remove_categories` |
||||
|
remove_categories=("${API_CATEGORIES[@]}") |
||||
|
fi |
||||
|
|
||||
|
# Remove extra spaces in the XML file to simplify node removal |
||||
|
xmllint --noblanks "$privacy_manifest_file" -o "$privacy_manifest_file" |
||||
|
|
||||
|
# Build a sed command to remove all matching nodes at once |
||||
|
local sed_pattern="" |
||||
|
for category in "${remove_categories[@]}"; do |
||||
|
# Find the node for the current category |
||||
|
local remove_node="$(xmllint --xpath "//dict[string='$category']" "$privacy_manifest_file" 2>/dev/null || true)" |
||||
|
|
||||
|
# If the node is found, escape special characters and append it to the sed pattern |
||||
|
if [ -n "$remove_node" ]; then |
||||
|
local escaped_node=$(echo "$remove_node" | sed 's/[\/&]/\\&/g') |
||||
|
sed_pattern+="s/$escaped_node//g;" |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
# Apply the combined sed pattern to the file if it's not empty |
||||
|
if [ -n "$sed_pattern" ]; then |
||||
|
sed -i "" "$sed_pattern" "$privacy_manifest_file" |
||||
|
fi |
||||
|
|
||||
|
# Reformat the XML file to restore spaces for readability |
||||
|
xmllint --format "$privacy_manifest_file" -o "$privacy_manifest_file" |
||||
|
fi |
||||
|
|
||||
|
resign "$path" |
||||
|
|
||||
|
echo "✅ Privacy manifest file fixed: $privacy_manifest_file." |
||||
|
} |
||||
|
|
||||
|
# Fix privacy manifests for all frameworks |
||||
|
function fix_frameworks() { |
||||
|
if ! [ -d "$frameworks_dir" ]; then |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
echo "🛠️ Fixing Frameworks..." |
||||
|
for path in "$frameworks_dir"/*; do |
||||
|
if [ -d "$path" ]; then |
||||
|
local dep_name="$(get_dependency_name "$path")" |
||||
|
local versions_dir="$path/Versions" |
||||
|
|
||||
|
if [ -d "$versions_dir" ]; then |
||||
|
for version in $(ls -1 "$versions_dir" | grep -vE '^Current$'); do |
||||
|
local version_path="Versions/$version" |
||||
|
echo "Analyzing $dep_name ($version_path) ..." |
||||
|
fix "$path" "$version_path" false |
||||
|
echo "" |
||||
|
done |
||||
|
else |
||||
|
echo "Analyzing $dep_name ..." |
||||
|
fix "$path" "" false |
||||
|
echo "" |
||||
|
fi |
||||
|
fi |
||||
|
done |
||||
|
} |
||||
|
|
||||
|
# Fix the privacy manifest for the app |
||||
|
function fix_app() { |
||||
|
echo "🛠️ Fixing $(basename "$app_path" .app) App..." |
||||
|
# Since the framework may have undergone fixes, the app must be forcefully re-signed |
||||
|
fix "$app_path" "" true |
||||
|
echo "" |
||||
|
} |
||||
|
|
||||
|
# Generate the privacy access report for the app |
||||
|
function generate_report() { |
||||
|
local original="$1" |
||||
|
|
||||
|
if [ "$silent" == true ]; then |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
local app_name="$(basename "$app_path")" |
||||
|
local name="${app_name%.*}" |
||||
|
local report_name="" |
||||
|
|
||||
|
# Adjust output names if the app is flagged as original |
||||
|
if [ "$original" == true ]; then |
||||
|
app_name="${name}-original.app" |
||||
|
report_name="report-original.html" |
||||
|
else |
||||
|
app_name="$name.app" |
||||
|
report_name="report.html" |
||||
|
fi |
||||
|
|
||||
|
local target_app_path="$build_dir/$app_name" |
||||
|
local report_path="$build_dir/$report_name" |
||||
|
|
||||
|
echo "Copy app to $target_app_path" |
||||
|
rsync -a "$app_path/" "$target_app_path/" |
||||
|
|
||||
|
# Generate the privacy access report using the script |
||||
|
sh "$report_script" "$target_app_path" "$report_path" "${template_usage_records[@]}" |
||||
|
echo "" |
||||
|
} |
||||
|
|
||||
|
start_time=$(date +%s) |
||||
|
|
||||
|
generate_report true |
||||
|
fix_frameworks |
||||
|
fix_app |
||||
|
generate_report false |
||||
|
|
||||
|
end_time=$(date +%s) |
||||
|
|
||||
|
echo "🎉 All fixed! ⏰ $((end_time - start_time)) seconds" |
||||
|
echo "🌟 If you found this script helpful, please consider giving it a star on GitHub. Your support is appreciated. Thank you!" |
||||
|
echo "🔗 Homepage: https://github.com/crasowas/app_privacy_manifest_fixer" |
||||
|
echo "🐛 Report issues: https://github.com/crasowas/app_privacy_manifest_fixer/issues" |
@ -0,0 +1,71 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2024, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Check if at least one argument (project_path) is provided |
||||
|
if [[ "$#" -lt 1 ]]; then |
||||
|
echo "Usage: $0 <project_path> [options...]" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
project_path="$1" |
||||
|
|
||||
|
shift |
||||
|
|
||||
|
options=() |
||||
|
install_builds_only=false |
||||
|
|
||||
|
# Check if the `--install-builds-only` option is provided and separate it from other options |
||||
|
for arg in "$@"; do |
||||
|
if [ "$arg" == "--install-builds-only" ]; then |
||||
|
install_builds_only=true |
||||
|
else |
||||
|
options+=("$arg") |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
# Verify Ruby installation |
||||
|
if ! command -v ruby &>/dev/null; then |
||||
|
echo "Ruby is not installed. Please install Ruby and try again." |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Check if xcodeproj gem is installed |
||||
|
if ! gem list -i xcodeproj &>/dev/null; then |
||||
|
echo "The 'xcodeproj' gem is not installed." |
||||
|
read -p "Would you like to install it now? [Y/n] " response |
||||
|
if [[ "$response" =~ ^[Nn]$ ]]; then |
||||
|
echo "Please install 'xcodeproj' manually and re-run the script." |
||||
|
exit 1 |
||||
|
fi |
||||
|
gem install xcodeproj || { echo "Failed to install 'xcodeproj'."; exit 1; } |
||||
|
fi |
||||
|
|
||||
|
# Convert project path to an absolute path if it is relative |
||||
|
if [[ ! "$project_path" = /* ]]; then |
||||
|
project_path="$(realpath "$project_path")" |
||||
|
fi |
||||
|
|
||||
|
# Absolute path of the script and the tool's root directory |
||||
|
script_path="$(realpath "$0")" |
||||
|
tool_root_path="$(dirname "$script_path")" |
||||
|
|
||||
|
tool_portable_path="$tool_root_path" |
||||
|
# If the tool's root directory is inside the project path, make the path portable |
||||
|
if [[ "$tool_root_path" == "$project_path"* ]]; then |
||||
|
# Extract the path of the tool's root directory relative to the project path |
||||
|
tool_relative_path="${tool_root_path#$project_path}" |
||||
|
# Formulate a portable path using the `PROJECT_DIR` environment variable provided by Xcode |
||||
|
tool_portable_path="\${PROJECT_DIR}${tool_relative_path}" |
||||
|
fi |
||||
|
|
||||
|
run_script_content="\"$tool_portable_path/fixer.sh\" ${options[@]}" |
||||
|
|
||||
|
# Execute the Ruby helper script |
||||
|
ruby "$tool_root_path/Helper/xcode_install_helper.rb" "$project_path" "$run_script_content" "$install_builds_only" |
@ -0,0 +1,46 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2024, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Check if the project path is provided |
||||
|
if [[ $# -eq 0 ]]; then |
||||
|
echo "Usage: $0 <project_path>" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
project_path="$1" |
||||
|
|
||||
|
# Verify Ruby installation |
||||
|
if ! command -v ruby &>/dev/null; then |
||||
|
echo "Ruby is not installed. Please install Ruby and try again." |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Check if xcodeproj gem is installed |
||||
|
if ! gem list -i xcodeproj &>/dev/null; then |
||||
|
echo "The 'xcodeproj' gem is not installed." |
||||
|
read -p "Would you like to install it now? [Y/n] " response |
||||
|
if [[ "$response" =~ ^[Nn]$ ]]; then |
||||
|
echo "Please install 'xcodeproj' manually and re-run the script." |
||||
|
exit 1 |
||||
|
fi |
||||
|
gem install xcodeproj || { echo "Failed to install 'xcodeproj'."; exit 1; } |
||||
|
fi |
||||
|
|
||||
|
# Convert project path to an absolute path if it is relative |
||||
|
if [[ ! "$project_path" = /* ]]; then |
||||
|
project_path="$(realpath "$project_path")" |
||||
|
fi |
||||
|
|
||||
|
# Absolute path of the script and the tool's root directory |
||||
|
script_path="$(realpath "$0")" |
||||
|
tool_root_path="$(dirname "$script_path")" |
||||
|
|
||||
|
# Execute the Ruby helper script |
||||
|
ruby "$tool_root_path/Helper/xcode_uninstall_helper.rb" "$project_path" |
@ -0,0 +1,108 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# Copyright (c) 2024, crasowas. |
||||
|
# |
||||
|
# Use of this source code is governed by a MIT-style license |
||||
|
# that can be found in the LICENSE file or at |
||||
|
# https://opensource.org/licenses/MIT. |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Absolute path of the script and the tool's root directory |
||||
|
script_path="$(realpath "$0")" |
||||
|
tool_root_path="$(dirname "$script_path")" |
||||
|
|
||||
|
# Repository details |
||||
|
readonly REPO_OWNER="crasowas" |
||||
|
readonly REPO_NAME="app_privacy_manifest_fixer" |
||||
|
|
||||
|
# URL to fetch the latest release information |
||||
|
readonly LATEST_RELEASE_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest" |
||||
|
|
||||
|
# Fetch the release information from GitHub API |
||||
|
release_info=$(curl -s "$LATEST_RELEASE_URL") |
||||
|
|
||||
|
# Extract the latest release version, download URL, and published time |
||||
|
latest_version=$(echo "$release_info" | grep -o '"tag_name": "[^"]*' | sed 's/"tag_name": "//') |
||||
|
download_url=$(echo "$release_info" | grep -o '"zipball_url": "[^"]*' | sed 's/"zipball_url": "//') |
||||
|
published_time=$(echo "$release_info" | grep -o '"published_at": "[^"]*' | sed 's/"published_at": "//') |
||||
|
|
||||
|
# Ensure the latest version, download URL, and published time are successfully retrieved |
||||
|
if [ -z "$latest_version" ] || [ -z "$download_url" ] || [ -z "$published_time" ]; then |
||||
|
echo "Unable to fetch the latest release information." |
||||
|
echo "Request URL: $LATEST_RELEASE_URL" |
||||
|
echo "Response Data: $release_info" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Convert UTC time to local time |
||||
|
published_time=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$published_time" +"%s" | xargs -I{} date -j -r {} +"%Y-%m-%d %H:%M:%S %z") |
||||
|
|
||||
|
# Read the current tool's version from the VERSION file |
||||
|
tool_version_file="$tool_root_path/VERSION" |
||||
|
if [ ! -f "$tool_version_file" ]; then |
||||
|
echo "VERSION file not found." |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
local_version="$(cat "$tool_version_file")" |
||||
|
|
||||
|
# Skip upgrade if the current version is already the latest |
||||
|
if [ "$local_version" == "$latest_version" ]; then |
||||
|
echo "Version $latest_version • $published_time" |
||||
|
echo "Already up-to-date." |
||||
|
exit 0 |
||||
|
fi |
||||
|
|
||||
|
# Create a temporary directory for downloading the release |
||||
|
temp_dir=$(mktemp -d) |
||||
|
trap "rm -rf $temp_dir" EXIT |
||||
|
|
||||
|
download_file_name="latest-release.tar.gz" |
||||
|
|
||||
|
# Download the latest release archive |
||||
|
echo "Downloading version $latest_version..." |
||||
|
curl -L "$download_url" -o "$temp_dir/$download_file_name" |
||||
|
|
||||
|
# Check if the download was successful |
||||
|
if [ $? -ne 0 ]; then |
||||
|
echo "Download failed, please check your network connection and try again." |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Extract the downloaded release archive |
||||
|
echo "Extracting files..." |
||||
|
tar -xzf "$temp_dir/$download_file_name" -C "$temp_dir" |
||||
|
|
||||
|
# Find the extracted release |
||||
|
extracted_release_path=$(find "$temp_dir" -mindepth 1 -maxdepth 1 -type d -name "*$REPO_NAME*" | head -n 1) |
||||
|
|
||||
|
# Verify that an extracted release was found |
||||
|
if [ -z "$extracted_release_path" ]; then |
||||
|
echo "No extracted release found for the latest version." |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
user_templates_dir="$tool_root_path/Templates/UserTemplates" |
||||
|
user_templates_backup_dir="$temp_dir/Templates/UserTemplates" |
||||
|
|
||||
|
# Backup the user templates directory if it exists |
||||
|
if [ -d "$user_templates_dir" ]; then |
||||
|
echo "Backing up user templates..." |
||||
|
mkdir -p "$user_templates_backup_dir" |
||||
|
rsync -a --exclude='.*' "$user_templates_dir/" "$user_templates_backup_dir/" |
||||
|
fi |
||||
|
|
||||
|
# Replace old version files with the new version files |
||||
|
echo "Replacing old version files..." |
||||
|
rsync -a --delete "$extracted_release_path/" "$tool_root_path/" |
||||
|
|
||||
|
# Restore the user templates from the backup |
||||
|
if [ -d "$user_templates_backup_dir" ]; then |
||||
|
echo "Restoring user templates..." |
||||
|
rsync -a --exclude='.*' "$user_templates_backup_dir/" "$user_templates_dir/" |
||||
|
fi |
||||
|
|
||||
|
# Upgrade complete |
||||
|
echo "Version $latest_version • $published_time" |
||||
|
echo "Upgrade completed successfully!" |
Loading…
Reference in new issue