Files
retroSoC/scripts/install_toolchain.py
T

122 lines
4.5 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import posixpath
import shutil
import stat
import sys
import tarfile
import tempfile
from itertools import chain
from pathlib import Path, PurePosixPath
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.dependency_lock import DEFAULT_LOCK, load_lock # noqa: E402
from scripts.setup_helpers import atomic_write, download_file # noqa: E402
def safe_extract(archive: Path, destination: Path) -> None:
with tarfile.open(archive, "r:*") as bundle:
for member in bundle.getmembers():
member_path = PurePosixPath(member.name)
if member_path.is_absolute() or ".." in member_path.parts:
raise ValueError(f"unsafe archive member path: {member.name}")
if member.ischr() or member.isblk() or member.isfifo():
raise ValueError(f"unsupported archive member type: {member.name}")
if member.issym() or member.islnk():
link = PurePosixPath(member.linkname)
combined = member_path.parent / link if member.issym() else link
normalized = PurePosixPath(posixpath.normpath(str(combined)))
escapes_root = normalized.parts[:1] == ("..",)
if link.is_absolute() or normalized.is_absolute() or escapes_root:
raise ValueError(
f"unsafe archive link: {member.name} -> {member.linkname}"
)
bundle.extractall(destination)
def make_tree_world_readable(root: Path) -> None:
"""Make a public tool archive usable by accounts other than its installer."""
for path in chain((root,), root.rglob("*")):
if path.is_symlink():
continue
mode = stat.S_IMODE(path.stat().st_mode)
if path.is_dir():
path.chmod(mode | 0o555)
elif path.is_file():
shared_mode = mode | 0o444
if mode & 0o111:
shared_mode |= 0o111
path.chmod(shared_mode)
def install(name: str, spec: dict[str, Any], cache: Path, update: bool) -> Path:
downloads = cache / "downloads"
archive = downloads / spec["archive"]
download_file(
spec["url"],
archive,
spec["sha256"],
update=update,
timeout=spec.get("download_timeout_seconds", 120),
resume=spec.get("resume", False),
)
destination = cache / "toolchains" / f"{name}-{spec['version']}"
marker = destination / ".complete"
if marker.is_file() and marker.read_text(encoding="utf-8").strip() == spec["sha256"]:
make_tree_world_readable(destination)
return destination / spec["path"]
destination.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=f".{destination.name}.", dir=destination.parent) as temp:
extracted = Path(temp) / "content"
extracted.mkdir()
safe_extract(archive, extracted)
atomic_write(extracted / ".complete", spec["sha256"] + "\n")
make_tree_world_readable(extracted)
if destination.exists():
shutil.rmtree(destination)
os.replace(extracted, destination)
return destination / spec["path"]
def main() -> int:
parser = argparse.ArgumentParser(description="Install pinned retroSoC toolchains")
parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK)
parser.add_argument("--platform", default="ubuntu-22.04")
parser.add_argument("--cache", type=Path, required=True)
parser.add_argument("--tool", action="append", required=True)
parser.add_argument("--github-path", type=Path)
parser.add_argument("--update", action="store_true")
args = parser.parse_args()
lock = load_lock(args.lock)
try:
available = lock["toolchains"][args.platform]
except KeyError as error:
raise SystemExit(f"unsupported toolchain platform: {args.platform}") from error
paths: list[Path] = []
for name in args.tool:
if name not in available:
raise SystemExit(f"toolchain is not locked for {args.platform}: {name}")
path = install(name, available[name], args.cache.resolve(), args.update)
paths.append(path)
print(f"{name}: {path}")
if args.github_path:
args.github_path.parent.mkdir(parents=True, exist_ok=True)
with args.github_path.open("a", encoding="utf-8") as stream:
for path in paths:
stream.write(str(path.resolve()) + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())