#!/usr/bin/python3 # SPDX-License-Identifier: MIT """mdbp backend using mmdebstrap""" import argparse import pathlib import shlex import subprocess import sys 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("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)) if build.get("lintian", {}).get("run"): cmd = ["lintian", *build["lintian"].get("options", ()), "%s_%s.changes" % (dscpath.stem, hostarch)] script.write("%s\n%s\n" % (priv_drop(["apt-get", "install", "--yes", "lintian"], chroot=True), priv_drop(cmd, chroot=True, setuid="build", chdir=str(buildpath.parent)))) # 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) sys.exit(proc.wait()) if __name__ == "__main__": main()