#!/usr/bin/env python3 """Create a GitHub Actions release package from Rust release binaries.""" from __future__ import annotations import argparse import hashlib import json import shutil import stat import tarfile from datetime import datetime, timezone from pathlib import Path SKIP_DIRS = {"deps", "build", "examples", "incremental"} SKIP_SUFFIXES = {".d", ".rlib", ".rmeta"} def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: while True: chunk = handle.read(1024 * 1024) if not chunk: break digest.update(chunk) return digest.hexdigest() def is_binary(path: Path) -> bool: if not path.is_file(): return False if path.name in SKIP_DIRS: return False if path.suffix in SKIP_SUFFIXES: return False return bool(path.stat().st_mode & stat.S_IXUSR) def collect(release_dir: Path) -> list[Path]: items = [item for item in sorted(release_dir.iterdir()) if is_binary(item)] if not items: raise SystemExit(f"No release binaries found in {release_dir}") return items def write(path: Path, text: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") def copy_release_file(src: Path, dst: Path) -> Path: """Copy file contents without preserving metadata that some mounts reject.""" dst.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(src, dst) try: dst.chmod(src.stat().st_mode & 0o777) except PermissionError: # Some removable/network filesystems reject chmod/utime metadata changes. # The package remains valid because the archive manifest/checksums are # based on file contents, not filesystem timestamps. pass return dst def write_archive_checksum(archive: Path) -> None: write(archive.with_suffix(archive.suffix + ".sha256"), f"{sha256(archive)} {archive.name}\n") def create_compatibility_aliases(out_dir: Path, archive: Path) -> None: """Create both linux-x86_64 and linux_x86_64 artifact paths.""" out_alias = Path(str(out_dir).replace("linux-x86_64", "linux_x86_64")) if out_alias != out_dir: if out_alias.exists(): shutil.rmtree(out_alias) out_alias.mkdir(parents=True) for item in out_dir.iterdir(): if item.is_file(): copy_release_file(item, out_alias / item.name) archive_alias = Path(str(archive).replace("linux-x86_64", "linux_x86_64")) if archive_alias != archive: copy_release_file(archive, archive_alias) write_archive_checksum(archive_alias) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--release-dir", type=Path, required=True) parser.add_argument("--out-dir", type=Path, required=True) parser.add_argument("--archive", type=Path, required=True) parser.add_argument("--target", default="linux-x86_64") parser.add_argument("--commit", default="unknown") parser.add_argument("--ref", default="unknown") parser.add_argument("--run-id", default="unknown") args = parser.parse_args() release_dir = args.release_dir.resolve() out_dir = args.out_dir.resolve() archive = args.archive.resolve() if out_dir.exists(): shutil.rmtree(out_dir) out_dir.mkdir(parents=True) binaries = collect(release_dir) for binary in binaries: copy_release_file(binary, out_dir / binary.name) names = [binary.name for binary in binaries] write(out_dir / "BINARIES.txt", "\n".join(names) + "\n") checksum_lines = [] manifest_binaries = [] for name in names: packaged = out_dir / name digest = sha256(packaged) checksum_lines.append(f"{digest} {name}") manifest_binaries.append( {"name": name, "size_bytes": packaged.stat().st_size, "sha256": digest} ) write(out_dir / "SHA256SUMS.txt", "\n".join(checksum_lines) + "\n") manifest = { "project": "AWatch-rus", "target": args.target, "commit": args.commit, "ref": args.ref, "run_id": args.run_id, "build_time_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"), "binaries": manifest_binaries, } write(out_dir / "BUILD_MANIFEST.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n") archive.parent.mkdir(parents=True, exist_ok=True) with tarfile.open(archive, "w:gz") as tar: tar.add(out_dir, arcname=out_dir.name) write_archive_checksum(archive) create_compatibility_aliases(out_dir, archive) if __name__ == "__main__": main()