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 28 29# typing.Dict is used for type-hints. 30from typing import Any, Callable, Dict, Iterable, Optional # noqa: F401 31 32from sh import docker, git, nproc, uname # type: ignore 33 34try: 35 # Python before 3.8 doesn't have TypedDict, so reroute to standard 'dict'. 36 from typing import TypedDict 37except Exception: 38 39 class TypedDict(dict): # type: ignore 40 # We need to do this to eat the 'total' argument. 41 def __init_subclass__(cls, **kwargs: Any) -> None: 42 super().__init_subclass__() 43 44 45# Declare some variables used in package definitions. 46prefix = "/usr/local" 47proc_count = nproc().strip() 48 49 50class PackageDef(TypedDict, total=False): 51 """Package Definition for packages dictionary.""" 52 53 # rev [optional]: Revision of package to use. 54 rev: str 55 # url [optional]: lambda function to create URL: (package, rev) -> url. 56 url: Callable[[str, str], str] 57 # depends [optional]: List of package dependencies. 58 depends: Iterable[str] 59 # build_type [required]: Build type used for package. 60 # Currently supported: autoconf, cmake, custom, make, meson 61 build_type: str 62 # build_steps [optional]: Steps to run for 'custom' build_type. 63 build_steps: Iterable[str] 64 # config_flags [optional]: List of options to pass configuration tool. 65 config_flags: Iterable[str] 66 # config_env [optional]: List of environment variables to set for config. 67 config_env: Iterable[str] 68 # custom_post_dl [optional]: List of steps to run after download, but 69 # before config / build / install. 70 custom_post_dl: Iterable[str] 71 # custom_post_install [optional]: List of steps to run after install. 72 custom_post_install: Iterable[str] 73 74 # __tag [private]: Generated Docker tag name for package stage. 75 __tag: str 76 # __package [private]: Package object associated with this package. 77 __package: Any # Type is Package, but not defined yet. 78 79 80# Packages to include in image. 81packages = { 82 "boost": PackageDef( 83 rev="1.83.0", 84 url=( 85 lambda pkg, rev: f"https://boostorg.jfrog.io/artifactory/main/release/{rev}/source/{pkg}_{rev.replace('.', '_')}.tar.gz" # noqa: E501 86 ), 87 build_type="custom", 88 build_steps=[ 89 ( 90 "curl --remote-name" 91 " https://github.com/williamspatrick/beast/commit/98f8b1fbd059a35754c2c7b2841769cf8d021272.patch" 92 " && patch -p2 <" 93 " 98f8b1fbd059a35754c2c7b2841769cf8d021272.patch &&" 94 " ./bootstrap.sh" 95 f" --prefix={prefix} --with-libraries=context,coroutine,url" 96 ), 97 "./b2", 98 f"./b2 install --prefix={prefix}", 99 ], 100 ), 101 "USCiLab/cereal": PackageDef( 102 rev="v1.3.2", 103 build_type="custom", 104 build_steps=[f"cp -a include/cereal/ {prefix}/include/"], 105 ), 106 "danmar/cppcheck": PackageDef( 107 rev="2.12.1", 108 build_type="cmake", 109 ), 110 "CLIUtils/CLI11": PackageDef( 111 rev="v2.3.2", 112 build_type="cmake", 113 config_flags=[ 114 "-DBUILD_TESTING=OFF", 115 "-DCLI11_BUILD_DOCS=OFF", 116 "-DCLI11_BUILD_EXAMPLES=OFF", 117 ], 118 ), 119 "fmtlib/fmt": PackageDef( 120 rev="10.1.1", 121 build_type="cmake", 122 config_flags=[ 123 "-DFMT_DOC=OFF", 124 "-DFMT_TEST=OFF", 125 ], 126 ), 127 "Naios/function2": PackageDef( 128 rev="4.2.4", 129 build_type="custom", 130 build_steps=[ 131 f"mkdir {prefix}/include/function2", 132 f"cp include/function2/function2.hpp {prefix}/include/function2/", 133 ], 134 ), 135 "google/googletest": PackageDef( 136 rev="v1.14.0", 137 build_type="cmake", 138 config_env=["CXXFLAGS=-std=c++20"], 139 config_flags=["-DTHREADS_PREFER_PTHREAD_FLAG=ON"], 140 ), 141 "nghttp2/nghttp2": PackageDef( 142 rev="v1.54.0", 143 build_type="cmake", 144 config_env=["CXXFLAGS=-std=c++20"], 145 config_flags=[ 146 "-DENABLE_LIB_ONLY=ON", 147 "-DENABLE_STATIC_LIB=ON", 148 ], 149 ), 150 "nlohmann/json": PackageDef( 151 rev="v3.11.2", 152 build_type="cmake", 153 config_flags=["-DJSON_BuildTests=OFF"], 154 custom_post_install=[ 155 ( 156 f"ln -s {prefix}/include/nlohmann/json.hpp" 157 f" {prefix}/include/json.hpp" 158 ), 159 ], 160 ), 161 "json-c/json-c": PackageDef( 162 rev="json-c-0.16-20220414", 163 build_type="cmake", 164 ), 165 # Snapshot from 2019-05-24 166 "linux-test-project/lcov": PackageDef( 167 rev="v1.15", 168 build_type="make", 169 ), 170 "LibVNC/libvncserver": PackageDef( 171 rev="LibVNCServer-0.9.13", 172 build_type="cmake", 173 ), 174 "leethomason/tinyxml2": PackageDef( 175 rev="9.0.0", 176 build_type="cmake", 177 ), 178 "tristanpenman/valijson": PackageDef( 179 rev="v0.7", 180 build_type="cmake", 181 config_flags=[ 182 "-Dvalijson_BUILD_TESTS=0", 183 "-Dvalijson_INSTALL_HEADERS=1", 184 ], 185 ), 186 "open-power/pdbg": PackageDef(build_type="autoconf"), 187 "openbmc/gpioplus": PackageDef( 188 depends=["openbmc/stdplus"], 189 build_type="meson", 190 config_flags=[ 191 "-Dexamples=false", 192 "-Dtests=disabled", 193 ], 194 ), 195 "openbmc/phosphor-dbus-interfaces": PackageDef( 196 depends=["openbmc/sdbusplus"], 197 build_type="meson", 198 config_flags=["-Dgenerate_md=false"], 199 ), 200 "openbmc/phosphor-logging": PackageDef( 201 depends=[ 202 "USCiLab/cereal", 203 "openbmc/phosphor-dbus-interfaces", 204 "openbmc/sdbusplus", 205 "openbmc/sdeventplus", 206 ], 207 build_type="meson", 208 config_flags=[ 209 "-Dlibonly=true", 210 "-Dtests=disabled", 211 f"-Dyamldir={prefix}/share/phosphor-dbus-yaml/yaml", 212 ], 213 ), 214 "openbmc/phosphor-objmgr": PackageDef( 215 depends=[ 216 "CLIUtils/CLI11", 217 "boost", 218 "leethomason/tinyxml2", 219 "openbmc/phosphor-dbus-interfaces", 220 "openbmc/phosphor-logging", 221 "openbmc/sdbusplus", 222 ], 223 build_type="meson", 224 config_flags=[ 225 "-Dtests=disabled", 226 ], 227 ), 228 "openbmc/libpeci": PackageDef( 229 build_type="meson", 230 config_flags=[ 231 "-Draw-peci=disabled", 232 ], 233 ), 234 "openbmc/libpldm": PackageDef( 235 build_type="meson", 236 config_flags=[ 237 "-Dabi=deprecated,stable", 238 "-Doem-ibm=enabled", 239 "-Dtests=disabled", 240 ], 241 ), 242 "openbmc/sdbusplus": PackageDef( 243 build_type="meson", 244 custom_post_dl=[ 245 "cd tools", 246 f"./setup.py install --root=/ --prefix={prefix}", 247 "cd ..", 248 ], 249 config_flags=[ 250 "-Dexamples=disabled", 251 "-Dtests=disabled", 252 ], 253 ), 254 "openbmc/sdeventplus": PackageDef( 255 depends=[ 256 "Naios/function2", 257 "openbmc/stdplus", 258 ], 259 build_type="meson", 260 config_flags=[ 261 "-Dexamples=false", 262 "-Dtests=disabled", 263 ], 264 ), 265 "openbmc/stdplus": PackageDef( 266 depends=[ 267 "fmtlib/fmt", 268 "google/googletest", 269 "Naios/function2", 270 ], 271 build_type="meson", 272 config_flags=[ 273 "-Dexamples=false", 274 "-Dtests=disabled", 275 "-Dgtest=enabled", 276 ], 277 ), 278} # type: Dict[str, PackageDef] 279 280# Define common flags used for builds 281configure_flags = " ".join( 282 [ 283 f"--prefix={prefix}", 284 ] 285) 286cmake_flags = " ".join( 287 [ 288 "-DBUILD_SHARED_LIBS=ON", 289 "-DCMAKE_BUILD_TYPE=RelWithDebInfo", 290 f"-DCMAKE_INSTALL_PREFIX:PATH={prefix}", 291 "-GNinja", 292 "-DCMAKE_MAKE_PROGRAM=ninja", 293 ] 294) 295meson_flags = " ".join( 296 [ 297 "--wrap-mode=nodownload", 298 f"-Dprefix={prefix}", 299 ] 300) 301 302 303class Package(threading.Thread): 304 """Class used to build the Docker stages for each package. 305 306 Generally, this class should not be instantiated directly but through 307 Package.generate_all(). 308 """ 309 310 # Copy the packages dictionary. 311 packages = packages.copy() 312 313 # Lock used for thread-safety. 314 lock = threading.Lock() 315 316 def __init__(self, pkg: str): 317 """pkg - The name of this package (ex. foo/bar )""" 318 super(Package, self).__init__() 319 320 self.package = pkg 321 self.exception = None # type: Optional[Exception] 322 323 # Reference to this package's 324 self.pkg_def = Package.packages[pkg] 325 self.pkg_def["__package"] = self 326 327 def run(self) -> None: 328 """Thread 'run' function. Builds the Docker stage.""" 329 330 # In case this package has no rev, fetch it from Github. 331 self._update_rev() 332 333 # Find all the Package objects that this package depends on. 334 # This section is locked because we are looking into another 335 # package's PackageDef dict, which could be being modified. 336 Package.lock.acquire() 337 deps: Iterable[Package] = [ 338 Package.packages[deppkg]["__package"] 339 for deppkg in self.pkg_def.get("depends", []) 340 ] 341 Package.lock.release() 342 343 # Wait until all the depends finish building. We need them complete 344 # for the "COPY" commands. 345 for deppkg in deps: 346 deppkg.join() 347 348 # Generate this package's Dockerfile. 349 dockerfile = f""" 350FROM {docker_base_img_name} 351{self._df_copycmds()} 352{self._df_build()} 353""" 354 355 # Generate the resulting tag name and save it to the PackageDef. 356 # This section is locked because we are modifying the PackageDef, 357 # which can be accessed by other threads. 358 Package.lock.acquire() 359 tag = Docker.tagname(self._stagename(), dockerfile) 360 self.pkg_def["__tag"] = tag 361 Package.lock.release() 362 363 # Do the build / save any exceptions. 364 try: 365 Docker.build(self.package, tag, dockerfile) 366 except Exception as e: 367 self.exception = e 368 369 @classmethod 370 def generate_all(cls) -> None: 371 """Ensure a Docker stage is created for all defined packages. 372 373 These are done in parallel but with appropriate blocking per 374 package 'depends' specifications. 375 """ 376 377 # Create a Package for each defined package. 378 pkg_threads = [Package(p) for p in cls.packages.keys()] 379 380 # Start building them all. 381 # This section is locked because threads depend on each other, 382 # based on the packages, and they cannot 'join' on a thread 383 # which is not yet started. Adding a lock here allows all the 384 # threads to start before they 'join' their dependencies. 385 Package.lock.acquire() 386 for t in pkg_threads: 387 t.start() 388 Package.lock.release() 389 390 # Wait for completion. 391 for t in pkg_threads: 392 t.join() 393 # Check if the thread saved off its own exception. 394 if t.exception: 395 print(f"Package {t.package} failed!", file=sys.stderr) 396 raise t.exception 397 398 @staticmethod 399 def df_all_copycmds() -> str: 400 """Formulate the Dockerfile snippet necessary to copy all packages 401 into the final image. 402 """ 403 return Package.df_copycmds_set(Package.packages.keys()) 404 405 @classmethod 406 def depcache(cls) -> str: 407 """Create the contents of the '/tmp/depcache'. 408 This file is a comma-separated list of "<pkg>:<rev>". 409 """ 410 411 # This needs to be sorted for consistency. 412 depcache = "" 413 for pkg in sorted(cls.packages.keys()): 414 depcache += "%s:%s," % (pkg, cls.packages[pkg]["rev"]) 415 return depcache 416 417 def _update_rev(self) -> None: 418 """Look up the HEAD for missing a static rev.""" 419 420 if "rev" in self.pkg_def: 421 return 422 423 # Check if Jenkins/Gerrit gave us a revision and use it. 424 if gerrit_project == self.package and gerrit_rev: 425 print( 426 f"Found Gerrit revision for {self.package}: {gerrit_rev}", 427 file=sys.stderr, 428 ) 429 self.pkg_def["rev"] = gerrit_rev 430 return 431 432 # Ask Github for all the branches. 433 lookup = git( 434 "ls-remote", "--heads", f"https://github.com/{self.package}" 435 ) 436 437 # Find the branch matching {branch} (or fallback to master). 438 # This section is locked because we are modifying the PackageDef. 439 Package.lock.acquire() 440 for line in lookup.split("\n"): 441 if f"refs/heads/{branch}" in line: 442 self.pkg_def["rev"] = line.split()[0] 443 elif ( 444 "refs/heads/master" in line or "refs/heads/main" in line 445 ) 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 setup 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: Optional[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( 613 f"Image {tag} already exists. Skipping.", file=sys.stderr 614 ) 615 return 616 617 # Build it. 618 # Capture the output of the 'docker build' command and send it to 619 # stderr (prefixed with the package name). This allows us to see 620 # progress but not polute stdout. Later on we output the final 621 # docker tag to stdout and we want to keep that pristine. 622 # 623 # Other unusual flags: 624 # --no-cache: Bypass the Docker cache if 'force_build'. 625 # --force-rm: Clean up Docker processes if they fail. 626 docker.build( 627 proxy_args, 628 "--network=host", 629 "--force-rm", 630 "--no-cache=true" if force_build else "--no-cache=false", 631 "-t", 632 tag, 633 "-", 634 _in=dockerfile, 635 _out=( 636 lambda line: print( 637 pkg + ":", line, end="", file=sys.stderr, flush=True 638 ) 639 ), 640 ) 641 642 643# Read a bunch of environment variables. 644docker_image_name = os.environ.get( 645 "DOCKER_IMAGE_NAME", "openbmc/ubuntu-unit-test" 646) 647force_build = os.environ.get("FORCE_DOCKER_BUILD") 648is_automated_ci_build = os.environ.get("BUILD_URL", False) 649distro = os.environ.get("DISTRO", "ubuntu:mantic") 650branch = os.environ.get("BRANCH", "master") 651ubuntu_mirror = os.environ.get("UBUNTU_MIRROR") 652http_proxy = os.environ.get("http_proxy") 653 654gerrit_project = os.environ.get("GERRIT_PROJECT") 655gerrit_rev = os.environ.get("GERRIT_PATCHSET_REVISION") 656 657# Ensure appropriate docker build output to see progress and identify 658# any issues 659os.environ["BUILDKIT_PROGRESS"] = "plain" 660 661# Set up some common variables. 662username = os.environ.get("USER", "root") 663homedir = os.environ.get("HOME", "/root") 664gid = os.getgid() 665uid = os.getuid() 666 667# Use well-known constants if user is root 668if username == "root": 669 homedir = "/root" 670 gid = 0 671 uid = 0 672 673# Determine the architecture for Docker. 674arch = uname("-m").strip() 675if arch == "ppc64le": 676 docker_base = "ppc64le/" 677elif arch == "x86_64": 678 docker_base = "" 679elif arch == "aarch64": 680 docker_base = "arm64v8/" 681else: 682 print( 683 f"Unsupported system architecture({arch}) found for docker image", 684 file=sys.stderr, 685 ) 686 sys.exit(1) 687 688# Special flags if setting up a deb mirror. 689mirror = "" 690if "ubuntu" in distro and ubuntu_mirror: 691 mirror = f""" 692RUN echo "deb {ubuntu_mirror} \ 693 $(. /etc/os-release && echo $VERSION_CODENAME) \ 694 main restricted universe multiverse" > /etc/apt/sources.list && \\ 695 echo "deb {ubuntu_mirror} \ 696 $(. /etc/os-release && echo $VERSION_CODENAME)-updates \ 697 main restricted universe multiverse" >> /etc/apt/sources.list && \\ 698 echo "deb {ubuntu_mirror} \ 699 $(. /etc/os-release && echo $VERSION_CODENAME)-security \ 700 main restricted universe multiverse" >> /etc/apt/sources.list && \\ 701 echo "deb {ubuntu_mirror} \ 702 $(. /etc/os-release && echo $VERSION_CODENAME)-proposed \ 703 main restricted universe multiverse" >> /etc/apt/sources.list && \\ 704 echo "deb {ubuntu_mirror} \ 705 $(. /etc/os-release && echo $VERSION_CODENAME)-backports \ 706 main restricted universe multiverse" >> /etc/apt/sources.list 707""" 708 709# Special flags for proxying. 710proxy_cmd = "" 711proxy_keyserver = "" 712proxy_args = [] 713if http_proxy: 714 proxy_cmd = f""" 715RUN echo "[http]" >> {homedir}/.gitconfig && \ 716 echo "proxy = {http_proxy}" >> {homedir}/.gitconfig 717""" 718 proxy_keyserver = f"--keyserver-options http-proxy={http_proxy}" 719 720 proxy_args.extend( 721 [ 722 "--build-arg", 723 f"http_proxy={http_proxy}", 724 "--build-arg", 725 f"https_proxy={http_proxy}", 726 ] 727 ) 728 729# Create base Dockerfile. 730dockerfile_base = f""" 731FROM {docker_base}{distro} 732 733{mirror} 734 735ENV DEBIAN_FRONTEND noninteractive 736 737ENV PYTHONPATH "/usr/local/lib/python3.10/site-packages/" 738 739# Sometimes the ubuntu key expires and we need a way to force an execution 740# of the apt-get commands for the dbgsym-keyring. When this happens we see 741# an error like: "Release: The following signatures were invalid:" 742# Insert a bogus echo that we can change here when we get this error to force 743# the update. 744RUN echo "ubuntu keyserver rev as of 2021-04-21" 745 746# We need the keys to be imported for dbgsym repos 747# New releases have a package, older ones fall back to manual fetching 748# https://wiki.ubuntu.com/Debug%20Symbol%20Packages 749# Known issue with gpg to get keys via proxy - 750# https://bugs.launchpad.net/ubuntu/+source/gnupg2/+bug/1788190, hence using 751# curl to get keys. 752RUN apt-get update && apt-get dist-upgrade -yy && \ 753 ( apt-get install -yy gpgv ubuntu-dbgsym-keyring || \ 754 ( apt-get install -yy dirmngr curl && \ 755 curl -sSL \ 756 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622' \ 757 | apt-key add - )) 758 759# Parse the current repo list into a debug repo list 760RUN sed -n '/^deb /s,^deb [^ ]* ,deb http://ddebs.ubuntu.com ,p' \ 761 /etc/apt/sources.list >/etc/apt/sources.list.d/debug.list 762 763# Remove non-existent debug repos 764RUN sed -i '/-\\(backports\\|security\\) /d' /etc/apt/sources.list.d/debug.list 765 766RUN cat /etc/apt/sources.list.d/debug.list 767 768RUN apt-get update && apt-get dist-upgrade -yy && apt-get install -yy \ 769 abi-compliance-checker \ 770 abi-dumper \ 771 autoconf \ 772 autoconf-archive \ 773 bison \ 774 clang-17 \ 775 clang-format-17 \ 776 clang-tidy-17 \ 777 clang-tools-17 \ 778 cmake \ 779 curl \ 780 dbus \ 781 device-tree-compiler \ 782 flex \ 783 g++-13 \ 784 gcc-13 \ 785 git \ 786 iproute2 \ 787 iputils-ping \ 788 libaudit-dev \ 789 libc6-dbg \ 790 libc6-dev \ 791 libconfig++-dev \ 792 libcryptsetup-dev \ 793 libdbus-1-dev \ 794 libevdev-dev \ 795 libgpiod-dev \ 796 libi2c-dev \ 797 libjpeg-dev \ 798 libjson-perl \ 799 libldap2-dev \ 800 libmimetic-dev \ 801 libnl-3-dev \ 802 libnl-genl-3-dev \ 803 libpam0g-dev \ 804 libpciaccess-dev \ 805 libperlio-gzip-perl \ 806 libpng-dev \ 807 libprotobuf-dev \ 808 libsnmp-dev \ 809 libssl-dev \ 810 libsystemd-dev \ 811 libtool \ 812 liburing-dev \ 813 libxml2-utils \ 814 libxml-simple-perl \ 815 ninja-build \ 816 npm \ 817 pkg-config \ 818 protobuf-compiler \ 819 python3 \ 820 python3-dev\ 821 python3-git \ 822 python3-mako \ 823 python3-pip \ 824 python3-setuptools \ 825 python3-socks \ 826 python3-yaml \ 827 rsync \ 828 shellcheck \ 829 sudo \ 830 systemd \ 831 valgrind \ 832 valgrind-dbgsym \ 833 vim \ 834 wget \ 835 xxd 836 837RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 \ 838 --slave /usr/bin/g++ g++ /usr/bin/g++-13 \ 839 --slave /usr/bin/gcov gcov /usr/bin/gcov-13 \ 840 --slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-13 \ 841 --slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-13 842RUN update-alternatives --remove cpp /usr/bin/cpp && \ 843 update-alternatives --install /usr/bin/cpp cpp /usr/bin/cpp-13 13 844 845RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 1000 \ 846 --slave /usr/bin/clang++ clang++ /usr/bin/clang++-17 \ 847 --slave /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-17 \ 848 --slave /usr/bin/clang-format clang-format /usr/bin/clang-format-17 \ 849 --slave /usr/bin/run-clang-tidy run-clang-tidy.py \ 850 /usr/bin/run-clang-tidy-17 \ 851 --slave /usr/bin/scan-build scan-build /usr/bin/scan-build-17 852 853""" 854 855if is_automated_ci_build: 856 dockerfile_base += f""" 857# Run an arbitrary command to polute the docker cache regularly force us 858# to re-run `apt-get update` daily. 859RUN echo {Docker.timestamp()} 860RUN apt-get update && apt-get dist-upgrade -yy 861 862""" 863 864dockerfile_base += """ 865RUN pip3 install --break-system-packages \ 866 beautysh \ 867 black \ 868 codespell \ 869 flake8 \ 870 gitlint \ 871 inflection \ 872 isort \ 873 jsonschema \ 874 meson==1.2.3 \ 875 protobuf \ 876 requests 877 878RUN npm install -g \ 879 eslint@latest eslint-plugin-json@latest \ 880 markdownlint-cli@latest \ 881 prettier@latest 882""" 883 884# Build the base and stage docker images. 885docker_base_img_name = Docker.tagname("base", dockerfile_base) 886Docker.build("base", docker_base_img_name, dockerfile_base) 887Package.generate_all() 888 889# Create the final Dockerfile. 890dockerfile = f""" 891# Build the final output image 892FROM {docker_base_img_name} 893{Package.df_all_copycmds()} 894 895# Some of our infrastructure still relies on the presence of this file 896# even though it is no longer needed to rebuild the docker environment 897# NOTE: The file is sorted to ensure the ordering is stable. 898RUN echo '{Package.depcache()}' > /tmp/depcache 899 900# Ensure the group, user, and home directory are created (or rename them if 901# they already exist). 902RUN if grep -q ":{gid}:" /etc/group ; then \ 903 groupmod -n {username} $(awk -F : '{{ if ($3 == {gid}) {{ print $1 }} }}' /etc/group) ; \ 904 else \ 905 groupadd -f -g {gid} {username} ; \ 906 fi 907RUN mkdir -p "{os.path.dirname(homedir)}" 908RUN if grep -q ":{uid}:" /etc/passwd ; then \ 909 usermod -l {username} -d {homedir} -m $(awk -F : '{{ if ($3 == {uid}) {{ print $1 }} }}' /etc/passwd) ; \ 910 else \ 911 useradd -d {homedir} -m -u {uid} -g {gid} {username} ; \ 912 fi 913RUN sed -i '1iDefaults umask=000' /etc/sudoers 914RUN echo "{username} ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers 915 916# Ensure user has ability to write to /usr/local for different tool 917# and data installs 918RUN chown -R {username}:{username} /usr/local/share 919 920{proxy_cmd} 921 922RUN /bin/bash 923""" 924 925# Do the final docker build 926docker_final_img_name = Docker.tagname(None, dockerfile) 927Docker.build("final", docker_final_img_name, dockerfile) 928 929# Print the tag of the final image. 930print(docker_final_img_name) 931