feat: 服务端集成自动打包流程
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user