sandcastle-ng

Scripts for the deployment of Sandcastle (GNU Taler)
Log | Files | Refs | README

sandcastle-build-generic (6757B)


      1 #!/usr/bin/env python3
      2 # This file is in the public domain.
      3 """Build one generic package or a group from one pnpm workspace."""
      4 
      5 from __future__ import annotations
      6 
      7 import json
      8 import os
      9 import shutil
     10 import subprocess
     11 import sys
     12 from email.utils import formatdate
     13 from pathlib import Path
     14 
     15 sys.path.insert(0, str(Path(__file__).resolve().parent))
     16 from package_config import ConfigError, load_config
     17 
     18 
     19 def run_cmd(cmd, shell=False, cwd=None, env=None):
     20     command_env = os.environ.copy()
     21     if env:
     22         command_env.update(env)
     23     sys.stdout.flush()
     24     subprocess.check_call(cmd, shell=shell, cwd=cwd, env=command_env)
     25 
     26 
     27 def get_tag_debver(tag):
     28     if tag.startswith("v"):
     29         devsuff = "-dev."
     30         position = tag.find(devsuff)
     31         if position < 0:
     32             return tag[1:]
     33         return tag[1:position] + "~dev" + tag[position + len(devsuff) :]
     34     if tag.startswith("deb-v"):
     35         tag = tag[5:]
     36         if "-" in tag:
     37             version, revision = tag.split("-", 1)
     38             return version + "-" + revision
     39         return tag
     40     raise ValueError(f"unexpected tag format: {tag}")
     41 
     42 
     43 def make_codename_version(deb_version, codename):
     44     if "-" in deb_version:
     45         return f"{deb_version}+{codename}"
     46     return f"{deb_version}-0+{codename}"
     47 
     48 
     49 def scan_local_repository():
     50     os.chdir("/pkgdir")
     51     run_cmd(
     52         ["bash", "-o", "pipefail", "-c", "dpkg-scanpackages . /dev/null | xz - > Packages.xz"]
     53     )
     54     Path("/etc/apt/sources.list.d/taler-packaging-local.list").write_text(
     55         "deb [trusted=yes] file:/pkgdir ./\n", encoding="utf-8"
     56     )
     57     run_cmd(["apt-get", "update"])
     58 
     59 
     60 def validate_group(config, names):
     61     if not names or len(set(names)) != len(names):
     62         raise ConfigError("packages must be specified once")
     63     packages = []
     64     keys = set()
     65     for name in names:
     66         try:
     67             package = config.packages[name]
     68         except KeyError as exc:
     69             raise ConfigError(f"unknown package {name!r}") from exc
     70         repository = config.repository_for(package)
     71         builder = config.builder_for(package)
     72         keys.add((repository.url, package.tag, builder))
     73         packages.append(package)
     74     if len(keys) != 1:
     75         raise ConfigError("grouped packages must have the same repository, tag, and builder")
     76     repository_url, tag, builder = keys.pop()
     77     if builder == "generic" and len(packages) != 1:
     78         raise ConfigError("the generic builder accepts exactly one package")
     79     return packages, repository_url, tag, builder
     80 
     81 
     82 def install_build_dependencies(source_dir, package_paths):
     83     tool = "apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes"
     84     controls = [str(package_path / "debian" / "control") for package_path in package_paths]
     85     run_cmd(["mk-build-deps", "--install", f"--tool={tool}", *controls], cwd=source_dir)
     86 
     87 
     88 def prepare_pnpm_workspace(source_dir, package_paths):
     89     filters = []
     90     for package_path in package_paths:
     91         package_json = package_path / "package.json"
     92         with package_json.open(encoding="utf-8") as package_file:
     93             workspace_name = json.load(package_file).get("name")
     94         if not isinstance(workspace_name, str) or not workspace_name:
     95             raise ConfigError(f"{package_json} has no package name")
     96         filters.extend(["--filter", f"{workspace_name}..."])
     97     run_cmd(["pnpm", "install", "--frozen-lockfile", *filters], cwd=source_dir)
     98     run_cmd(["pnpm", "run", *filters, "build"], cwd=source_dir)
     99 
    100 
    101 def write_changelog(package_path, package_name, version):
    102     changelog = f"""\
    103 {package_name} ({version}) unstable; urgency=low
    104 
    105   * Release {version} (for sandcastle-ng).
    106 
    107  -- Taler Packaging Team <deb@taler.net>  {formatdate(localtime=True)}
    108 """
    109     (package_path / "debian" / "changelog").write_text(changelog, encoding="utf-8")
    110 
    111 
    112 def build_package(package, package_path, version, prebuilt):
    113     write_changelog(package_path, package.name, version)
    114     debug_repository = package_path / "debian" / ".debhelper"
    115     debug_repository.mkdir(parents=True, exist_ok=True)
    116     (debug_repository / "debian-symbols-pool").touch()
    117     environment = {
    118         "DEB_BUILD_MAINT_OPTIONS": "debug",
    119         "DEB_DBG_SYMBOLS_REPO": "debian/.debhelper/",
    120     }
    121     if prebuilt:
    122         environment["TALER_PACKAGING_PREBUILT"] = "1"
    123 
    124     output_dir = package_path.parent
    125     before = set(output_dir.glob("*.deb"))
    126     run_cmd(
    127         ["dpkg-buildpackage", "-rfakeroot", "-b", "-uc", "-us"],
    128         cwd=package_path,
    129         env=environment,
    130     )
    131     deb_files = sorted(set(output_dir.glob("*.deb")) - before)
    132     if not deb_files:
    133         raise RuntimeError(f"build of {package.name} produced no .deb packages")
    134     destination = Path("/out") / package.name
    135     for deb_file in deb_files:
    136         shutil.copy(deb_file, destination)
    137 
    138 
    139 def build_packages(codename, architecture, names):
    140     del architecture  # Package metadata determines whether outputs are arch-specific.
    141     os.environ.pop("LD_LIBRARY_PATH", None)
    142     config = load_config("/packages.toml")
    143     packages, repository_url, tag, builder = validate_group(config, names)
    144     print(f"Building {' '.join(names)} with {builder} build logic", file=sys.stderr)
    145 
    146     scan_local_repository()
    147     source_dir = Path("/build/source")
    148     source_dir.parent.mkdir(parents=True, exist_ok=True)
    149     run_cmd(["git", "config", "--global", "advice.detachedHead", "false"])
    150     run_cmd(
    151         ["git", "clone", "--depth=1", f"--branch={tag}", repository_url, str(source_dir)]
    152     )
    153     run_cmd(["./bootstrap"], cwd=source_dir)
    154 
    155     version = make_codename_version(get_tag_debver(tag), codename)
    156     package_paths = [source_dir / package.debian_path for package in packages]
    157     for package, package_path in zip(packages, package_paths):
    158         control = package_path / "debian" / "control"
    159         if not control.is_file():
    160             raise ConfigError(f"{package.name} has no debian/control at tag {tag}: {control}")
    161         (package_path / ".version").write_text(version + "\n", encoding="utf-8")
    162 
    163     install_build_dependencies(source_dir, package_paths)
    164     if builder == "pnpm-workspace":
    165         prepare_pnpm_workspace(source_dir, package_paths)
    166     for package, package_path in zip(packages, package_paths):
    167         build_package(package, package_path, version, builder == "pnpm-workspace")
    168 
    169 
    170 def main():
    171     if len(sys.argv) < 4:
    172         print(
    173             f"Usage: {sys.argv[0]} <CODENAME> <ARCH> <PACKAGE>...",
    174             file=sys.stderr,
    175         )
    176         return 1
    177     try:
    178         build_packages(sys.argv[1], sys.argv[2], sys.argv[3:])
    179     except ConfigError as exc:
    180         print(f"configuration error: {exc}", file=sys.stderr)
    181         return 1
    182     return 0
    183 
    184 
    185 if __name__ == "__main__":
    186     raise SystemExit(main())