feat: dSYM 按上传配置一并保存到远端,本地清理后回退到远端地址下载
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c17b7d5ad9
commit
1466e24c83
@@ -23,7 +23,7 @@ from ..config import (
|
||||
mask_git_remote_url,
|
||||
)
|
||||
from .log_streamer import log_streamer
|
||||
from .distribution import DistributionError, publish_ipa
|
||||
from .distribution import DistributionError, publish_dsym, publish_ipa
|
||||
from .notification import NotificationError, send_dingtalk_notification
|
||||
from .project_patcher import ProjectPatchError, apply_project_config
|
||||
|
||||
@@ -455,7 +455,7 @@ async def run_build_task(task_id: str):
|
||||
ipa_path = await build_project(task_id, task, config_data, build_dir)
|
||||
|
||||
# 查找 dSYM
|
||||
dsym_path = await find_dsym(task_id, build_dir)
|
||||
dsym_path = await find_dsym(task_id, build_dir, config_data.get("SCHEME", ""))
|
||||
|
||||
# 查找混淆映射表
|
||||
obf_maps_path = build_dir / "obfuscation_maps"
|
||||
@@ -477,6 +477,13 @@ async def run_build_task(task_id: str):
|
||||
upload_fields["oss_url"] = oss_url
|
||||
if qr_code_path:
|
||||
upload_fields["qr_code_path"] = qr_code_path
|
||||
|
||||
# dSYM 用于后续崩溃符号化,按上传配置一并保存到远端;失败不影响本次打包结果。
|
||||
if dsym_path:
|
||||
dsym_url = await upload_dsym(task_id, task, config_data, dsym_path, build_dir)
|
||||
if dsym_url:
|
||||
upload_fields["dsym_url"] = dsym_url
|
||||
|
||||
if upload_fields:
|
||||
await asyncio.to_thread(_db_update, db, task, **upload_fields)
|
||||
|
||||
@@ -914,15 +921,22 @@ async def build_project(task_id: str, task, config_data: dict, build_dir: Path)
|
||||
return ipa_path
|
||||
|
||||
|
||||
async def find_dsym(task_id: str, build_dir: Path) -> Path:
|
||||
"""查找 dSYM 文件"""
|
||||
async def find_dsym(task_id: str, build_dir: Path, scheme: str = "") -> Path:
|
||||
"""查找当前 target(主 App)对应的 dSYM 文件,忽略 Extension/Framework 各自的 dSYM"""
|
||||
derived_data = build_dir / "build" / "derived_data"
|
||||
if not derived_data.exists():
|
||||
return None
|
||||
|
||||
for dsym in derived_data.rglob("*.dSYM"):
|
||||
return dsym
|
||||
return None
|
||||
dsyms = list(derived_data.rglob("*.dSYM"))
|
||||
if not dsyms:
|
||||
return None
|
||||
|
||||
if scheme:
|
||||
for dsym in dsyms:
|
||||
if dsym.name == f"{scheme}.app.dSYM":
|
||||
return dsym
|
||||
|
||||
return dsyms[0]
|
||||
|
||||
|
||||
async def upload_ipa(task_id: str, task, config_data: dict, ipa_path: Path, build_dir: Path) -> tuple:
|
||||
@@ -938,3 +952,13 @@ async def upload_ipa(task_id: str, task, config_data: dict, ipa_path: Path, buil
|
||||
return await asyncio.to_thread(publish_ipa, config_data, ipa_path, build_dir)
|
||||
except DistributionError as exc:
|
||||
raise BuildError(str(exc), category="upload") from exc
|
||||
|
||||
|
||||
async def upload_dsym(task_id: str, task, config_data: dict, dsym_path: Path, build_dir: Path) -> str:
|
||||
"""上传 dSYM,返回下载 URL;上传失败仅记录警告,不影响已完成的打包结果"""
|
||||
await log_streamer.emit_step(task_id, "上传 dSYM")
|
||||
try:
|
||||
return await asyncio.to_thread(publish_dsym, config_data, dsym_path, build_dir)
|
||||
except DistributionError as exc:
|
||||
await log_streamer.emit(task_id, f"dSYM 上传失败: {exc}", level="warn")
|
||||
return ""
|
||||
|
||||
@@ -192,6 +192,12 @@ def _write_download_page(config: dict, html: Path, manifest_url: str):
|
||||
)
|
||||
|
||||
|
||||
def _dsym_remote_path(config: dict) -> str:
|
||||
stem = _artifact_stem(config)
|
||||
folder = config.get("OSS_FLODER", "ios-builds").strip("/") or "ios-builds"
|
||||
return posixpath.join(folder, "iOS", f"{stem}.dSYM.zip")
|
||||
|
||||
|
||||
def _remote_paths(config: dict) -> tuple[str, str, str, str]:
|
||||
stem = _artifact_stem(config)
|
||||
folder = config.get("OSS_FLODER", "ios-builds").strip("/") or "ios-builds"
|
||||
@@ -295,6 +301,20 @@ def delete_published_artifacts(
|
||||
deleter(upload_config, remote_paths)
|
||||
|
||||
|
||||
def delete_published_dsym(build_config: dict, upload_config: dict):
|
||||
"""删除任务对应的远端 dSYM 压缩包。
|
||||
|
||||
dSYM 的远端对象键完全由构建配置(APPID/版本/分支/构建类型)决定,
|
||||
无需像 IPA 那样从已保存的下载链接反推,直接按命名规则删除即可。
|
||||
"""
|
||||
mode = upload_config.get("mode", "")
|
||||
if mode not in {"oss", "webdav"}:
|
||||
raise DistributionError("请选择 OSS 或 WebDAV 上传方式")
|
||||
|
||||
deleter = _delete_oss if mode == "oss" else _delete_webdav
|
||||
deleter(upload_config, [_dsym_remote_path(build_config)])
|
||||
|
||||
|
||||
def get_published_download_url(build_config: dict, upload_config: dict, published_url: str) -> str:
|
||||
"""返回用于受控下载的短时链接;WebDAV 保持原公开链接。"""
|
||||
if upload_config.get("mode") != "oss":
|
||||
@@ -409,3 +429,22 @@ def publish_ipa(config: dict, ipa_path: Path, build_dir: Path) -> tuple[str, str
|
||||
qrcode.make(urls[".html"]).save(qr_path)
|
||||
urls.update(uploader(upload, [(qr_path, qr_remote)]))
|
||||
return urls[".html"], urls[".png"]
|
||||
|
||||
|
||||
def publish_dsym(config: dict, dsym_path: Path, build_dir: Path) -> str:
|
||||
"""压缩并上传主 target 的 dSYM,返回下载 URL。"""
|
||||
upload = config.get("_upload_config", {})
|
||||
mode = upload.get("mode", "")
|
||||
if mode not in {"oss", "webdav"}:
|
||||
raise DistributionError("请选择 OSS 或 WebDAV 上传方式")
|
||||
|
||||
output_dir = build_dir / "distribution"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
zip_base = output_dir / f"{_artifact_stem(config)}.dSYM"
|
||||
zip_path = Path(shutil.make_archive(
|
||||
str(zip_base), "zip", root_dir=str(dsym_path.parent), base_dir=dsym_path.name,
|
||||
))
|
||||
|
||||
uploader = _upload_oss if mode == "oss" else _upload_webdav
|
||||
urls = uploader(upload, [(zip_path, _dsym_remote_path(config))])
|
||||
return urls[".zip"]
|
||||
|
||||
Reference in New Issue
Block a user