forked from trent_larson/crowd-funder-for-time-pwa
add a privacy-fixer project that may have fixed the GoogleToolboxForMac privacy manifext problem
https://github.com/crasowas/app_privacy_manifest_fixer
This commit is contained in:
@@ -106,6 +106,7 @@
|
|||||||
504EC3011FED79650016851F /* Frameworks */,
|
504EC3011FED79650016851F /* Frameworks */,
|
||||||
504EC3021FED79650016851F /* Resources */,
|
504EC3021FED79650016851F /* Resources */,
|
||||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||||
|
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -141,8 +142,6 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 504EC2FB1FED79650016851F;
|
mainGroup = 504EC2FB1FED79650016851F;
|
||||||
packageReferences = (
|
|
||||||
);
|
|
||||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@@ -169,6 +168,26 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Fix Privacy Manifest";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" ";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
|||||||
5
ios/App/app_privacy_manifest_fixer/.gitignore
vendored
Normal file
5
ios/App/app_privacy_manifest_fixer/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Build
|
||||||
|
/Build/
|
||||||
58
ios/App/app_privacy_manifest_fixer/CHANGELOG.md
Normal file
58
ios/App/app_privacy_manifest_fixer/CHANGELOG.md
Normal file
@@ -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.
|
||||||
80
ios/App/app_privacy_manifest_fixer/Common/constants.sh
Executable file
80
ios/App/app_privacy_manifest_fixer/Common/constants.sh
Executable file
@@ -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"
|
||||||
|
)
|
||||||
125
ios/App/app_privacy_manifest_fixer/Common/utils.sh
Executable file
125
ios/App/app_privacy_manifest_fixer/Common/utils.sh
Executable file
@@ -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
|
||||||
21
ios/App/app_privacy_manifest_fixer/LICENSE
Normal file
21
ios/App/app_privacy_manifest_fixer/LICENSE
Normal file
@@ -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.
|
||||||
240
ios/App/app_privacy_manifest_fixer/README.md
Normal file
240
ios/App/app_privacy_manifest_fixer/README.md
Normal file
@@ -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!
|
||||||
240
ios/App/app_privacy_manifest_fixer/README.zh-CN.md
Normal file
240
ios/App/app_privacy_manifest_fixer/README.zh-CN.md
Normal file
@@ -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 修复、文档改进等。请确保遵循项目规范,并保持代码风格一致。感谢你的支持!
|
||||||
124
ios/App/app_privacy_manifest_fixer/Report/report-template.html
Normal file
124
ios/App/app_privacy_manifest_fixer/Report/report-template.html
Normal file
@@ -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>
|
||||||
285
ios/App/app_privacy_manifest_fixer/Report/report.sh
Executable file
285
ios/App/app_privacy_manifest_fixer/Report/report.sh
Executable file
@@ -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>
|
||||||
1
ios/App/app_privacy_manifest_fixer/VERSION
Normal file
1
ios/App/app_privacy_manifest_fixer/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v1.4.1
|
||||||
29
ios/App/app_privacy_manifest_fixer/clean.sh
Executable file
29
ios/App/app_privacy_manifest_fixer/clean.sh
Executable file
@@ -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
|
||||||
490
ios/App/app_privacy_manifest_fixer/fixer.sh
Executable file
490
ios/App/app_privacy_manifest_fixer/fixer.sh
Executable file
@@ -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"
|
||||||
71
ios/App/app_privacy_manifest_fixer/install.sh
Executable file
71
ios/App/app_privacy_manifest_fixer/install.sh
Executable file
@@ -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"
|
||||||
46
ios/App/app_privacy_manifest_fixer/uninstall.sh
Executable file
46
ios/App/app_privacy_manifest_fixer/uninstall.sh
Executable file
@@ -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"
|
||||||
108
ios/App/app_privacy_manifest_fixer/upgrade.sh
Executable file
108
ios/App/app_privacy_manifest_fixer/upgrade.sh
Executable file
@@ -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!"
|
||||||
Reference in New Issue
Block a user