xref: /aosp_15_r20/external/toolchain-utils/check_portable_toolchains.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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