diff options
Diffstat (limited to 'mdbp/mmdebstrap.py')
-rw-r--r-- | mdbp/mmdebstrap.py | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/mdbp/mmdebstrap.py b/mdbp/mmdebstrap.py new file mode 100644 index 0000000..df96e47 --- /dev/null +++ b/mdbp/mmdebstrap.py @@ -0,0 +1,152 @@ +#!/usr/bin/python3 +# SPDX-License-Identifier: MIT +"""mdbp backend using mmdebstrap""" + +import argparse +import pathlib +import shlex +import subprocess +import tempfile +import typing + +from .common import buildjson, compute_env, get_dsc, get_dsc_files, \ + profile_option + +# pylint: disable=W0102 # as we do not modify env +def priv_drop(cmd: typing.List[str], *, chroot: bool = False, + chdir: typing.Optional[str] = None, + setuid: typing.Optional[str] = None, + privnet: bool = False, + env: typing.Dict[str, str] = {}) -> str: + """Returns shlext.join(cmd) with certain privilege dropping operations + applied: + * When chroot is True, chroot to "$1". + * When chdir is set, chdir to it after chrooting. Should be absolute. + * When setuid is set, change the user id and group id. Names allowed. + * When privnet is True, create a new network namespace with an active + loopback interface. + * Environment variables from env will be set. + """ + if chdir or env: + envcmd = ["env"] + if chdir: + envcmd.extend(["--chdir", chdir]) + envcmd.extend(map("%s=%s".__mod__, env.items())) + cmd = envcmd + cmd + cmdstring = "" + if setuid or chroot: + cmdstring = "chroot " + if setuid: + cmdstring += "--userspec %s " % \ + shlex.quote("%s:%s" % (setuid, setuid)) + cmdstring += '"$1" ' if chroot else "/ " + cmdstring += shlex.join(cmd) + if privnet: + cmd = ["unshare", "--net", + "sh", "-c", "ip link set dev lo up && " + cmdstring] + cmdstring = shlex.join(cmd) + if chroot: + cmdstring += ' exec "$1"' + return cmdstring + +def main() -> None: + """Entry point for mdbp-mmdebstrap backend""" + parser = argparse.ArgumentParser() + parser.add_argument("--mirror", type=str, action="store", + help="mirror url to fetch packages from") + parser.add_argument("buildjson", type=buildjson) + args = parser.parse_args() + build = args.buildjson + + if build.get("lintian", {}).get("run"): + raise ValueError("running lintian not supported") + if build.get("bd-uninstallable-explainer") not in (None, "apt"): + raise ValueError("bd-uinstallable-explainer %r not supported" % + build.get("bd-uinstallable-explainer")) + + buildarch = build.get("buildarch") or \ + subprocess.check_output(["dpkg", "--print-architecture"], + encoding="ascii").strip() + hostarch = build.get("hostarch") or buildarch + + if buildarch == hostarch: + buildessential = set(("build-essential",)) + else: + buildessential = set(("crossbuild-essential-" + hostarch, + "libc-dev:" + hostarch, + "libstdc++-dev:" + hostarch)) + buildpath = pathlib.PurePath(build.get("buildpath", "/build/build")) + + with get_dsc(build) as dscpath, \ + tempfile.NamedTemporaryFile("w+") as script: + dscfiles = [dscpath] + get_dsc_files(dscpath) + + script.write("#!/bin/sh\nset -u\nset -e\n") + script.write("%s\n" % priv_drop(["chown", "-R", "build:build", + str(buildpath.parent)], + chroot=True)) + cmd = ["apt-get", "build-dep", "--yes", + "--host-architecture", hostarch] + cmd.extend(dict(any=["--arch-only"], + all=["--indep-only"]).get(build.get("type"), [])) + cmd.extend(profile_option(build, "--build-profiles")) + cmd.append(str(buildpath.parent / dscpath.name)) + if build.get("bd-uninstallable-explainer") == "apt": + script.write("if ! %s\nthen\n" % priv_drop(cmd, chroot=True)) + cmd[-1:-1] = ['-oDebug::pkgProblemResolver=true', + '-oDebug::pkgDepCache::Marker=1', + '-oDebug::pkgDepCache::AutoInstall=1', + '-oDebug::BuildDeps=1'] + script.write("%s\nfi\n" % priv_drop(cmd, chroot=True)) + else: + script.write("%s\n" % priv_drop(cmd, chroot=True)) + script.write("%s\n" % priv_drop( + ["dpkg-source", "--no-check", "--extract", dscpath.name, + buildpath.name], + setuid="build", chroot=True, chdir=str(buildpath.parent))) + script.write("%s\n" % priv_drop(["rm"] + [f.name for f in dscfiles], + chroot=True, + chdir=str(buildpath.parent))) + cmd = ["dpkg-buildpackage", "-uc", "--host-arch=" + hostarch, + "--build=" + build.get("type", "binary")] + cmd.extend(profile_option(build, "--build-profiles=")) + script.write("%s\n" % priv_drop( + cmd, chroot=True, setuid="build", + privnet=not build.get("network") in ("enable", "try-enable"), + chdir=str(buildpath), env=compute_env(build))) + script.write("%s\n" % priv_drop(["rm", "-R", str(buildpath)], + chroot=True)) + # Only close the file object, script.close would delete it. + # Unfortunatly, mypy doesn't grok the indirection and thinks that + # script already is the file. + script.file.close() # type: ignore[attr-defined] + # World readable as the hook may be run by a different uid. + pathlib.Path(script.name).chmod(0o755) + cmd = [ + "mmdebstrap", + "--verbose", + "--mode=unshare", + "--variant=apt", + "--architectures=" + + ",".join(dict.fromkeys((buildarch, hostarch))), + "--include=" + ",".join(buildessential), + '--customize-hook=chroot "$1" useradd --user-group --create-home ' + '--home-dir %s build --skel /nonexistent' % + shlex.quote(str(buildpath.parent)), + "--customize-hook=copy-in %s %s" % + (shlex.join(map(str, dscfiles)), + shlex.quote(str(buildpath.parent))), + "--customize-hook=" + script.name, + "--customize-hook=sync-out " + + shlex.join([str(buildpath.parent), + build["output"]["directory"]]), + build["distribution"], + "/dev/null", + args.mirror or "http://deb.debian.org/debian", + ] + cmd.extend(build.get("extrarepositories", ())) + proc = subprocess.Popen(cmd) + proc.wait() + +if __name__ == "__main__": + main() |