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