summaryrefslogtreecommitdiff
path: root/mdbp/mmdebstrap.py
blob: 0452b45850dc15ce26cfb9e84291d15538dd2d55 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/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()