feat: 强化打包配置与服务安全
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user