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