• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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