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