xref: /aosp_15_r20/external/executorch/build/resolve_buck.py (revision 523fa7a60841cd1ecfb9cc4201f1ca8b03ed023a)
1#!/usr/bin/env python3
2# Copyright (c) Meta Platforms, Inc. and affiliates.
3# Copyright 2024 Arm Limited and/or its affiliates.
4# All rights reserved.
5#
6# This source code is licensed under the BSD-style license found in the
7# LICENSE file in the root directory of this source tree.
8
9import argparse
10import os
11import platform
12import stat
13import sys
14import tempfile
15import urllib.request
16
17from dataclasses import dataclass
18from pathlib import Path
19from typing import Sequence, Union
20
21import buck_util
22import zstd
23
24"""
25Locate or download the version of buck2 needed to build ExecuTorch.
26It is intended to be invoked from the CMake build logic, and it returns
27the path to 'buck2' via stdout. Log messages are written to stderr.
28
29It uses the following logic, in order of precedence, to locate or download
30buck2:
31
32 1) If BUCK2 argument is set explicitly, use it. Warn if the version is
33    incorrect.
34 2) Look for a binary named buck2 on the system path. Take it if it is
35    the correct version.
36 3) Check for a previously downloaded buck2 binary (from step 4).
37 4) Download and cache correct version of buck2.
38
39"""
40
41# Path to the file containing BUCK2 version (build date) for ExecuTorch.
42# Note that this path is relative to this script file, not the working
43# directory.
44BUCK_VERSION_FILE = "../.ci/docker/ci_commit_pins/buck2.txt"
45
46
47@dataclass
48class BuckInfo:
49    archive_name: str
50    target_versions: Sequence[str]
51
52
53# Mapping of os family and architecture to buck2 binary versions. The
54# target version is the hash given by running 'buck2 --version'. The
55# archive name is the archive file name to download, as seen under
56# https://github.com/facebook/buck2/releases/.
57#
58# To add or update versions, download the appropriate version of buck2
59# and run 'buck2 --version'. Add the corresponding entry to the platform
60# map below, and if adding new os families or architectures, update the
61# platform detection logic in resolve_buck2().
62#
63# Some platforms (linux) provide multiple binaries (GNU and MUSL). All
64# versions in the list are accepted when validating a user-provided or
65# system buck2.
66BUCK_PLATFORM_MAP = {
67    ("linux", "x86_64"): BuckInfo(
68        archive_name="buck2-x86_64-unknown-linux-musl.zst",
69        target_versions=[
70            # MUSL
71            "3bbde7daa94987db468d021ad625bc93dc62ba7fcb16945cb09b64aab077f284",
72            # GNU
73            "029b0bcc6f8e399185c1d0f574eba204934722b5",
74        ],
75    ),
76    ("linux", "aarch64"): BuckInfo(
77        archive_name="buck2-aarch64-unknown-linux-gnu.zst",
78        target_versions=["49670bee56a7d8a7696409ca6fbf7551d2469787"],
79    ),
80    ("darwin", "aarch64"): BuckInfo(
81        archive_name="buck2-aarch64-apple-darwin.zst",
82        target_versions=["99773fe6f7963a72ae5f7b737c02836e"],
83    ),
84    ("darwin", "x86_64"): BuckInfo(
85        archive_name="buck2-x86_64-apple-darwin.zst",
86        target_versions=["3eb1ae97ea963086866b4d2d9ffa966d"],
87    ),
88}
89
90
91def parse_args() -> argparse.Namespace:
92    parser = argparse.ArgumentParser(
93        description="Locates or downloads the appropriate version of buck2.",
94    )
95    parser.add_argument(
96        "--buck2",
97        default="",
98        help="Optional user-provided 'buck2' path. If provided, it will be "
99        "used. If the version is incorrect, a warning will be logged.",
100    )
101    parser.add_argument(
102        "--cache_dir",
103        help="Directory to cache downloaded versions of buck2.",
104    )
105    return parser.parse_args()
106
107
108# Returns the path to buck2 on success or a return code on failure.
109def resolve_buck2(args: argparse.Namespace) -> Union[str, int]:
110    # Find buck2, in order of priority:
111    #  1) Explicit buck2 argument.
112    #  2) System buck2 (if correct version).
113    #  3) Cached buck2 (previously downloaded).
114    #  3) Download buck2.
115
116    # Read the target version (build date) from the CI pin file. Note that
117    # this path is resolved relative to the directory containing this script.
118    script_dir = os.path.dirname(__file__)
119    version_file_path = Path(script_dir) / BUCK_VERSION_FILE
120    with open(version_file_path.absolute().as_posix()) as f:
121        target_buck_version = f.read().strip()
122
123    # Determine the target buck2 version string according to the current
124    # platform. If the platform isn't linux or darwin, we won't perform
125    # any version validation.
126    machine = platform.machine().lower()
127    arch = "unknown"
128    if machine == "x86" or machine == "x86_64" or machine == "amd64":
129        arch = "x86_64"
130    elif machine == "arm64" or machine == "aarch64":
131        arch = "aarch64"
132
133    os_family = "unknown"
134    if sys.platform.startswith("linux"):
135        os_family = "linux"
136    elif sys.platform.startswith("darwin"):
137        os_family = "darwin"
138
139    platform_key = (os_family, arch)
140    if platform_key not in BUCK_PLATFORM_MAP:
141        print(
142            f"Unknown platform {platform_key}. Buck2 binary must be downloaded manually.",
143            file=sys.stderr,
144        )
145        return args.buck2 or "buck2"
146
147    buck_info = BUCK_PLATFORM_MAP[platform_key]
148
149    if args.buck2:
150        # If we have an explicit buck2 arg, check the version and fail if
151        # there is a mismatch.
152        ver = buck_util.get_buck2_version(args.buck2)
153        if ver in buck_info.target_versions:
154            return args.buck2
155        else:
156            print(
157                f'The provided buck2 binary "{args.buck2}" reports version '
158                f'"{ver}", but ExecuTorch needs version '
159                f'"{buck_info.target_versions[0]}". Ensure that the correct buck2'
160                " version is installed or avoid explicitly passing the BUCK2 "
161                "version to automatically download the correct version.",
162                file=sys.stderr,
163            )
164
165            # Return an error, since the build will fail later. This lets us
166            # give the user a more useful error message. Note that an exit
167            # code of 2 allows us to distinguish from an unexpected error,
168            # such as a failed import, which exits with 1.
169            return 2
170    else:
171        # Look for system buck2 and check version. Note that this can return
172        # None.
173        ver = buck_util.get_buck2_version("buck2")
174        if ver in buck_info.target_versions:
175            # Use system buck2.
176            return "buck2"
177        else:
178            # Download buck2 or used previously cached download.
179            cache_dir = Path(args.cache_dir)
180            os.makedirs(cache_dir, exist_ok=True)
181
182            buck2_local_path = (
183                (cache_dir / f"buck2-{buck_info.target_versions[0]}")
184                .absolute()
185                .as_posix()
186            )
187
188            # Check for a previously cached buck2 binary. The filename includes
189            # the version hash, so we don't have to worry about using an
190            # outdated binary, in the event that the target version is updated.
191            if os.path.isfile(buck2_local_path):
192                return buck2_local_path
193
194            buck2_archive_url = f"https://github.com/facebook/buck2/releases/download/{target_buck_version}/{buck_info.archive_name}"
195
196            with tempfile.NamedTemporaryFile() as archive_file:
197                print(f"Downloading buck2 from {buck2_archive_url}...", file=sys.stderr)
198                urllib.request.urlretrieve(buck2_archive_url, archive_file.name)
199
200                # Extract and chmod.
201                with open(archive_file.name, "rb") as f:
202                    data = f.read()
203                    decompressed_bytes = zstd.decompress(data)
204
205                with open(buck2_local_path, "wb") as f:
206                    f.write(decompressed_bytes)
207
208                file_stat = os.stat(buck2_local_path)
209                os.chmod(buck2_local_path, file_stat.st_mode | stat.S_IEXEC)
210
211            return buck2_local_path
212
213
214def main():
215    args = parse_args()
216    resolved_path_or_error = resolve_buck2(args)
217    if isinstance(resolved_path_or_error, str):
218        print(resolved_path_or_error)
219    else:
220        sys.exit(resolved_path_or_error)
221
222
223if __name__ == "__main__":
224    main()
225