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