團隊將 iOS 專案遷移到雲端 Mac 後,建置成功並不代表隱私聲明已完整涵蓋。相依套件升級時可能新增 PrivacyInfo.xcprivacy,資源複製設定也可能導致主專案的清單未被納入最終 App。更穩妥的做法,是將隱私清單視為建置產物的一部分:先檢查原始碼,再檢查產物,最後透過版本化基準阻擋未經審查的變更。
先定義閘門檢查範圍
一套可執行的閘門至少應涵蓋四個層面:清單能否由 plist 解析、頂層欄位型別是否正確、所有相依套件的清單是否都納入檢查範圍,以及最終 App 中是否存在預期的清單。不要只在儲存庫根目錄尋找單一檔案;原始碼相依套件、預編譯框架,以及套件管理器取出的元件,都可能各自附帶聲明。
先在乾淨的工作區中建立清單索引:
find . \
-path './.git' -prune -o \
-path './DerivedData' -prune -o \
-name PrivacyInfo.xcprivacy -print \
| LC_ALL=C sort > privacy-manifests.current
建議將專案明確維護的路徑寫入 privacy-manifests.baseline。由流水線比較這兩個檔案;如有新增、刪除或路徑變更,應先讓檢查失敗,待程式碼擁有者確認後再更新基準。如此可避免相依套件升級所造成的聲明變更被悄然忽略。
基準並非合規證明。它只能證明目前的變更已經過明確審查,無法取代對程式碼實際行為的判斷。
驗證 plist 與頂層結構
plutil -lint 能找出 XML 或二進位 plist 的損壞問題,但不會代為判斷欄位型別。可以使用 Python 標準函式庫增加一層輕量的結構檢查,無須安裝額外相依套件:
import pathlib
import plistlib
import sys
allowed = {
"NSPrivacyTracking": bool,
"NSPrivacyTrackingDomains": list,
"NSPrivacyCollectedDataTypes": list,
"NSPrivacyAccessedAPITypes": list,
}
failed = False
files = sorted(pathlib.Path(".").rglob("PrivacyInfo.xcprivacy"))
if not files:
print("No privacy manifest found")
sys.exit(1)
for path in files:
try:
with path.open("rb") as stream:
data = plistlib.load(stream)
if not isinstance(data, dict):
raise TypeError("Root must be a dictionary")
for key, value in data.items():
expected = allowed.get(key)
if expected is None:
raise KeyError(f"Unknown top-level key: {key}")
if not isinstance(value, expected):
raise TypeError(f"{key} must be {expected.__name__}")
print(path)
except Exception as error:
failed = True
print(f"{path}: {error}")
sys.exit(1 if failed else 0)
將指令碼儲存為 Scripts/validate_privacy_manifests.py,並在建置前執行。如果專案使用組織內部的擴充欄位,不應直接允許所有未知鍵,而應逐項加入允許清單並記錄其用途。
建立 Required Reason API 複核表
結構正確仍不代表所填理由正確。檔案時間戳記、系統啟動時間、磁碟空間和偏好設定等呼叫,都應納入程式碼複核範圍。自動掃描可以提供線索,但巨集、封裝層與二進位相依套件都可能造成漏報或誤報,因此不要讓單次文字搜尋直接決定理由代碼。
為每項聲明維護一筆可稽核的記錄:
| 檢查項目 | 應記錄內容 | 失敗條件 |
|---|---|---|
| API 類別 | 清單中的類別識別碼 | 類別沒有對應的程式碼路徑 |
| 使用位置 | 模組、檔案與負責人 | 無法定位呼叫來源 |
| 使用目的 | 提供給使用者的實際功能 | 理由與行為不一致 |
| 相依來源 | 自有程式碼或具體元件 | 二進位檔來源不明確 |
| 複核觸發條件 | 程式碼或相依套件版本變更 | 變更後未重新確認 |
對於閉源二進位元件,至少應記錄元件版本、清單雜湊值,以及導入該元件的業務功能。若無法解釋聲明的來源,不應靠猜測補齊,而應向相依套件負責人確認。
在最終 App 中再次檢查
原始碼中的檔案可能因 target membership 或資源階段設定錯誤而未被複製。流水線應完成一次未簽署的 Release 建置,再檢查實際產物:
rm -rf .build/privacy
xcodebuild \
-scheme "$SCHEME" \
-configuration Release \
-sdk iphoneos \
-derivedDataPath .build/privacy \
CODE_SIGNING_ALLOWED=NO \
build
APP_PATH="$(find .build/privacy/Build/Products -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
find "$APP_PATH" -name PrivacyInfo.xcprivacy -print | LC_ALL=C sort
主 App、嵌入式框架與擴充功能應依照專案預期分別出現。不要將 DerivedData 的隨機目錄寫死,也不要只檢查原始碼中的清單數量,因為多個原始碼清單可能在建置過程中被合併、取代或遺漏。
儲存產物清單
將產物中每份清單的相對路徑與 SHA-256 儲存為流水線附件:
find "$APP_PATH" -name PrivacyInfo.xcprivacy -print0 |
while IFS= read -r -d '' file; do
relative="${file#"$APP_PATH"/}"
digest="$(shasum -a 256 "$file" | awk '{print $1}')"
printf '%s %s
' "$digest" "$relative"
done | LC_ALL=C sort > privacy-artifact.sha256
發生審核問題時,這份記錄可以回答「該次建置實際包含哪些內容」,比只查看目前分支更可靠。
將失敗條件納入日常流程
建議將檢查分為快速與完整兩種層級。合併請求先執行清單搜尋、plist 解析、欄位型別與基準差異檢查;主分支再執行 Release 建置及 App 產物複核。快速檢查失敗時,通常可在數十秒內指出問題路徑;完整檢查則負責找出資源階段與嵌入式框架的問題。
上線前保留以下檢查項目:
- 清單索引與已核准的基準一致。
- 每個檔案都通過
plutil -lint與結構檢查指令碼。 - 新增的 API 類別已關聯至程式碼位置、負責人與實際用途。
- 相依套件升級後的清單差異已經人工複核。
- 最終 App、擴充功能與嵌入式框架中的清單符合預期。
- 產物路徑與雜湊值已隨本次建置封存。
這套閘門的目的並非自動取代團隊作出合規判斷,而是讓遺漏明確轉化為建置失敗,並確保每次聲明變更都能追溯至程式碼、相依套件與審查記錄。
常見問題
專案中已有 PrivacyInfo.xcprivacy 就足夠嗎?
不夠。還要確認檔案可正確解析、欄位型別符合預期,而且確實進入最終 App 或框架建置產物。
自動腳本能判斷每個 Required Reason API 理由是否正確嗎?
不能完全判斷。腳本適合找出缺漏、格式錯誤與未審查變更,理由是否符合實際呼叫情境仍需程式碼負責人複核。
相依套件更新後為什麼要重新產生隱私清單基準?
相依套件可能新增或修改自己的隱私清單。應先審查差異並確認宣告符合實際行為,再更新基準。
為下一次建置選擇獨享雲端 Mac
依工作負載選擇 Mac mini 機型、租期與區域。每筆訂單都對應獨立實體節點,實際可用性以控制台即時回傳結果為準。