| #!/usr/bin/env python3 |
| # Copyright 2017 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Builds crosvm in debug/release mode on all supported target architectures. |
| |
| A sysroot for each target architectures is required. The defaults are all generic boards' sysroots, |
| but they can be changed with the command line arguments. |
| |
| To test changes more quickly, set the --noclean option. This prevents the target directories from |
| being removed before building and testing. |
| |
| For easy binary size comparison, use the --size-only option to only do builds that will result in a |
| binary size output, which are non-test release builds. |
| |
| This script automatically determines which packages will need to be tested based on the directory |
| structure with Cargo.toml files. Only top-level crates are tested directly. To skip a top-level |
| package, add an empty .build_test_skip file to the directory. Rarely, if a package needs to have its |
| tests run single-threaded, add an empty .build_test_serial file to the directory. |
| """ |
| |
| from __future__ import print_function |
| import argparse |
| import functools |
| import multiprocessing.pool |
| import os |
| import shutil |
| import subprocess |
| import sys |
| |
| sys.path.append(os.path.dirname(sys.path[0])) |
| |
| from enabled_features import ENABLED_FEATURES, BUILD_FEATURES |
| from files_to_include import DLLS, BINARIES |
| from prepare_dlls import build_dlls, copy_dlls |
| |
| # Is Windows |
| IS_WINDOWS = os.name == "nt" |
| |
| ARM_TRIPLE = os.getenv("ARM_TRIPLE", "armv7a-cros-linux-gnueabihf") |
| AARCH64_TRIPLE = os.getenv("AARCH64_TRIPLE", "aarch64-cros-linux-gnu") |
| X86_64_TRIPLE = os.getenv("X86_64_TRIPLE", "x86_64-unknown-linux-gnu") |
| X86_64_WIN_MSVC_TRIPLE = os.getenv("X86_64_WIN_MSVC_TRIPLE", "x86_64-pc-windows-msvc") |
| SYMBOL_EXPORTS = ["NvOptimusEnablement", "AmdPowerXpressRequestHighPerformance"] |
| |
| LINUX_BUILD_ONLY_MODULES = [ |
| "io_jail", |
| "poll_token_derive", |
| "wire_format_derive", |
| "bit_field_derive", |
| "linux_input_sys", |
| "vfio_sys", |
| ] |
| |
| # Bright green. |
| PASS_COLOR = "\033[1;32m" |
| # Bright red. |
| FAIL_COLOR = "\033[1;31m" |
| # Default color. |
| END_COLOR = "\033[0m" |
| |
| |
| def crosvm_binary_name(): |
| return "crosvm.exe" if IS_WINDOWS else "crosvm" |
| |
| |
| def get_target_path(triple, kind, test_it): |
| """Constructs a target path based on the configuration parameters. |
| |
| Args: |
| triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. |
| kind: 'debug' or 'release'. |
| test_it: If this target is tested. |
| """ |
| target_path = os.path.abspath(os.path.join(os.sep, "tmp", "{}_{}".format(triple, kind))) |
| if test_it: |
| target_path += "_test" |
| return target_path |
| |
| |
| def validate_symbols(triple, is_release): |
| kind = "release" if is_release else "debug" |
| target_path = get_target_path(triple, kind, False) |
| binary_path = os.path.join(target_path, triple, kind, crosvm_binary_name()) |
| with open(binary_path, mode="rb") as f: |
| contents = f.read().decode("ascii", errors="ignore") |
| return all(symbol in contents for symbol in SYMBOL_EXPORTS) |
| |
| |
| def build_target( |
| triple, |
| is_release, |
| env, |
| only_build_targets, |
| test_module_parallel, |
| test_module_serial, |
| ): |
| """Does a cargo build for the triple in release or debug mode. |
| |
| Args: |
| triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. |
| is_release: True to build a release version. |
| env: Enviroment variables to run cargo with. |
| only_build_targets: Only build packages that will be tested. |
| """ |
| args = ["cargo", "build", "--target=%s" % triple] |
| |
| if is_release: |
| args.append("--release") |
| |
| if only_build_targets: |
| test_modules = test_module_parallel + test_module_serial |
| if not IS_WINDOWS: |
| test_modules += LINUX_BUILD_ONLY_MODULES |
| for mod in test_modules: |
| args.append("-p") |
| args.append(mod) |
| |
| args.append("--features") |
| args.append(",".join(BUILD_FEATURES)) |
| |
| if subprocess.Popen(args, env=env).wait() != 0: |
| return False, "build error" |
| if IS_WINDOWS and not validate_symbols(triple, is_release): |
| return False, "error validating discrete gpu symbols" |
| |
| return True, "pass" |
| |
| |
| def test_target_modules(triple, is_release, env, no_run, modules, parallel): |
| """Does a cargo test on given modules for the triple and configuration. |
| |
| Args: |
| triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. |
| is_release: True to build a release version. |
| env: Enviroment variables to run cargo with. |
| no_run: True to pass --no-run flag to cargo test. |
| modules: List of module strings to test. |
| parallel: True to run the tests in parallel threads. |
| """ |
| args = ["cargo", "test", "--target=%s" % triple] |
| |
| if is_release: |
| args.append("--release") |
| |
| if no_run: |
| args.append("--no-run") |
| |
| for mod in modules: |
| args.append("-p") |
| args.append(mod) |
| |
| args.append("--features") |
| args.append(",".join(ENABLED_FEATURES)) |
| |
| if not parallel: |
| args.append("--") |
| args.append("--test-threads=1") |
| return subprocess.Popen(args, env=env).wait() == 0 |
| |
| |
| def test_target(triple, is_release, env, no_run, test_modules_parallel, test_modules_serial): |
| """Does a cargo test for the given triple and configuration. |
| |
| Args: |
| triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. |
| is_release: True to build a release version. |
| env: Enviroment variables to run cargo with. |
| no_run: True to pass --no-run flag to cargo test. |
| """ |
| |
| parallel_result = test_target_modules( |
| triple, is_release, env, no_run, test_modules_parallel, True |
| ) |
| |
| serial_result = test_target_modules(triple, is_release, env, no_run, test_modules_serial, False) |
| |
| return parallel_result and serial_result |
| |
| |
| def build_or_test( |
| sysroot, |
| triple, |
| kind, |
| skip_file_name, |
| test_it=False, |
| no_run=False, |
| clean=False, |
| copy_output=False, |
| copy_directory=None, |
| only_build_targets=False, |
| ): |
| """Runs relevant builds/tests for the given triple and configuration |
| |
| Args: |
| sysroot: path to the target's sysroot directory. |
| triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. |
| kind: 'debug' or 'release'. |
| skip_file_name: Skips building and testing a crate if this file is found in |
| crate's root directory. |
| test_it: True to test this triple and kind. |
| no_run: True to just compile and not run tests (only if test_it=True) |
| clean: True to skip cleaning the target path. |
| copy_output: True to copy build artifacts to external directory. |
| output_directory: Destination of copy of build artifacts. |
| only_build_targets: Only build packages that will be tested. |
| """ |
| if not os.path.isdir(sysroot) and not IS_WINDOWS: |
| return False, "sysroot missing" |
| |
| target_path = get_target_path(triple, kind, test_it) |
| |
| if clean: |
| shutil.rmtree(target_path, True) |
| |
| is_release = kind == "release" |
| |
| env = os.environ.copy() |
| env["TARGET_CC"] = "%s-clang" % triple |
| env["SYSROOT"] = sysroot |
| env["CARGO_TARGET_DIR"] = target_path |
| |
| if not IS_WINDOWS: |
| # The lib dir could be in either lib or lib64 depending on the target. Rather than checking to see |
| # which one is valid, just add both and let the dynamic linker and pkg-config search. |
| libdir = os.path.join(sysroot, "usr", "lib") |
| lib64dir = os.path.join(sysroot, "usr", "lib64") |
| libdir_pc = os.path.join(libdir, "pkgconfig") |
| lib64dir_pc = os.path.join(lib64dir, "pkgconfig") |
| |
| # This line that changes the dynamic library path is needed for upstream, but breaks |
| # downstream's crosvm linux kokoro presubmits. |
| # env['LD_LIBRARY_PATH'] = libdir + ':' + lib64dir |
| env["PKG_CONFIG_ALLOW_CROSS"] = "1" |
| env["PKG_CONFIG_LIBDIR"] = libdir_pc + ":" + lib64dir_pc |
| env["PKG_CONFIG_SYSROOT_DIR"] = sysroot |
| if "KOKORO_JOB_NAME" not in os.environ: |
| env["RUSTFLAGS"] = "-C linker=" + env["TARGET_CC"] |
| if is_release: |
| env["RUSTFLAGS"] += " -Cembed-bitcode=yes -Clto" |
| |
| if IS_WINDOWS and not test_it: |
| for symbol in SYMBOL_EXPORTS: |
| env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -C link-args=/EXPORT:{}".format(symbol) |
| |
| deps_dir = os.path.join(target_path, triple, kind, "deps") |
| if not os.path.exists(deps_dir): |
| os.makedirs(deps_dir) |
| |
| target_dirs = [deps_dir] |
| if copy_output: |
| os.makedirs(os.path.join(copy_directory, kind), exist_ok=True) |
| if not test_it: |
| target_dirs.append(os.path.join(copy_directory, kind)) |
| |
| copy_dlls(os.getcwd(), target_dirs, kind) |
| |
| (test_modules_parallel, test_modules_serial) = get_test_modules(skip_file_name) |
| print("modules to test in parallel:\n", test_modules_parallel) |
| print("modules to test serially:\n", test_modules_serial) |
| |
| if not test_modules_parallel and not test_modules_serial: |
| print("All build and tests skipped.") |
| return True, "pass" |
| |
| if test_it: |
| if not test_target( |
| triple, is_release, env, no_run, test_modules_parallel, test_modules_serial |
| ): |
| return False, "test error" |
| else: |
| res, err = build_target( |
| triple, |
| is_release, |
| env, |
| only_build_targets, |
| test_modules_parallel, |
| test_modules_serial, |
| ) |
| if not res: |
| return res, err |
| |
| # We only care about the non-test binaries, so only copy the output from cargo build. |
| if copy_output and not test_it: |
| binary_src = os.path.join(target_path, triple, kind, crosvm_binary_name()) |
| pdb_src = binary_src.replace(".exe", "") + ".pdb" |
| binary_dst = os.path.join(copy_directory, kind) |
| shutil.copy(binary_src, binary_dst) |
| shutil.copy(pdb_src, binary_dst) |
| |
| return True, "pass" |
| |
| |
| def get_test_modules(skip_file_name): |
| """Returns a list of modules to test. |
| Args: |
| skip_file_name: Skips building and testing a crate if this file is found in |
| crate's root directory. |
| """ |
| if IS_WINDOWS and not os.path.isfile(skip_file_name): |
| test_modules_parallel = ["crosvm"] |
| else: |
| test_modules_parallel = [] |
| test_modules_serial = [] |
| |
| file_in_crate = lambda file_name: os.path.isfile(os.path.join(crate.path, file_name)) |
| serial_file_name = "{}build_test_serial".format(".win_" if IS_WINDOWS else ".") |
| with os.scandir() as it: |
| for crate in it: |
| if file_in_crate("Cargo.toml"): |
| if file_in_crate(skip_file_name): |
| continue |
| if file_in_crate(serial_file_name): |
| test_modules_serial.append(crate.name) |
| else: |
| test_modules_parallel.append(crate.name) |
| |
| test_modules_parallel.sort() |
| test_modules_serial.sort() |
| |
| return (test_modules_parallel, test_modules_serial) |
| |
| |
| def get_stripped_size(triple): |
| """Returns the formatted size of the given triple's release binary. |
| |
| Args: |
| triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. |
| """ |
| target_path = get_target_path(triple, "release", False) |
| bin_path = os.path.join(target_path, triple, "release", crosvm_binary_name()) |
| proc = subprocess.Popen(["%s-strip" % triple, bin_path]) |
| |
| if proc.wait() != 0: |
| return "failed" |
| |
| return "%dKiB" % (os.path.getsize(bin_path) / 1024) |
| |
| |
| def get_parser(): |
| """Gets the argument parser""" |
| parser = argparse.ArgumentParser(description=__doc__) |
| if IS_WINDOWS: |
| parser.add_argument( |
| "--x86_64-msvc-sysroot", |
| default="build/amd64-msvc", |
| help="x86_64 sysroot directory (default=%(default)s)", |
| ) |
| else: |
| parser.add_argument( |
| "--arm-sysroot", |
| default="/build/arm-generic", |
| help="ARM sysroot directory (default=%(default)s)", |
| ) |
| parser.add_argument( |
| "--aarch64-sysroot", |
| default="/build/arm64-generic", |
| help="AARCH64 sysroot directory (default=%(default)s)", |
| ) |
| parser.add_argument( |
| "--x86_64-sysroot", |
| default="/build/amd64-generic", |
| help="x86_64 sysroot directory (default=%(default)s)", |
| ) |
| |
| parser.add_argument( |
| "--noclean", |
| dest="clean", |
| default=True, |
| action="store_false", |
| help="Keep the tempororary build directories.", |
| ) |
| parser.add_argument( |
| "--copy", |
| default=False, |
| help="Copies .exe files to an output directory for later use", |
| ) |
| parser.add_argument( |
| "--copy-directory", |
| default="/output", |
| help="Destination of .exe files when using --copy", |
| ) |
| parser.add_argument( |
| "--serial", |
| default=True, |
| action="store_false", |
| dest="parallel", |
| help="Run cargo build serially rather than in parallel", |
| ) |
| # TODO(b/154029826): Remove this option once all sysroots are available. |
| parser.add_argument( |
| "--x86_64-only", |
| default=False, |
| action="store_true", |
| help="Only runs tests on x86_64 sysroots", |
| ) |
| parser.add_argument( |
| "--only-build-targets", |
| default=False, |
| action="store_true", |
| help="Builds only the tested modules. If false, builds the entire crate", |
| ) |
| parser.add_argument( |
| "--size-only", |
| dest="size_only", |
| default=False, |
| action="store_true", |
| help="Only perform builds that output their binary size (i.e. release non-test).", |
| ) |
| parser.add_argument( |
| "--job_type", |
| default="local", |
| choices=["kokoro", "local"], |
| help="Set to kokoro if this script is executed by a kokoro job, otherwise local", |
| ) |
| parser.add_argument( |
| "--skip_file_name", |
| default=".win_build_test_skip" if IS_WINDOWS else ".build_test_skip", |
| choices=[ |
| ".build_test_skip", |
| ".win_build_test_skip", |
| ".windows_build_test_skip", |
| ], |
| help="Skips building and testing a crate if the crate contains specified file in its root directory.", |
| ) |
| parser.add_argument( |
| "--build_mode", |
| default="release", |
| choices=["release", "debug"], |
| help="Build mode of the binaries.", |
| ) |
| |
| return parser |
| |
| |
| def main(argv): |
| opts = get_parser().parse_args(argv) |
| os.environ["RUST_BACKTRACE"] = "1" |
| if IS_WINDOWS: |
| if opts.build_mode == "release": |
| build_test_cases = [ |
| # (sysroot path, target triple, debug/release, skip_file_name, should test?) |
| ( |
| opts.x86_64_msvc_sysroot, |
| X86_64_WIN_MSVC_TRIPLE, |
| "release", |
| opts.skip_file_name, |
| True, |
| ), |
| ( |
| opts.x86_64_msvc_sysroot, |
| X86_64_WIN_MSVC_TRIPLE, |
| "release", |
| opts.skip_file_name, |
| False, |
| ), |
| ] |
| elif opts.build_mode == "debug": |
| build_test_cases = [ |
| ( |
| opts.x86_64_msvc_sysroot, |
| X86_64_WIN_MSVC_TRIPLE, |
| "debug", |
| opts.skip_file_name, |
| True, |
| ), |
| ] |
| else: |
| build_test_cases = [ |
| # (sysroot path, target triple, debug/release, skip_file_name, should test?) |
| (opts.x86_64_sysroot, X86_64_TRIPLE, "debug", opts.skip_file_name, False), |
| (opts.x86_64_sysroot, X86_64_TRIPLE, "release", opts.skip_file_name, False), |
| (opts.x86_64_sysroot, X86_64_TRIPLE, "debug", opts.skip_file_name, True), |
| (opts.x86_64_sysroot, X86_64_TRIPLE, "release", opts.skip_file_name, True), |
| ] |
| if not opts.x86_64_only: |
| build_test_cases = [ |
| # (sysroot path, target triple, debug/release, skip_file_name, should test?) |
| (opts.arm_sysroot, ARM_TRIPLE, "debug", opts.skip_file_name, False), |
| (opts.arm_sysroot, ARM_TRIPLE, "release", opts.skip_file_name, False), |
| ( |
| opts.aarch64_sysroot, |
| AARCH64_TRIPLE, |
| "debug", |
| opts.skip_file_name, |
| False, |
| ), |
| ( |
| opts.aarch64_sysroot, |
| AARCH64_TRIPLE, |
| "release", |
| opts.skip_file_name, |
| False, |
| ), |
| ] + build_test_cases |
| os.chdir(os.path.dirname(sys.argv[0])) |
| |
| if opts.size_only: |
| # Only include non-test release builds |
| build_test_cases = [ |
| case for case in build_test_cases if case[2] == "release" and not case[4] |
| ] |
| |
| # First we need to build necessary DLLs. |
| # Because build_or_test may be called by multithreads in parallel, |
| # we want to build the DLLs only once up front. |
| modes = set() |
| for case in build_test_cases: |
| modes.add(case[2]) |
| for mode in modes: |
| build_dlls(os.getcwd(), mode, opts.job_type, BUILD_FEATURES) |
| |
| # set keyword args to build_or_test based on opts |
| build_partial = functools.partial( |
| build_or_test, |
| no_run=True, |
| clean=opts.clean, |
| copy_output=opts.copy, |
| copy_directory=opts.copy_directory, |
| only_build_targets=opts.only_build_targets, |
| ) |
| |
| if opts.parallel: |
| pool = multiprocessing.pool.Pool(len(build_test_cases)) |
| results = pool.starmap(build_partial, build_test_cases, 1) |
| else: |
| results = [build_partial(*case) for case in build_test_cases] |
| |
| print_summary("build", build_test_cases, results, opts) |
| |
| # exit early if any builds failed |
| if not all([r[0] for r in results]): |
| return 1 |
| |
| # run tests for cases where should_test is True |
| test_cases = [case for case in build_test_cases if case[4]] |
| |
| # Run tests serially. We set clean=False so it re-uses the results of the build phase. |
| results = [ |
| build_or_test( |
| *case, |
| no_run=False, |
| clean=False, |
| copy_output=opts.copy, |
| copy_directory=opts.copy_directory, |
| only_build_targets=opts.only_build_targets, |
| ) |
| for case in test_cases |
| ] |
| |
| print_summary("test", test_cases, results, opts) |
| |
| if not all([r[0] for r in results]): |
| return 1 |
| |
| return 0 |
| |
| |
| def print_summary(title, cases, results, opts): |
| print("---") |
| print(f"{title} summary:") |
| for test_case, result in zip(cases, results): |
| _, triple, kind, _, test_it = test_case |
| title = "%s_%s" % (triple.split("-")[0], kind) |
| if test_it: |
| title += "_test" |
| |
| success, result_msg = result |
| |
| result_color = FAIL_COLOR |
| if success: |
| result_color = PASS_COLOR |
| |
| display_size = "" |
| # Stripped binary isn't available when only certain packages are built, the tool is not available |
| # on Windows. |
| if ( |
| success |
| and kind == "release" |
| and not test_it |
| and not opts.only_build_targets |
| and not IS_WINDOWS |
| ): |
| display_size = get_stripped_size(triple) + " stripped binary" |
| |
| print("%20s: %s%15s%s %s" % (title, result_color, result_msg, END_COLOR, display_size)) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |