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