feat: 强化打包配置与服务安全

This commit is contained in:
shen
2026-07-18 13:30:56 +08:00
parent d1f070b251
commit a48909f3bc
26 changed files with 779 additions and 203 deletions
+96 -92
View File
@@ -16,6 +16,7 @@ from ..config import (
SKINS_DIR,
AUTOMATION_DIR,
get_source_dir,
get_shared_source_dir,
get_git_remote_url,
mask_git_remote_url,
)
@@ -33,6 +34,11 @@ class BuildError(Exception):
self.detail = detail
# 共享源码目录只能同时被一个任务切换、清理和复制。
# 后续在各自 build_dir 中执行的配置、依赖安装和编译不受此锁限制。
source_prepare_lock = asyncio.Lock()
# 错误分类规则:(关键词列表, category, 友好提示)
_ERROR_RULES = [
(["No signing certificate", "Signing certificate \"", "Code Signing Error",
@@ -136,18 +142,18 @@ def _db_update(db, task, **fields):
async def update_source(task_id: str, source_dir: Path, branch: str):
"""确保分支源码目录存在且为最新"""
"""将共享源码工作目录强制同步到指定远程分支。"""
remote_url = get_git_remote_url(with_credentials=True)
remote_url_masked = mask_git_remote_url(remote_url)
if not source_dir.exists():
# 首次:从远程 clone
cloned = not source_dir.exists()
if cloned:
if not remote_url:
raise Exception(f"源码目录不存在且未配置 GIT_REMOTE_URL: {source_dir}")
source_dir.parent.mkdir(parents=True, exist_ok=True)
await log_streamer.emit(task_id, f"克隆仓库: {remote_url_masked}{branch}")
await log_streamer.emit(task_id, f"初始化共享源码目录: {remote_url_masked}")
process = await asyncio.create_subprocess_exec(
"git", "clone", "--depth", "1", "-b", branch, "--single-branch", remote_url, str(source_dir),
"git", "clone", remote_url, str(source_dir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
@@ -157,25 +163,32 @@ async def update_source(task_id: str, source_dir: Path, branch: str):
await log_streamer.emit(task_id, decoded)
await process.wait()
if process.returncode != 0:
raise Exception(f"git clone 失败: {branch}")
else:
# 已存在:fetch + checkout + pull
await log_streamer.emit(task_id, f"更新分支源码: {branch}")
raise Exception("git clone 失败")
if remote_url:
process = await asyncio.create_subprocess_exec(
"git", "remote", "set-url", "origin", remote_url,
cwd=str(source_dir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
await process.wait()
# git clone 成功即保证目录有效;已有目录则额外拦截误配置路径。
if not cloned and not (source_dir / ".git").exists():
raise Exception(f"共享源码目录不是 Git 仓库: {source_dir}")
# fetch
await log_streamer.emit(task_id, f"同步共享源码到分支: {branch}")
if remote_url:
process = await asyncio.create_subprocess_exec(
"git", "fetch", "origin",
cwd=str(source_dir),
stdout=asyncio.subprocess.PIPE,
"git", "remote", "set-url", "origin", remote_url,
cwd=str(source_dir), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
)
await process.wait()
if process.returncode != 0:
raise Exception("git remote set-url 失败")
commands = [
(("git", "fetch", "origin"), "git fetch 失败"),
(("git", "checkout", "-B", branch, f"origin/{branch}"), f"git checkout {branch} 失败"),
(("git", "reset", "--hard", f"origin/{branch}"), f"git reset {branch} 失败"),
(("git", "clean", "-ffdx"), "git clean 失败"),
]
for command, error_message in commands:
process = await asyncio.create_subprocess_exec(
*command, cwd=str(source_dir), stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
async for line in process.stdout:
@@ -184,41 +197,47 @@ async def update_source(task_id: str, source_dir: Path, branch: str):
await log_streamer.emit(task_id, decoded)
await process.wait()
if process.returncode != 0:
raise Exception("git fetch 失败")
# checkout
process = await asyncio.create_subprocess_exec(
"git", "checkout", "-B", branch, f"origin/{branch}",
cwd=str(source_dir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
async for line in process.stdout:
decoded = line.decode("utf-8", errors="replace").strip()
if decoded:
await log_streamer.emit(task_id, decoded)
await process.wait()
if process.returncode != 0:
raise Exception(f"git checkout {branch} 失败")
# pull
process = await asyncio.create_subprocess_exec(
"git", "pull", "origin", branch,
cwd=str(source_dir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
async for line in process.stdout:
decoded = line.decode("utf-8", errors="replace").strip()
if decoded:
await log_streamer.emit(task_id, decoded)
await process.wait()
if process.returncode != 0:
raise Exception(f"git pull {branch} 失败")
raise Exception(error_message)
await log_streamer.emit(task_id, f"源码已就绪: {source_dir}")
async def get_source_commit(source_dir: Path) -> str:
"""读取当前共享源码快照的 commit SHA。"""
process = await asyncio.create_subprocess_exec(
"git", "rev-parse", "HEAD", cwd=str(source_dir),
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
)
output = await process.stdout.read()
await process.wait()
if process.returncode != 0:
raise Exception("读取源码 commit 失败")
return output.decode("utf-8", errors="replace").strip()
async def prepare_source_snapshot(task_id: str, task) -> tuple[Path, str]:
"""准备任务独立源码快照;远程仓库模式下此过程全局串行。"""
if not get_git_remote_url():
source_dir = get_source_dir(task.branch)
if not source_dir.exists():
raise Exception(
f"分支源码目录不存在: {source_dir},请先在 GIT_SOURCE_BASE 下准备分支代码,或配置 GIT_REMOTE_URL"
)
await log_streamer.emit(task_id, f"使用分支源码: {source_dir}")
return await copy_source_code(task_id, task, source_dir), ""
source_dir = get_shared_source_dir()
await log_streamer.emit(task_id, "等待共享源码准备队列...")
async with source_prepare_lock:
await log_streamer.emit(task_id, "开始准备共享源码快照")
await update_source(task_id, source_dir, task.branch)
commit = await get_source_commit(source_dir)
await log_streamer.emit(task_id, f"源码版本: {task.branch} @ {commit}")
build_dir = await copy_source_code(task_id, task, source_dir)
await log_streamer.emit(task_id, "共享源码快照已就绪,开始并行打包")
return build_dir, commit
async def run_build_task(task_id: str):
"""执行打包任务"""
from ..database import SessionLocal
@@ -251,25 +270,16 @@ async def run_build_task(task_id: str):
async def _do_build():
nonlocal build_dir
# 1. 更新分支源码并拷贝
source_dir = get_source_dir(task.branch)
await asyncio.to_thread(_db_update, db, task, current_step="copy")
if get_git_remote_url():
# 配置了远程仓库,从分支目录获取源码
await update_source(task_id, source_dir, task.branch)
elif source_dir.exists():
# 分支目录已存在,直接使用
await log_streamer.emit(task_id, f"使用分支源码: {source_dir}")
else:
raise Exception(
f"分支源码目录不存在: {source_dir},请先在 GIT_SOURCE_BASE 下准备分支代码,或配置 GIT_REMOTE_URL"
)
build_dir = await copy_source_code(task_id, task, source_dir)
# 1. 串行准备源码快照;后续步骤在任务独立目录中并行执行。
await asyncio.to_thread(_db_update, db, task, current_step="source_prepare")
build_dir, source_commit = await prepare_source_snapshot(task_id, task)
await asyncio.to_thread(_db_update, db, task, build_dir=str(build_dir))
# 2. 生成配置
await asyncio.to_thread(_db_update, db, task, current_step="config")
config_data = await generate_config(task_id, task, build_dir)
config_data["SOURCE_BRANCH"] = task.branch
config_data["SOURCE_COMMIT"] = source_commit
await asyncio.to_thread(_db_update, db, task,
config_json=json.dumps(config_data, ensure_ascii=False))
@@ -661,14 +671,12 @@ async def build_project(task_id: str, task, config_data: dict, build_dir: Path)
# 清理
await log_streamer.emit(task_id, "清理项目...")
clean_cmd = (
f"xcodebuild clean -workspace {workspace_path.name} "
f"-scheme {scheme} "
f"-configuration Release "
f"-derivedDataPath {export_path / 'derived_data'}"
)
process = await asyncio.create_subprocess_shell(
clean_cmd,
process = await asyncio.create_subprocess_exec(
"xcodebuild", "clean",
"-workspace", workspace_path.name,
"-scheme", scheme,
"-configuration", "Release",
"-derivedDataPath", str(export_path / "derived_data"),
cwd=str(build_dir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
@@ -681,17 +689,16 @@ async def build_project(task_id: str, task, config_data: dict, build_dir: Path)
# Archive
await log_streamer.emit(task_id, "开始 Archive...")
archive_cmd = (
f"xcodebuild archive -workspace {workspace_path.name} "
f"-scheme {scheme} "
f"-configuration Release "
f"-archivePath {archive_path} "
f"-derivedDataPath {export_path / 'derived_data'} "
f"-destination generic/platform=ios -quiet"
)
archive_output = []
process = await asyncio.create_subprocess_shell(
archive_cmd,
process = await asyncio.create_subprocess_exec(
"xcodebuild", "archive",
"-workspace", workspace_path.name,
"-scheme", scheme,
"-configuration", "Release",
"-archivePath", str(archive_path),
"-derivedDataPath", str(export_path / "derived_data"),
"-destination", "generic/platform=ios",
"-quiet",
cwd=str(build_dir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
@@ -710,15 +717,12 @@ async def build_project(task_id: str, task, config_data: dict, build_dir: Path)
# 导出 IPA
await log_streamer.emit(task_id, "导出 IPA...")
export_plist = build_dir / "exportOptions.plist"
export_cmd = (
f"xcodebuild -exportArchive "
f"-archivePath {archive_path} "
f"-exportPath {export_path} "
f"-exportOptionsPlist {export_plist}"
)
export_output = []
process = await asyncio.create_subprocess_shell(
export_cmd,
process = await asyncio.create_subprocess_exec(
"xcodebuild", "-exportArchive",
"-archivePath", str(archive_path),
"-exportPath", str(export_path),
"-exportOptionsPlist", str(export_plist),
cwd=str(build_dir),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,