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