#!/usr/bin/python3 # SPDX-License-Identifier: MIT """mdbp backend using sbuild""" import argparse import contextlib import pathlib import shlex import subprocess import sys import typing 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]] perl_escape_map = { ord("\\"): r"\\", ord('"'): r'\"', ord("@"): r"\@", ord("$"): r"\$", ord("\n"): r"\n", ord("\r"): r"\r", } def perl_repr(value: PerlValue) -> str: """Encode a Python value as a string parseable to Perl.""" if value is None: return "undef" if isinstance(value, bool): return str(int(value)) if isinstance(value, str): return '"%s"' % value.translate(perl_escape_map) if isinstance(value, list): return "[%s]" % ",".join(map(perl_repr, value)) if isinstance(value, dict): return "{%s}" % ",".join("%s => %s" % (perl_repr(key), perl_repr(value)) for key, value in value.items()) raise TypeError("unexpected value type %r" % type(value)) def perl_conf(conf: typing.Dict[str, PerlValue]) -> str: """Turn a mapping into a Perl source assigning variables.""" return "".join("$%s=%s;\n" % (key, perl_repr(value)) for key, value in conf.items()) + "1;\n" 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. 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.replace("%", "%%")) def main() -> None: """Entry point for mdbp-sbuild backend""" parser = argparse.ArgumentParser() parser.add_argument( "--sbuildopt", dest="sbuildopts", action="append", metavar="OPT", default=[], help="a custom option to be passed down to sbuild, can be specified " "multiple times and mixed with --sbuildopts", ) parser.add_argument( "--sbuildopts", action=AddSpaceSeparatedValues, metavar="OPTS", default=[], help="space-separated options to be passed down to sbuild, can be " "specified multiple times and mixed with --sbuildopt", ) parser.add_argument("buildjson", type=buildjson) args = parser.parse_args() build = args.buildjson if build.get("network") == "disable": raise ValueError("disabling network not supported with sbuild") sbc = dict( nolog=True, apt_update=True, apt_distupgrade=True, distribution=build["distribution"], build_arch_all=build.get("type") != "any", build_arch_any=build.get("type") != "all", bd_uninstallable_explainer=build.get("bd-uninstallable-explainer", ""), run_lintian=bool(build.get("lintian", {}).get("run")), extra_repositories=build.get("extrarepositories", []), build_environment=compute_env(build), ) with contextlib.suppress(KeyError): sbc["lintian_opts"] = build["lintian"]["options"] with contextlib.suppress(KeyError): sbc["build_arch"] = build["build_architecture"] with contextlib.suppress(KeyError): sbc["host_arch"] = build["host_architecture"] with contextlib.suppress(KeyError): sbc["build_profiles"] = " ".join(build["build_profiles"]) with contextlib.suppress(KeyError): sbc["build_path"] = build["build_path"] if build.get("network") == "try-disable": add_external_command( sbc, "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: 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: 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) ), ) if build.get("network") == "try-disable": add_external_command( sbc, "starting-build-commands", "mv /etc/resolv.conf /etc/resolv.conf.disabled", ) sbuildconf = stack.enter_context(temporary_static_file(perl_conf(sbc))) ret = subprocess.call(["sbuild", *args.sbuildopts, thing], env=dict(SBUILD_CONFIG=str(sbuildconf), PATH="/usr/bin:/bin"), cwd=build["output"]["directory"], stdout=None if build["output"].get("log", True) else subprocess.DEVNULL, stderr=subprocess.STDOUT if build["output"].get("log", True) else subprocess.DEVNULL) clean_dir(pathlib.Path(build["output"]["directory"]), build["output"].get("artifacts", ["*"])) sys.exit(ret) if __name__ == "__main__": main()