feat: 服务端集成自动打包流程

This commit is contained in:
shen
2026-07-17 22:34:04 +08:00
parent a2437a039d
commit d1f070b251
221 changed files with 7284 additions and 447 deletions
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
构建期 dylib 白名单生成脚本
优先扫描最终 .app Bundle 内实际嵌入并会被 dyld 加载的镜像,生成:
1. RDKnownImages.plist — 白名单 + HMAC-SHA256 摘要
2. Generated/RDWhitelistSecret.swift — 仅在缺失时补生成
在 Xcode Build Phase 中调用时,会复用现有密钥并更新最终 app bundle 内的 plist
手动执行时若无法定位 app bundle,则退回扫描仓库中的 framework/dylib 来源。
"""
import os
import sys
import hmac
import hashlib
import plistlib
import secrets
import re
from pathlib import Path
# 所有 framework 来源目录(相对于项目根目录)
FRAMEWORK_SEARCH_PATHS = [
'Pods',
'readoor/BookShelf/Classes/Factorys',
'readoor/3.0/Factorys',
'readoor/3.0/Features/ThirdPartyService',
]
# 输出路径
PLIST_OUTPUT = 'readoor/3.0/Base/Security/RDKnownImages.plist'
SECRET_OUTPUT = 'readoor/3.0/Base/Security/Generated/RDWhitelistSecret.swift'
def scan_source_frameworks(search_paths, app_name):
"""扫描仓库中的 framework 和 dylib 来源,作为无法定位 app bundle 时的兜底方案。"""
known = []
seen = set()
for search_dir in search_paths:
if not os.path.isdir(search_dir):
continue
for root, dirs, files in os.walk(search_dir):
# .framework 在 macOS 上是目录,出现在 dirs 中
for d in dirs:
if d.endswith('.framework'):
basename = d.replace('.framework', '')
rel_path = os.path.relpath(os.path.join(root, d), '.')
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
for f in files:
if f.endswith('.dylib'):
basename = f.replace('.dylib', '')
rel_path = os.path.relpath(os.path.join(root, f), '.')
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
# App 自身
app_key = (app_name, app_name)
if app_key not in seen:
known.append({'name': app_name, 'path': app_name})
# 按 name 排序保证稳定性
known.sort(key=lambda x: x['name'])
return known
def scan_bundled_images(app_bundle_path, executable_path):
"""扫描最终 app bundle 中会实际被加载的镜像,路径使用 bundle 相对路径。"""
known = []
seen = set()
executable_name = Path(executable_path).name if executable_path else Path(app_bundle_path).stem
main_key = (executable_name, executable_path or executable_name)
seen.add(main_key)
known.append({'name': executable_name, 'path': executable_path or executable_name})
for root, dirs, files in os.walk(app_bundle_path):
dirs.sort()
files.sort()
for d in dirs:
if not d.endswith('.framework'):
continue
framework_dir = os.path.join(root, d)
basename = d.replace('.framework', '')
# if basename == 'MJExtension': # 故意排除
# continue
binary_path = os.path.join(framework_dir, basename)
if not os.path.isfile(binary_path):
continue
rel_path = os.path.relpath(binary_path, app_bundle_path)
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
for f in files:
if not f.endswith('.dylib'):
continue
dylib_path = os.path.join(root, f)
basename = f.replace('.dylib', '')
rel_path = os.path.relpath(dylib_path, app_bundle_path)
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
known.sort(key=lambda x: (x['name'], x['path']))
return known
def compute_digest(entries, secret):
"""对排序后的白名单内容计算 HMAC-SHA256 摘要"""
parts = [f"{e['name']}|{e['path']}" for e in entries]
message = '\n'.join(parts).encode('utf-8')
return hmac.new(secret, message, hashlib.sha256).hexdigest()
def generate_swift_secret(secret_bytes):
"""将构建期密钥写入 Swift 源码"""
hex_bytes = ', '.join(f'0x{b:02x}' for b in secret_bytes)
source = f"""// Auto-generated by generate_image_whitelist.py — do not edit manually
import Foundation
enum RDWhitelistSecret {{
static let key: [UInt8] = [{hex_bytes}]
}}
"""
os.makedirs(os.path.dirname(SECRET_OUTPUT), exist_ok=True)
with open(SECRET_OUTPUT, 'w') as f:
f.write(source)
def load_existing_secret():
if not os.path.isfile(SECRET_OUTPUT):
return None
with open(SECRET_OUTPUT, 'r', encoding='utf-8') as f:
content = f.read()
hex_values = re.findall(r'0x([0-9a-fA-F]{2})', content)
if not hex_values:
return None
return bytes(int(value, 16) for value in hex_values)
def resolve_app_bundle_path():
target_build_dir = os.environ.get('TARGET_BUILD_DIR')
wrapper_name = os.environ.get('WRAPPER_NAME')
if target_build_dir and wrapper_name:
app_bundle_path = os.path.join(target_build_dir, wrapper_name)
if os.path.isdir(app_bundle_path):
return app_bundle_path
return None
def main():
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.chdir(root)
build_secret = load_existing_secret()
if build_secret is None:
build_secret = secrets.token_bytes(32)
generate_swift_secret(build_secret)
print(f'[whitelist] Generated {SECRET_OUTPUT}')
app_bundle_path = resolve_app_bundle_path()
executable_path = os.environ.get('EXECUTABLE_PATH', 'readoor')
if app_bundle_path:
frameworks = scan_bundled_images(app_bundle_path, executable_path)
print(f'[whitelist] Scanned app bundle images from {app_bundle_path}')
else:
frameworks = scan_source_frameworks(FRAMEWORK_SEARCH_PATHS, 'readoor')
print('[whitelist] App bundle not found, fallback to source framework scan')
digest = compute_digest(frameworks, build_secret)
output = {
'known_images': frameworks,
'digest': digest
}
if app_bundle_path:
destination = os.path.join(app_bundle_path, os.path.basename(PLIST_OUTPUT))
os.makedirs(os.path.dirname(destination), exist_ok=True)
with open(destination, 'wb') as f:
plistlib.dump(output, f)
print(f'[whitelist] Generated app bundle plist {destination} with {len(frameworks)} dynamic entries')
else:
os.makedirs(os.path.dirname(PLIST_OUTPUT), exist_ok=True)
with open(PLIST_OUTPUT, 'wb') as f:
plistlib.dump(output, f)
print(f'[whitelist] Generated {PLIST_OUTPUT} with {len(frameworks)} dynamic entries')
print(f'[whitelist] digest={digest[:16]}...')
return 0
if __name__ == '__main__':
sys.exit(main())