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