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# default is ubuntu:eoan 10# BRANCH: <optional, branch to build from each of the openbmc/ 11# repositories> 12# default is master, which will be used if input branch not 13# provided or not found 14# UBUNTU_MIRROR: <optional, the URL of a mirror of Ubuntu to override the 15# default ones in /etc/apt/sources.list> 16# default is empty, and no mirror is used. 17# http_proxy The HTTP address of the proxy server to connect to. 18# Default: "", proxy is not setup if this is not set 19 20import os 21import sys 22import threading 23from datetime import date 24from hashlib import sha256 25from sh import docker, git, nproc, uname 26 27# Read a bunch of environment variables. 28docker_image_name = os.environ.get("DOCKER_IMAGE_NAME", "openbmc/ubuntu-unit-test") 29distro = os.environ.get("DISTRO", "ubuntu:focal") 30branch = os.environ.get("BRANCH", "master") 31ubuntu_mirror = os.environ.get("UBUNTU_MIRROR") 32http_proxy = os.environ.get("http_proxy") 33prefix = "/usr/local" 34 35# Set up some common variables. 36proc_count = nproc().strip() 37username = os.environ.get("USER") 38homedir = os.environ.get("HOME") 39gid = os.getgid() 40uid = os.getuid() 41 42# Determine the architecture for Docker. 43arch = uname("-m").strip() 44if arch == "ppc64le": 45 docker_base = "ppc64le/" 46elif arch == "x86_64": 47 docker_base = "" 48else: 49 print( 50 f"Unsupported system architecture({arch}) found for docker image", 51 file=sys.stderr, 52 ) 53 sys.exit(1) 54 55# Packages to include in image. 56packages = { 57 "boost": { 58 "rev": "1.74.0", 59 "url": ( 60 lambda pkg, rev: f"https://dl.bintray.com/boostorg/release/{rev}/source/{pkg}_{rev.replace('.', '_')}.tar.bz2" 61 ), 62 "build_type": "custom", 63 "build_steps": [ 64 f"./bootstrap.sh --prefix={prefix} --with-libraries=context,coroutine", 65 "./b2", 66 f"./b2 install --prefix={prefix}", 67 ], 68 }, 69 "USCiLab/cereal": { 70 "rev": "v1.3.0", 71 "build_type": "custom", 72 "build_steps": [f"cp -a include/cereal/ {prefix}/include/"], 73 }, 74 "catchorg/Catch2": { 75 "rev": "v2.12.2", 76 "build_type": "cmake", 77 "config_flags": ["-DBUILD_TESTING=OFF", "-DCATCH_INSTALL_DOCS=OFF"], 78 }, 79 "CLIUtils/CLI11": { 80 "rev": "v1.9.0", 81 "build_type": "cmake", 82 "config_flags": [ 83 "-DBUILD_TESTING=OFF", 84 "-DCLI11_BUILD_DOCS=OFF", 85 "-DCLI11_BUILD_EXAMPLES=OFF", 86 ], 87 }, 88 "fmtlib/fmt": { 89 "rev": "6.2.1", 90 "build_type": "cmake", 91 "config_flags": [ 92 "-DFMT_DOC=OFF", 93 "-DFMT_TEST=OFF", 94 ], 95 }, 96 # Snapshot from 2020-01-03 97 "Naios/function2": { 98 "rev": "3a0746bf5f601dfed05330aefcb6854354fce07d", 99 "build_type": "custom", 100 "build_steps": [ 101 f"mkdir {prefix}/include/function2", 102 f"cp include/function2/function2.hpp {prefix}/include/function2/", 103 ], 104 }, 105 # Snapshot from 2020-02-13 106 "google/googletest": { 107 "rev": "23b2a3b1cf803999fb38175f6e9e038a4495c8a5", 108 "build_type": "cmake", 109 "config_env": ["CXXFLAGS=-std=c++17"], 110 "config_flags": ["-DTHREADS_PREFER_PTHREAD_FLAG=ON"], 111 }, 112 # Release 2020-08-06 113 "nlohmann/json": { 114 "rev": "v3.9.1", 115 "build_type": "custom", 116 "build_steps": [ 117 f"mkdir {prefix}/include/nlohmann", 118 f"cp include/nlohmann/json.hpp {prefix}/include/nlohmann", 119 f"ln -s {prefix}/include/nlohmann/json.hpp {prefix}/include/json.hpp", 120 ], 121 }, 122 # Snapshot from 2019-05-24 123 "linux-test-project/lcov": { 124 "rev": "75fbae1cfc5027f818a0bb865bf6f96fab3202da", 125 "build_type": "make", 126 }, 127 # dev-5.0 2019-05-03 128 "openbmc/linux": { 129 "rev": "8bf6567e77f7aa68975b7c9c6d044bba690bf327", 130 "build_type": "custom", 131 "build_steps": [ 132 f"make -j{proc_count} defconfig", 133 f"make INSTALL_HDR_PATH={prefix} headers_install", 134 ], 135 }, 136 # Snapshot from 2019-09-03 137 "LibVNC/libvncserver": { 138 "rev": "1354f7f1bb6962dab209eddb9d6aac1f03408110", 139 "build_type": "cmake", 140 }, 141 "martinmoene/span-lite": { 142 "rev": "v0.7.0", 143 "build_type": "cmake", 144 "config_flags": [ 145 "-DSPAN_LITE_OPT_BUILD_TESTS=OFF", 146 ], 147 }, 148 # version from meta-openembedded/meta-oe/recipes-support/libtinyxml2/libtinyxml2_5.0.1.bb 149 "leethomason/tinyxml2": { 150 "rev": "37bc3aca429f0164adf68c23444540b4a24b5778", 151 "build_type": "cmake", 152 }, 153 # version from /meta-openembedded/meta-oe/recipes-devtools/boost-url/boost-url_git.bb 154 "CPPAlliance/url": { 155 "rev": "a56ae0df6d3078319755fbaa67822b4fa7fd352b", 156 "build_type": "cmake", 157 "config_flags": [ 158 "-DBOOST_URL_BUILD_EXAMPLES=OFF", 159 "-DBOOST_URL_BUILD_TESTS=OFF", 160 "-DBOOST_URL_STANDALONE=ON", 161 ], 162 }, 163 # version from meta-openembedded/meta-oe/recipes-devtools/valijson/valijson_git.bb 164 "tristanpenman/valijson": { 165 "rev": "c2f22fddf599d04dc33fcd7ed257c698a05345d9", 166 "build_type": "cmake", 167 "config_flags": [ 168 "-DBUILD_TESTS=0", 169 "-DINSTALL_HEADERS=1", 170 ], 171 }, 172 # version from meta-openembedded/meta-oe/recipes-devtools/nlohmann-fifo/nlohmann-fifo_git.bb 173 "nlohmann/fifo_map": { 174 "rev": "0dfbf5dacbb15a32c43f912a7e66a54aae39d0f9", 175 "build_type": "custom", 176 "build_steps": [f"cp src/fifo_map.hpp {prefix}/include/"], 177 }, 178 "open-power/pdbg": {"build_type": "autoconf"}, 179 "openbmc/gpioplus": { 180 "depends": ["openbmc/stdplus"], 181 "build_type": "meson", 182 "config_flags": [ 183 "-Dexamples=false", 184 "-Dtests=disabled", 185 ], 186 }, 187 "openbmc/phosphor-dbus-interfaces": { 188 "depends": ["openbmc/sdbusplus"], 189 "build_type": "meson", 190 "config_flags": [ 191 "-Ddata_com_ibm=true", 192 "-Ddata_org_open_power=true", 193 ], 194 }, 195 "openbmc/phosphor-logging": { 196 "depends": [ 197 "USCiLab/cereal", 198 "nlohmann/fifo_map", 199 "openbmc/phosphor-dbus-interfaces", 200 "openbmc/sdbusplus", 201 "openbmc/sdeventplus", 202 ], 203 "build_type": "autoconf", 204 "config_flags": [ 205 "--enable-metadata-processing", 206 f"YAML_DIR={prefix}/share/phosphor-dbus-yaml/yaml", 207 ], 208 }, 209 "openbmc/phosphor-objmgr": { 210 "depends": [ 211 "boost", 212 "leethomason/tinyxml2", 213 "openbmc/phosphor-logging", 214 "openbmc/sdbusplus", 215 ], 216 "build_type": "autoconf", 217 }, 218 "openbmc/pldm": { 219 "depends": [ 220 "CLIUtils/CLI11", 221 "boost", 222 "nlohmann/json", 223 "openbmc/phosphor-dbus-interfaces", 224 "openbmc/phosphor-logging", 225 "openbmc/sdbusplus", 226 "openbmc/sdeventplus", 227 ], 228 "build_type": "meson", 229 "config_flags": [ 230 "-Dlibpldm-only=enabled", 231 "-Doem-ibm=enabled", 232 "-Dtests=disabled", 233 ], 234 }, 235 "openbmc/sdbusplus": { 236 "build_type": "meson", 237 "custom_post_dl": [ 238 "cd tools", 239 f"./setup.py install --root=/ --prefix={prefix}", 240 "cd ..", 241 ], 242 "config_flags": [ 243 "-Dexamples=disabled", 244 "-Dtests=disabled", 245 ], 246 }, 247 "openbmc/sdeventplus": { 248 "depends": ["Naios/function2", "openbmc/stdplus"], 249 "build_type": "meson", 250 "config_flags": [ 251 "-Dexamples=false", 252 "-Dtests=disabled", 253 ], 254 }, 255 "openbmc/stdplus": { 256 "depends": ["fmtlib/fmt", "martinmoene/span-lite"], 257 "build_type": "meson", 258 "config_flags": [ 259 "-Dexamples=false", 260 "-Dtests=disabled", 261 ], 262 }, 263} 264 265 266def pkg_rev(pkg): 267 return packages[pkg]["rev"] 268 269 270def pkg_stagename(pkg): 271 return pkg.replace("/", "-").lower() 272 273 274def pkg_url(pkg): 275 if "url" in packages[pkg]: 276 return packages[pkg]["url"](pkg, pkg_rev(pkg)) 277 return f"https://github.com/{pkg}/archive/{pkg_rev(pkg)}.tar.gz" 278 279 280def pkg_download(pkg): 281 url = pkg_url(pkg) 282 if ".tar." not in url: 283 raise NotImplementedError(f"Unhandled download type for {pkg}: {url}") 284 cmd = f"curl -L {url} | tar -x" 285 if url.endswith(".bz2"): 286 cmd += "j" 287 if url.endswith(".gz"): 288 cmd += "z" 289 return cmd 290 291 292def pkg_copycmds(pkg=None): 293 pkgs = [] 294 if pkg: 295 if "depends" not in packages[pkg]: 296 return "" 297 pkgs = sorted(packages[pkg]["depends"]) 298 else: 299 pkgs = sorted(packages.keys()) 300 301 copy_cmds = "" 302 for p in pkgs: 303 copy_cmds += f"COPY --from={packages[p]['__tag']} {prefix} {prefix}\n" 304 # Workaround for upstream docker bug and multiple COPY cmds 305 # https://github.com/moby/moby/issues/37965 306 copy_cmds += "RUN true\n" 307 return copy_cmds 308 309 310def pkg_cd_srcdir(pkg): 311 return f"cd {pkg.split('/')[-1]}* && " 312 313 314def pkg_build(pkg): 315 result = f"RUN {pkg_download(pkg)} && " 316 result += pkg_cd_srcdir(pkg) 317 318 if "custom_post_dl" in packages[pkg]: 319 result += " && ".join(packages[pkg]["custom_post_dl"]) + " && " 320 321 build_type = packages[pkg]["build_type"] 322 if build_type == "autoconf": 323 result += pkg_build_autoconf(pkg) 324 elif build_type == "cmake": 325 result += pkg_build_cmake(pkg) 326 elif build_type == "custom": 327 result += pkg_build_custom(pkg) 328 elif build_type == "make": 329 result += pkg_build_make(pkg) 330 elif build_type == "meson": 331 result += pkg_build_meson(pkg) 332 else: 333 raise NotImplementedError( 334 f"Unhandled build type for {pkg}: {packages[pkg]['build_type']}" 335 ) 336 337 return result 338 339 340def pkg_build_autoconf(pkg): 341 options = " ".join(packages[pkg].get("config_flags", [])) 342 env = " ".join(packages[pkg].get("config_env", [])) 343 result = "./bootstrap.sh && " 344 result += f"{env} ./configure {configure_flags} {options} && " 345 result += f"make -j{proc_count} && " 346 result += "make install " 347 return result 348 349 350def pkg_build_cmake(pkg): 351 options = " ".join(packages[pkg].get("config_flags", [])) 352 env = " ".join(packages[pkg].get("config_env", [])) 353 result = "mkdir builddir && cd builddir && " 354 result += f"{env} cmake {cmake_flags} {options} .. && " 355 result += "cmake --build . --target all && " 356 result += "cmake --build . --target install && " 357 result += "cd .. " 358 return result 359 360 361def pkg_build_custom(pkg): 362 return " && ".join(packages[pkg].get("build_steps", [])) 363 364 365def pkg_build_make(pkg): 366 result = f"make -j{proc_count} && " 367 result += "make install " 368 return result 369 370 371def pkg_build_meson(pkg): 372 options = " ".join(packages[pkg].get("config_flags", [])) 373 env = " ".join(packages[pkg].get("config_env", [])) 374 result = f"{env} meson builddir {meson_flags} {options} && " 375 result += "ninja -C builddir && ninja -C builddir install " 376 return result 377 378 379pkg_lock = threading.Lock() 380 381 382def pkg_generate(pkg): 383 class pkg_thread(threading.Thread): 384 def run(self): 385 pkg_lock.acquire() 386 deps = [ 387 packages[deppkg]["__thread"] 388 for deppkg in sorted(packages[pkg].get("depends", [])) 389 ] 390 pkg_lock.release() 391 for deppkg in deps: 392 deppkg.join() 393 394 dockerfile = f""" 395FROM {docker_base_img_name} 396{pkg_copycmds(pkg)} 397{pkg_build(pkg)} 398""" 399 400 pkg_lock.acquire() 401 tag = docker_img_tagname(pkg_stagename(pkg), dockerfile) 402 packages[pkg]["__tag"] = tag 403 pkg_lock.release() 404 405 try: 406 self.exception = None 407 docker_img_build(pkg, tag, dockerfile) 408 except Exception as e: 409 self.package = pkg 410 self.exception = e 411 412 packages[pkg]["__thread"] = pkg_thread() 413 414 415def pkg_generate_packages(): 416 for pkg in packages.keys(): 417 pkg_generate(pkg) 418 419 pkg_lock.acquire() 420 pkg_threads = [packages[p]["__thread"] for p in packages.keys()] 421 for t in pkg_threads: 422 t.start() 423 pkg_lock.release() 424 425 for t in pkg_threads: 426 t.join() 427 if t.exception: 428 print(f"Package {t.package} failed!", file=sys.stderr) 429 raise t.exception 430 431 432def docker_img_tagname(pkgname, dockerfile): 433 result = docker_image_name 434 if pkgname: 435 result += "-" + pkgname 436 result += ":" + date.today().isoformat() 437 result += "-" + sha256(dockerfile.encode()).hexdigest()[0:16] 438 return result 439 440 441def docker_img_build(pkg, tag, dockerfile): 442 docker.build( 443 proxy_args, 444 "--network=host", 445 "--force-rm", 446 "-t", 447 tag, 448 "-", 449 _in=dockerfile, 450 _out=( 451 lambda line: print(pkg + ":", line, end="", file=sys.stderr, flush=True) 452 ), 453 ) 454 455 456# Look up the HEAD for missing a static rev. 457pkg_lookups = {} 458for pkg in packages.keys(): 459 if "rev" in packages[pkg]: 460 continue 461 pkg_lookups[pkg] = git( 462 "ls-remote", "--heads", f"https://github.com/{pkg}", _bg=True 463 ) 464for pkg, result in pkg_lookups.items(): 465 for line in result.stdout.decode().split("\n"): 466 if f"refs/heads/{branch}" in line: 467 packages[pkg]["rev"] = line.strip().split()[0] 468 elif "refs/heads/master" in line and p not in packages: 469 packages[pkg]["rev"] = line.strip().split()[0] 470 471# Create the contents of the '/tmp/depcache'. 472# This needs to be sorted for consistency. 473depcache = "" 474for pkg in sorted(packages.keys()): 475 depcache += "%s:%s," % (pkg, pkg_rev(pkg)) 476 477# Define common flags used for builds 478configure_flags = " ".join( 479 [ 480 f"--prefix={prefix}", 481 ] 482) 483cmake_flags = " ".join( 484 [ 485 "-DBUILD_SHARED_LIBS=ON", 486 "-DCMAKE_BUILD_TYPE=RelWithDebInfo", 487 f"-DCMAKE_INSTALL_PREFIX:PATH={prefix}", 488 "-GNinja", 489 "-DCMAKE_MAKE_PROGRAM=ninja", 490 ] 491) 492meson_flags = " ".join( 493 [ 494 "--wrap-mode=nodownload", 495 f"-Dprefix={prefix}", 496 ] 497) 498 499# Special flags if setting up a deb mirror. 500mirror = "" 501if "ubuntu" in distro and ubuntu_mirror: 502 mirror = f""" 503RUN echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME) main restricted universe multiverse" > /etc/apt/sources.list && \\ 504 echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-updates main restricted universe multiverse" >> /etc/apt/sources.list && \\ 505 echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-security main restricted universe multiverse" >> /etc/apt/sources.list && \\ 506 echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-proposed main restricted universe multiverse" >> /etc/apt/sources.list && \\ 507 echo "deb {ubuntu_mirror} $(. /etc/os-release && echo $VERSION_CODENAME)-backports main restricted universe multiverse" >> /etc/apt/sources.list 508""" 509 510# Special flags for proxying. 511proxy_cmd = "" 512proxy_args = [] 513if http_proxy: 514 proxy_cmd = f""" 515RUN echo "[http]" >> {homedir}/.gitconfig && \ 516 echo "proxy = {http_proxy}" >> {homedir}/.gitconfig 517""" 518 proxy_args.extend( 519 [ 520 "--build-arg", 521 f"http_proxy={http_proxy}", 522 "--build-arg", 523 "https_proxy={https_proxy}", 524 ] 525 ) 526 527# Create docker image that can run package unit tests 528dockerfile_base = f""" 529FROM {docker_base}{distro} 530 531{mirror} 532 533ENV DEBIAN_FRONTEND noninteractive 534 535ENV PYTHONPATH "/usr/local/lib/python3.8/site-packages/" 536 537# We need the keys to be imported for dbgsym repos 538# New releases have a package, older ones fall back to manual fetching 539# https://wiki.ubuntu.com/Debug%20Symbol%20Packages 540RUN apt-get update && ( apt-get install ubuntu-dbgsym-keyring || ( apt-get install -yy dirmngr && \ 541 apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622 ) ) 542 543# Parse the current repo list into a debug repo list 544RUN sed -n '/^deb /s,^deb [^ ]* ,deb http://ddebs.ubuntu.com ,p' /etc/apt/sources.list >/etc/apt/sources.list.d/debug.list 545 546# Remove non-existent debug repos 547RUN sed -i '/-\(backports\|security\) /d' /etc/apt/sources.list.d/debug.list 548 549RUN cat /etc/apt/sources.list.d/debug.list 550 551RUN apt-get update && apt-get dist-upgrade -yy && apt-get install -yy \ 552 gcc-10 \ 553 g++-10 \ 554 libc6-dbg \ 555 libc6-dev \ 556 libtool \ 557 bison \ 558 libdbus-1-dev \ 559 flex \ 560 cmake \ 561 python3 \ 562 python3-dev\ 563 python3-yaml \ 564 python3-mako \ 565 python3-pip \ 566 python3-setuptools \ 567 python3-git \ 568 python3-socks \ 569 pkg-config \ 570 autoconf \ 571 autoconf-archive \ 572 libsystemd-dev \ 573 systemd \ 574 libssl-dev \ 575 libevdev-dev \ 576 libevdev2-dbgsym \ 577 libjpeg-dev \ 578 libpng-dev \ 579 ninja-build \ 580 sudo \ 581 curl \ 582 git \ 583 dbus \ 584 iputils-ping \ 585 clang-10 \ 586 clang-format-10 \ 587 clang-tidy-10 \ 588 clang-tools-10 \ 589 shellcheck \ 590 npm \ 591 iproute2 \ 592 libnl-3-dev \ 593 libnl-genl-3-dev \ 594 libconfig++-dev \ 595 libsnmp-dev \ 596 valgrind \ 597 valgrind-dbg \ 598 libpam0g-dev \ 599 xxd \ 600 libi2c-dev \ 601 wget \ 602 libldap2-dev \ 603 libprotobuf-dev \ 604 libperlio-gzip-perl \ 605 libjson-perl \ 606 protobuf-compiler \ 607 libgpiod-dev \ 608 device-tree-compiler \ 609 cppcheck \ 610 libpciaccess-dev \ 611 libmimetic-dev \ 612 libxml2-utils \ 613 libxml-simple-perl 614 615RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 1000 \ 616 --slave /usr/bin/g++ g++ /usr/bin/g++-10 \ 617 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 \ 618 --slave /usr/bin/gcov-dump gcov-dump /usr/bin/gcov-dump-10 \ 619 --slave /usr/bin/gcov-tool gcov-tool /usr/bin/gcov-tool-10 620 621 622RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-10 1000 \ 623 --slave /usr/bin/clang++ clang++ /usr/bin/clang++-10 \ 624 --slave /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-10 \ 625 --slave /usr/bin/clang-format clang-format /usr/bin/clang-format-10 \ 626 --slave /usr/bin/run-clang-tidy.py run-clang-tidy.py /usr/bin/run-clang-tidy-10.py 627 628RUN pip3 install inflection 629RUN pip3 install pycodestyle 630RUN pip3 install jsonschema 631RUN pip3 install meson==0.54.3 632RUN pip3 install protobuf 633""" 634 635# Build the stage docker images. 636docker_base_img_name = docker_img_tagname("base", dockerfile_base) 637docker_img_build("base", docker_base_img_name, dockerfile_base) 638pkg_generate_packages() 639 640dockerfile = f""" 641# Build the final output image 642FROM {docker_base_img_name} 643{pkg_copycmds()} 644 645# Some of our infrastructure still relies on the presence of this file 646# even though it is no longer needed to rebuild the docker environment 647# NOTE: The file is sorted to ensure the ordering is stable. 648RUN echo '{depcache}' > /tmp/depcache 649 650# Final configuration for the workspace 651RUN grep -q {gid} /etc/group || groupadd -g {gid} {username} 652RUN mkdir -p "{os.path.dirname(homedir)}" 653RUN grep -q {uid} /etc/passwd || useradd -d {homedir} -m -u {uid} -g {gid} {username} 654RUN sed -i '1iDefaults umask=000' /etc/sudoers 655RUN echo "{username} ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers 656 657{proxy_cmd} 658 659RUN /bin/bash 660""" 661 662# Do the final docker build 663docker_img_build("final", docker_image_name, dockerfile) 664