mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-25 14:15:02 +00:00
187 lines
6.1 KiB
Python
187 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Download and cache the latest release of the `act` CLI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import stat
|
|
import tarfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
RELEASE_URL = "https://api.github.com/repos/nektos/act/releases/latest"
|
|
USER_AGENT = "gh-actions-local-docker"
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ActBinaryError(SystemExit):
|
|
"""Raised when the requested `act` binary cannot be provided."""
|
|
|
|
|
|
def _default_cache_root() -> Path:
|
|
explicit = os.environ.get("ACT_CACHE_DIR")
|
|
if explicit:
|
|
return Path(explicit).expanduser()
|
|
if os.name == "nt":
|
|
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
return base / "act"
|
|
env = os.environ.get("XDG_CACHE_HOME")
|
|
if env:
|
|
return Path(env).expanduser() / "act"
|
|
return Path.home() / ".cache" / "act"
|
|
|
|
|
|
def _expected_asset_name(system: str, machine: str) -> str:
|
|
system = system.lower()
|
|
normalized = machine.lower()
|
|
if system == "linux":
|
|
if normalized in {"x86_64", "amd64"}:
|
|
return "act_Linux_x86_64.tar.gz"
|
|
if normalized in {"aarch64", "arm64"}:
|
|
return "act_Linux_arm64.tar.gz"
|
|
if normalized in {"armv6l", "armv6"}:
|
|
return "act_Linux_armv6.tar.gz"
|
|
if normalized in {"armv7l", "armv7"}:
|
|
return "act_Linux_armv7.tar.gz"
|
|
if normalized in {"i386", "i686", "x86"}:
|
|
return "act_Linux_i386.tar.gz"
|
|
if normalized == "riscv64":
|
|
return "act_Linux_riscv64.tar.gz"
|
|
if system == "darwin":
|
|
if normalized in {"arm64"}:
|
|
return "act_Darwin_arm64.tar.gz"
|
|
if normalized in {"x86_64", "amd64"}:
|
|
return "act_Darwin_x86_64.tar.gz"
|
|
if system == "windows":
|
|
if normalized in {"arm64"}:
|
|
return "act_Windows_arm64.zip"
|
|
if normalized in {"armv7", "armv7l"}:
|
|
return "act_Windows_armv7.zip"
|
|
if normalized in {"i386", "i686", "x86"}:
|
|
return "act_Windows_i386.zip"
|
|
if normalized in {"x86_64", "amd64"}:
|
|
return "act_Windows_x86_64.zip"
|
|
raise ActBinaryError(
|
|
f"Unsupported platform for act binary: system={system} machine={machine}"
|
|
)
|
|
|
|
|
|
def _select_asset(release: dict[str, Any]) -> dict[str, Any]:
|
|
assets = release.get("assets")
|
|
if not isinstance(assets, list):
|
|
raise ActBinaryError("Release metadata missing assets list.")
|
|
asset_name = _expected_asset_name(platform.system(), platform.machine())
|
|
for asset in assets:
|
|
if asset.get("name") == asset_name:
|
|
return asset
|
|
raise ActBinaryError(
|
|
f"Could not find act release asset {asset_name} in {release.get('tag_name')}"
|
|
)
|
|
|
|
|
|
def _fetch_latest_release() -> dict[str, Any]:
|
|
req = urllib.request.Request(
|
|
RELEASE_URL,
|
|
headers={"User-Agent": USER_AGENT},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
if resp.status != 200:
|
|
raise ActBinaryError(
|
|
"Unexpected response while fetching act release metadata."
|
|
)
|
|
return json.load(resp)
|
|
except urllib.error.URLError as exc:
|
|
raise ActBinaryError(f"Failed to download act release metadata: {exc}") from exc
|
|
|
|
|
|
def _download_asset(url: str, dest: Path) -> None:
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as resp, dest.open("wb") as out:
|
|
shutil.copyfileobj(resp, out)
|
|
except urllib.error.URLError as exc:
|
|
raise ActBinaryError(f"Failed to download act asset {url}: {exc}") from exc
|
|
|
|
|
|
def _extract_archive(archive: Path, dest_dir: Path) -> None:
|
|
if archive.name.endswith(".zip"):
|
|
with zipfile.ZipFile(archive, "r") as zf:
|
|
zf.extractall(dest_dir)
|
|
return
|
|
with tarfile.open(archive, "r:gz") as tf:
|
|
tf.extractall(dest_dir)
|
|
|
|
|
|
def _binary_name_for_asset(asset_name: str) -> str:
|
|
if asset_name.lower().endswith(".zip"):
|
|
return "act.exe"
|
|
return "act"
|
|
|
|
|
|
def _set_executable(path: Path) -> None:
|
|
mode = path.stat().st_mode
|
|
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
|
|
def ensure_act_binary(act_path: Path | None = None) -> Path:
|
|
"""Resolve the act binary, downloading it into the cache if necessary."""
|
|
|
|
if act_path:
|
|
resolved = act_path.expanduser().resolve()
|
|
if not resolved.exists():
|
|
raise ActBinaryError(f"act binary not found at {resolved}")
|
|
logger.info("Using act binary from %s", resolved)
|
|
return resolved
|
|
|
|
release = _fetch_latest_release()
|
|
asset = _select_asset(release)
|
|
cache_root = _default_cache_root()
|
|
release_tag = release.get("tag_name", "latest")
|
|
release_dir = cache_root / release_tag
|
|
release_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
asset_name = asset["name"]
|
|
asset_path = release_dir / asset_name
|
|
binary_name = _binary_name_for_asset(asset_name)
|
|
binary_path = release_dir / binary_name
|
|
|
|
if binary_path.exists():
|
|
logger.info("Using cached act binary at %s", binary_path)
|
|
return binary_path
|
|
if not asset_path.exists():
|
|
logger.info("Downloading act asset %s", asset["browser_download_url"])
|
|
_download_asset(asset["browser_download_url"], asset_path)
|
|
logger.info("Downloaded act asset %s", asset_path)
|
|
else:
|
|
logger.debug("Reusing previously downloaded asset %s", asset_path)
|
|
logger.info("Extracting act asset %s", asset_path)
|
|
_extract_archive(asset_path, release_dir)
|
|
if not binary_path.exists():
|
|
raise ActBinaryError("act binary missing after extracting release asset")
|
|
_set_executable(binary_path)
|
|
logger.info("Act binary ready at %s", binary_path)
|
|
return binary_path
|
|
|
|
|
|
def main() -> None:
|
|
logging.basicConfig(level=logging.INFO)
|
|
try:
|
|
path = ensure_act_binary()
|
|
except ActBinaryError as exc:
|
|
raise SystemExit(exc)
|
|
print(path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|