1#!/usr/bin/env python3 2# Copyright 2017 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Builds crosvm in debug/release mode on all supported target architectures. 7 8A sysroot for each target architectures is required. The defaults are all generic boards' sysroots, 9but they can be changed with the command line arguments. 10 11To test changes more quickly, set the --noclean option. This prevents the target directories from 12being removed before building and testing. 13 14For easy binary size comparison, use the --size-only option to only do builds that will result in a 15binary size output, which are non-test release builds. 16 17This script automatically determines which packages will need to be tested based on the directory 18structure with Cargo.toml files. Only top-level crates are tested directly. To skip a top-level 19package, add an empty .build_test_skip file to the directory. Rarely, if a package needs to have its 20tests run single-threaded, add an empty .build_test_serial file to the directory. 21""" 22 23from __future__ import print_function 24import argparse 25import functools 26import multiprocessing.pool 27import os 28import shutil 29import subprocess 30import sys 31 32sys.path.append(os.path.dirname(sys.path[0])) 33 34from enabled_features import ENABLED_FEATURES, BUILD_FEATURES 35from files_to_include import DLLS, BINARIES 36from prepare_dlls import build_dlls, copy_dlls 37 38# Is Windows 39IS_WINDOWS = os.name == "nt" 40 41ARM_TRIPLE = os.getenv("ARM_TRIPLE", "armv7a-cros-linux-gnueabihf") 42AARCH64_TRIPLE = os.getenv("AARCH64_TRIPLE", "aarch64-cros-linux-gnu") 43X86_64_TRIPLE = os.getenv("X86_64_TRIPLE", "x86_64-unknown-linux-gnu") 44X86_64_WIN_MSVC_TRIPLE = os.getenv("X86_64_WIN_MSVC_TRIPLE", "x86_64-pc-windows-msvc") 45SYMBOL_EXPORTS = ["NvOptimusEnablement", "AmdPowerXpressRequestHighPerformance"] 46 47LINUX_BUILD_ONLY_MODULES = [ 48 "io_jail", 49 "poll_token_derive", 50 "wire_format_derive", 51 "bit_field_derive", 52 "linux_input_sys", 53 "vfio_sys", 54] 55 56# Bright green. 57PASS_COLOR = "\033[1;32m" 58# Bright red. 59FAIL_COLOR = "\033[1;31m" 60# Default color. 61END_COLOR = "\033[0m" 62 63 64def crosvm_binary_name(): 65 return "crosvm.exe" if IS_WINDOWS else "crosvm" 66 67 68def get_target_path(triple, kind, test_it): 69 """Constructs a target path based on the configuration parameters. 70 71 Args: 72 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 73 kind: 'debug' or 'release'. 74 test_it: If this target is tested. 75 """ 76 target_path = os.path.abspath(os.path.join(os.sep, "tmp", "{}_{}".format(triple, kind))) 77 if test_it: 78 target_path += "_test" 79 return target_path 80 81 82def validate_symbols(triple, is_release): 83 kind = "release" if is_release else "debug" 84 target_path = get_target_path(triple, kind, False) 85 binary_path = os.path.join(target_path, triple, kind, crosvm_binary_name()) 86 with open(binary_path, mode="rb") as f: 87 contents = f.read().decode("ascii", errors="ignore") 88 return all(symbol in contents for symbol in SYMBOL_EXPORTS) 89 90 91def build_target( 92 triple, 93 is_release, 94 env, 95 only_build_targets, 96 test_module_parallel, 97 test_module_serial, 98): 99 """Does a cargo build for the triple in release or debug mode. 100 101 Args: 102 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 103 is_release: True to build a release version. 104 env: Enviroment variables to run cargo with. 105 only_build_targets: Only build packages that will be tested. 106 """ 107 args = ["cargo", "build", "--target=%s" % triple] 108 109 if is_release: 110 args.append("--release") 111 112 if only_build_targets: 113 test_modules = test_module_parallel + test_module_serial 114 if not IS_WINDOWS: 115 test_modules += LINUX_BUILD_ONLY_MODULES 116 for mod in test_modules: 117 args.append("-p") 118 args.append(mod) 119 120 args.append("--features") 121 args.append(",".join(BUILD_FEATURES)) 122 123 if subprocess.Popen(args, env=env).wait() != 0: 124 return False, "build error" 125 if IS_WINDOWS and not validate_symbols(triple, is_release): 126 return False, "error validating discrete gpu symbols" 127 128 return True, "pass" 129 130 131def test_target_modules(triple, is_release, env, no_run, modules, parallel): 132 """Does a cargo test on given modules for the triple and configuration. 133 134 Args: 135 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 136 is_release: True to build a release version. 137 env: Enviroment variables to run cargo with. 138 no_run: True to pass --no-run flag to cargo test. 139 modules: List of module strings to test. 140 parallel: True to run the tests in parallel threads. 141 """ 142 args = ["cargo", "test", "--target=%s" % triple] 143 144 if is_release: 145 args.append("--release") 146 147 if no_run: 148 args.append("--no-run") 149 150 for mod in modules: 151 args.append("-p") 152 args.append(mod) 153 154 args.append("--features") 155 args.append(",".join(ENABLED_FEATURES)) 156 157 if not parallel: 158 args.append("--") 159 args.append("--test-threads=1") 160 return subprocess.Popen(args, env=env).wait() == 0 161 162 163def test_target(triple, is_release, env, no_run, test_modules_parallel, test_modules_serial): 164 """Does a cargo test for the given triple and configuration. 165 166 Args: 167 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 168 is_release: True to build a release version. 169 env: Enviroment variables to run cargo with. 170 no_run: True to pass --no-run flag to cargo test. 171 """ 172 173 parallel_result = test_target_modules( 174 triple, is_release, env, no_run, test_modules_parallel, True 175 ) 176 177 serial_result = test_target_modules(triple, is_release, env, no_run, test_modules_serial, False) 178 179 return parallel_result and serial_result 180 181 182def build_or_test( 183 sysroot, 184 triple, 185 kind, 186 skip_file_name, 187 test_it=False, 188 no_run=False, 189 clean=False, 190 copy_output=False, 191 copy_directory=None, 192 only_build_targets=False, 193): 194 """Runs relevant builds/tests for the given triple and configuration 195 196 Args: 197 sysroot: path to the target's sysroot directory. 198 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 199 kind: 'debug' or 'release'. 200 skip_file_name: Skips building and testing a crate if this file is found in 201 crate's root directory. 202 test_it: True to test this triple and kind. 203 no_run: True to just compile and not run tests (only if test_it=True) 204 clean: True to skip cleaning the target path. 205 copy_output: True to copy build artifacts to external directory. 206 output_directory: Destination of copy of build artifacts. 207 only_build_targets: Only build packages that will be tested. 208 """ 209 if not os.path.isdir(sysroot) and not IS_WINDOWS: 210 return False, "sysroot missing" 211 212 target_path = get_target_path(triple, kind, test_it) 213 214 if clean: 215 shutil.rmtree(target_path, True) 216 217 is_release = kind == "release" 218 219 env = os.environ.copy() 220 env["TARGET_CC"] = "%s-clang" % triple 221 env["SYSROOT"] = sysroot 222 env["CARGO_TARGET_DIR"] = target_path 223 224 if not IS_WINDOWS: 225 # The lib dir could be in either lib or lib64 depending on the target. Rather than checking to see 226 # which one is valid, just add both and let the dynamic linker and pkg-config search. 227 libdir = os.path.join(sysroot, "usr", "lib") 228 lib64dir = os.path.join(sysroot, "usr", "lib64") 229 libdir_pc = os.path.join(libdir, "pkgconfig") 230 lib64dir_pc = os.path.join(lib64dir, "pkgconfig") 231 232 # This line that changes the dynamic library path is needed for upstream, but breaks 233 # downstream's crosvm linux kokoro presubmits. 234 # env['LD_LIBRARY_PATH'] = libdir + ':' + lib64dir 235 env["PKG_CONFIG_ALLOW_CROSS"] = "1" 236 env["PKG_CONFIG_LIBDIR"] = libdir_pc + ":" + lib64dir_pc 237 env["PKG_CONFIG_SYSROOT_DIR"] = sysroot 238 if "KOKORO_JOB_NAME" not in os.environ: 239 env["RUSTFLAGS"] = "-C linker=" + env["TARGET_CC"] 240 if is_release: 241 env["RUSTFLAGS"] += " -Cembed-bitcode=yes -Clto" 242 243 if IS_WINDOWS and not test_it: 244 for symbol in SYMBOL_EXPORTS: 245 env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -C link-args=/EXPORT:{}".format(symbol) 246 247 deps_dir = os.path.join(target_path, triple, kind, "deps") 248 if not os.path.exists(deps_dir): 249 os.makedirs(deps_dir) 250 251 target_dirs = [deps_dir] 252 if copy_output: 253 os.makedirs(os.path.join(copy_directory, kind), exist_ok=True) 254 if not test_it: 255 target_dirs.append(os.path.join(copy_directory, kind)) 256 257 copy_dlls(os.getcwd(), target_dirs, kind) 258 259 (test_modules_parallel, test_modules_serial) = get_test_modules(skip_file_name) 260 print("modules to test in parallel:\n", test_modules_parallel) 261 print("modules to test serially:\n", test_modules_serial) 262 263 if not test_modules_parallel and not test_modules_serial: 264 print("All build and tests skipped.") 265 return True, "pass" 266 267 if test_it: 268 if not test_target( 269 triple, is_release, env, no_run, test_modules_parallel, test_modules_serial 270 ): 271 return False, "test error" 272 else: 273 res, err = build_target( 274 triple, 275 is_release, 276 env, 277 only_build_targets, 278 test_modules_parallel, 279 test_modules_serial, 280 ) 281 if not res: 282 return res, err 283 284 # We only care about the non-test binaries, so only copy the output from cargo build. 285 if copy_output and not test_it: 286 binary_src = os.path.join(target_path, triple, kind, crosvm_binary_name()) 287 pdb_src = binary_src.replace(".exe", "") + ".pdb" 288 binary_dst = os.path.join(copy_directory, kind) 289 shutil.copy(binary_src, binary_dst) 290 shutil.copy(pdb_src, binary_dst) 291 292 return True, "pass" 293 294 295def get_test_modules(skip_file_name): 296 """Returns a list of modules to test. 297 Args: 298 skip_file_name: Skips building and testing a crate if this file is found in 299 crate's root directory. 300 """ 301 if IS_WINDOWS and not os.path.isfile(skip_file_name): 302 test_modules_parallel = ["crosvm"] 303 else: 304 test_modules_parallel = [] 305 test_modules_serial = [] 306 307 file_in_crate = lambda file_name: os.path.isfile(os.path.join(crate.path, file_name)) 308 serial_file_name = "{}build_test_serial".format(".win_" if IS_WINDOWS else ".") 309 with os.scandir() as it: 310 for crate in it: 311 if file_in_crate("Cargo.toml"): 312 if file_in_crate(skip_file_name): 313 continue 314 if file_in_crate(serial_file_name): 315 test_modules_serial.append(crate.name) 316 else: 317 test_modules_parallel.append(crate.name) 318 319 test_modules_parallel.sort() 320 test_modules_serial.sort() 321 322 return (test_modules_parallel, test_modules_serial) 323 324 325def get_stripped_size(triple): 326 """Returns the formatted size of the given triple's release binary. 327 328 Args: 329 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 330 """ 331 target_path = get_target_path(triple, "release", False) 332 bin_path = os.path.join(target_path, triple, "release", crosvm_binary_name()) 333 proc = subprocess.Popen(["%s-strip" % triple, bin_path]) 334 335 if proc.wait() != 0: 336 return "failed" 337 338 return "%dKiB" % (os.path.getsize(bin_path) / 1024) 339 340 341def get_parser(): 342 """Gets the argument parser""" 343 parser = argparse.ArgumentParser(description=__doc__) 344 if IS_WINDOWS: 345 parser.add_argument( 346 "--x86_64-msvc-sysroot", 347 default="build/amd64-msvc", 348 help="x86_64 sysroot directory (default=%(default)s)", 349 ) 350 else: 351 parser.add_argument( 352 "--arm-sysroot", 353 default="/build/arm-generic", 354 help="ARM sysroot directory (default=%(default)s)", 355 ) 356 parser.add_argument( 357 "--aarch64-sysroot", 358 default="/build/arm64-generic", 359 help="AARCH64 sysroot directory (default=%(default)s)", 360 ) 361 parser.add_argument( 362 "--x86_64-sysroot", 363 default="/build/amd64-generic", 364 help="x86_64 sysroot directory (default=%(default)s)", 365 ) 366 367 parser.add_argument( 368 "--noclean", 369 dest="clean", 370 default=True, 371 action="store_false", 372 help="Keep the tempororary build directories.", 373 ) 374 parser.add_argument( 375 "--copy", 376 default=False, 377 help="Copies .exe files to an output directory for later use", 378 ) 379 parser.add_argument( 380 "--copy-directory", 381 default="/output", 382 help="Destination of .exe files when using --copy", 383 ) 384 parser.add_argument( 385 "--serial", 386 default=True, 387 action="store_false", 388 dest="parallel", 389 help="Run cargo build serially rather than in parallel", 390 ) 391 # TODO(b/154029826): Remove this option once all sysroots are available. 392 parser.add_argument( 393 "--x86_64-only", 394 default=False, 395 action="store_true", 396 help="Only runs tests on x86_64 sysroots", 397 ) 398 parser.add_argument( 399 "--only-build-targets", 400 default=False, 401 action="store_true", 402 help="Builds only the tested modules. If false, builds the entire crate", 403 ) 404 parser.add_argument( 405 "--size-only", 406 dest="size_only", 407 default=False, 408 action="store_true", 409 help="Only perform builds that output their binary size (i.e. release non-test).", 410 ) 411 parser.add_argument( 412 "--job_type", 413 default="local", 414 choices=["kokoro", "local"], 415 help="Set to kokoro if this script is executed by a kokoro job, otherwise local", 416 ) 417 parser.add_argument( 418 "--skip_file_name", 419 default=".win_build_test_skip" if IS_WINDOWS else ".build_test_skip", 420 choices=[ 421 ".build_test_skip", 422 ".win_build_test_skip", 423 ".windows_build_test_skip", 424 ], 425 help="Skips building and testing a crate if the crate contains specified file in its root directory.", 426 ) 427 parser.add_argument( 428 "--build_mode", 429 default="release", 430 choices=["release", "debug"], 431 help="Build mode of the binaries.", 432 ) 433 434 return parser 435 436 437def main(argv): 438 opts = get_parser().parse_args(argv) 439 os.environ["RUST_BACKTRACE"] = "1" 440 if IS_WINDOWS: 441 if opts.build_mode == "release": 442 build_test_cases = [ 443 # (sysroot path, target triple, debug/release, skip_file_name, should test?) 444 ( 445 opts.x86_64_msvc_sysroot, 446 X86_64_WIN_MSVC_TRIPLE, 447 "release", 448 opts.skip_file_name, 449 True, 450 ), 451 ( 452 opts.x86_64_msvc_sysroot, 453 X86_64_WIN_MSVC_TRIPLE, 454 "release", 455 opts.skip_file_name, 456 False, 457 ), 458 ] 459 elif opts.build_mode == "debug": 460 build_test_cases = [ 461 ( 462 opts.x86_64_msvc_sysroot, 463 X86_64_WIN_MSVC_TRIPLE, 464 "debug", 465 opts.skip_file_name, 466 True, 467 ), 468 ] 469 else: 470 build_test_cases = [ 471 # (sysroot path, target triple, debug/release, skip_file_name, should test?) 472 (opts.x86_64_sysroot, X86_64_TRIPLE, "debug", opts.skip_file_name, False), 473 (opts.x86_64_sysroot, X86_64_TRIPLE, "release", opts.skip_file_name, False), 474 (opts.x86_64_sysroot, X86_64_TRIPLE, "debug", opts.skip_file_name, True), 475 (opts.x86_64_sysroot, X86_64_TRIPLE, "release", opts.skip_file_name, True), 476 ] 477 if not opts.x86_64_only: 478 build_test_cases = [ 479 # (sysroot path, target triple, debug/release, skip_file_name, should test?) 480 (opts.arm_sysroot, ARM_TRIPLE, "debug", opts.skip_file_name, False), 481 (opts.arm_sysroot, ARM_TRIPLE, "release", opts.skip_file_name, False), 482 ( 483 opts.aarch64_sysroot, 484 AARCH64_TRIPLE, 485 "debug", 486 opts.skip_file_name, 487 False, 488 ), 489 ( 490 opts.aarch64_sysroot, 491 AARCH64_TRIPLE, 492 "release", 493 opts.skip_file_name, 494 False, 495 ), 496 ] + build_test_cases 497 os.chdir(os.path.dirname(sys.argv[0])) 498 499 if opts.size_only: 500 # Only include non-test release builds 501 build_test_cases = [ 502 case for case in build_test_cases if case[2] == "release" and not case[4] 503 ] 504 505 # First we need to build necessary DLLs. 506 # Because build_or_test may be called by multithreads in parallel, 507 # we want to build the DLLs only once up front. 508 modes = set() 509 for case in build_test_cases: 510 modes.add(case[2]) 511 for mode in modes: 512 build_dlls(os.getcwd(), mode, opts.job_type, BUILD_FEATURES) 513 514 # set keyword args to build_or_test based on opts 515 build_partial = functools.partial( 516 build_or_test, 517 no_run=True, 518 clean=opts.clean, 519 copy_output=opts.copy, 520 copy_directory=opts.copy_directory, 521 only_build_targets=opts.only_build_targets, 522 ) 523 524 if opts.parallel: 525 pool = multiprocessing.pool.Pool(len(build_test_cases)) 526 results = pool.starmap(build_partial, build_test_cases, 1) 527 else: 528 results = [build_partial(*case) for case in build_test_cases] 529 530 print_summary("build", build_test_cases, results, opts) 531 532 # exit early if any builds failed 533 if not all([r[0] for r in results]): 534 return 1 535 536 # run tests for cases where should_test is True 537 test_cases = [case for case in build_test_cases if case[4]] 538 539 # Run tests serially. We set clean=False so it re-uses the results of the build phase. 540 results = [ 541 build_or_test( 542 *case, 543 no_run=False, 544 clean=False, 545 copy_output=opts.copy, 546 copy_directory=opts.copy_directory, 547 only_build_targets=opts.only_build_targets, 548 ) 549 for case in test_cases 550 ] 551 552 print_summary("test", test_cases, results, opts) 553 554 if not all([r[0] for r in results]): 555 return 1 556 557 return 0 558 559 560def print_summary(title, cases, results, opts): 561 print("---") 562 print(f"{title} summary:") 563 for test_case, result in zip(cases, results): 564 _, triple, kind, _, test_it = test_case 565 title = "%s_%s" % (triple.split("-")[0], kind) 566 if test_it: 567 title += "_test" 568 569 success, result_msg = result 570 571 result_color = FAIL_COLOR 572 if success: 573 result_color = PASS_COLOR 574 575 display_size = "" 576 # Stripped binary isn't available when only certain packages are built, the tool is not available 577 # on Windows. 578 if ( 579 success 580 and kind == "release" 581 and not test_it 582 and not opts.only_build_targets 583 and not IS_WINDOWS 584 ): 585 display_size = get_stripped_size(triple) + " stripped binary" 586 587 print("%20s: %s%15s%s %s" % (title, result_color, result_msg, END_COLOR, display_size)) 588 589 590if __name__ == "__main__": 591 sys.exit(main(sys.argv[1:])) 592