summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2022-09-21 13:55:21 +0200
committerHelmut Grohne <helmut@subdivi.de>2022-09-21 13:55:21 +0200
commit555bfe7c448d4ec1c60ca444bd4f37d41e74db56 (patch)
tree80d04f8a8a539f6ac391411477a171b3aa8f1886
parenta6eba58d0c84ae596be527078904447f29622ff1 (diff)
downloadmdbp-555bfe7c448d4ec1c60ca444bd4f37d41e74db56.tar.gz
add support for hooks
except for debspawn
-rw-r--r--mdbp/build_schema.json29
-rw-r--r--mdbp/common.py31
-rw-r--r--mdbp/debspawn.py2
-rw-r--r--mdbp/mmdebstrap.py32
-rw-r--r--mdbp/pbuilder.py31
-rw-r--r--mdbp/sbuild.py40
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),