asfml 0.1.0

CLI for reading Apache Pony Mail archives
#!/usr/bin/env python3
"""Stage asfml npm tarballs for release."""

from __future__ import annotations

import argparse
import importlib.util
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
BUILD_SCRIPT = REPO_ROOT / "scripts" / "build_npm_package.py"

_SPEC = importlib.util.spec_from_file_location("asfml_build_npm_package", BUILD_SCRIPT)
if _SPEC is None or _SPEC.loader is None:
    raise RuntimeError(f"Unable to load module from {BUILD_SCRIPT}")
_BUILD_MODULE = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(_BUILD_MODULE)

PACKAGE_NATIVE_COMPONENTS = getattr(_BUILD_MODULE, "PACKAGE_NATIVE_COMPONENTS", {})
PACKAGE_EXPANSIONS = getattr(_BUILD_MODULE, "PACKAGE_EXPANSIONS", {})
PLATFORM_PACKAGES = getattr(_BUILD_MODULE, "PLATFORM_PACKAGES", {})


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--release-version", required=True)
    parser.add_argument("--package", dest="packages", action="append", default=None)
    parser.add_argument("--vendor-src", type=Path, required=True)
    parser.add_argument("--output-dir", type=Path, default=REPO_ROOT / "dist" / "npm")
    parser.add_argument("--keep-staging-dirs", action="store_true")
    return parser.parse_args()


def expand_packages(packages: list[str]) -> list[str]:
    expanded: list[str] = []
    for package in packages:
        for expanded_package in PACKAGE_EXPANSIONS.get(package, [package]):
            if expanded_package not in expanded:
                expanded.append(expanded_package)
    return expanded


def collect_native_components(packages: list[str]) -> set[str]:
    components: set[str] = set()
    for package in packages:
        components.update(PACKAGE_NATIVE_COMPONENTS.get(package, []))
    return components


def tarball_name_for_package(package: str, version: str) -> str:
    if package in PLATFORM_PACKAGES:
        platform = package.removeprefix("asfml-")
        return f"asfml-npm-{platform}-{version}.tgz"
    return f"asfml-npm-{version}.tgz"


def run_command(cmd: list[str]) -> None:
    print("+", " ".join(cmd))
    subprocess.run(cmd, cwd=REPO_ROOT, check=True)


def main() -> int:
    args = parse_args()
    packages = args.packages or ["asfml"]
    expanded_packages = expand_packages(packages)
    native_components = collect_native_components(expanded_packages)

    if native_components and not args.vendor_src.exists():
        raise RuntimeError(f"Vendor source directory not found: {args.vendor_src}")

    output_dir = args.output_dir.resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    for package in expanded_packages:
        staging_dir = Path(tempfile.mkdtemp(prefix=f"npm-stage-{package}-"))
        pack_output = output_dir / tarball_name_for_package(package, args.release_version)
        cmd = [
            sys.executable,
            str(BUILD_SCRIPT),
            "--package",
            package,
            "--release-version",
            args.release_version,
            "--staging-dir",
            str(staging_dir),
            "--pack-output",
            str(pack_output),
        ]
        if PACKAGE_NATIVE_COMPONENTS.get(package):
            cmd.extend(["--vendor-src", str(args.vendor_src.resolve())])

        try:
            run_command(cmd)
        finally:
            if not args.keep_staging_dirs:
                shutil.rmtree(staging_dir, ignore_errors=True)

    return 0


if __name__ == "__main__":
    raise SystemExit(main())