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    "boost": PackageDef(
80        rev="1.79.0",
81        url=(
82            lambda pkg, rev: f"https://downloads.yoctoproject.org/mirror/sources/{pkg}_{rev.replace('.', '_')}.tar.bz2"
83        ),
84        build_type="custom",
85        build_steps=[
86            f"./bootstrap.sh --prefix={prefix} --with-libraries=context,coroutine",
87            "./b2",
88            f"./b2 install --prefix={prefix}",
89        ],
90    ),
91    "USCiLab/cereal": PackageDef(
92        rev="3e4d1b84cab4891368d2179a61a7ba06a5693e7f",
93        build_type="custom",
94        build_steps=[f"cp -a include/cereal/ {prefix}/include/"],
95    ),
96    "catchorg/Catch2": PackageDef(
97        rev="v2.13.6",
98        build_type="cmake",
99        config_flags=["-DBUILD_TESTING=OFF", "-DCATCH_INSTALL_DOCS=OFF"],
100    ),
101    "danmar/cppcheck": PackageDef(
102        rev="27578e9c4c1f90c62b6938867735a054082e178e",
103        build_type="cmake",
104    ),
105    "CLIUtils/CLI11": PackageDef(
106        rev="v1.9.1",
107        build_type="cmake",
108        config_flags=[
109            "-DBUILD_TESTING=OFF",
110            "-DCLI11_BUILD_DOCS=OFF",
111            "-DCLI11_BUILD_EXAMPLES=OFF",
112        ],
113    ),
114    "fmtlib/fmt": PackageDef(
115        rev="8.1.1",
116        build_type="cmake",
117        config_flags=[
118            "-DFMT_DOC=OFF",
119            "-DFMT_TEST=OFF",
120        ],
121    ),
122    "Naios/function2": PackageDef(
123        rev="4.1.0",
124        build_type="custom",
125        build_steps=[
126            f"mkdir {prefix}/include/function2",
127            f"cp include/function2/function2.hpp {prefix}/include/function2/",
128        ],
129    ),
130    # Release 2021-06-12
131    "google/googletest": PackageDef(
132        rev="9e712372214d75bb30ec2847a44bf124d48096f3",
133        build_type="cmake",
134        config_env=["CXXFLAGS=-std=c++20"],
135        config_flags=["-DTHREADS_PREFER_PTHREAD_FLAG=ON"],
136    ),
137    # Release 2020-08-06
138    "nlohmann/json": PackageDef(
139        rev="v3.10.4",
140        build_type="cmake",
141        config_flags=["-DJSON_BuildTests=OFF"],
142        custom_post_install=[
143            f"ln -s {prefix}/include/nlohmann/json.hpp {prefix}/include/json.hpp",
144        ],
145    ),
146    # Snapshot from 2019-05-24
147    "linux-test-project/lcov": PackageDef(
148        rev="v1.15",
149        build_type="make",
150    ),
151    # dev-5.8 2021-01-11
152    "openbmc/linux": PackageDef(
153        rev="3cc95ae40716e56f81b69615781f54c78079042d",
154        build_type="custom",
155        build_steps=[
156            f"make -j{proc_count} defconfig",
157            f"make INSTALL_HDR_PATH={prefix} headers_install",
158        ],
159    ),
160    # Snapshot from 2020-06-13
161    "LibVNC/libvncserver": PackageDef(
162        rev="LibVNCServer-0.9.13",
163        build_type="cmake",
164    ),
165    # version from meta-openembedded/meta-oe/recipes-support/libtinyxml2/libtinyxml2_8.0.0.bb
166    "leethomason/tinyxml2": PackageDef(
167        rev="8.0.0",
168        build_type="cmake",
169    ),
170    # version from /meta-openembedded/meta-oe/recipes-devtools/boost-url/boost-url_git.bb
171    "CPPAlliance/url": PackageDef(
172        rev="d740a92d38e3a8f4d5b2153f53b82f1c98e312ab",
173        build_type="custom",
174        build_steps=[f"cp -a include/** {prefix}/include/"],
175    ),
176    # version from meta-openembedded/meta-oe/dynamic-layers/networking-layer/recipes-devools/valijson/valijson_0.6.bb
177    "tristanpenman/valijson": PackageDef(
178        rev="v0.6",
179        build_type="cmake",
180        config_flags=[
181            "-Dvalijson_BUILD_TESTS=0",
182            "-Dvalijson_INSTALL_HEADERS=1",
183        ],
184    ),
185    # version from meta-openembedded/meta-oe/recipes-devtools/unifex/unifex_git.bb
186    "facebookexperimental/libunifex": PackageDef(
187        rev="9df21c58d34ce8a1cd3b15c3a7347495e29417a0",
188        build_type="cmake",
189        config_flags=[
190            "-DBUILD_SHARED_LIBS=ON",
191            "-DBUILD_TESTING=OFF",
192            "-DCMAKE_CXX_STANDARD=20",
193            "-DUNIFEX_BUILD_EXAMPLES=OFF",
194        ],
195    ),
196    "open-power/pdbg": PackageDef(build_type="autoconf"),
197    "openbmc/gpioplus": PackageDef(
198        depends=["openbmc/stdplus"],
199        build_type="meson",
200        config_flags=[
201            "-Dexamples=false",
202            "-Dtests=disabled",
203        ],
204    ),
205    "openbmc/phosphor-dbus-interfaces": PackageDef(
206        depends=["openbmc/sdbusplus"],
207        build_type="meson",
208        config_flags=["-Dgenerate_md=false"],
209    ),
210    "openbmc/phosphor-logging": PackageDef(
211        depends=[
212            "USCiLab/cereal",
213            "openbmc/phosphor-dbus-interfaces",
214            "openbmc/sdbusplus",
215            "openbmc/sdeventplus",
216        ],
217        build_type="meson",
218        config_flags=[
219            f"-Dyamldir={prefix}/share/phosphor-dbus-yaml/yaml",
220        ],
221    ),
222    "openbmc/phosphor-objmgr": PackageDef(
223        depends=[
224            "boost",
225            "leethomason/tinyxml2",
226            "openbmc/phosphor-logging",
227            "openbmc/sdbusplus",
228        ],
229        build_type="meson",
230        config_flags=[
231            "-Dtests=disabled",
232        ],
233    ),
234    "openbmc/pldm": PackageDef(
235        depends=[
236            "CLIUtils/CLI11",
237            "boost",
238            "nlohmann/json",
239            "openbmc/phosphor-dbus-interfaces",
240            "openbmc/phosphor-logging",
241            "openbmc/sdbusplus",
242            "openbmc/sdeventplus",
243        ],
244        build_type="meson",
245        config_flags=[
246            "-Dlibpldm-only=enabled",
247            "-Doem-ibm=enabled",
248            "-Dtests=disabled",
249        ],
250    ),
251    "openbmc/sdbusplus": PackageDef(
252        depends=[
253            "facebookexperimental/libunifex",
254        ],
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", "google/googletest"],
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:jammy")
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# Use well-known constants if user is root
660if username == "root":
661    homedir = "/root"
662    gid = 0
663    uid = 0
664
665# Determine the architecture for Docker.
666arch = uname("-m").strip()
667if arch == "ppc64le":
668    docker_base = "ppc64le/"
669elif arch == "x86_64":
670    docker_base = ""
671elif arch == "aarch64":
672    docker_base = "arm64v8/"
673else:
674    print(
675        f"Unsupported system architecture({arch}) found for docker image",
676        file=sys.stderr,
677    )
678    sys.exit(1)
679
680# Special flags if setting up a deb mirror.
681mirror = ""
682if "ubuntu" in distro and ubuntu_mirror:
683    mirror = f"""
684RUN echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME) main restricted universe multiverse" > /etc/apt/sources.list && \\
685    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-updates main restricted universe multiverse" >> /etc/apt/sources.list && \\
686    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-security main restricted universe multiverse" >> /etc/apt/sources.list && \\
687    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-proposed main restricted universe multiverse" >> /etc/apt/sources.list && \\
688    echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-backports main restricted universe multiverse" >> /etc/apt/sources.list
689"""
690
691# Special flags for proxying.
692proxy_cmd = ""
693proxy_keyserver = ""
694proxy_args = []
695if http_proxy:
696    proxy_cmd = f"""
697RUN echo "[http]" >> {homedir}/.gitconfig && \
698    echo "proxy = {http_proxy}" >> {homedir}/.gitconfig
699"""
700    proxy_keyserver = f"--keyserver-options http-proxy={http_proxy}"
701
702    proxy_args.extend(
703        [
704            "--build-arg",
705            f"http_proxy={http_proxy}",
706            "--build-arg",
707            f"https_proxy={http_proxy}",
708        ]
709    )
710
711# Create base Dockerfile.
712dockerfile_base = f"""
713FROM {docker_base}{distro}
714
715{mirror}
716
717ENV DEBIAN_FRONTEND noninteractive
718
719ENV PYTHONPATH "/usr/local/lib/python3.10/site-packages/"
720
721# Sometimes the ubuntu key expires and we need a way to force an execution
722# of the apt-get commands for the dbgsym-keyring.  When this happens we see
723# an error like: "Release: The following signatures were invalid:"
724# Insert a bogus echo that we can change here when we get this error to force
725# the update.
726RUN echo "ubuntu keyserver rev as of 2021-04-21"
727
728# We need the keys to be imported for dbgsym repos
729# New releases have a package, older ones fall back to manual fetching
730# https://wiki.ubuntu.com/Debug%20Symbol%20Packages
731RUN apt-get update && apt-get dist-upgrade -yy && \
732    ( apt-get install gpgv ubuntu-dbgsym-keyring || \
733        ( apt-get install -yy dirmngr && \
734          apt-key adv --keyserver keyserver.ubuntu.com \
735                      {proxy_keyserver} \
736                      --recv-keys F2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622 ) )
737
738# Parse the current repo list into a debug repo list
739RUN sed -n '/^deb /s,^deb [^ ]* ,deb http://ddebs.ubuntu.com ,p' /etc/apt/sources.list >/etc/apt/sources.list.d/debug.list
740
741# Remove non-existent debug repos
742RUN sed -i '/-\(backports\|security\) /d' /etc/apt/sources.list.d/debug.list
743
744RUN cat /etc/apt/sources.list.d/debug.list
745
746RUN apt-get update && apt-get dist-upgrade -yy && apt-get install -yy \
747    gcc-11 \
748    g++-11 \
749    libc6-dbg \
750    libc6-dev \
751    libtool \
752    bison \
753    libdbus-1-dev \
754    flex \
755    cmake \
756    python3 \
757    python3-dev\
758    python3-yaml \
759    python3-mako \
760    python3-pip \
761    python3-setuptools \
762    python3-git \
763    python3-socks \
764    pkg-config \
765    autoconf \
766    autoconf-archive \
767    libsystemd-dev \
768    systemd \
769    libssl-dev \
770    libevdev-dev \
771    libjpeg-dev \
772    libpng-dev \
773    ninja-build \
774    sudo \
775    curl \
776    git \
777    dbus \
778    iputils-ping \
779    clang-14 \
780    clang-format-14 \
781    clang-tidy-14 \
782    clang-tools-14 \
783    shellcheck \
784    npm \
785    iproute2 \
786    libnl-3-dev \
787    libnl-genl-3-dev \
788    libconfig++-dev \
789    libsnmp-dev \
790    valgrind \
791    valgrind-dbg \
792    libpam0g-dev \
793    xxd \
794    libi2c-dev \
795    wget \
796    libldap2-dev \
797    libprotobuf-dev \
798    liburing-dev \
799    liburing2-dbgsym \
800    libperlio-gzip-perl \
801    libjson-perl \
802    protobuf-compiler \
803    libgpiod-dev \
804    device-tree-compiler \
805    libpciaccess-dev \
806    libmimetic-dev \
807    libxml2-utils \
808    libxml-simple-perl \
809    rsync \
810    libcryptsetup-dev
811
812# Apply autoconf-archive-v2022.02.11 file ax_cxx_compile_stdcxx for C++20.
813RUN curl "http://git.savannah.gnu.org/gitweb/?p=autoconf-archive.git;a=blob_plain;f=m4/ax_cxx_compile_stdcxx.m4;hb=3311b6bdeff883c6a13952594a9dcb60bce6ba80" \
814  > /usr/share/aclocal/ax_cxx_compile_stdcxx.m4
815
816RUN npm install -g eslint@latest eslint-plugin-json@latest
817
818RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 11 \
819  --slave /usr/bin/g++ g++ /usr/bin/g++-11 \
820  --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \
821  --slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-11 \
822  --slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-11
823
824RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-14 1000 \
825  --slave /usr/bin/clang++ clang++ /usr/bin/clang++-14 \
826  --slave /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-14 \
827  --slave /usr/bin/clang-format clang-format /usr/bin/clang-format-14 \
828  --slave /usr/bin/run-clang-tidy run-clang-tidy.py /usr/bin/run-clang-tidy-14 \
829  --slave /usr/bin/scan-build scan-build /usr/bin/scan-build-14
830
831"""
832
833if is_automated_ci_build:
834    dockerfile_base += f"""
835# Run an arbitrary command to polute the docker cache regularly force us
836# to re-run `apt-get update` daily.
837RUN echo {Docker.timestamp()}
838RUN apt-get update && apt-get dist-upgrade -yy
839
840"""
841
842dockerfile_base += f"""
843RUN pip3 install inflection
844RUN pip3 install pycodestyle
845RUN pip3 install jsonschema
846RUN pip3 install meson==0.63.0
847RUN pip3 install protobuf
848RUN pip3 install codespell
849RUN pip3 install requests
850"""
851
852# Note, we use sha1s here because the newest gitlint release doesn't include
853# some features we need.  Next time they release, we can rely on a direct
854# release tag
855dockerfile_base += f"""
856RUN pip3 install git+https://github.com/jorisroovers/gitlint.git@8ede310d62d5794efa7518b235f899f8a8ad6a68\#subdirectory=gitlint-core
857RUN pip3 install git+https://github.com/jorisroovers/gitlint.git@8ede310d62d5794efa7518b235f899f8a8ad6a68
858"""
859
860# Build the base and stage docker images.
861docker_base_img_name = Docker.tagname("base", dockerfile_base)
862Docker.build("base", docker_base_img_name, dockerfile_base)
863Package.generate_all()
864
865# Create the final Dockerfile.
866dockerfile = f"""
867# Build the final output image
868FROM {docker_base_img_name}
869{Package.df_all_copycmds()}
870
871# Some of our infrastructure still relies on the presence of this file
872# even though it is no longer needed to rebuild the docker environment
873# NOTE: The file is sorted to ensure the ordering is stable.
874RUN echo '{Package.depcache()}' > /tmp/depcache
875
876# Final configuration for the workspace
877RUN grep -q {gid} /etc/group || groupadd -f -g {gid} {username}
878RUN mkdir -p "{os.path.dirname(homedir)}"
879RUN grep -q {uid} /etc/passwd || useradd -d {homedir} -m -u {uid} -g {gid} {username}
880RUN sed -i '1iDefaults umask=000' /etc/sudoers
881RUN echo "{username} ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers
882
883# Ensure user has ability to write to /usr/local for different tool
884# and data installs
885RUN chown -R {username}:{username} /usr/local/share
886
887{proxy_cmd}
888
889RUN /bin/bash
890"""
891
892# Do the final docker build
893docker_final_img_name = Docker.tagname(None, dockerfile)
894Docker.build("final", docker_final_img_name, dockerfile)
895
896# Print the tag of the final image.
897print(docker_final_img_name)
898