bin/create-repository-set.py

#!/usr/bin/env python3
"""Erzeugt eine lokale Repository-ZIP-Menge ueber engineering-tools."""
from __future__ import annotations

import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path


def parse_repositories(values: list[str]) -> list[tuple[str, Path]]:
    repositories: list[tuple[str, Path]] = []
    names: set[str] = set()
    for value in values:
        if "=" not in value:
            raise ValueError(f"Repository muss NAME=PFAD verwenden: {value}")
        name, raw_path = value.split("=", 1)
        if not re.fullmatch(r"[A-Za-z0-9._-]+", name) or name in names:
            raise ValueError(f"Ungueltiger oder doppelter Repositoryname: {name}")
        source = Path(raw_path).expanduser().resolve()
        if not source.is_dir():
            raise ValueError(f"Repositorypfad ist kein Verzeichnis: {source}")
        names.add(name)
        repositories.append((name, source))
    if not repositories:
        raise ValueError("Mindestens ein Repository ist erforderlich")
    return repositories


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repository", action="append", default=[], metavar="NAME=PFAD")
    parser.add_argument("--output-dir", required=True, type=Path)
    parser.add_argument(
        "--tool",
        type=Path,
        default=Path(
            os.environ.get(
                "NS_DETERMINISTIC_ZIP_TOOL",
                str(Path.home() / "engineering/engineering-tools/60-packaging/deterministic_zip.py"),
            )
        ),
    )
    args = parser.parse_args()
    try:
        repositories = parse_repositories(args.repository)
        tool = args.tool.expanduser().resolve()
        if not tool.is_file():
            raise ValueError(f"Deterministisches ZIP-Werkzeug nicht gefunden: {tool}")
        output = args.output_dir.expanduser().resolve()
        if output.exists():
            raise ValueError(f"Ausgabeziel existiert bereits: {output}")
        output.parent.mkdir(parents=True, exist_ok=True)
        staging = Path(tempfile.mkdtemp(prefix=f".{output.name}-", dir=output.parent))
        try:
            for name, source in repositories:
                archive = staging / f"{name}.zip"
                # Lokale Download-Historie ist weder Quellcode noch eine
                # fachliche Quelle des Workspace-Roundtrips. Sie bleibt auf
                # dem Rechner und darf Uploadpakete nicht aufblÀhen.
                excludes = ["--exclude", "downloads/**"] if name == "workspace" else []
                result = subprocess.run(
                    [sys.executable, str(tool), str(source), str(archive), *excludes],
                    check=False,
                )
                if result.returncode != 0:
                    raise ValueError(f"Repository-ZIP-Erzeugung fehlgeschlagen: {name}")
                if not zipfile.is_zipfile(archive):
                    raise ValueError(f"Repository-ZIP ist ungueltig: {archive}")
                with zipfile.ZipFile(archive) as handle:
                    if handle.testzip() is not None:
                        raise ValueError(f"Repository-ZIP ist beschaedigt: {archive}")
            os.replace(staging, output)
        except Exception:
            shutil.rmtree(staging, ignore_errors=True)
            raise
        print(f"Repository-Menge erzeugt: {output} ({len(repositories)} Repositorys)")
        return 0
    except (OSError, ValueError) as exc:
        parser.exit(2, f"FEHLER: {exc}\n")


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