"""服务端自带的 iOS 工程配置替换逻辑。""" import plistlib import re import shutil from pathlib import Path class ProjectPatchError(Exception): """工程配置无法完整写入。""" def _replace_lines(path: Path, keyword: str, replacement: str) -> int: if not path.exists(): raise ProjectPatchError(f"配置文件不存在: {path.relative_to(path.parent.parent)}") lines = path.read_text(encoding="utf-8").splitlines(keepends=True) count = 0 result = [] for line in lines: if keyword in line and not line.lstrip().startswith("//"): result.append(f"{replacement}\n") count += 1 else: result.append(line) path.write_text("".join(result), encoding="utf-8") return count def _update_plist(path: Path, config: dict): if not path.exists(): raise ProjectPatchError(f"Info.plist 不存在: {path}") with path.open("rb") as f: plist = plistlib.load(f) plist["CFBundleDisplayName"] = config.get("APPID_NAME", "") plist["AlivcLicenseKey"] = config.get("AlivcLicenseKey", "") or "" scheme = (config.get("BUNDLE_ID", "") or "").replace(".", "").lower() values = { "weixinlogin": config.get("weixinlogin", ""), "wechatpay": config.get("weixinpay", ""), "tencent": config.get("tencent", ""), "readoorUrlScheme": scheme, } url_types = [] for item in plist.get("CFBundleURLTypes", []): name = item.get("CFBundleURLName") value = values.get(name) if name in values: if not value: continue item["CFBundleURLSchemes"] = [value] url_types.append(item) plist["CFBundleURLTypes"] = url_types with path.open("wb") as f: plistlib.dump(plist, f, fmt=plistlib.FMT_XML) def _apply_theme(build_dir: Path, theme_dir: str, messages: list[str]): if not theme_dir: return source = Path(theme_dir) if not source.is_dir(): raise ProjectPatchError(f"皮肤目录不存在: {source}") target_assets = build_dir / "readoor" / "BookShelf" / "Resources" / "Images.xcassets" for item in source.iterdir(): if not item.is_dir(): continue destination = target_assets / item.name if destination.exists(): shutil.rmtree(destination) shutil.copytree(item, destination) messages.append(f"已替换皮肤资源: {destination.relative_to(build_dir)}") icon = source / "AppIcon.appiconset" / "icon-1024.png" logo = build_dir / "readoor" / "3.0" / "Resources" / "3.0.xcassets" / "AppLogo.imageset" / "icon-1024.png" if icon.exists() and logo.parent.exists(): shutil.copy2(icon, logo) messages.append("已替换应用 Logo") license_file = source / "license.crt" license_target = build_dir / "readoor" / "3.0" / "Resources" / "license.crt" if license_file.exists() and license_target.parent.exists(): shutil.copy2(license_file, license_target) messages.append("已替换渠道 License") def _write_export_options(build_dir: Path, config: dict): if config.get("NO_CERT"): data = { "destination": "export", "method": "development" if config.get("BUILD_TYPE") == "Ad_Hoc" else "app-store-connect", "stripSwiftSymbols": True, "thinning": "", } else: data = { "destination": "export", "method": "release-testing" if config.get("BUILD_TYPE") == "Ad_Hoc" else "app-store-connect", "teamID": config.get("TEAM_ID", ""), "provisioningProfiles": {config.get("BUNDLE_ID", ""): config.get("PROVISIONING_NAME", "")}, "signingCertificate": config.get("CERTIFICATE", ""), "signingStyle": "manual", "stripSwiftSymbols": True, "thinning": "", } with (build_dir / "exportOptions.plist").open("wb") as f: plistlib.dump(data, f) def _apply_dictionary_config(build_dir: Path, config: dict, messages: list[str]): macro = build_dir / "readoor" / "BookShelf" / "Classes" / "Macro" / "STSConstantsMacro.h" if not macro.exists(): raise ProjectPatchError(f"词典配置文件不存在: {macro}") content = macro.read_text(encoding="utf-8") version = config.get("VERSION", "") content, count = re.subn( r'^(#define\s+SOURCE_VERSION\s+)@"[^"]*"', rf'\1@"{version}"', content, flags=re.MULTILINE, ) if count == 0: raise ProjectPatchError("未找到词典 SOURCE_VERSION 配置") macro.write_text(content, encoding="utf-8") messages.append("已替换词典版本配置") def apply_project_config(build_dir: Path, config: dict) -> list[str]: """写入项目配置,返回适合实时日志展示的变更说明。""" messages: list[str] = [] if config.get("SCHEME") == "readoorDict": _apply_dictionary_config(build_dir, config, messages) _write_export_options(build_dir, config) return messages project = build_dir / "readoor.xcodeproj" / "project.pbxproj" swift = build_dir / "readoor" / "3.0" / "AppConfig" / "RDAppConfiguration.swift" entitlements = build_dir / "readoor" / "3.0" / "readoor31.entitlements" plist = build_dir / "readoor" / "3.0" / "readoor31.plist" replacements = [ (project, "MARKETING_VERSION =", f"\t\t\t\tMARKETING_VERSION = {config.get('VERSION', '')};"), (project, "CURRENT_PROJECT_VERSION =", f"\t\t\t\tCURRENT_PROJECT_VERSION = {config.get('BUILD_VERSION', '')};"), (swift, "let RD_APP_GUID: String =", f"let RD_APP_GUID: String = \"{config.get('APPID', '')}\""), (swift, "let RD_API_DOMAIN: String", f"let RD_API_DOMAIN: String = \"{config.get('API', '')}\""), (swift, "let RD_WECHAT_PAY_ID: String", f"let RD_WECHAT_PAY_ID: String = \"{config.get('weixinpay', '')}\""), (swift, "let RD_SOURCE_VERSION: String", f"let RD_SOURCE_VERSION: String = \"{config.get('VERSION', '')}\""), (entitlements, "applinks:", f"\t{config.get('ASSOCIATED_DOMAINS', '')}"), ] if config.get("BUNDLE_ID"): replacements.append((project, "PRODUCT_BUNDLE_IDENTIFIER =", f"\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = {config['BUNDLE_ID']};")) if config.get("TEAM_ID"): replacements.extend([ (project, "DEVELOPMENT_TEAM =", f"\t\t\t\tDEVELOPMENT_TEAM = {config['TEAM_ID']};"), (project, '"DEVELOPMENT_TEAM[sdk=iphoneos*]" =', f"\t\t\t\t\"DEVELOPMENT_TEAM[sdk=iphoneos*]\" = {config['TEAM_ID']};"), ]) if config.get("PROVISIONING_NAME"): replacements.extend([ (project, "PROVISIONING_PROFILE_SPECIFIER =", f"\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = {config['PROVISIONING_NAME']};"), (project, '"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" =', f"\t\t\t\t\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\" = {config['PROVISIONING_NAME']};"), ]) # 与 AutoPacking/replace_build_info.py 保持一致:工程内所有构建配置都使用 # Distribution identity。只更新 exportOptions.plist 不足以覆盖 Archive 阶段, # 否则 pbxproj 中残留的 iPhone/iOS Developer 会让 Xcode 查找开发证书。 replacements.extend([ (project, "CODE_SIGN_IDENTITY =", '\t\t\t\tCODE_SIGN_IDENTITY = "iPhone Distribution";'), (project, '"CODE_SIGN_IDENTITY[sdk=iphoneos*]" =', '\t\t\t\t"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";'), ]) missing = [] for path, keyword, replacement in replacements: if _replace_lines(path, keyword, replacement) == 0: missing.append(keyword) if missing: raise ProjectPatchError(f"未找到待替换配置: {', '.join(missing)}") _update_plist(plist, config) _apply_theme(build_dir, config.get("THEME", ""), messages) _write_export_options(build_dir, config) messages.append("工程、签名、版本和分发配置已更新") return messages