diff options
author | Helmut Grohne <helmut@subdivi.de> | 2022-09-21 13:55:21 +0200 |
---|---|---|
committer | Helmut Grohne <helmut@subdivi.de> | 2022-09-21 13:55:21 +0200 |
commit | 555bfe7c448d4ec1c60ca444bd4f37d41e74db56 (patch) | |
tree | 80d04f8a8a539f6ac391411477a171b3aa8f1886 | |
parent | a6eba58d0c84ae596be527078904447f29622ff1 (diff) | |
download | mdbp-555bfe7c448d4ec1c60ca444bd4f37d41e74db56.tar.gz |
add support for hooks
except for debspawn
-rw-r--r-- | mdbp/build_schema.json | 29 | ||||
-rw-r--r-- | mdbp/common.py | 31 | ||||
-rw-r--r-- | mdbp/debspawn.py | 2 | ||||
-rw-r--r-- | mdbp/mmdebstrap.py | 32 | ||||
-rw-r--r-- | mdbp/pbuilder.py | 31 | ||||
-rw-r--r-- | mdbp/sbuild.py | 40 |
6 files changed, 143 insertions, 22 deletions
diff --git a/mdbp/build_schema.json b/mdbp/build_schema.json index e083654..be90d7b 100644 --- a/mdbp/build_schema.json +++ b/mdbp/build_schema.json @@ -139,6 +139,35 @@ "enum": [ "enable", "disable", "try-disable", "try-enable" ], "description": "Decide whether the build should be able to access the internet. Without this property, the backend picks its own default. A try-prefixed value does not cause a failure when the request cannot be fulfilled." }, + "hooks": { + "type": "array", + "items": { + "type": "object", + "required": [ "type", "command" ], + "additionalProperties": false, + "properties": { + "type": { + "enum": [ "prebuild", "postbuildsuccess", "postbuildfailure" ], + "description": "Specifies when the hook is run. prebuild means before running dpkg-buildpackage and postbuild means after." + }, + "command": { + "type": "string", + "description": "this script is executed using /bin/sh" + }, + "user": { + "enum": [ "root", "builder" ], + "default": "root", + "description": "whether to run the hook as root or as the user performing the build" + }, + "cwd": { + "enum": [ "root", "sourcetree" ], + "default": "root", + "description": "whether to run the hook in the root directory or inside the unpacked source tree" + } + } + }, + "description": "customization hooks for influencing the build using shell scripts" + }, "output": { "type": "object", "required": [ "directory" ], diff --git a/mdbp/common.py b/mdbp/common.py index c03ab37..f22cfdb 100644 --- a/mdbp/common.py +++ b/mdbp/common.py @@ -9,12 +9,14 @@ import importlib.resources import json import multiprocessing import pathlib +import shlex import tarfile import tempfile import typing import urllib.parse import debian.deb822 +import debian.debian_support import requests try: @@ -170,6 +172,14 @@ def get_dsc_files(dscpath: pathlib.Path, return [dscpath.parent / item["name"] for item in (dscobj or parse_dsc(dscpath))["Files"]] + +def build_subdir(source: str, version: str) -> str: + """Compute the subdirectory that dpkg-source normally extracts to.""" + return "%s-%s" % \ + (source, + debian.debian_support.BaseVersion(version).upstream_version) + + def make_option(optname: str, value: typing.Optional[str]) -> typing.List[str]: """Construct a valued option if a value is given.""" if not value: @@ -219,3 +229,24 @@ class AddSpaceSeparatedValues(argparse.Action): option_string: typing.Optional[str] = None) -> None: assert isinstance(values, str) getattr(namespace, self.dest).extend(values.split()) + + +def hook_commands(hook: typing.Dict[str, str], sourcetreedir: str) -> \ + typing.Iterator[str]: + """Generate a sequence of shell commands to run the given hook object. The + hook object is described in build_schema.yaml. The sourcetreedir parameter + specifies the location of the source tree. Its value is assumed to be + properly shell quoted such that variables and globs can be used.""" + user = hook.get("user", "root") + cwd = hook.get("cwd", "root") + if user != "root" or cwd != "root": + yield "cd " + sourcetreedir + if user != "root": + yield "BUILD_USER=$(stat -c %U .)" + if cwd == "root": + yield "cd /" + if user == "root": + yield hook["command"] + else: + yield 'exec runuser -c %s "$BUILD_USER"' % \ + shlex.quote(hook["command"]) diff --git a/mdbp/debspawn.py b/mdbp/debspawn.py index a84bba0..90d111c 100644 --- a/mdbp/debspawn.py +++ b/mdbp/debspawn.py @@ -33,6 +33,8 @@ def main() -> None: raise ValueError("setting lintian options is not supported") if build.get("network") == "disable": raise ValueError("disabling network is not supported") + if build.get("hooks"): + raise ValueError("hooks are not supported") env = compute_env(build) if build.get("build_profiles"): diff --git a/mdbp/mmdebstrap.py b/mdbp/mmdebstrap.py index 9726a03..b3fa161 100644 --- a/mdbp/mmdebstrap.py +++ b/mdbp/mmdebstrap.py @@ -15,10 +15,9 @@ import subprocess import sys import typing -import debian.debian_support - -from .common import JsonObject, buildjson, clean_dir, compute_env, \ - download_dsc, get_dsc_files, json_load, parse_dsc, profile_option +from .common import JsonObject, build_subdir, buildjson, clean_dir, \ + compute_env, download_dsc, get_dsc_files, json_load, parse_dsc, \ + profile_option libc = ctypes.CDLL(ctypes.util.find_library("c")) def unshare_network() -> None: @@ -70,11 +69,6 @@ def native_architecture() -> str: return subprocess.check_output(["dpkg", "--print-architecture"], encoding="ascii").strip() -def build_subdir(dsc: debian.deb822.Dsc) -> str: - """Compute the subdirectory that dpkg-source normally extracts to.""" - ver = debian.debian_support.BaseVersion(dsc["Version"]).upstream_version - assert ver is not None # please mypy - return "%s-%s" % (dsc["Source"], ver) def hook_main(build: JsonObject, chroot: pathlib.Path) -> None: """The entry point for the --hook-helper invocation run from mmdebstrap.""" @@ -97,7 +91,7 @@ def hook_main(build: JsonObject, chroot: pathlib.Path) -> None: chroot=chroot, chdir=builddir, setuid="build") [dscpath] = fullbuilddir.glob(build["input"]["sourcename"] + "_*.dsc") dsc = parse_dsc(dscpath) - subdir = build_subdir(dsc) + subdir = build_subdir(dsc["Source"], dsc["Version"]) priv_drop(["dpkg-source", "--no-check", "--extract", dscpath.name, subdir], setuid="build", chroot=chroot, chdir=builddir) for path in [*get_dsc_files(dscpath, dsc), dscpath]: @@ -127,6 +121,19 @@ def hook_main(build: JsonObject, chroot: pathlib.Path) -> None: '-oDebug::pkgDepCache::AutoInstall=1', '-oDebug::BuildDeps=1'] priv_drop(cmd, chroot=chroot, chdir=builddir / subdir) + env = compute_env(build) + + def run_hooks(hooktype: str) -> None: + for hook in build.get("hooks", ()): + if hook["type"] != hooktype: + continue + priv_drop(["sh", "-c", hook["command"]], chroot=chroot, env=env, + setuid=None if hook.get("user", "root") == "root" + else "build", + chdir="/" if hook.get("cwd", "root") == "root" + else builddir / subdir) + + run_hooks("prebuild") try: priv_drop( [ @@ -140,10 +147,13 @@ def hook_main(build: JsonObject, chroot: pathlib.Path) -> None: setuid="build", privnet=not build.get("network") in ("enable", "try-enable"), chdir=builddir / subdir, - env=compute_env(build), + env=env, ) except subprocess.CalledProcessError as cpe: + run_hooks("postbuildfailure") sys.exit(cpe.returncode) + else: + run_hooks("postbuildsuccess") shutil.rmtree(fullbuilddir / subdir) if build.get("lintian", {}).get("run"): priv_drop([*apt_get, "install", "lintian"], chroot=chroot) diff --git a/mdbp/pbuilder.py b/mdbp/pbuilder.py index e074ed4..09ea202 100644 --- a/mdbp/pbuilder.py +++ b/mdbp/pbuilder.py @@ -12,9 +12,9 @@ import sys import tempfile import typing -from .common import AddSpaceSeparatedValues, buildjson, clean_dir, \ - compute_env, get_dsc, make_option, profile_option, \ - temporary_static_file +from .common import AddSpaceSeparatedValues, build_subdir, buildjson, \ + clean_dir, compute_env, get_dsc, hook_commands, make_option, \ + parse_dsc, profile_option, temporary_static_file def find_basetgz(distribution: str, basedir: str = "/var/cache/pbuiler") -> typing.Optional[str]: @@ -28,6 +28,15 @@ def find_basetgz(distribution: str, raise ValueError("unsupported distribution %s" % distribution) +def sourcetree_location(dscpath: pathlib.Path) -> str: + """Compute a shell expression that represents the source tree location + inside pbuilder. + """ + dsc = parse_dsc(dscpath) + subdir = build_subdir(dsc["Source"], dsc["Version"]) + return '"$BUILDDIR"/' + shlex.quote(subdir) + + @contextlib.contextmanager def hookdir() -> typing.Iterator[ typing.Tuple[pathlib.Path, typing.Callable[[str, str], None]] @@ -131,8 +140,22 @@ runuser -u pbuilder -- lintian %s "${BUILDDIR:-/tmp/buildd}"/*.changes shlex.join(build["lintian"].get("options", [])), ), ) + + dscpath = stack.enter_context(get_dsc(build)) + sourcetree = sourcetree_location(dscpath) + + for hook in build.get("hooks", ()): + addhook({ + "prebuild": "A", + "postbuildsuccess": "B", + "postbuildfailure": "C", + }[hook["type"]], + "#!/bin/sh\n%s\n" % + " || return $?\n".join( + hook_commands(hook, sourcetree))) + cmd.extend(["--hookdir", str(hookdirname), *args.pbuilderopts, - str(stack.enter_context(get_dsc(build)))]) + str(dscpath)]) ret = subprocess.call(cmd, env=compute_env(build), stdout=None if enablelog else subprocess.DEVNULL, diff --git a/mdbp/sbuild.py b/mdbp/sbuild.py index 8330e6e..7041090 100644 --- a/mdbp/sbuild.py +++ b/mdbp/sbuild.py @@ -5,12 +5,15 @@ import argparse import contextlib import pathlib +import shlex import subprocess import sys import typing -from .common import AddSpaceSeparatedValues, buildjson, clean_dir, \ - compute_env, get_dsc, temporary_static_file +from .common import AddSpaceSeparatedValues, build_subdir, buildjson, \ + clean_dir, compute_env, get_dsc, hook_commands, parse_dsc, \ + temporary_static_file + PerlValue = typing.Union[None, str, typing.List[typing.Any], typing.Dict[str, typing.Any]] @@ -45,13 +48,13 @@ def add_external_command( conf: typing.Dict[str, PerlValue], stage: str, command: str ) -> None: """Modify the given conf object to add the given command on the given - external_commands stage. + external_commands stage. The special meaning of % in sbuild is escaped. """ extcomm = conf.setdefault("external_commands", {}) assert isinstance(extcomm, dict) comms = extcomm.setdefault(stage, []) assert isinstance(comms, list) - comms.append(command) + comms.append(command.replace("%", "%%")) def main() -> None: @@ -114,16 +117,39 @@ def main() -> None: "finished-build-commands", "mv /etc/resolv.conf.disabled /etc/resolv.conf", ) + hooknamemap = { + "prebuild": "starting-build-commands", + "postbuildsuccess": "finished-build-commands", + "postbuildfailure": "build-failed-commands", + } with contextlib.ExitStack() as stack: - sbuildconf = stack.enter_context(temporary_static_file(perl_conf(sbc))) - try: thing = build["input"]["sourcename"] + subdir = shlex.quote(thing) + "-*" with contextlib.suppress(KeyError): thing += "_" + build["input"]["version"] + subdir = shlex.quote( + build_subdir( + build["input"]["sourcename"], build["input"]["version"] + ) + ) except KeyError: - thing = str(stack.enter_context(get_dsc(build)).absolute()) + dscpath = stack.enter_context(get_dsc(build)) + thing = str(dscpath.absolute()) + dsc = parse_dsc(dscpath) + subdir = shlex.quote(build_subdir(dsc["Source"], dsc["Version"])) + + for hook in build.get("hooks", ()): + add_external_command( + sbc, + hooknamemap[hook["type"]], + " || return $?; ".join( + hook_commands(hook, "./" + subdir) + ), + ) + + sbuildconf = stack.enter_context(temporary_static_file(perl_conf(sbc))) ret = subprocess.call(["sbuild", *args.sbuildopts, thing], env=dict(SBUILD_CONFIG=str(sbuildconf), |