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 6"""Verify that a given portable toolchain SDK version can link and compile. 7 8Used to test that new portable toolchain SDKs work. See go/crostc-mage for 9when to use this script. 10""" 11 12import argparse 13import json 14import logging 15import os 16from pathlib import Path 17import re 18import subprocess 19import sys 20import tempfile 21from typing import List, Optional, Tuple 22 23 24ABIS = ( 25 "aarch64-cros-linux-gnu", 26 "armv7a-cros-linux-gnueabihf", 27 "x86_64-cros-linux-gnu", 28) 29 30GS_PREFIX = "gs://staging-chromiumos-sdk" 31 32# Type alias to make clear when a string is a specially 33# formatted timestamp-version string. 34Version = str 35 36HELLO_WORLD = """#include <iostream> 37 38int main() { 39 std::cout << "Hello world!" << std::endl; 40} 41""" 42 43_COLOR_RED = "\033[91m" 44_COLOR_GREEN = "\033[92m" 45_COLOR_RESET = "\033[0m" 46 47 48def main() -> int: 49 logging.basicConfig( 50 format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: " 51 "%(message)s", 52 level=logging.INFO, 53 ) 54 args = parse_args() 55 56 version = args.version 57 if not version: 58 version = _autodetect_latest_llvm_next_sdk_version() 59 60 errors: List[Tuple[str, Exception]] = [] 61 for abi in ABIS: 62 res = check_abi(args.bucket_prefix, abi, version) 63 if res: 64 errors.append((abi, res)) 65 if errors: 66 logging.error( 67 "%sAt least one ABI failed to validate: %s%s", 68 _COLOR_RED, 69 ", ".join(abi for (abi, _) in errors), 70 _COLOR_RESET, 71 ) 72 return 1 73 logging.info( 74 "%sAll ABIs successfully validated :)%s", 75 _COLOR_GREEN, 76 _COLOR_RESET, 77 ) 78 return 0 79 80 81def check_abi( 82 bucket_prefix: str, abi: str, version: Version 83) -> Optional[Exception]: 84 """Verify that a given ABI target triplet is okay.""" 85 year, month, _ = _split_version(version) 86 toolchain_name = f"{abi}-{version}.tar.xz" 87 artifact_path = f"{bucket_prefix}/{year}/{month}/{toolchain_name}" 88 try: 89 with tempfile.TemporaryDirectory() as tmpdir_str: 90 tmpdir = Path(tmpdir_str) 91 92 def run(*args, **kwargs): 93 return subprocess.run(*args, check=True, cwd=tmpdir, **kwargs) 94 95 logging.info( 96 "Downloading the toolchain %s into %s", 97 artifact_path, 98 tmpdir, 99 ) 100 run(["gsutil.py", "cp", artifact_path, tmpdir]) 101 102 logging.info("Extracting the toolchain %s", toolchain_name) 103 run(["tar", "-axf", tmpdir / toolchain_name]) 104 105 logging.info("Checking if can find ld linker") 106 proc = run( 107 [f"bin/{abi}-clang", "-print-prog-name=ld"], 108 stdout=subprocess.PIPE, 109 encoding="utf-8", 110 ) 111 linker_path = tmpdir / proc.stdout.strip() 112 logging.info("linker binary path: %s", linker_path) 113 if not linker_path.exists(): 114 raise RuntimeError(f"{linker_path} does not exist") 115 if not os.access(linker_path, os.X_OK): 116 raise RuntimeError(f"{linker_path} is not executable") 117 118 logging.info("Building a simple c++ binary") 119 hello_world_file = tmpdir / "hello_world.cc" 120 hello_world_file.write_text(HELLO_WORLD, encoding="utf-8") 121 hello_world_output = tmpdir / "hello_world" 122 cmd = [ 123 f"bin/{abi}-clang++", 124 "-o", 125 hello_world_output, 126 hello_world_file, 127 ] 128 run(cmd) 129 if not hello_world_output.exists(): 130 raise RuntimeError(f"{hello_world_output} does not exist") 131 proc = run( 132 [f"bin/{abi}-clang++", "--version"], 133 stdout=subprocess.PIPE, 134 encoding="utf-8", 135 ) 136 logging.info( 137 "%s-clang++ --version:\n%s", 138 abi, 139 "> " + "\n> ".join(proc.stdout.strip().split("\n")), 140 ) 141 142 logging.info( 143 "%s[PASS] %s was validated%s", _COLOR_GREEN, abi, _COLOR_RESET 144 ) 145 except Exception as e: 146 logging.exception( 147 "%s[FAIL] %s could not be validated%s", 148 _COLOR_RED, 149 abi, 150 _COLOR_RESET, 151 ) 152 return e 153 return None 154 155 156def _autodetect_latest_llvm_next_sdk_version() -> str: 157 output = subprocess.run( 158 [ 159 "bb", 160 "ls", 161 "-json", 162 "-n", 163 "1", 164 "-status", 165 "success", 166 "chromeos/infra/build-chromiumos-sdk-llvm-next", 167 ], 168 check=True, 169 stdin=subprocess.DEVNULL, 170 stdout=subprocess.PIPE, 171 ).stdout 172 builder_summary = json.loads(output)["summaryMarkdown"] 173 # Builder summary looks like: 174 # ``` 175 # Built SDK version [2023.12.11.140022](https://link-redacted) 176 # Launched SDK uprev build: https://link-redacted 177 # ``` 178 matches = re.findall(r"\[(\d+\.\d+\.\d+\.\d+)\]\(", builder_summary) 179 if len(matches) != 1: 180 raise ValueError( 181 f"Expected exactly 1 match of version in {builder_summary!r}." 182 f" Got {matches}. You can pass --version to disable auto-detection." 183 ) 184 version = matches[0] 185 logging.info("Found latest llvm-next SDK version: %s", version) 186 return version 187 188 189def _split_version(version: Version) -> Tuple[str, str, str]: 190 y, m, rest = version.split(".", 2) 191 return y, m, rest 192 193 194def _verify_version(version: str) -> Version: 195 _split_version(version) # Raises a ValueError if invalid. 196 return version 197 198 199def parse_args() -> argparse.Namespace: 200 """Parse arguments.""" 201 parser = argparse.ArgumentParser( 202 "check_portable_toolchains", description=__doc__ 203 ) 204 parser.add_argument( 205 "--version", 206 help=""" 207 Version/Timestamp formatted as 'YYYY.MM.DD.HHMMSS'. e.g. 208 '2023.09.01.221258'. Generally this comes from a 209 'build-chromiumos-sdk-llvm-next' run. Will autodetect if none is 210 specified. 211 """, 212 type=_verify_version, 213 ) 214 parser.add_argument( 215 "-p", 216 "--bucket-prefix", 217 default=GS_PREFIX, 218 help="Top level gs:// path. (default: %(default)s)", 219 ) 220 return parser.parse_args() 221 222 223if __name__ == "__main__": 224 sys.exit(main()) 225