xref: /openbmc/openbmc-build-scripts/scripts/build-unit-test-docker (revision 4a5ba95dd01845da0d1c9365ea710bb1929605f5)
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#   DOCKER_REG:       <optional, the URL of a docker registry to utilize
21#                     instead of our default (public.ecr.aws/ubuntu)
22#                     (ex. docker.io)
23#   http_proxy        The HTTP address of the proxy server to connect to.
24#                     Default: "", proxy is not setup if this is not set
25
26import json
27import os
28import re
29import sys
30import threading
31import urllib.request
32from datetime import date
33from hashlib import sha256
34
35# typing.Dict is used for type-hints.
36from typing import Any, Callable, Dict, Iterable, Optional  # noqa: F401
37
38from sh import git, nproc  # type: ignore
39
40try:
41    # System may have docker or it may have podman, try docker first
42    from sh import docker
43
44    container = docker
45except ImportError:
46    try:
47        from sh import podman
48
49        container = podman
50    except Exception:
51        print("No docker or podman found on system")
52        exit(1)
53
54try:
55    # Python before 3.8 doesn't have TypedDict, so reroute to standard 'dict'.
56    from typing import TypedDict
57except Exception:
58
59    class TypedDict(dict):  # type: ignore
60        # We need to do this to eat the 'total' argument.
61        def __init_subclass__(cls, **kwargs: Any) -> None:
62            super().__init_subclass__()
63
64
65# Declare some variables used in package definitions.
66prefix = "/usr/local"
67proc_count = nproc().strip()
68
69
70class PackageDef(TypedDict, total=False):
71    """Package Definition for packages dictionary."""
72
73    # rev [optional]: Revision of package to use.
74    rev: str
75    # url [optional]: lambda function to create URL: (package, rev) -> url.
76    url: Callable[[str, str], str]
77    # depends [optional]: List of package dependencies.
78    depends: Iterable[str]
79    # build_type [required]: Build type used for package.
80    #   Currently supported: autoconf, cmake, custom, make, meson
81    build_type: str
82    # build_steps [optional]: Steps to run for 'custom' build_type.
83    build_steps: Iterable[str]
84    # config_flags [optional]: List of options to pass configuration tool.
85    config_flags: Iterable[str]
86    # config_env [optional]: List of environment variables to set for config.
87    config_env: Iterable[str]
88    # custom_post_dl [optional]: List of steps to run after download, but
89    #   before config / build / install.
90    custom_post_dl: Iterable[str]
91    # custom_post_install [optional]: List of steps to run after install.
92    custom_post_install: Iterable[str]
93
94    # __tag [private]: Generated Docker tag name for package stage.
95    __tag: str
96    # __package [private]: Package object associated with this package.
97    __package: Any  # Type is Package, but not defined yet.
98
99
100# Packages to include in image.
101packages = {
102    "boost": PackageDef(
103        rev="1.86.0",
104        url=(
105            lambda pkg, rev: f"https://github.com/boostorg/{pkg}/releases/download/{pkg}-{rev}/{pkg}-{rev}-cmake.tar.gz"
106        ),
107        build_type="custom",
108        build_steps=[
109            (
110                "./bootstrap.sh"
111                f" --prefix={prefix} --with-libraries=atomic,context,coroutine,filesystem,process,url"
112            ),
113            "./b2",
114            f"./b2 install --prefix={prefix} valgrind=on",
115        ],
116    ),
117    "USCiLab/cereal": PackageDef(
118        rev="v1.3.2",
119        build_type="custom",
120        build_steps=[f"cp -a include/cereal/ {prefix}/include/"],
121    ),
122    "danmar/cppcheck": PackageDef(
123        rev="2.12.1",
124        build_type="cmake",
125    ),
126    "CLIUtils/CLI11": PackageDef(
127        rev="v2.3.2",
128        build_type="cmake",
129        config_flags=[
130            "-DBUILD_TESTING=OFF",
131            "-DCLI11_BUILD_DOCS=OFF",
132            "-DCLI11_BUILD_EXAMPLES=OFF",
133        ],
134    ),
135    "fmtlib/fmt": PackageDef(
136        rev="10.1.1",
137        build_type="cmake",
138        config_flags=[
139            "-DFMT_DOC=OFF",
140            "-DFMT_TEST=OFF",
141        ],
142    ),
143    "Naios/function2": PackageDef(
144        rev="4.2.4",
145        build_type="custom",
146        build_steps=[
147            f"mkdir {prefix}/include/function2",
148            f"cp include/function2/function2.hpp {prefix}/include/function2/",
149        ],
150    ),
151    "google/googletest": PackageDef(
152        rev="v1.15.2",
153        build_type="cmake",
154        config_env=["CXXFLAGS=-std=c++20"],
155        config_flags=["-DTHREADS_PREFER_PTHREAD_FLAG=ON"],
156    ),
157    "nghttp2/nghttp2": PackageDef(
158        rev="v1.61.0",
159        build_type="cmake",
160        config_env=["CXXFLAGS=-std=c++20"],
161        config_flags=[
162            "-DENABLE_LIB_ONLY=ON",
163            "-DENABLE_STATIC_LIB=ON",
164        ],
165    ),
166    "nlohmann/json": PackageDef(
167        rev="v3.11.2",
168        build_type="cmake",
169        config_flags=["-DJSON_BuildTests=OFF"],
170        custom_post_install=[
171            (
172                f"ln -s {prefix}/include/nlohmann/json.hpp"
173                f" {prefix}/include/json.hpp"
174            ),
175        ],
176    ),
177    "json-c/json-c": PackageDef(
178        rev="json-c-0.17-20230812",
179        build_type="cmake",
180    ),
181    "LibVNC/libvncserver": PackageDef(
182        rev="LibVNCServer-0.9.14",
183        build_type="cmake",
184    ),
185    "leethomason/tinyxml2": PackageDef(
186        rev="9.0.0",
187        build_type="cmake",
188    ),
189    "tristanpenman/valijson": PackageDef(
190        rev="v1.0.1",
191        build_type="cmake",
192        config_flags=[
193            "-Dvalijson_BUILD_TESTS=0",
194            "-Dvalijson_INSTALL_HEADERS=1",
195        ],
196    ),
197    "open-power/pdbg": PackageDef(build_type="autoconf"),
198    "openbmc/gpioplus": PackageDef(
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            "-Dlibonly=true",
220            "-Dtests=disabled",
221        ],
222    ),
223    "openbmc/phosphor-objmgr": PackageDef(
224        depends=[
225            "CLIUtils/CLI11",
226            "boost",
227            "leethomason/tinyxml2",
228            "openbmc/phosphor-dbus-interfaces",
229            "openbmc/phosphor-logging",
230            "openbmc/sdbusplus",
231        ],
232        build_type="meson",
233        config_flags=[
234            "-Dtests=disabled",
235        ],
236    ),
237    "openbmc/libpeci": PackageDef(
238        build_type="meson",
239        config_flags=[
240            "-Draw-peci=disabled",
241        ],
242    ),
243    "openbmc/libpldm": PackageDef(
244        build_type="meson",
245        config_flags=[
246            "-Dabi=deprecated,stable",
247            "-Dtests=false",
248            "-Dabi-compliance-check=false",
249        ],
250    ),
251    "openbmc/sdbusplus": PackageDef(
252        depends=[
253            "nlohmann/json",
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=[
268            "openbmc/stdplus",
269        ],
270        build_type="meson",
271        config_flags=[
272            "-Dexamples=false",
273            "-Dtests=disabled",
274        ],
275    ),
276    "openbmc/stdplus": PackageDef(
277        depends=[
278            "fmtlib/fmt",
279            "google/googletest",
280            "Naios/function2",
281        ],
282        build_type="meson",
283        config_flags=[
284            "-Dexamples=false",
285            "-Dtests=disabled",
286            "-Dgtest=enabled",
287        ],
288    ),
289}  # type: Dict[str, PackageDef]
290
291# Define common flags used for builds
292configure_flags = " ".join(
293    [
294        f"--prefix={prefix}",
295    ]
296)
297cmake_flags = " ".join(
298    [
299        "-DBUILD_SHARED_LIBS=ON",
300        "-DCMAKE_BUILD_TYPE=RelWithDebInfo",
301        f"-DCMAKE_INSTALL_PREFIX:PATH={prefix}",
302        "-GNinja",
303        "-DCMAKE_MAKE_PROGRAM=ninja",
304    ]
305)
306meson_flags = " ".join(
307    [
308        "--wrap-mode=nodownload",
309        f"-Dprefix={prefix}",
310    ]
311)
312
313
314class Package(threading.Thread):
315    """Class used to build the Docker stages for each package.
316
317    Generally, this class should not be instantiated directly but through
318    Package.generate_all().
319    """
320
321    # Copy the packages dictionary.
322    packages = packages.copy()
323
324    # Lock used for thread-safety.
325    lock = threading.Lock()
326
327    def __init__(self, pkg: str):
328        """pkg - The name of this package (ex. foo/bar )"""
329        super(Package, self).__init__()
330
331        self.package = pkg
332        self.exception = None  # type: Optional[Exception]
333
334        # Reference to this package's
335        self.pkg_def = Package.packages[pkg]
336        self.pkg_def["__package"] = self
337
338    def run(self) -> None:
339        """Thread 'run' function.  Builds the Docker stage."""
340
341        # In case this package has no rev, fetch it from Github.
342        self._update_rev()
343
344        # Find all the Package objects that this package depends on.
345        #   This section is locked because we are looking into another
346        #   package's PackageDef dict, which could be being modified.
347        Package.lock.acquire()
348        deps: Iterable[Package] = [
349            Package.packages[deppkg]["__package"]
350            for deppkg in self.pkg_def.get("depends", [])
351        ]
352        Package.lock.release()
353
354        # Wait until all the depends finish building.  We need them complete
355        # for the "COPY" commands.
356        for deppkg in deps:
357            deppkg.join()
358
359        # Generate this package's Dockerfile.
360        dockerfile = f"""
361FROM {docker_base_img_name}
362{self._df_copycmds()}
363{self._df_build()}
364"""
365
366        # Generate the resulting tag name and save it to the PackageDef.
367        #   This section is locked because we are modifying the PackageDef,
368        #   which can be accessed by other threads.
369        Package.lock.acquire()
370        tag = Docker.tagname(self._stagename(), dockerfile)
371        self.pkg_def["__tag"] = tag
372        Package.lock.release()
373
374        # Do the build / save any exceptions.
375        try:
376            Docker.build(self.package, tag, dockerfile)
377        except Exception as e:
378            self.exception = e
379
380    @classmethod
381    def generate_all(cls) -> None:
382        """Ensure a Docker stage is created for all defined packages.
383
384        These are done in parallel but with appropriate blocking per
385        package 'depends' specifications.
386        """
387
388        # Create a Package for each defined package.
389        pkg_threads = [Package(p) for p in cls.packages.keys()]
390
391        # Start building them all.
392        #   This section is locked because threads depend on each other,
393        #   based on the packages, and they cannot 'join' on a thread
394        #   which is not yet started.  Adding a lock here allows all the
395        #   threads to start before they 'join' their dependencies.
396        Package.lock.acquire()
397        for t in pkg_threads:
398            t.start()
399        Package.lock.release()
400
401        # Wait for completion.
402        for t in pkg_threads:
403            t.join()
404            # Check if the thread saved off its own exception.
405            if t.exception:
406                print(f"Package {t.package} failed!", file=sys.stderr)
407                raise t.exception
408
409    @staticmethod
410    def df_all_copycmds() -> str:
411        """Formulate the Dockerfile snippet necessary to copy all packages
412        into the final image.
413        """
414        return Package.df_copycmds_set(Package.packages.keys())
415
416    @classmethod
417    def depcache(cls) -> str:
418        """Create the contents of the '/tmp/depcache'.
419        This file is a comma-separated list of "<pkg>:<rev>".
420        """
421
422        # This needs to be sorted for consistency.
423        depcache = ""
424        for pkg in sorted(cls.packages.keys()):
425            depcache += "%s:%s," % (pkg, cls.packages[pkg]["rev"])
426        return depcache
427
428    def _check_gerrit_topic(self) -> bool:
429        if not gerrit_topic:
430            return False
431        if not self.package.startswith("openbmc/"):
432            return False
433        if gerrit_project == self.package and gerrit_rev:
434            return False
435
436        try:
437            commits = json.loads(
438                urllib.request.urlopen(
439                    f"https://gerrit.openbmc.org/changes/?q=status:open+project:{self.package}+topic:{gerrit_topic}"
440                )
441                .read()
442                .splitlines()[-1]
443            )
444
445            if len(commits) == 0:
446                return False
447            if len(commits) > 1:
448                print(
449                    f"{self.package} has more than 1 commit under {gerrit_topic}; using lastest upstream: {len(commits)}",
450                    file=sys.stderr,
451                )
452                return False
453
454            change_id = commits[0]["id"]
455
456            commit = json.loads(
457                urllib.request.urlopen(
458                    f"https://gerrit.openbmc.org/changes/{change_id}/revisions/current/commit"
459                )
460                .read()
461                .splitlines()[-1]
462            )["commit"]
463
464            print(
465                f"Using {commit} from {gerrit_topic} for {self.package}",
466                file=sys.stderr,
467            )
468            self.pkg_def["rev"] = commit
469            return True
470
471        except urllib.error.HTTPError as e:
472            print(
473                f"Error loading topic {gerrit_topic} for {self.package}: ",
474                e,
475                file=sys.stderr,
476            )
477            return False
478
479    def _update_rev(self) -> None:
480        """Look up the HEAD for missing a static rev."""
481
482        if "rev" in self.pkg_def:
483            return
484
485        if self._check_gerrit_topic():
486            return
487
488        # Check if Jenkins/Gerrit gave us a revision and use it.
489        if gerrit_project == self.package and gerrit_rev:
490            print(
491                f"Found Gerrit revision for {self.package}: {gerrit_rev}",
492                file=sys.stderr,
493            )
494            self.pkg_def["rev"] = gerrit_rev
495            return
496
497        # Ask Github for all the branches.
498        lookup = git(
499            "ls-remote", "--heads", f"https://github.com/{self.package}"
500        )
501
502        # Find the branch matching {branch} (or fallback to master).
503        #   This section is locked because we are modifying the PackageDef.
504        Package.lock.acquire()
505        for line in lookup.split("\n"):
506            if re.fullmatch(f".*{branch}$", line.strip()):
507                self.pkg_def["rev"] = line.split()[0]
508                break
509            elif (
510                "refs/heads/master" in line or "refs/heads/main" in line
511            ) and "rev" not in self.pkg_def:
512                self.pkg_def["rev"] = line.split()[0]
513        Package.lock.release()
514
515    def _stagename(self) -> str:
516        """Create a name for the Docker stage associated with this pkg."""
517        return self.package.replace("/", "-").lower()
518
519    def _url(self) -> str:
520        """Get the URL for this package."""
521        rev = self.pkg_def["rev"]
522
523        # If the lambda exists, call it.
524        if "url" in self.pkg_def:
525            return self.pkg_def["url"](self.package, rev)
526
527        # Default to the github archive URL.
528        return f"https://github.com/{self.package}/archive/{rev}.tar.gz"
529
530    def _cmd_download(self) -> str:
531        """Formulate the command necessary to download and unpack to source."""
532
533        url = self._url()
534        if ".tar." not in url:
535            raise NotImplementedError(
536                f"Unhandled download type for {self.package}: {url}"
537            )
538
539        cmd = f"curl -L {url} | tar -x"
540
541        if url.endswith(".bz2"):
542            cmd += "j"
543        elif url.endswith(".gz"):
544            cmd += "z"
545        else:
546            raise NotImplementedError(
547                f"Unknown tar flags needed for {self.package}: {url}"
548            )
549
550        return cmd
551
552    def _cmd_cd_srcdir(self) -> str:
553        """Formulate the command necessary to 'cd' into the source dir."""
554        return f"cd {self.package.split('/')[-1]}*"
555
556    def _df_copycmds(self) -> str:
557        """Formulate the dockerfile snippet necessary to COPY all depends."""
558
559        if "depends" not in self.pkg_def:
560            return ""
561        return Package.df_copycmds_set(self.pkg_def["depends"])
562
563    @staticmethod
564    def df_copycmds_set(pkgs: Iterable[str]) -> str:
565        """Formulate the Dockerfile snippet necessary to COPY a set of
566        packages into a Docker stage.
567        """
568
569        copy_cmds = ""
570
571        # Sort the packages for consistency.
572        for p in sorted(pkgs):
573            tag = Package.packages[p]["__tag"]
574            copy_cmds += f"COPY --from={tag} {prefix} {prefix}\n"
575            # Workaround for upstream docker bug and multiple COPY cmds
576            # https://github.com/moby/moby/issues/37965
577            copy_cmds += "RUN true\n"
578
579        return copy_cmds
580
581    def _df_build(self) -> str:
582        """Formulate the Dockerfile snippet necessary to download, build, and
583        install a package into a Docker stage.
584        """
585
586        # Download and extract source.
587        result = f"RUN {self._cmd_download()} && {self._cmd_cd_srcdir()} && "
588
589        # Handle 'custom_post_dl' commands.
590        custom_post_dl = self.pkg_def.get("custom_post_dl")
591        if custom_post_dl:
592            result += " && ".join(custom_post_dl) + " && "
593
594        # Build and install package based on 'build_type'.
595        build_type = self.pkg_def["build_type"]
596        if build_type == "autoconf":
597            result += self._cmd_build_autoconf()
598        elif build_type == "cmake":
599            result += self._cmd_build_cmake()
600        elif build_type == "custom":
601            result += self._cmd_build_custom()
602        elif build_type == "make":
603            result += self._cmd_build_make()
604        elif build_type == "meson":
605            result += self._cmd_build_meson()
606        else:
607            raise NotImplementedError(
608                f"Unhandled build type for {self.package}: {build_type}"
609            )
610
611        # Handle 'custom_post_install' commands.
612        custom_post_install = self.pkg_def.get("custom_post_install")
613        if custom_post_install:
614            result += " && " + " && ".join(custom_post_install)
615
616        return result
617
618    def _cmd_build_autoconf(self) -> str:
619        options = " ".join(self.pkg_def.get("config_flags", []))
620        env = " ".join(self.pkg_def.get("config_env", []))
621        result = "./bootstrap.sh && "
622        result += f"{env} ./configure {configure_flags} {options} && "
623        result += f"make -j{proc_count} && make install"
624        return result
625
626    def _cmd_build_cmake(self) -> str:
627        options = " ".join(self.pkg_def.get("config_flags", []))
628        env = " ".join(self.pkg_def.get("config_env", []))
629        result = "mkdir builddir && cd builddir && "
630        result += f"{env} cmake {cmake_flags} {options} .. && "
631        result += "cmake --build . --target all && "
632        result += "cmake --build . --target install && "
633        result += "cd .."
634        return result
635
636    def _cmd_build_custom(self) -> str:
637        return " && ".join(self.pkg_def.get("build_steps", []))
638
639    def _cmd_build_make(self) -> str:
640        return f"make -j{proc_count} && make install"
641
642    def _cmd_build_meson(self) -> str:
643        options = " ".join(self.pkg_def.get("config_flags", []))
644        env = " ".join(self.pkg_def.get("config_env", []))
645        result = f"{env} meson setup builddir {meson_flags} {options} && "
646        result += "ninja -C builddir && ninja -C builddir install"
647        return result
648
649
650class Docker:
651    """Class to assist with Docker interactions.  All methods are static."""
652
653    @staticmethod
654    def timestamp() -> str:
655        """Generate a timestamp for today using the ISO week."""
656        today = date.today().isocalendar()
657        return f"{today[0]}-W{today[1]:02}"
658
659    @staticmethod
660    def tagname(pkgname: Optional[str], dockerfile: str) -> str:
661        """Generate a tag name for a package using a hash of the Dockerfile."""
662        result = docker_image_name
663        if pkgname:
664            result += "-" + pkgname
665
666        result += ":" + Docker.timestamp()
667        result += "-" + sha256(dockerfile.encode()).hexdigest()[0:16]
668
669        return result
670
671    @staticmethod
672    def build(pkg: str, tag: str, dockerfile: str) -> None:
673        """Build a docker image using the Dockerfile and tagging it with 'tag'."""
674
675        # If we're not forcing builds, check if it already exists and skip.
676        if not force_build:
677            if container.image.ls(
678                tag, "--format", '"{{.Repository}}:{{.Tag}}"'
679            ):
680                print(
681                    f"Image {tag} already exists.  Skipping.", file=sys.stderr
682                )
683                return
684
685        # Build it.
686        #   Capture the output of the 'docker build' command and send it to
687        #   stderr (prefixed with the package name).  This allows us to see
688        #   progress but not pollute stdout.  Later on we output the final
689        #   docker tag to stdout and we want to keep that pristine.
690        #
691        #   Other unusual flags:
692        #       --no-cache: Bypass the Docker cache if 'force_build'.
693        #       --force-rm: Clean up Docker processes if they fail.
694        container.build(
695            proxy_args,
696            "--network=host",
697            "--force-rm",
698            "--no-cache=true" if force_build else "--no-cache=false",
699            "-t",
700            tag,
701            "-",
702            _in=dockerfile,
703            _out=(
704                lambda line: print(
705                    pkg + ":", line, end="", file=sys.stderr, flush=True
706                )
707            ),
708            _err_to_out=True,
709        )
710
711
712# Read a bunch of environment variables.
713docker_image_name = os.environ.get(
714    "DOCKER_IMAGE_NAME", "openbmc/ubuntu-unit-test"
715)
716force_build = os.environ.get("FORCE_DOCKER_BUILD")
717is_automated_ci_build = os.environ.get("BUILD_URL", False)
718distro = os.environ.get("DISTRO", "ubuntu:oracular")
719branch = os.environ.get("BRANCH", "master")
720ubuntu_mirror = os.environ.get("UBUNTU_MIRROR")
721docker_reg = os.environ.get("DOCKER_REG", "public.ecr.aws/ubuntu")
722http_proxy = os.environ.get("http_proxy")
723
724gerrit_project = os.environ.get("GERRIT_PROJECT")
725gerrit_rev = os.environ.get("GERRIT_PATCHSET_REVISION")
726gerrit_topic = os.environ.get("GERRIT_TOPIC")
727
728# Ensure appropriate docker build output to see progress and identify
729# any issues
730os.environ["BUILDKIT_PROGRESS"] = "plain"
731
732# Set up some common variables.
733username = os.environ.get("USER", "root")
734homedir = os.environ.get("HOME", "/root")
735gid = os.getgid()
736uid = os.getuid()
737
738# Use well-known constants if user is root
739if username == "root":
740    homedir = "/root"
741    gid = 0
742    uid = 0
743
744# Special flags if setting up a deb mirror.
745mirror = ""
746if "ubuntu" in distro and ubuntu_mirror:
747    mirror = f"""
748RUN echo "deb {ubuntu_mirror} \
749        $(. /etc/os-release && echo $VERSION_CODENAME) \
750        main restricted universe multiverse" > /etc/apt/sources.list && \\
751    echo "deb {ubuntu_mirror} \
752        $(. /etc/os-release && echo $VERSION_CODENAME)-updates \
753            main restricted universe multiverse" >> /etc/apt/sources.list && \\
754    echo "deb {ubuntu_mirror} \
755        $(. /etc/os-release && echo $VERSION_CODENAME)-security \
756            main restricted universe multiverse" >> /etc/apt/sources.list && \\
757    echo "deb {ubuntu_mirror} \
758        $(. /etc/os-release && echo $VERSION_CODENAME)-proposed \
759            main restricted universe multiverse" >> /etc/apt/sources.list && \\
760    echo "deb {ubuntu_mirror} \
761        $(. /etc/os-release && echo $VERSION_CODENAME)-backports \
762            main restricted universe multiverse" >> /etc/apt/sources.list
763"""
764
765# Special flags for proxying.
766proxy_cmd = ""
767proxy_keyserver = ""
768proxy_args = []
769if http_proxy:
770    proxy_cmd = f"""
771RUN echo "[http]" >> {homedir}/.gitconfig && \
772    echo "proxy = {http_proxy}" >> {homedir}/.gitconfig
773"""
774    proxy_keyserver = f"--keyserver-options http-proxy={http_proxy}"
775
776    proxy_args.extend(
777        [
778            "--build-arg",
779            f"http_proxy={http_proxy}",
780            "--build-arg",
781            f"https_proxy={http_proxy}",
782        ]
783    )
784
785# Create base Dockerfile.
786dockerfile_base = f"""
787FROM {docker_reg}/{distro}
788
789{mirror}
790
791ENV DEBIAN_FRONTEND noninteractive
792
793ENV PYTHONPATH "/usr/local/lib/python3.10/site-packages/"
794
795# Sometimes the ubuntu key expires and we need a way to force an execution
796# of the apt-get commands for the dbgsym-keyring.  When this happens we see
797# an error like: "Release: The following signatures were invalid:"
798# Insert a bogus echo that we can change here when we get this error to force
799# the update.
800RUN echo "ubuntu keyserver rev as of 2021-04-21"
801
802# We need the keys to be imported for dbgsym repos
803# New releases have a package, older ones fall back to manual fetching
804# https://wiki.ubuntu.com/Debug%20Symbol%20Packages
805# Known issue with gpg to get keys via proxy -
806# https://bugs.launchpad.net/ubuntu/+source/gnupg2/+bug/1788190, hence using
807# curl to get keys.
808RUN apt-get update && apt-get dist-upgrade -yy && \
809    ( apt-get install -yy gpgv ubuntu-dbgsym-keyring || \
810        ( apt-get install -yy dirmngr curl && \
811          curl -sSL \
812          'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622' \
813          | apt-key add - ))
814
815# Parse the current repo list into a debug repo list
816RUN sed -n '/^deb /s,^deb [^ ]* ,deb http://ddebs.ubuntu.com ,p' \
817        /etc/apt/sources.list >/etc/apt/sources.list.d/debug.list
818
819# Remove non-existent debug repos
820RUN sed -i '/-\\(backports\\|security\\) /d' /etc/apt/sources.list.d/debug.list
821
822RUN cat /etc/apt/sources.list.d/debug.list
823
824RUN apt-get update && apt-get dist-upgrade -yy && apt-get install -yy \
825    abi-compliance-checker \
826    abi-dumper \
827    autoconf \
828    autoconf-archive \
829    bison \
830    cmake \
831    curl \
832    dbus \
833    device-tree-compiler \
834    flex \
835    g++-14 \
836    gcc-14 \
837    git \
838    glib-2.0 \
839    gnupg \
840    iproute2 \
841    iputils-ping \
842    libaudit-dev \
843    libc6-dbg \
844    libc6-dev \
845    libcjson-dev \
846    libconfig++-dev \
847    libcryptsetup-dev \
848    libcurl4-openssl-dev \
849    libdbus-1-dev \
850    libevdev-dev \
851    libgpiod-dev \
852    libi2c-dev \
853    libjpeg-dev \
854    libjson-perl \
855    libldap2-dev \
856    libmimetic-dev \
857    libnl-3-dev \
858    libnl-genl-3-dev \
859    libpam0g-dev \
860    libpciaccess-dev \
861    libperlio-gzip-perl \
862    libpng-dev \
863    libprotobuf-dev \
864    libsnmp-dev \
865    libssl-dev \
866    libsystemd-dev \
867    libtool \
868    liburing-dev \
869    libxml2-utils \
870    libxml-simple-perl \
871    lsb-release \
872    ninja-build \
873    npm \
874    pkg-config \
875    protobuf-compiler \
876    python3 \
877    python3-dev\
878    python3-git \
879    python3-mako \
880    python3-pip \
881    python3-protobuf \
882    python3-setuptools \
883    python3-socks \
884    python3-yaml \
885    rsync \
886    shellcheck \
887    socat \
888    software-properties-common \
889    sudo \
890    systemd \
891    systemd-dev \
892    valgrind \
893    vim \
894    wget \
895    xxd
896
897RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 14 \
898  --slave /usr/bin/g++ g++ /usr/bin/g++-14 \
899  --slave /usr/bin/gcov gcov /usr/bin/gcov-14 \
900  --slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-14 \
901  --slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-14
902RUN update-alternatives --remove cpp /usr/bin/cpp && \
903    update-alternatives --install /usr/bin/cpp cpp /usr/bin/cpp-14 14
904
905# Set up LLVM apt repository.
906RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" -- 19
907
908# Install extra clang tools
909RUN apt-get install -y \
910        clang-19 \
911        clang-format-19 \
912        clang-tidy-19
913
914RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 1000 \
915  --slave /usr/bin/clang++ clang++ /usr/bin/clang++-19 \
916  --slave /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-19 \
917  --slave /usr/bin/clang-format clang-format /usr/bin/clang-format-19 \
918  --slave /usr/bin/run-clang-tidy run-clang-tidy.py \
919        /usr/bin/run-clang-tidy-19 \
920  --slave /usr/bin/scan-build scan-build /usr/bin/scan-build-19
921
922"""
923
924if is_automated_ci_build:
925    dockerfile_base += f"""
926# Run an arbitrary command to pollute the docker cache regularly force us
927# to re-run `apt-get update` daily.
928RUN echo {Docker.timestamp()}
929RUN apt-get update && apt-get dist-upgrade -yy
930
931"""
932
933dockerfile_base += """
934RUN pip3 install --break-system-packages \
935        beautysh \
936        black \
937        codespell \
938        flake8 \
939        gcovr \
940        gitlint \
941        inflection \
942        isoduration \
943        isort \
944        jsonschema \
945        meson==1.5.1 \
946        requests
947
948RUN npm install -g \
949        eslint@v8.56.0 eslint-plugin-json@v3.1.0 \
950        markdownlint-cli@latest \
951        prettier@latest
952"""
953
954# Build the base and stage docker images.
955docker_base_img_name = Docker.tagname("base", dockerfile_base)
956Docker.build("base", docker_base_img_name, dockerfile_base)
957Package.generate_all()
958
959# Create the final Dockerfile.
960dockerfile = f"""
961# Build the final output image
962FROM {docker_base_img_name}
963{Package.df_all_copycmds()}
964
965# Some of our infrastructure still relies on the presence of this file
966# even though it is no longer needed to rebuild the docker environment
967# NOTE: The file is sorted to ensure the ordering is stable.
968RUN echo '{Package.depcache()}' > /tmp/depcache
969
970# Ensure the group, user, and home directory are created (or rename them if
971# they already exist).
972RUN if grep -q ":{gid}:" /etc/group ; then \
973        groupmod -n {username} $(awk -F : '{{ if ($3 == {gid}) {{ print $1 }} }}' /etc/group) ; \
974    else \
975        groupadd -f -g {gid} {username} ; \
976    fi
977RUN mkdir -p "{os.path.dirname(homedir)}"
978RUN if grep -q ":{uid}:" /etc/passwd ; then \
979        usermod -l {username} -d {homedir} -m $(awk -F : '{{ if ($3 == {uid}) {{ print $1 }} }}' /etc/passwd) ; \
980    else \
981        useradd -d {homedir} -m -u {uid} -g {gid} {username} ; \
982    fi
983RUN sed -i '1iDefaults umask=000' /etc/sudoers
984RUN echo "{username} ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers
985
986# Ensure user has ability to write to /usr/local for different tool
987# and data installs
988RUN chown -R {username}:{username} /usr/local/share
989
990# Update library cache
991RUN ldconfig
992
993{proxy_cmd}
994
995RUN /bin/bash
996"""
997
998# Do the final docker build
999docker_final_img_name = Docker.tagname(None, dockerfile)
1000Docker.build("final", docker_final_img_name, dockerfile)
1001
1002# Print the tag of the final image.
1003print(docker_final_img_name)
1004