mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 15:13:51 +08:00
- 将各平台驱动拆分为独立 ZIP,并为 DuckDB 同包携带运行库 - 仅生成 ZIP 下载候选,按 Gatewaysentry 与 GitHub 短测速结果调整优先级 - 扩展发布索引的归档摘要与条目元数据,强化跨平台资产校验 - 禁止裸驱动与 CI 总包进入 VPS 镜像,并校验载荷摘要和索引一致性 - 显式控制驱动镜像发布,自动清理失败暂存、过期目录和旧版本
172 lines
5.8 KiB
Python
172 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
LEGAL_FILENAMES = ("LICENSE", "NOTICE")
|
|
DRIVER_AGENT_RE = re.compile(
|
|
r"^.+-driver-agent(?:-v[0-9]+)?-(?:darwin|linux|windows)-(?:amd64|arm64)(?:\.exe)?$"
|
|
)
|
|
DUCKDB_WINDOWS_AGENT = "duckdb-driver-agent-windows-amd64.exe"
|
|
DUCKDB_WINDOWS_LIBRARY = "duckdb.dll"
|
|
|
|
|
|
def individual_archive_name(asset_name: str) -> str:
|
|
if asset_name.lower().endswith(".exe"):
|
|
return asset_name[:-4] + ".zip"
|
|
return asset_name + ".zip"
|
|
|
|
|
|
def sha256_file(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def write_individual_archive(
|
|
archive_path: Path,
|
|
asset: Path,
|
|
arcname: str,
|
|
legal_files: list[Path],
|
|
) -> list[tuple[Path, str]]:
|
|
entries = [(asset, arcname)]
|
|
if asset.name == DUCKDB_WINDOWS_AGENT:
|
|
support_file = asset.with_name(DUCKDB_WINDOWS_LIBRARY)
|
|
if not support_file.is_file():
|
|
raise RuntimeError(
|
|
f"DuckDB Windows runtime dependency not found: {support_file}"
|
|
)
|
|
entries.append(
|
|
(
|
|
support_file,
|
|
(Path(arcname).parent / DUCKDB_WINDOWS_LIBRARY).as_posix(),
|
|
)
|
|
)
|
|
|
|
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
for source, entry_name in entries:
|
|
zf.write(source, entry_name)
|
|
for legal_file in legal_files:
|
|
zf.write(legal_file, legal_file.name)
|
|
return entries
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
raise SystemExit("usage: package-driver-release-assets.py <drivers-dir> <output-dir>")
|
|
|
|
drivers_dir = Path(sys.argv[1]).resolve()
|
|
output_dir = Path(sys.argv[2]).resolve()
|
|
|
|
if not drivers_dir.is_dir():
|
|
raise SystemExit(f"drivers dir not found: {drivers_dir}")
|
|
|
|
legal_files = [ROOT / filename for filename in LEGAL_FILENAMES]
|
|
missing_legal_files = [path for path in legal_files if not path.is_file()]
|
|
if missing_legal_files:
|
|
missing = ", ".join(str(path) for path in missing_legal_files)
|
|
raise SystemExit(f"legal notice files not found: {missing}")
|
|
|
|
out_name = "GoNavi-DriverAgents.zip"
|
|
index_name = "GoNavi-DriverAgents-Index.json"
|
|
manifest_name = "GoNavi-DriverAgents-Manifest.json"
|
|
|
|
if output_dir.exists():
|
|
shutil.rmtree(output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
out_path = output_dir / out_name
|
|
index_path = output_dir / index_name
|
|
manifest_path = output_dir / manifest_name
|
|
|
|
size_index = {}
|
|
archive_sha256_index = {}
|
|
entry_index = {}
|
|
individual_archives = []
|
|
source_assets = []
|
|
with zipfile.ZipFile(out_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
for asset in sorted(drivers_dir.rglob("*")):
|
|
if not asset.is_file():
|
|
continue
|
|
arcname = asset.relative_to(drivers_dir).as_posix()
|
|
if any(existing.name == asset.name for existing, _ in source_assets):
|
|
raise RuntimeError(f"driver asset name conflict: {asset.name}")
|
|
# Dedicated source packages are regenerated below from their raw
|
|
# agent and support files. Keeping a zip inside the CI bundle only
|
|
# wastes space and is not needed by the completion step.
|
|
if asset.suffix.lower() == ".zip":
|
|
continue
|
|
zf.write(asset, arcname)
|
|
source_assets.append((asset, arcname))
|
|
|
|
for legal_file in legal_files:
|
|
zf.write(legal_file, legal_file.name)
|
|
standalone_path = output_dir / legal_file.name
|
|
if standalone_path.exists():
|
|
raise RuntimeError(f"release asset already exists: {standalone_path}")
|
|
shutil.copy2(legal_file, standalone_path)
|
|
|
|
for asset, arcname in source_assets:
|
|
if not DRIVER_AGENT_RE.fullmatch(asset.name):
|
|
continue
|
|
archive_name = individual_archive_name(asset.name)
|
|
archive_path = output_dir / archive_name
|
|
if archive_path.exists():
|
|
raise RuntimeError(f"release asset already exists: {archive_path}")
|
|
archive_entries = write_individual_archive(
|
|
archive_path,
|
|
asset,
|
|
arcname,
|
|
legal_files,
|
|
)
|
|
size_index[archive_name] = archive_path.stat().st_size
|
|
archive_sha256_index[archive_name] = sha256_file(archive_path)
|
|
for source, entry_path in archive_entries:
|
|
if source.name in entry_index:
|
|
raise RuntimeError(f"driver archive entry name conflict: {source.name}")
|
|
entry_index[source.name] = {
|
|
"archive": archive_name,
|
|
"path": entry_path,
|
|
"size": source.stat().st_size,
|
|
"sha256": sha256_file(source),
|
|
}
|
|
individual_archives.append(archive_name)
|
|
|
|
if not individual_archives:
|
|
raise RuntimeError("no driver agent binaries found")
|
|
|
|
index_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"assets": size_index,
|
|
"assetSha256": archive_sha256_index,
|
|
"entries": entry_index,
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
print(f"created {out_name} size={out_path.stat().st_size} bytes")
|
|
print(f"created {index_name} entries={len(size_index)}")
|
|
print(f"published individual driver archives={len(individual_archives)}")
|
|
print(f"bundled legal files={len(legal_files)}")
|
|
print(f"reserved manifest output path: {manifest_path}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|