build_test.py 19 KB

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