1#!/usr/bin/env python3
2#
3# Build the required docker image to run package unit tests
4#
5# Script Variables:
6#   DOCKER_IMG_NAME:  <optional, the name of the docker image to generate>
7#                     default is openbmc/ubuntu-unit-test
8#   DISTRO:           <optional, the distro to build a docker image against>
9#   FORCE_DOCKER_BUILD: <optional, a non-zero value with force all Docker
10#                     images to be rebuilt rather than reusing caches.>
11#   BUILD_URL:        <optional, used to detect running under CI context
12#                     (ex. Jenkins)>
13#   BRANCH:           <optional, branch to build from each of the openbmc/
14#                     repositories>
15#                     default is master, which will be used if input branch not
16#                     provided or not found
17#   UBUNTU_MIRROR:    <optional, the URL of a mirror of Ubuntu to override the
18#                     default ones in /etc/apt/sources.list>
19#                     default is empty, and no mirror is used.
20#   http_proxy        The HTTP address of the proxy server to connect to.
21#                     Default: "", proxy is not setup if this is not set
22
23import os
24import sys
25import threading
26from datetime import date
27from hashlib import sha256
28from sh import docker, git, nproc, uname  # type: ignore
29from typing import Any, Callable, Dict, Iterable, Optional
30
31try:
32    # Python before 3.8 doesn't have TypedDict, so reroute to standard 'dict'.
33    from typing import TypedDict
34except:
35
36    class TypedDict(dict):  # type: ignore
37        # We need to do this to eat the 'total' argument.
38        def __init_subclass__(cls, **kwargs):
39            super().__init_subclass__()
40
41
42# Declare some variables used in package definitions.
43prefix = "/usr/local"
44proc_count = nproc().strip()
45
46
47class PackageDef(TypedDict, total=False):
48    """ Package Definition for packages dictionary. """
49
50    # rev [optional]: Revision of package to use.
51    rev: str
52    # url [optional]: lambda function to create URL: (package, rev) -> url.
53    url: Callable[[str, str], str]
54    # depends [optional]: List of package dependencies.
55    depends: Iterable[str]
56    # build_type [required]: Build type used for package.
57    #   Currently supported: autoconf, cmake, custom, make, meson
58    build_type: str
59    # build_steps [optional]: Steps to run for 'custom' build_type.
60    build_steps: Iterable[str]
61    # config_flags [optional]: List of options to pass configuration tool.
62    config_flags: Iterable[str]
63    # config_env [optional]: List of environment variables to set for config.
64    config_env: Iterable[str]
65    # custom_post_dl [optional]: List of steps to run after download, but
66    #   before config / build / install.
67    custom_post_dl: Iterable[str]
68    # custom_post_install [optional]: List of steps to run after install.
69    custom_post_install: Iterable[str]
70
71    # __tag [private]: Generated Docker tag name for package stage.
72    __tag: str
73    # __package [private]: Package object associated with this package.
74    __package: Any  # Type is Package, but not defined yet.
75
76
77# Packages to include in image.
78packages = {
79    # Install OpenSSL 3.x.
80    # Generally we want to rely on the version of OpenSSL from the OS, but v3.x
81    # was a major API change.  It is included in Yocto but not Ubuntu until
82    # 22.04.  Install it manually so that our CI can target the OpenSSL 3.x
83    # APIs.
84    "openssl/openssl": PackageDef(
85        rev="openssl-3.0.1",
86        build_type="custom",
87        build_steps=[
88            f"./Configure --prefix={prefix} --libdir=lib",
89            f"make -j{proc_count}",
90            f"make -j{proc_count} install"
91        ],
92    ),
93    "boost": PackageDef(
94        rev="1.78.0",
95        url=(
96            lambda pkg, rev: f"https://downloads.yoctoproject.org/mirror/sources/{pkg}_{rev.replace('.', '_')}.tar.bz2"
97        ),
98        build_type="custom",
99        build_steps=[
100            f"./bootstrap.sh --prefix={prefix} --with-libraries=context,coroutine",
101            "./b2",
102            f"./b2 install --prefix={prefix}",
103        ],
104    ),
105    "USCiLab/cereal": PackageDef(
106        rev="3e4d1b84cab4891368d2179a61a7ba06a5693e7f",
107        build_type="custom",
108        build_steps=[f"cp -a include/cereal/ {prefix}/include/"],
109    ),
110    "catchorg/Catch2": PackageDef(
111        rev="v2.13.6",
112        build_type="cmake",
113        config_flags=["-DBUILD_TESTING=OFF", "-DCATCH_INSTALL_DOCS=OFF"],
114    ),
115    "CLIUtils/CLI11": PackageDef(
116        rev="v1.9.1",
117        build_type="cmake",
118        config_flags=[
119            "-DBUILD_TESTING=OFF",
120            "-DCLI11_BUILD_DOCS=OFF",
121            "-DCLI11_BUILD_EXAMPLES=OFF",
122        ],
123    ),
124    "fmtlib/fmt": PackageDef(
125        rev="8.1.1",
126        build_type="cmake",
127        config_flags=[
128            "-DFMT_DOC=OFF",
129            "-DFMT_TEST=OFF",
130        ],
131    ),
132    "Naios/function2": PackageDef(
133        rev="4.1.0",
134        build_type="custom",
135        build_steps=[
136            f"mkdir {prefix}/include/function2",
137            f"cp include/function2/function2.hpp {prefix}/include/function2/",
138        ],
139    ),
140    # Release 2021-06-12
141    "google/googletest": PackageDef(
142        rev="9e712372214d75bb30ec2847a44bf124d48096f3",
143        build_type="cmake",
144        config_env=["CXXFLAGS=-std=c++20"],
145        config_flags=["-DTHREADS_PREFER_PTHREAD_FLAG=ON"],
146    ),
147    # Release 2020-08-06
148    "nlohmann/json": PackageDef(
149        rev="v3.10.4",
150        build_type="cmake",
151        config_flags=["-DJSON_BuildTests=OFF"],
152        custom_post_install=[
153            f"ln -s {prefix}/include/nlohmann/json.hpp {prefix}/include/json.hpp",
154        ],
155    ),
156    # Snapshot from 2019-05-24
157    "linux-test-project/lcov": PackageDef(
158        rev="v1.15",
159        build_type="make",
160    ),
161    # dev-5.8 2021-01-11
162    "openbmc/linux": PackageDef(
163        rev="3cc95ae40716e56f81b69615781f54c78079042d",
164        build_type="custom",
165        build_steps=[
166            f"make -j{proc_count} defconfig",
167            f"make INSTALL_HDR_PATH={prefix} headers_install",
168        ],
169    ),
170    # Snapshot from 2020-06-13
171    "LibVNC/libvncserver": PackageDef(
172        rev="LibVNCServer-0.9.13",
173        build_type="cmake",
174    ),
175    "martinmoene/span-lite": PackageDef(
176        rev="v0.9.2",
177        build_type="cmake",
178        config_flags=[
179            "-DSPAN_LITE_OPT_BUILD_TESTS=OFF",
180        ],
181    ),
182    # version from meta-openembedded/meta-oe/recipes-support/libtinyxml2/libtinyxml2_8.0.0.bb
183    "leethomason/tinyxml2": PackageDef(
184        rev="8.0.0",
185        build_type="cmake",
186    ),
187    # version from /meta-openembedded/meta-oe/recipes-devtools/boost-url/boost-url_git.bb
188    "CPPAlliance/url": PackageDef(
189        rev="d740a92d38e3a8f4d5b2153f53b82f1c98e312ab",
190        build_type="custom",
191        build_steps=[f"cp -a include/** {prefix}/include/"],
192    ),
193    # version from meta-openembedded/meta-oe/dynamic-layers/networking-layer/recipes-devools/valijson/valijson_0.6.bb
194    "tristanpenman/valijson": PackageDef(
195        rev="v0.6",
196        build_type="cmake",
197        config_flags=[
198            "-Dvalijson_BUILD_TESTS=0",
199            "-Dvalijson_INSTALL_HEADERS=1",
200        ],
201    ),
202    # version from meta-openembedded/meta-oe/recipes-devtools/nlohmann-fifo/nlohmann-fifo_git.bb
203    "nlohmann/fifo_map": PackageDef(
204        rev="0dfbf5dacbb15a32c43f912a7e66a54aae39d0f9",
205        build_type="custom",
206        build_steps=[f"cp src/fifo_map.hpp {prefix}/include/"],
207    ),
208    # version from meta-openembedded/meta-oe/recipes-devtools/unifex/unifex_git.bb
209    "facebookexperimental/libunifex": PackageDef(
210        rev="9df21c58d34ce8a1cd3b15c3a7347495e29417a0",
211        build_type="cmake",
212        config_flags=[
213            "-DBUILD_SHARED_LIBS=ON",
214            "-DBUILD_TESTING=OFF",
215            "-DCMAKE_CXX_STANDARD=20",
216            "-DUNIFEX_BUILD_EXAMPLES=OFF",
217        ],
218    ),
219    "open-power/pdbg": PackageDef(build_type="autoconf"),
220    "openbmc/gpioplus": PackageDef(
221        depends=["openbmc/stdplus"],
222        build_type="meson",
223        config_flags=[
224            "-Dexamples=false",
225            "-Dtests=disabled",
226        ],
227    ),
228    "openbmc/phosphor-dbus-interfaces": PackageDef(
229        depends=["openbmc/sdbusplus"],
230        build_type="meson",
231    ),
232    "openbmc/phosphor-logging": PackageDef(
233        depends=[
234            "USCiLab/cereal",
235            "nlohmann/fifo_map",
236            "openbmc/phosphor-dbus-interfaces",
237            "openbmc/sdbusplus",
238            "openbmc/sdeventplus",
239        ],
240        build_type="meson",
241        config_flags=[
242            f"-Dyaml_dir={prefix}/share/phosphor-dbus-yaml/yaml",
243        ],
244    ),
245    "openbmc/phosphor-objmgr": PackageDef(
246        depends=[
247            "boost",
248            "leethomason/tinyxml2",
249            "openbmc/phosphor-logging",
250            "openbmc/sdbusplus",
251        ],
252        build_type="meson",
253        config_flags=[
254            "-Dtests=disabled",
255        ],
256    ),
257    "openbmc/pldm": PackageDef(
258        depends=[
259            "CLIUtils/CLI11",
260            "boost",
261            "nlohmann/json",
262            "openbmc/phosphor-dbus-interfaces",
263            "openbmc/phosphor-logging",
264            "openbmc/sdbusplus",
265            "openbmc/sdeventplus",
266        ],
267        build_type="meson",
268        config_flags=[
269            "-Dlibpldm-only=enabled",
270            "-Doem-ibm=enabled",
271            "-Dtests=disabled",
272        ],
273    ),
274    "openbmc/sdbusplus": PackageDef(
275        depends=[
276            "facebookexperimental/libunifex",
277        ],
278        build_type="meson",
279        custom_post_dl=[
280            "cd tools",
281            f"./setup.py install --root=/ --prefix={prefix}",
282            "cd ..",
283        ],
284        config_flags=[
285            "-Dexamples=disabled",
286            "-Dtests=disabled",
287        ],
288    ),
289    "openbmc/sdeventplus": PackageDef(
290        depends=["Naios/function2", "openbmc/stdplus"],
291        build_type="meson",
292        config_flags=[
293            "-Dexamples=false",
294            "-Dtests=disabled",
295        ],
296    ),
297    "openbmc/stdplus": PackageDef(
298        depends=["fmtlib/fmt", "martinmoene/span-lite"],
299        build_type="meson",
300        config_flags=[
301            "-Dexamples=false",
302            "-Dtests=disabled",
303        ],
304    ),
305}  # type: Dict[str, PackageDef]
306
307# Define common flags used for builds
308configure_flags = " ".join(
309    [
310        f"--prefix={prefix}",
311    ]
312)
313cmake_flags = " ".join(
314    [
315        "-DBUILD_SHARED_LIBS=ON",
316        "-DCMAKE_BUILD_TYPE=RelWithDebInfo",
317        f"-DCMAKE_INSTALL_PREFIX:PATH={prefix}",
318        "-GNinja",
319        "-DCMAKE_MAKE_PROGRAM=ninja",
320    ]
321)
322meson_flags = " ".join(
323    [
324        "--wrap-mode=nodownload",
325        f"-Dprefix={prefix}",
326    ]
327)
328
329
330class Package(threading.Thread):
331    """Class used to build the Docker stages for each package.
332
333    Generally, this class should not be instantiated directly but through
334    Package.generate_all().
335    """
336
337    # Copy the packages dictionary.
338    packages = packages.copy()
339
340    # Lock used for thread-safety.
341    lock = threading.Lock()
342
343    def __init__(self, pkg: str):
344        """ pkg - The name of this package (ex. foo/bar ) """
345        super(Package, self).__init__()
346
347        self.package = pkg
348        self.exception = None  # type: Optional[Exception]
349
350        # Reference to this package's
351        self.pkg_def = Package.packages[pkg]
352        self.pkg_def["__package"] = self
353
354    def run(self) -> None:
355        """ Thread 'run' function.  Builds the Docker stage. """
356
357        # In case this package has no rev, fetch it from Github.
358        self._update_rev()
359
360        # Find all the Package objects that this package depends on.
361        #   This section is locked because we are looking into another
362        #   package's PackageDef dict, which could be being modified.
363        Package.lock.acquire()
364        deps: Iterable[Package] = [
365            Package.packages[deppkg]["__package"]
366            for deppkg in self.pkg_def.get("depends", [])
367        ]
368        Package.lock.release()
369
370        # Wait until all the depends finish building.  We need them complete
371        # for the "COPY" commands.
372        for deppkg in deps:
373            deppkg.join()
374
375        # Generate this package's Dockerfile.
376        dockerfile = f"""
377FROM {docker_base_img_name}
378{self._df_copycmds()}
379{self._df_build()}
380"""
381
382        # Generate the resulting tag name and save it to the PackageDef.
383        #   This section is locked because we are modifying the PackageDef,
384        #   which can be accessed by other threads.
385        Package.lock.acquire()
386        tag = Docker.tagname(self._stagename(), dockerfile)
387        self.pkg_def["__tag"] = tag
388        Package.lock.release()
389
390        # Do the build / save any exceptions.
391        try:
392            Docker.build(self.package, tag, dockerfile)
393        except Exception as e:
394            self.exception = e
395
396    @classmethod
397    def generate_all(cls) -> None:
398        """Ensure a Docker stage is created for all defined packages.
399
400        These are done in parallel but with appropriate blocking per
401        package 'depends' specifications.
402        """
403
404        # Create a Package for each defined package.
405        pkg_threads = [Package(p) for p in cls.packages.keys()]
406
407        # Start building them all.
408        #   This section is locked because threads depend on each other,
409        #   based on the packages, and they cannot 'join' on a thread
410        #   which is not yet started.  Adding a lock here allows all the
411        #   threads to start before they 'join' their dependencies.
412        Package.lock.acquire()
413        for t in pkg_threads:
414            t.start()
415        Package.lock.release()
416
417        # Wait for completion.
418        for t in pkg_threads:
419            t.join()
420            # Check if the thread saved off its own exception.
421            if t.exception:
422                print(f"Package {t.package} failed!", file=sys.stderr)
423                raise t.exception
424
425    @staticmethod
426    def df_all_copycmds() -> str:
427        """Formulate the Dockerfile snippet necessary to copy all packages
428        into the final image.
429        """
430        return Package.df_copycmds_set(Package.packages.keys())
431
432    @classmethod
433    def depcache(cls) -> str:
434        """Create the contents of the '/tmp/depcache'.
435        This file is a comma-separated list of "<pkg>:<rev>".
436        """
437
438        # This needs to be sorted for consistency.
439        depcache = ""
440        for pkg in sorted(cls.packages.keys()):
441            depcache += "%s:%s," % (pkg, cls.packages[pkg]["rev"])
442        return depcache
443
444    def _update_rev(self) -> None:
445        """ Look up the HEAD for missing a static rev. """
446
447        if "rev" in self.pkg_def:
448            return
449
450        # Check if Jenkins/Gerrit gave us a revision and use it.
451        if gerrit_project == self.package and gerrit_rev:
452            print(
453                f"Found Gerrit revision for {self.package}: {gerrit_rev}",
454                file=sys.stderr,
455            )
456            self.pkg_def["rev"] = gerrit_rev
457            return
458
459        # Ask Github for all the branches.
460        lookup = git("ls-remote", "--heads", f"https://github.com/{self.package}")
461
462        # Find the branch matching {branch} (or fallback to master).
463        #   This section is locked because we are modifying the PackageDef.
464        Package.lock.acquire()
465        for line in lookup.split("\n"):
466            if f"refs/heads/{branch}" in line:
467                self.pkg_def["rev"] = line.split()[0]
468            elif f"refs/heads/master" in line and "rev" not in self.pkg_def:
469                self.pkg_def["rev"] = line.split()[0]
470        Package.lock.release()
471
472    def _stagename(self) -> str:
473        """ Create a name for the Docker stage associated with this pkg. """
474        return self.package.replace("/", "-").lower()
475
476    def _url(self) -> str:
477        """ Get the URL for this package. """
478        rev = self.pkg_def["rev"]
479
480        # If the lambda exists, call it.
481        if "url" in self.pkg_def:
482            return self.pkg_def["url"](self.package, rev)
483
484        # Default to the github archive URL.
485        return f"https://github.com/{self.package}/archive/{rev}.tar.gz"
486
487    def _cmd_download(self) -> str:
488        """Formulate the command necessary to download and unpack to source."""
489
490        url = self._url()
491        if ".tar." not in url:
492            raise NotImplementedError(
493                f"Unhandled download type for {self.package}: {url}"
494            )
495
496        cmd = f"curl -L {url} | tar -x"
497
498        if url.endswith(".bz2"):
499            cmd += "j"
500        elif url.endswith(".gz"):
501            cmd += "z"
502        else:
503            raise NotImplementedError(
504                f"Unknown tar flags needed for {self.package}: {url}"
505            )
506
507        return cmd
508
509    def _cmd_cd_srcdir(self) -> str:
510        """ Formulate the command necessary to 'cd' into the source dir. """
511        return f"cd {self.package.split('/')[-1]}*"
512
513    def _df_copycmds(self) -> str:
514        """ Formulate the dockerfile snippet necessary to COPY all depends. """
515
516        if "depends" not in self.pkg_def:
517            return ""
518        return Package.df_copycmds_set(self.pkg_def["depends"])
519
520    @staticmethod
521    def df_copycmds_set(pkgs: Iterable[str]) -> str:
522        """Formulate the Dockerfile snippet necessary to COPY a set of
523        packages into a Docker stage.
524        """
525
526        copy_cmds = ""
527
528        # Sort the packages for consistency.
529        for p in sorted(pkgs):
530            tag = Package.packages[p]["__tag"]
531            copy_cmds += f"COPY --from={tag} {prefix} {prefix}\n"
532            # Workaround for upstream docker bug and multiple COPY cmds
533            # https://github.com/moby/moby/issues/37965
534            copy_cmds += "RUN true\n"
535
536        return copy_cmds
537
538    def _df_build(self) -> str:
539        """Formulate the Dockerfile snippet necessary to download, build, and
540        install a package into a Docker stage.
541        """
542
543        # Download and extract source.
544        result = f"RUN {self._cmd_download()} && {self._cmd_cd_srcdir()} && "
545
546        # Handle 'custom_post_dl' commands.
547        custom_post_dl = self.pkg_def.get("custom_post_dl")
548        if custom_post_dl:
549            result += " && ".join(custom_post_dl) + " && "
550
551        # Build and install package based on 'build_type'.
552        build_type = self.pkg_def["build_type"]
553        if build_type == "autoconf":
554            result += self._cmd_build_autoconf()
555        elif build_type == "cmake":
556            result += self._cmd_build_cmake()
557        elif build_type == "custom":
558            result += self._cmd_build_custom()
559        elif build_type == "make":
560            result += self._cmd_build_make()
561        elif build_type == "meson":
562            result += self._cmd_build_meson()
563        else:
564            raise NotImplementedError(
565                f"Unhandled build type for {self.package}: {build_type}"
566            )
567
568        # Handle 'custom_post_install' commands.
569        custom_post_install = self.pkg_def.get("custom_post_install")
570        if custom_post_install:
571            result += " && " + " && ".join(custom_post_install)
572
573        return result
574
575    def _cmd_build_autoconf(self) -> str:
576        options = " ".join(self.pkg_def.get("config_flags", []))
577        env = " ".join(self.pkg_def.get("config_env", []))
578        result = "./bootstrap.sh && "
579        result += f"{env} ./configure {configure_flags} {options} && "
580        result += f"make -j{proc_count} && make install"
581        return result
582
583    def _cmd_build_cmake(self) -> str:
584        options = " ".join(self.pkg_def.get("config_flags", []))
585        env = " ".join(self.pkg_def.get("config_env", []))
586        result = "mkdir builddir && cd builddir && "
587        result += f"{env} cmake {cmake_flags} {options} .. && "
588        result += "cmake --build . --target all && "
589        result += "cmake --build . --target install && "
590        result += "cd .."
591        return result
592
593    def _cmd_build_custom(self) -> str:
594        return " && ".join(self.pkg_def.get("build_steps", []))
595
596    def _cmd_build_make(self) -> str:
597        return f"make -j{proc_count} && make install"
598
599    def _cmd_build_meson(self) -> str:
600        options = " ".join(self.pkg_def.get("config_flags", []))
601        env = " ".join(self.pkg_def.get("config_env", []))
602        result = f"{env} meson builddir {meson_flags} {options} && "
603        result += "ninja -C builddir && ninja -C builddir install"
604        return result
605
606
607class Docker:
608    """Class to assist with Docker interactions.  All methods are static."""
609
610    @staticmethod
611    def timestamp() -> str:
612        """ Generate a timestamp for today using the ISO week. """
613        today = date.today().isocalendar()
614        return f"{today[0]}-W{today[1]:02}"
615
616    @staticmethod
617    def tagname(pkgname: str, dockerfile: str) -> str:
618        """ Generate a tag name for a package using a hash of the Dockerfile. """
619        result = docker_image_name
620        if pkgname:
621            result += "-" + pkgname
622
623        result += ":" + Docker.timestamp()
624        result += "-" + sha256(dockerfile.encode()).hexdigest()[0:16]
625
626        return result
627
628    @staticmethod
629    def build(pkg: str, tag: str, dockerfile: str) -> None:
630        """Build a docker image using the Dockerfile and tagging it with 'tag'."""
631
632        # If we're not forcing builds, check if it already exists and skip.
633        if not force_build:
634            if docker.image.ls(tag, "--format", '"{{.Repository}}:{{.Tag}}"'):
635                print(f"Image {tag} already exists.  Skipping.", file=sys.stderr)
636                return
637
638        # Build it.
639        #   Capture the output of the 'docker build' command and send it to
640        #   stderr (prefixed with the package name).  This allows us to see
641        #   progress but not polute stdout.  Later on we output the final
642        #   docker tag to stdout and we want to keep that pristine.
643        #
644        #   Other unusual flags:
645        #       --no-cache: Bypass the Docker cache if 'force_build'.
646        #       --force-rm: Clean up Docker processes if they fail.
647        docker.build(
648            proxy_args,
649            "--network=host",
650            "--force-rm",
651            "--no-cache=true" if force_build else "--no-cache=false",
652            "-t",
653            tag,
654            "-",
655            _in=dockerfile,
656            _out=(
657                lambda line: print(
658                    pkg + ":", line, end="", file=sys.stderr, flush=True
659                )
660            ),
661        )
662
663
664# Read a bunch of environment variables.
665docker_image_name = os.environ.get("DOCKER_IMAGE_NAME", "openbmc/ubuntu-unit-test")
666force_build = os.environ.get("FORCE_DOCKER_BUILD")
667is_automated_ci_build = os.environ.get("BUILD_URL", False)
668distro = os.environ.get("DISTRO", "ubuntu:impish")
669branch = os.environ.get("BRANCH", "master")
670ubuntu_mirror = os.environ.get("UBUNTU_MIRROR")
671http_proxy = os.environ.get("http_proxy")
672
673gerrit_project = os.environ.get("GERRIT_PROJECT")
674gerrit_rev = os.environ.get("GERRIT_PATCHSET_REVISION")
675
676# Set up some common variables.
677username = os.environ.get("USER", "root")
678homedir = os.environ.get("HOME", "/root")
679gid = os.getgid()
680uid = os.getuid()
681
682# Use well-known constants if user is root
683if username == "root":
684    homedir = "/root"
685    gid = 0
686    uid = 0
687
688# Determine the architecture for Docker.
689arch = uname("-m").strip()
690if arch == "ppc64le":
691    docker_base = "ppc64le/"
692elif arch == "x86_64":
693    docker_base = ""
694elif arch == "aarch64":
695    docker_base = "arm64v8/"
696else:
697    print(
698        f"Unsupported system architecture({arch}) found for docker image",
699        file=sys.stderr,
700    )
701    sys.exit(1)
702
703# Special flags if setting up a deb mirror.
704mirror = ""
705if "ubuntu" in distro and ubuntu_mirror:
706    mirror = f"""
707RUN echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME) main restricted universe multiverse" > /etc/apt/sources.list && \\
708    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-updates main restricted universe multiverse" >> /etc/apt/sources.list && \\
709    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-security main restricted universe multiverse" >> /etc/apt/sources.list && \\
710    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-proposed main restricted universe multiverse" >> /etc/apt/sources.list && \\
711    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-backports main restricted universe multiverse" >> /etc/apt/sources.list
712"""
713
714# Special flags for proxying.
715proxy_cmd = ""
716proxy_keyserver = ""
717proxy_args = []
718if http_proxy:
719    proxy_cmd = f"""
720RUN echo "[http]" >> {homedir}/.gitconfig && \
721    echo "proxy = {http_proxy}" >> {homedir}/.gitconfig
722"""
723    proxy_keyserver = f"--keyserver-options http-proxy={http_proxy}"
724
725    proxy_args.extend(
726        [
727            "--build-arg",
728            f"http_proxy={http_proxy}",
729            "--build-arg",
730            f"https_proxy={http_proxy}",
731        ]
732    )
733
734# Create base Dockerfile.
735dockerfile_base = f"""
736FROM {docker_base}{distro}
737
738{mirror}
739
740ENV DEBIAN_FRONTEND noninteractive
741
742ENV PYTHONPATH "/usr/local/lib/python3.9/site-packages/"
743
744# Sometimes the ubuntu key expires and we need a way to force an execution
745# of the apt-get commands for the dbgsym-keyring.  When this happens we see
746# an error like: "Release: The following signatures were invalid:"
747# Insert a bogus echo that we can change here when we get this error to force
748# the update.
749RUN echo "ubuntu keyserver rev as of 2021-04-21"
750
751# We need the keys to be imported for dbgsym repos
752# New releases have a package, older ones fall back to manual fetching
753# https://wiki.ubuntu.com/Debug%20Symbol%20Packages
754RUN apt-get update && apt-get dist-upgrade -yy && \
755    ( apt-get install gpgv ubuntu-dbgsym-keyring || \
756        ( apt-get install -yy dirmngr && \
757          apt-key adv --keyserver keyserver.ubuntu.com \
758                      {proxy_keyserver} \
759                      --recv-keys F2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622 ) )
760
761# Parse the current repo list into a debug repo list
762RUN sed -n '/^deb /s,^deb [^ ]* ,deb http://ddebs.ubuntu.com ,p' /etc/apt/sources.list >/etc/apt/sources.list.d/debug.list
763
764# Remove non-existent debug repos
765RUN sed -i '/-\(backports\|security\) /d' /etc/apt/sources.list.d/debug.list
766
767RUN cat /etc/apt/sources.list.d/debug.list
768
769RUN apt-get update && apt-get dist-upgrade -yy && apt-get install -yy \
770    gcc-11 \
771    g++-11 \
772    libc6-dbg \
773    libc6-dev \
774    libtool \
775    bison \
776    libdbus-1-dev \
777    flex \
778    cmake \
779    python3 \
780    python3-dev\
781    python3-yaml \
782    python3-mako \
783    python3-pip \
784    python3-setuptools \
785    python3-git \
786    python3-socks \
787    pkg-config \
788    autoconf \
789    autoconf-archive \
790    libsystemd-dev \
791    systemd \
792    libssl-dev \
793    libevdev-dev \
794    libjpeg-dev \
795    libpng-dev \
796    ninja-build \
797    sudo \
798    curl \
799    git \
800    dbus \
801    iputils-ping \
802    clang-13 \
803    clang-format-13 \
804    clang-tidy-13 \
805    clang-tools-13 \
806    shellcheck \
807    npm \
808    iproute2 \
809    libnl-3-dev \
810    libnl-genl-3-dev \
811    libconfig++-dev \
812    libsnmp-dev \
813    valgrind \
814    valgrind-dbg \
815    libpam0g-dev \
816    xxd \
817    libi2c-dev \
818    wget \
819    libldap2-dev \
820    libprotobuf-dev \
821    liburing-dev \
822    liburing1-dbgsym \
823    libperlio-gzip-perl \
824    libjson-perl \
825    protobuf-compiler \
826    libgpiod-dev \
827    device-tree-compiler \
828    cppcheck \
829    libpciaccess-dev \
830    libmimetic-dev \
831    libxml2-utils \
832    libxml-simple-perl \
833    rsync \
834    libcryptsetup-dev
835
836# Apply autoconf-archive-v2022.02.11 file ax_cxx_compile_stdcxx for C++20.
837RUN curl "http://git.savannah.gnu.org/gitweb/?p=autoconf-archive.git;a=blob_plain;f=m4/ax_cxx_compile_stdcxx.m4;hb=3311b6bdeff883c6a13952594a9dcb60bce6ba80" \
838  > /usr/share/aclocal/ax_cxx_compile_stdcxx.m4
839
840RUN npm install -g eslint@latest eslint-plugin-json@latest
841
842RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 11 \
843  --slave /usr/bin/g++ g++ /usr/bin/g++-11 \
844  --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \
845  --slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-11 \
846  --slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-11
847
848RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-13 1000 \
849  --slave /usr/bin/clang++ clang++ /usr/bin/clang++-13 \
850  --slave /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-13 \
851  --slave /usr/bin/clang-format clang-format /usr/bin/clang-format-13 \
852  --slave /usr/bin/run-clang-tidy run-clang-tidy.py /usr/bin/run-clang-tidy-13 \
853  --slave /usr/bin/scan-build scan-build /usr/bin/scan-build-13
854
855"""
856
857if is_automated_ci_build:
858    dockerfile_base += f"""
859# Run an arbitrary command to polute the docker cache regularly force us
860# to re-run `apt-get update` daily.
861RUN echo {Docker.timestamp()}
862RUN apt-get update && apt-get dist-upgrade -yy
863
864"""
865
866dockerfile_base += f"""
867RUN pip3 install inflection
868RUN pip3 install pycodestyle
869RUN pip3 install jsonschema
870RUN pip3 install meson==0.58.1
871RUN pip3 install protobuf
872RUN pip3 install codespell
873RUN pip3 install requests
874"""
875
876# Build the base and stage docker images.
877docker_base_img_name = Docker.tagname("base", dockerfile_base)
878Docker.build("base", docker_base_img_name, dockerfile_base)
879Package.generate_all()
880
881# Create the final Dockerfile.
882dockerfile = f"""
883# Build the final output image
884FROM {docker_base_img_name}
885{Package.df_all_copycmds()}
886
887# Some of our infrastructure still relies on the presence of this file
888# even though it is no longer needed to rebuild the docker environment
889# NOTE: The file is sorted to ensure the ordering is stable.
890RUN echo '{Package.depcache()}' > /tmp/depcache
891
892# Final configuration for the workspace
893RUN grep -q {gid} /etc/group || groupadd -f -g {gid} {username}
894RUN mkdir -p "{os.path.dirname(homedir)}"
895RUN grep -q {uid} /etc/passwd || useradd -d {homedir} -m -u {uid} -g {gid} {username}
896RUN sed -i '1iDefaults umask=000' /etc/sudoers
897RUN echo "{username} ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers
898
899# Ensure user has ability to write to /usr/local for different tool
900# and data installs
901RUN chown -R {username}:{username} /usr/local/share
902
903{proxy_cmd}
904
905RUN /bin/bash
906"""
907
908# Do the final docker build
909docker_final_img_name = Docker.tagname(None, dockerfile)
910Docker.build("final", docker_final_img_name, dockerfile)
911
912# Print the tag of the final image.
913print(docker_final_img_name)
914