之前把 wxworkSchema 当成 Info.plist 顶层键写入,但真实 readoor31.plist 里企业微信 Schema 位于 CFBundleURLTypes(CFBundleURLName == "wxwork", 占位值 "1"),顶层键无效、占位符从未被替换。 改为并入 _update_plist 已有的 CFBundleURLTypes 替换逻辑:填写则替换 CFBundleURLSchemes,未配置则整条移除,与 weixinlogin/wechatpay/tencent 一致。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZULZ8fkiaiJrxqf318xTn
193 lines
8.3 KiB
Python
193 lines
8.3 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", ""),
|
||
# 企业微信 Schema 与微信登录 AppID 同样存在 CFBundleURLTypes 中
|
||
# (CFBundleURLName == "wxwork"),未配置则整条移除。
|
||
"wxwork": (config.get("wxworkSchema", "") or "").strip(),
|
||
"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
|
||
# 跳过 macOS 压缩包内的 __MACOSX 及隐藏目录,仅复制真实皮肤资源
|
||
if item.name == "__MACOSX" or item.name.startswith("."):
|
||
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
|