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