xref: /aosp_15_r20/external/crosvm/tools/build_release (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1#!/usr/bin/env python3
2# Copyright 2023 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
6import argparse
7import json
8import os
9import subprocess
10import sys
11from itertools import chain, product, starmap
12from pathlib import Path
13from typing import Dict, Iterable, List, NamedTuple
14
15from impl.common import CROSVM_ROOT, Triple, verbose
16from impl.test_config import DO_NOT_BUILD_RISCV64
17
18USAGE = """\
19Build crosvm with release (optimized) profile.
20
21To target local machine:
22
23    $ ./tools/build_release
24
25To cross-compile for aarch64, armhf or windows you can use:
26
27    $ ./tools/build_release --platform=aarch64
28    $ ./tools/build_release --platform=armhf
29    $ ./tools/build_release --platform=mingw64
30"""
31
32# We only need PGO for main binary, but for consistency, only exclude incompatible parts from PGO
33PGO_EXCLUDE = ["crosvm_control"]
34
35
36class Executable(NamedTuple):
37    """Container for info about an executable generated by cargo build/test."""
38
39    binary_path: Path
40    crate_name: str
41    cargo_target: str
42    kind: str
43    is_test: bool
44    is_fresh: bool
45
46    @property
47    def name(self):
48        return f"{self.crate_name}:{self.cargo_target}"
49
50
51def cargo(
52    cargo_command: str,
53    cwd: Path,
54    flags: List[str],
55    env: Dict[str, str],
56) -> Iterable[Executable]:
57    """
58    Executes a cargo command and returns the list of test binaries generated.
59
60    The build log will be hidden by default and only printed if the build
61    fails. In VERBOSE mode the output will be streamed directly.
62
63    Note: Exits the program if the build fails.
64    """
65    message_format = "json-diagnostic-rendered-ansi" if sys.stdout.isatty() else "json"
66    cmd = [
67        "cargo",
68        cargo_command,
69        f"--message-format={message_format}",
70        *flags,
71    ]
72    if verbose():
73        print("$", " ".join(cmd))
74    process = subprocess.Popen(
75        cmd,
76        cwd=cwd,
77        stdout=subprocess.PIPE,
78        stderr=subprocess.STDOUT,
79        text=True,
80        env=env,
81    )
82
83    messages: List[str] = []
84
85    # Read messages as cargo is running.
86    assert process.stdout
87    for line in iter(process.stdout.readline, ""):
88        # any non-json line is a message to print
89        if not line.startswith("{"):
90            if verbose():
91                print(line.rstrip())
92            messages.append(line.rstrip())
93            continue
94        json_line = json.loads(line)
95
96        # 'message' type lines will be printed
97        if json_line.get("message"):
98            message = json_line.get("message").get("rendered")
99            if verbose():
100                print(message)
101            messages.append(message)
102
103        # Collect info about test executables produced
104        elif json_line.get("executable"):
105            yield Executable(
106                Path(json_line.get("executable")),
107                crate_name=json_line.get("package_id", "").split(" ")[0],
108                cargo_target=json_line.get("target").get("name"),
109                kind=json_line.get("target").get("kind")[0],
110                is_test=json_line.get("profile", {}).get("test", False),
111                is_fresh=json_line.get("fresh", False),
112            )
113
114    if process.wait() != 0:
115        if not verbose():
116            for message in messages:
117                print(message)
118        sys.exit(-1)
119
120
121def main():
122    parser = argparse.ArgumentParser(usage=USAGE)
123    parser.add_argument(
124        "--build-target",
125        "--platform",
126        "-p",
127        help=(
128            "Override the cargo triple to build. Shorthands are available: (x86_64, armhf, "
129            + "aarch64, mingw64, msvc64)."
130        ),
131    )
132    parser.add_argument(
133        "--json",
134        action="store_true",
135        help="Output in JSON instead of human readable format.",
136    )
137    parser.add_argument("--strip", action="store_true", help="Strip output binaries")
138    parser.add_argument(
139        "--build-profile", help="Select compile profile, default to release.", default="release"
140    )
141    pgo_group = parser.add_mutually_exclusive_group()
142    pgo_group.add_argument(
143        "--profile-generate",
144        help="Target directory to generate profile when running, must use absolute path",
145    )
146    pgo_group.add_argument(
147        "--profile-use", help="Profile file used for PGO, must use absolute path"
148    )
149    parser.add_argument("cargo_arguments", nargs="*", help="Extra arguments pass to cargo")
150
151    args = parser.parse_args()
152
153    if args.profile_generate and (
154        not os.path.isabs(args.profile_generate) or not os.path.isdir(args.profile_generate)
155    ):
156        raise ValueError("--profile-generate argument is not an absolute path to a folder")
157    if args.profile_use and (
158        not os.path.isabs(args.profile_use) or not os.path.isfile(args.profile_use)
159    ):
160        raise ValueError("--profile-use argument is not an absolute path to a file")
161
162    build_target = Triple.from_shorthand(args.build_target) if args.build_target else None
163    build_target = build_target or Triple.host_default()
164
165    exclude_args = [
166        f"--exclude={x}" for x in PGO_EXCLUDE if args.profile_generate or args.profile_use
167    ]
168    if build_target == Triple.from_shorthand("riscv64"):
169        exclude_args += ["--exclude=" + s for s in DO_NOT_BUILD_RISCV64]
170
171    features = build_target.feature_flag
172    cargo_args = [
173        "--profile",
174        args.build_profile,
175        "--features=" + features,
176        f"--target={build_target}",
177        "--workspace",
178        *exclude_args,
179        *args.cargo_arguments,
180    ]
181
182    build_env = os.environ.copy()
183    build_env.update(build_target.get_cargo_env())
184    build_env.setdefault("RUSTFLAGS", "")
185    build_env["RUSTFLAGS"] += " -D warnings"
186    if args.strip:
187        build_env["RUSTFLAGS"] += " -C strip=symbols"
188    if args.profile_generate:
189        build_env["RUSTFLAGS"] += " -C profile-generate=" + args.profile_generate
190    if args.profile_use:
191        build_env["RUSTFLAGS"] += " -C profile-use=" + args.profile_use
192
193    executables = list(cargo("build", CROSVM_ROOT, cargo_args, build_env))
194
195    if args.json:
196        result = {}
197        for exe in executables:
198            assert exe.cargo_target not in result
199            result[exe.cargo_target] = str(exe.binary_path)
200        print(json.dumps(result))
201    else:
202        print("Release binaries:")
203        for exe in executables:
204            print(f"Name: {exe.cargo_target}")
205            print(f"Path: {str(exe.binary_path)}")
206            print()
207
208
209if __name__ == "__main__":
210    main()
211