Files
iOSBuildServer/backend/services/project_patcher.py
T
shenleiandClaude Sonnet 5 fa559101b9 fix: 企业微信 Schema 未替换,改写 CFBundleURLTypes 中 wxwork 条目
之前把 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
2026-09-01 11:02:50 +09:00

193 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""服务端自带的 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