DazedTL/util/wolfdawn/build_tools.py
2026-07-05 11:06:18 -05:00

215 lines
6.6 KiB
Python

"""Build WolfDawn ``wolf`` binaries from upstream source (temporary shallow clone)."""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import tempfile
import urllib.parse
import urllib.request
from pathlib import Path
from util.wolfdawn import (
WolfDawnError,
_DOWNLOAD_UA,
_GITGUD_HOST,
bundled_binary_path,
)
WOLFDAWN_PROJECT = "zero64801/wolfdawn"
WOLFDAWN_BRANCH = "master"
GITGUD_API = "https://gitgud.io/api/v4"
CLONE_URL = f"{_GITGUD_HOST}/{WOLFDAWN_PROJECT}.git"
_BUILD_TARGETS: dict[str, dict[str, str | None]] = {
"linux": {"triple": None, "exe": "wolf"},
"windows": {"triple": "x86_64-pc-windows-gnu", "exe": "wolf.exe"},
}
def _platform_dir() -> str:
import sys
if sys.platform.startswith("win"):
return "windows"
if sys.platform == "darwin":
return "macos"
return "linux"
def _log(msg: str, log_fn) -> None:
if log_fn:
log_fn(msg)
else:
print(msg, flush=True)
def _project_id() -> str:
return urllib.parse.quote(WOLFDAWN_PROJECT, safe="")
def upstream_commit() -> str:
url = f"{GITGUD_API}/projects/{_project_id()}/repository/commits/{WOLFDAWN_BRANCH}"
req = urllib.request.Request(url, headers={"User-Agent": _DOWNLOAD_UA})
with urllib.request.urlopen(req, timeout=60) as resp:
data = json.loads(resp.read().decode("utf-8"))
commit = data.get("id") or data.get("sha") or ""
if not commit:
raise RuntimeError(f"Could not resolve upstream commit for {WOLFDAWN_PROJECT}")
return commit
def _cargo_available() -> bool:
return shutil.which("cargo") is not None
def _git_available() -> bool:
return shutil.which("git") is not None
def _mingw_linker_available() -> bool:
return bool(shutil.which("x86_64-w64-mingw32-gcc"))
def buildable_from_source(platform: str) -> bool:
"""Return True when ``platform`` can plausibly be built on this machine."""
if not _cargo_available():
return False
host = _platform_dir()
if platform == host:
return True
if platform == "windows" and host == "linux":
return _mingw_linker_available()
return False
def _run(cmd: list[str], *, cwd: Path | None = None, env: dict[str, str] | None = None, log_fn=None) -> bool:
_log(f"$ {' '.join(cmd)}", log_fn)
try:
proc = subprocess.run(
cmd,
cwd=str(cwd) if cwd else None,
env=env,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
except OSError as exc:
_log(f"ERROR: {exc}", log_fn)
return False
if proc.stdout.strip():
for line in proc.stdout.strip().splitlines()[-8:]:
_log(line, log_fn)
if proc.returncode != 0:
err = (proc.stderr or proc.stdout or "").strip()
if err:
for line in err.splitlines()[-8:]:
_log(line, log_fn)
return False
return True
def _ensure_rust_target(triple: str | None, log_fn) -> bool:
if not triple:
return True
return _run(["rustup", "target", "add", triple], log_fn=log_fn)
def clone_source(dest: Path, log_fn=print) -> bool:
"""Shallow-clone WolfDawn ``master`` into ``dest``."""
if not _git_available():
_log("ERROR: git not found; cannot clone WolfDawn source.", log_fn)
return False
if dest.exists():
shutil.rmtree(dest)
return _run(
["git", "clone", "--depth", "1", "--branch", WOLFDAWN_BRANCH, CLONE_URL, str(dest)],
log_fn=log_fn,
)
def _artifact_path(source_dir: Path, platform: str) -> Path:
spec = _BUILD_TARGETS[platform]
triple = spec["triple"]
exe = spec["exe"]
if triple:
return source_dir / "target" / triple / "release" / exe
return source_dir / "target" / "release" / exe
def build_platform(source_dir: Path, platform: str, log_fn=print) -> Path | None:
"""Compile ``wolf`` for ``platform`` inside an existing source checkout."""
if platform not in _BUILD_TARGETS:
_log(f"ERROR: unsupported WolfDawn build platform '{platform}'.", log_fn)
return None
if not buildable_from_source(platform):
_log(f"Skipping source build for '{platform}' on this machine.", log_fn)
return None
spec = _BUILD_TARGETS[platform]
triple = spec["triple"]
if not _ensure_rust_target(triple, log_fn):
return None
cmd = ["cargo", "build", "--release", "-p", "wolf-cli"]
if triple:
cmd += ["--target", triple]
target_dir = source_dir / "target"
env = os.environ.copy()
env["CARGO_TARGET_DIR"] = str(target_dir)
if not _run(cmd, cwd=source_dir, env=env, log_fn=log_fn):
return None
built = _artifact_path(source_dir, platform)
if not built.is_file():
_log(f"ERROR: expected build output missing: {built}", log_fn)
return None
return built
def install_built_binary(artifact: Path, platform: str, log_fn=print) -> Path:
"""Copy a built ``wolf`` executable into the bundled offline path."""
dest = bundled_binary_path(platform)
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(artifact, dest)
try:
dest.chmod(0o755)
except OSError:
pass
_log(f"Installed source-built WolfDawn binary at {dest}", log_fn)
return dest
def build_and_install_platform(source_dir: Path, platform: str, log_fn=print) -> bool:
artifact = build_platform(source_dir, platform, log_fn=log_fn)
if artifact is None:
return False
install_built_binary(artifact, platform, log_fn=log_fn)
return True
def build_and_install_platforms(
platforms: tuple[str, ...] = ("linux", "windows"),
log_fn=print,
) -> dict[str, bool]:
"""Clone upstream source to a temp dir, build each platform, install successes."""
results = {platform: False for platform in platforms}
buildable = [p for p in platforms if buildable_from_source(p)]
if not buildable:
_log("No WolfDawn platforms can be built from source on this machine.", log_fn)
return results
if not _cargo_available():
_log("cargo not found; skipping WolfDawn source builds.", log_fn)
return results
_log(f"Cloning WolfDawn ({WOLFDAWN_BRANCH}) for source build…", log_fn)
with tempfile.TemporaryDirectory(prefix="wolfdawn-src-") as tmp:
source_dir = Path(tmp) / "wolfdawn"
if not clone_source(source_dir, log_fn=log_fn):
return results
for platform in buildable:
results[platform] = build_and_install_platform(source_dir, platform, log_fn=log_fn)
return results