crosvm/tools/windows/build_test.py
Dennis Kempin ba4adc0efb Add python presubmit checks
This change adds python type and formatting checks and
consolidates code health checks in ./tools/health-check.

Dealing with relative imports in python is tricky, so
we are making ./tools/impl a proper package with no
directly executable files.

Some of the bash shorthands in ./tools had to be converted
to python for this.

To make the new checks pass, we run the formatter and fix
some mypy type checks.

TEST=./tools/health-check
BUG=b:218559722,b:219965702

Change-Id: Ie18d3d6dd2f5a033141e167a6e1aa762791941d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/3558592
Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
Tested-by: kokoro <noreply+kokoro@google.com>
Commit-Queue: Dennis Kempin <denniskempin@google.com>
2022-04-15 19:22:53 +00:00

581 lines
19 KiB
Python

#!/usr/bin/env python3
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# 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.",
)
return parser
def main(argv):
opts = get_parser().parse_args(argv)
os.environ["RUST_BACKTRACE"] = "1"
if IS_WINDOWS:
build_test_cases = [
# (sysroot path, target triple, debug/release, skip_file_name, should test?)
(
opts.x86_64_msvc_sysroot,
X86_64_WIN_MSVC_TRIPLE,
"debug",
opts.skip_file_name,
True,
),
(
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,
),
]
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:]))