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())