186 lines
7.9 KiB
Python
186 lines
7.9 KiB
Python
"""服务端自带的 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": "<none>",
|
|
}
|
|
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": "<none>",
|
|
}
|
|
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, "<string>applinks:", f"\t<string>{config.get('ASSOCIATED_DOMAINS', '')}</string>"),
|
|
]
|
|
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
|