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