from __future__ import annotations
import argparse
import hashlib
import os
import stat
import tarfile
import tempfile
from pathlib import Path
from typing import Dict, Iterable, List, Tuple
try:
import tomllib except ModuleNotFoundError: import tomli as tomllib
HEADER = "# Auto-generated artifact bindings. Update via `scripts/update_artifacts_from_metadata.py` or CI."
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--metadata-dir", required=True, help="Directory containing *.toml metadata files")
parser.add_argument("--release-tag", required=True, help="Git tag for the release (e.g. v1.0.0)")
parser.add_argument("--repo", required=True, help="GitHub repo in the form owner/name")
parser.add_argument(
"--artifacts-toml",
default="julia/PlotPx/Artifacts.toml",
help="Destination Artifacts.toml path to rewrite",
)
return parser.parse_args()
def list_metadata_files(metadata_dir: Path) -> List[Path]:
files: List[Path] = []
for root, _, filenames in os.walk(metadata_dir):
for filename in filenames:
if filename.endswith(".toml"):
files.append(Path(root, filename))
files.sort()
if not files:
raise FileNotFoundError(f"No metadata files found under {metadata_dir}")
return files
def platform_fields(triple: str) -> Dict[str, str]:
normalized = triple.strip().lower()
if normalized in {"x86_64-unknown-linux-gnu", "x86_64-linux-gnu"}:
return {"arch": "x86_64", "os": "linux", "libc": "glibc"}
if normalized in {"aarch64-apple-darwin", "arm64-apple-darwin"}:
return {"arch": "aarch64", "os": "macos"}
if normalized in {"x86_64-pc-windows-msvc", "x86_64-w64-mingw32"}:
return {"arch": "x86_64", "os": "windows", "call": "msvc"}
raise ValueError(f"Unsupported platform triple: {triple}")
def compute_blob_hash(data: bytes, executable: bool) -> Tuple[bytes, bytes]:
mode = b"100755" if executable else b"100644"
header = b"blob " + str(len(data)).encode("ascii") + b"\0"
digest = hashlib.sha1(header + data).digest()
return mode, digest
def compute_tree_hash(root: Path) -> str:
def walk_directory(path: Path) -> bytes:
entries: List[bytes] = []
for name in sorted(os.listdir(path)):
full = path / name
if os.path.islink(full):
raise ValueError(f"Symlinks are not supported in artifacts: {full}")
if full.is_dir():
subtree = walk_directory(full)
mode = b"040000"
digest = hashlib.sha1(subtree).digest()
else:
with open(full, "rb") as fh:
data = fh.read()
executable = bool(os.stat(full).st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
mode, digest = compute_blob_hash(data, executable)
entry = mode + b" " + name.encode("utf-8") + b"\0" + digest
entries.append(entry)
content = b"".join(entries)
header = b"tree " + str(len(content)).encode("ascii") + b"\0"
return header + content
tree = walk_directory(root)
return hashlib.sha1(tree).hexdigest()
def extract_tarball(tarball: Path, dest: Path) -> None:
name = tarball.name.lower()
mode = "r:gz" if name.endswith(".tar.gz") or name.endswith(".tgz") else "r"
with tarfile.open(tarball, mode) as tf:
tf.extractall(dest)
def artifact_entry(tarball: Path, triple: str, url: str, sha256: str) -> Dict[str, object]:
with tempfile.TemporaryDirectory() as tmpdir:
extract_tarball(tarball, Path(tmpdir))
tree_hash = compute_tree_hash(Path(tmpdir))
entry: Dict[str, object] = platform_fields(triple)
entry["git-tree-sha1"] = tree_hash
entry["download"] = [{"url": url, "sha256": sha256}]
return entry
def write_artifacts(path: Path, artifacts: Dict[str, List[Dict[str, object]]]) -> None:
lines: List[str] = [HEADER, ""]
for name in sorted(artifacts):
for entry in artifacts[name]:
lines.append(f"[[{name}]]")
for key in ("arch", "os", "libc", "call", "git-tree-sha1"):
if key in entry:
lines.append(f"{key} = \"{entry[key]}\"")
for download in entry.get("download", []):
lines.append(f"[[{name}.download]]")
lines.append(f"url = \"{download['url']}\"")
lines.append(f"sha256 = \"{download['sha256']}\"")
lines.append("")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines), encoding="utf-8")
def load_metadata(metadata_files: Iterable[Path], repo: str, release_tag: str) -> Dict[str, List[Dict[str, object]]]:
artifacts: Dict[str, List[Dict[str, object]]] = {}
for meta_path in metadata_files:
with meta_path.open("rb") as fh:
data = tomllib.load(fh)
name = data.get("name", "plotpx")
triple = data["platform"]
archive = data["archive"]
sha256 = data["sha256"]
tarball_path = meta_path.parent / archive
if not tarball_path.exists():
raise FileNotFoundError(f"Tarball {tarball_path} referenced by {meta_path} was not found")
url = f"https://github.com/{repo}/releases/download/{release_tag}/{archive}"
entry = artifact_entry(tarball_path, triple, url, sha256)
artifacts.setdefault(name, []).append(entry)
return artifacts
def main() -> None:
args = parse_args()
metadata_dir = Path(args.metadata_dir)
metadata_files = list_metadata_files(metadata_dir)
artifacts = load_metadata(metadata_files, args.repo, args.release_tag)
write_artifacts(Path(args.artifacts_toml), artifacts)
if __name__ == "__main__":
main()