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