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