1#!/usr/bin/env python3 2# Copyright 2017 The Chromium OS Authors. All rights reserved. 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 31sys.path.append(os.path.join(sys.path[0], "script_utils")) 32from enabled_features import ENABLED_FEATURES, BUILD_FEATURES 33from files_to_include import DLLS, BINARIES 34from prepare_dlls import build_dlls, copy_dlls 35 36# Is Windows 37IS_WINDOWS = os.name == 'nt' 38 39ARM_TRIPLE = os.getenv('ARM_TRIPLE', 'armv7a-cros-linux-gnueabihf') 40AARCH64_TRIPLE = os.getenv('AARCH64_TRIPLE', 'aarch64-cros-linux-gnu') 41X86_64_TRIPLE = os.getenv('X86_64_TRIPLE', 'x86_64-unknown-linux-gnu') 42X86_64_WIN_MSVC_TRIPLE = os.getenv('X86_64_WIN_MSVC_TRIPLE', 'x86_64-pc-windows-msvc') 43SYMBOL_EXPORTS = ['NvOptimusEnablement', 'AmdPowerXpressRequestHighPerformance'] 44 45LINUX_BUILD_ONLY_MODULES = [ 46 'io_jail', 47 'poll_token_derive', 48 'wire_format_derive', 49 'bit_field_derive', 50 'linux_input_sys', 51 'vfio_sys', 52] 53 54# Bright green. 55PASS_COLOR = '\033[1;32m' 56# Bright red. 57FAIL_COLOR = '\033[1;31m' 58# Default color. 59END_COLOR = '\033[0m' 60 61def crosvm_binary_name(): 62 return 'crosvm.exe' if IS_WINDOWS else 'crosvm' 63 64def get_target_path(triple, kind, test_it): 65 """Constructs a target path based on the configuration parameters. 66 67 Args: 68 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 69 kind: 'debug' or 'release'. 70 test_it: If this target is tested. 71 """ 72 target_path = os.path.abspath(os.path.join(os.sep, 'tmp', '{}_{}'.format(triple, kind))) 73 if test_it: 74 target_path += '_test' 75 return target_path 76 77def validate_symbols(triple, is_release): 78 kind = 'release' if is_release else 'debug' 79 target_path = get_target_path(triple, kind, False) 80 binary_path = os.path.join(target_path, triple, kind, crosvm_binary_name()) 81 with open(binary_path, mode='rb') as f: 82 contents = f.read().decode('ascii', errors='ignore') 83 return all(symbol in contents for symbol in SYMBOL_EXPORTS) 84 85def build_target( 86 triple, is_release, env, only_build_targets, test_module_parallel, test_module_serial): 87 """Does a cargo build for the triple in release or debug mode. 88 89 Args: 90 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 91 is_release: True to build a release version. 92 env: Enviroment variables to run cargo with. 93 only_build_targets: Only build packages that will be tested. 94 """ 95 args = ['cargo', 'build', '--target=%s' % triple] 96 97 if is_release: 98 args.append('--release') 99 100 if only_build_targets: 101 test_modules = test_module_parallel + test_module_serial 102 if not IS_WINDOWS: 103 test_modules += LINUX_BUILD_ONLY_MODULES 104 for mod in test_modules: 105 args.append('-p') 106 args.append(mod) 107 108 args.append('--features') 109 args.append(','.join(BUILD_FEATURES)) 110 111 if subprocess.Popen(args, env=env).wait() != 0: 112 return False, 'build error' 113 if IS_WINDOWS and not validate_symbols(triple, is_release): 114 return False, 'error validating discrete gpu symbols' 115 116 return True, 'pass' 117 118def test_target_modules(triple, is_release, env, no_run, modules, parallel): 119 """Does a cargo test on given modules for the triple and configuration. 120 121 Args: 122 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 123 is_release: True to build a release version. 124 env: Enviroment variables to run cargo with. 125 no_run: True to pass --no-run flag to cargo test. 126 modules: List of module strings to test. 127 parallel: True to run the tests in parallel threads. 128 """ 129 args = ['cargo', 'test', '--target=%s' % triple] 130 131 if is_release: 132 args.append('--release') 133 134 if no_run: 135 args.append('--no-run') 136 137 for mod in modules: 138 args.append('-p') 139 args.append(mod) 140 141 args.append('--features') 142 args.append(','.join(ENABLED_FEATURES)) 143 144 if not parallel: 145 args.append('--') 146 args.append('--test-threads=1') 147 return subprocess.Popen(args, env=env).wait() == 0 148 149 150def test_target(triple, is_release, env, no_run, test_modules_parallel, test_modules_serial): 151 """Does a cargo test for the given triple and configuration. 152 153 Args: 154 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 155 is_release: True to build a release version. 156 env: Enviroment variables to run cargo with. 157 no_run: True to pass --no-run flag to cargo test. 158 """ 159 160 parallel_result = test_target_modules( 161 triple, is_release, env, no_run, test_modules_parallel, True) 162 163 serial_result = test_target_modules( 164 triple, is_release, env, no_run, test_modules_serial, False) 165 166 return parallel_result and serial_result 167 168 169def build_or_test(sysroot, triple, kind, skip_file_name, test_it=False, no_run=False, clean=False, 170 copy_output=False, copy_directory=None, only_build_targets=False): 171 """Runs relevant builds/tests for the given triple and configuration 172 173 Args: 174 sysroot: path to the target's sysroot directory. 175 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 176 kind: 'debug' or 'release'. 177 skip_file_name: Skips building and testing a crate if this file is found in 178 crate's root directory. 179 test_it: True to test this triple and kind. 180 no_run: True to just compile and not run tests (only if test_it=True) 181 clean: True to skip cleaning the target path. 182 copy_output: True to copy build artifacts to external directory. 183 output_directory: Destination of copy of build artifacts. 184 only_build_targets: Only build packages that will be tested. 185 """ 186 if not os.path.isdir(sysroot) and not IS_WINDOWS: 187 return False, 'sysroot missing' 188 189 target_path = get_target_path(triple, kind, test_it) 190 191 if clean: 192 shutil.rmtree(target_path, True) 193 194 is_release = kind == 'release' 195 196 env = os.environ.copy() 197 env['TARGET_CC'] = '%s-clang'%triple 198 env['SYSROOT'] = sysroot 199 env['CARGO_TARGET_DIR'] = target_path 200 201 if not IS_WINDOWS: 202 # The lib dir could be in either lib or lib64 depending on the target. Rather than checking to see 203 # which one is valid, just add both and let the dynamic linker and pkg-config search. 204 libdir = os.path.join(sysroot, 'usr', 'lib') 205 lib64dir = os.path.join(sysroot, 'usr', 'lib64') 206 libdir_pc = os.path.join(libdir, 'pkgconfig') 207 lib64dir_pc = os.path.join(lib64dir, 'pkgconfig') 208 209 # This line that changes the dynamic library path is needed for upstream, but breaks 210 # downstream's CrosVM linux kokoro presubmits. 211 # env['LD_LIBRARY_PATH'] = libdir + ':' + lib64dir 212 env['PKG_CONFIG_ALLOW_CROSS'] = '1' 213 env['PKG_CONFIG_LIBDIR'] = libdir_pc + ':' + lib64dir_pc 214 env['PKG_CONFIG_SYSROOT_DIR'] = sysroot 215 if 'KOKORO_JOB_NAME' not in os.environ: 216 env['RUSTFLAGS'] = '-C linker=' + env['TARGET_CC'] 217 if is_release: 218 env['RUSTFLAGS'] += ' -Cembed-bitcode=yes -Clto' 219 220 221 if IS_WINDOWS and not test_it: 222 for symbol in SYMBOL_EXPORTS: 223 env['RUSTFLAGS'] = env.get('RUSTFLAGS', '') + ' -C link-args=/EXPORT:{}'.format(symbol) 224 225 deps_dir = os.path.join(target_path, triple, kind, 'deps') 226 if not os.path.exists(deps_dir): 227 os.makedirs(deps_dir) 228 229 target_dirs = [deps_dir] 230 if copy_output: 231 os.makedirs(os.path.join(copy_directory, kind), exist_ok=True) 232 if not test_it: 233 target_dirs.append(os.path.join(copy_directory, kind)) 234 235 copy_dlls(os.getcwd(), target_dirs, kind) 236 237 (test_modules_parallel, test_modules_serial) = get_test_modules(skip_file_name) 238 print("modules to test in parallel:\n", test_modules_parallel) 239 print("modules to test serially:\n", test_modules_serial) 240 241 if not test_modules_parallel and not test_modules_serial: 242 print("All build and tests skipped.") 243 return True, 'pass' 244 245 if test_it: 246 if not test_target(triple, is_release, env, no_run, test_modules_parallel, test_modules_serial): 247 return False, 'test error' 248 else: 249 res, err = build_target( 250 triple, is_release, env, only_build_targets, test_modules_parallel, test_modules_serial) 251 if not res: 252 return res, err 253 254 # We only care about the non-test binaries, so only copy the output from cargo build. 255 if copy_output and not test_it: 256 binary_src = os.path.join(target_path, triple, kind, crosvm_binary_name()) 257 pdb_src = binary_src.replace(".exe", "") + ".pdb" 258 binary_dst = os.path.join(copy_directory, kind) 259 shutil.copy(binary_src, binary_dst) 260 shutil.copy(pdb_src, binary_dst) 261 262 return True, 'pass' 263 264def get_test_modules(skip_file_name): 265 """ Returns a list of modules to test. 266 Args: 267 skip_file_name: Skips building and testing a crate if this file is found in 268 crate's root directory. 269 """ 270 if IS_WINDOWS and not os.path.isfile(skip_file_name): 271 test_modules_parallel = ['crosvm'] 272 else: 273 test_modules_parallel = [] 274 test_modules_serial = [] 275 276 file_in_crate = lambda file_name: os.path.isfile(os.path.join(crate.path, file_name)) 277 serial_file_name = '{}build_test_serial'.format('.win_' if IS_WINDOWS else '.') 278 with os.scandir() as it: 279 for crate in it: 280 if file_in_crate('Cargo.toml'): 281 if file_in_crate(skip_file_name): 282 continue 283 if file_in_crate(serial_file_name): 284 test_modules_serial.append(crate.name) 285 else: 286 test_modules_parallel.append(crate.name) 287 288 test_modules_parallel.sort() 289 test_modules_serial.sort() 290 291 return (test_modules_parallel, test_modules_serial) 292 293def get_stripped_size(triple): 294 """Returns the formatted size of the given triple's release binary. 295 296 Args: 297 triple: Target triple. Example: 'x86_64-unknown-linux-gnu'. 298 """ 299 target_path = get_target_path(triple, 'release', False) 300 bin_path = os.path.join(target_path, triple, 'release', crosvm_binary_name()) 301 proc = subprocess.Popen(['%s-strip' % triple, bin_path]) 302 303 if proc.wait() != 0: 304 return 'failed' 305 306 return '%dKiB' % (os.path.getsize(bin_path) / 1024) 307 308 309def get_parser(): 310 """Gets the argument parser""" 311 parser = argparse.ArgumentParser(description=__doc__) 312 if IS_WINDOWS: 313 parser.add_argument('--x86_64-msvc-sysroot', 314 default='build/amd64-msvc', 315 help='x86_64 sysroot directory (default=%(default)s)') 316 else: 317 parser.add_argument('--arm-sysroot', 318 default='/build/arm-generic', 319 help='ARM sysroot directory (default=%(default)s)') 320 parser.add_argument('--aarch64-sysroot', 321 default='/build/arm64-generic', 322 help='AARCH64 sysroot directory (default=%(default)s)') 323 parser.add_argument('--x86_64-sysroot', 324 default='/build/amd64-generic', 325 help='x86_64 sysroot directory (default=%(default)s)') 326 327 parser.add_argument('--noclean', dest='clean', default=True, 328 action='store_false', 329 help='Keep the tempororary build directories.') 330 parser.add_argument('--copy', default=False, 331 help='Copies .exe files to an output directory for later use') 332 parser.add_argument('--copy-directory', default="/output", 333 help='Destination of .exe files when using --copy') 334 parser.add_argument('--serial', default=True, 335 action='store_false', dest='parallel', 336 help='Run cargo build serially rather than in parallel') 337 # TODO(b/154029826): Remove this option once all sysroots are available. 338 parser.add_argument('--x86_64-only', default=False, action='store_true', 339 help='Only runs tests on x86_64 sysroots') 340 parser.add_argument('--only-build-targets', default=False, action='store_true', 341 help='Builds only the tested modules. If false, builds the entire crate') 342 parser.add_argument('--size-only', dest='size_only', default=False, 343 action='store_true', 344 help='Only perform builds that output their binary size (i.e. release non-test).') 345 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') 346 parser.add_argument('--skip_file_name', default='.win_build_test_skip' if IS_WINDOWS else '.build_test_skip', 347 choices=['.build_test_skip', '.win_build_test_skip', '.windows_build_test_skip'], 348 help='Skips building and testing a crate if the crate contains specified file in its root directory.') 349 return parser 350 351 352def main(argv): 353 opts = get_parser().parse_args(argv) 354 os.environ["RUST_BACKTRACE"] = "1" 355 if IS_WINDOWS: 356 build_test_cases = [ 357 #(sysroot path, target triple, debug/release, skip_file_name, should test?) 358 (opts.x86_64_msvc_sysroot, X86_64_WIN_MSVC_TRIPLE, "debug", opts.skip_file_name, True), 359 (opts.x86_64_msvc_sysroot, X86_64_WIN_MSVC_TRIPLE, "release", opts.skip_file_name, True), 360 (opts.x86_64_msvc_sysroot, X86_64_WIN_MSVC_TRIPLE, "release", opts.skip_file_name, False), 361 ] 362 else: 363 build_test_cases = [ 364 #(sysroot path, target triple, debug/release, skip_file_name, should test?) 365 (opts.x86_64_sysroot, X86_64_TRIPLE, "debug", opts.skip_file_name, False), 366 (opts.x86_64_sysroot, X86_64_TRIPLE, "release", opts.skip_file_name, False), 367 (opts.x86_64_sysroot, X86_64_TRIPLE, "debug", opts.skip_file_name, True), 368 (opts.x86_64_sysroot, X86_64_TRIPLE, "release", opts.skip_file_name, True), 369 ] 370 if not opts.x86_64_only: 371 build_test_cases = [ 372 #(sysroot path, target triple, debug/release, skip_file_name, should test?) 373 (opts.arm_sysroot, ARM_TRIPLE, "debug", opts.skip_file_name, False), 374 (opts.arm_sysroot, ARM_TRIPLE, "release", opts.skip_file_name, False), 375 (opts.aarch64_sysroot, AARCH64_TRIPLE, "debug", opts.skip_file_name, False), 376 (opts.aarch64_sysroot, AARCH64_TRIPLE, "release", opts.skip_file_name, False), 377 ] + build_test_cases 378 os.chdir(os.path.dirname(sys.argv[0])) 379 380 if opts.size_only: 381 # Only include non-test release builds 382 build_test_cases = [case for case in build_test_cases 383 if case[2] == 'release' and not case[4]] 384 385 # First we need to build necessary DLLs. 386 # Because build_or_test may be called by multithreads in parallel, 387 # we want to build the DLLs only once up front. 388 modes = set() 389 for case in build_test_cases: 390 modes.add(case[2]) 391 for mode in modes: 392 build_dlls(os.getcwd(), mode, opts.job_type, BUILD_FEATURES) 393 394 # set keyword args to build_or_test based on opts 395 build_partial = functools.partial( 396 build_or_test, no_run=True, clean=opts.clean, copy_output=opts.copy, 397 copy_directory=opts.copy_directory, 398 only_build_targets=opts.only_build_targets) 399 400 if opts.parallel: 401 pool = multiprocessing.pool.Pool(len(build_test_cases)) 402 results = pool.starmap(build_partial, build_test_cases, 1) 403 else: 404 results = [build_partial(*case) for case in build_test_cases] 405 406 print_summary("build", build_test_cases, results, opts) 407 408 # exit early if any builds failed 409 if not all([r[0] for r in results]): 410 return 1 411 412 # run tests for cases where should_test is True 413 test_cases = [case for case in build_test_cases if case[4]] 414 415 # Run tests serially. We set clean=False so it re-uses the results of the build phase. 416 results = [build_or_test(*case, no_run=False, clean=False, 417 copy_output=opts.copy, 418 copy_directory=opts.copy_directory, 419 only_build_targets=opts.only_build_targets) for case in test_cases] 420 421 print_summary("test", test_cases, results, opts) 422 423 if not all([r[0] for r in results]): 424 return 1 425 426 return 0 427 428 429def print_summary(title, cases, results, opts): 430 print('---') 431 print(f'{title} summary:') 432 for test_case, result in zip(cases, results): 433 _, triple, kind, _, test_it = test_case 434 title = '%s_%s' % (triple.split('-')[0], kind) 435 if test_it: 436 title += "_test" 437 438 success, result_msg = result 439 440 result_color = FAIL_COLOR 441 if success: 442 result_color = PASS_COLOR 443 444 display_size = '' 445 # Stripped binary isn't available when only certain packages are built, the tool is not available 446 # on Windows. 447 if success and kind == 'release' and not test_it and not opts.only_build_targets and not IS_WINDOWS: 448 display_size = get_stripped_size(triple) + ' stripped binary' 449 450 print('%20s: %s%15s%s %s' % 451 (title, result_color, result_msg, END_COLOR, display_size)) 452 453 454if __name__ == '__main__': 455 sys.exit(main(sys.argv[1:])) 456