#!/usr/bin/env python3"""起動経路監査 — OS 更新前に見るべき順序と権利まわりだけを拾う"""import os, re, sys, json# 起動経路に現れる「順序が意味を持つ」呼び出し。左から順に並んでいてほしいORDERED_CALLS = [ ("att_request", r"requestTrackingAuthorization"), ("ads_start", r"(MobileAds|GADMobileAds)[^\n]{0,40}\.(start|startWithCompletionHandler)"),]# 起動直後に走ると詰まりやすい同期処理BLOCKING_IN_LAUNCH = r"(Data\(contentsOf:|contentsOfFile:|semaphore\.wait|DispatchSemaphore)"# 旧世代の課金 API の痕跡(現行の権利参照へ寄せ切れていない箇所)LEGACY_STOREKIT = r"(SKPaymentQueue|SKPayment\b|restoreCompletedTransactions)"# 画面寸法のハードコード分岐(既知のポイント値との比較だけを拾う)DEVICE_POINTS = {375, 390, 393, 402, 414, 420, 428, 430, 440, 812, 844, 852, 874, 896, 912, 926, 932, 956}HARDCODED_SIZE = re.compile(r"[=<>!]=?\s*(\d{3})(?:\.0)?\b|\b(\d{3})(?:\.0)?\s*[=<>!]=")LAUNCH_ENTRY = r"(didFinishLaunchingWithOptions|@main|applicationDidFinishLaunching)"USAGE_KEYS = [ "NSUserTrackingUsageDescription", "NSPhotoLibraryAddUsageDescription", "NSLocalNetworkUsageDescription",]def scan(root): report = {"launch_files": [], "order": [], "blocking": [], "legacy_storekit": [], "hardcoded_size": [], "missing_usage_keys": []} for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in {".git", "Pods", "build", "DerivedData"}] for fn in filenames: p = os.path.join(dirpath, fn) if fn.endswith((".swift", ".m", ".mm", ".h")): try: src = open(p, encoding="utf-8", errors="ignore").read() except OSError: continue rel = os.path.relpath(p, root) if re.search(LAUNCH_ENTRY, src): report["launch_files"].append(rel) pos = {} for name, pat in ORDERED_CALLS: m = re.search(pat, src) if m: pos[name] = m.start() # 両方が同じファイルに揃ったときだけ判定する(揃わなければ黙って通す) if len(pos) == len(ORDERED_CALLS): seq = [k for k, _ in ORDERED_CALLS] if [pos[k] for k in seq] != sorted(pos[k] for k in seq): report["order"].append({"file": rel, "found": pos}) for m in re.finditer(BLOCKING_IN_LAUNCH, src): report["blocking"].append( {"file": rel, "line": src[:m.start()].count("\n") + 1, "hit": m.group(0)}) for m in re.finditer(LEGACY_STOREKIT, src): report["legacy_storekit"].append( {"file": rel, "line": src[:m.start()].count("\n") + 1, "hit": m.group(0)}) for m in HARDCODED_SIZE.finditer(src): value = int(m.group(1) or m.group(2)) if value in DEVICE_POINTS: report["hardcoded_size"].append( {"file": rel, "line": src[:m.start()].count("\n") + 1, "value": value}) elif fn == "Info.plist": try: plist = open(p, encoding="utf-8", errors="ignore").read() except OSError: continue rel = os.path.relpath(p, root) missing = [k for k in USAGE_KEYS if k not in plist] if missing: report["missing_usage_keys"].append({"file": rel, "missing": missing}) return reportif __name__ == "__main__": root = sys.argv[1] if len(sys.argv) > 1 else "." r = scan(root) print(json.dumps(r, ensure_ascii=False, indent=2)) # 順序違反と起動時ブロッキングだけを非ゼロ終了にする sys.exit(1 if (r["order"] or r["blocking"]) else 0)