Skip to content

Commit 14996cc

Browse files
GregoryComerfacebook-github-bot
authored andcommitted
Automatically download buck2 when not provided (#2619)
Summary: Resolve buck2, in order of precedence: 1) Use explicitly provided buck2 path (via -DBUCK2). Warn if version is incorrect. 2) Check system buck2. Use if version is correct. 3) Look for a cached (previously downloaded) buck2 binary. 3) Download and cache buck2. Pull Request resolved: #2619 Test Plan: Repeated on x86_64 linux and m1 mac. Build with explicit buck2 (correct version). Confirm that provided buck is used without warning. Build with explicit buck2 (wrong version). Confirm that warning is shown. Build without explicit buck (system buck wrong version). Confirm that buck is downloaded and cached. Build succeeds. Build without explicit buck (system buck correct version). Confirm that buck is downloaded and cached. Build succeeds. Build without explicit buck (no system buck). Confirm that buck is downloaded and cached. Build succeeds. Reviewed By: dbort Differential Revision: D55292710 Pulled By: GregoryComer fbshipit-source-id: c29a79eaf737c21c33543a6175049eb7ced8da2e
1 parent 3527bd7 commit 14996cc

File tree

6 files changed

+321
-27
lines changed

6 files changed

+321
-27
lines changed

CMakeLists.txt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,6 @@ option(EXECUTORCH_BUILD_XNNPACK "Build the XNNPACK backend" OFF)
170170

171171
option(EXECUTORCH_BUILD_VULKAN "Build the Vulkan backend" OFF)
172172

173-
if(NOT BUCK2)
174-
set(BUCK2 buck2)
175-
endif()
176-
177173
if(NOT PYTHON_EXECUTABLE)
178174
set(PYTHON_EXECUTABLE python3)
179175
endif()
@@ -189,6 +185,9 @@ set(_common_include_directories ${CMAKE_CURRENT_SOURCE_DIR}/..)
189185
#
190186

191187
if(NOT EXECUTORCH_SRCS_FILE)
188+
# Find or download buck2 binary.
189+
resolve_buck2()
190+
192191
# A file wasn't provided. Run a script to extract the source lists from the
193192
# buck2 build system and write them to a file we can include.
194193
#

build/Utils.cmake

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,16 @@ function(extract_sources sources_file)
135135
else()
136136
set(executorch_root ${CMAKE_CURRENT_SOURCE_DIR})
137137
endif()
138+
138139
execute_process(
139-
COMMAND
140-
${PYTHON_EXECUTABLE} ${executorch_root}/build/extract_sources.py
141-
--buck2=${BUCK2} --config=${executorch_root}/build/cmake_deps.toml
142-
--out=${sources_file}
140+
COMMAND ${PYTHON_EXECUTABLE} ${executorch_root}/build/extract_sources.py
141+
--config=${executorch_root}/build/cmake_deps.toml --out=${sources_file}
142+
--buck2=${BUCK2}
143143
OUTPUT_VARIABLE gen_srcs_output
144144
ERROR_VARIABLE gen_srcs_error
145145
RESULT_VARIABLE gen_srcs_exit_code
146146
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
147+
147148
if(NOT gen_srcs_exit_code EQUAL 0)
148149
message("Error while generating ${sources_file}. "
149150
"Exit code: ${gen_srcs_exit_code}")
@@ -153,3 +154,47 @@ function(extract_sources sources_file)
153154
endif()
154155
endif()
155156
endfunction()
157+
158+
# Sets the value of the BUCK2 variable by searching for a buck2 binary
159+
# with the correct version.
160+
#
161+
# The resolve_buck.py script uses the following logic to find buck2:
162+
# 1) If BUCK2 argument is set explicitly, use it. Warn if the version is
163+
# incorrect.
164+
# 2) Look for a binary named buck2 on the system path. Take it if it is
165+
# the correct version.
166+
# 3) Check for a previously downloaded buck2 binary (from step 4).
167+
# 4) Download and cache correct version of buck2.
168+
function(resolve_buck2)
169+
if(EXECUTORCH_ROOT)
170+
set(executorch_root ${EXECUTORCH_ROOT})
171+
else()
172+
set(executorch_root ${CMAKE_CURRENT_SOURCE_DIR})
173+
endif()
174+
175+
set(resolve_buck2_command
176+
${PYTHON_EXECUTABLE} ${executorch_root}/build/resolve_buck.py
177+
--cache_dir=${CMAKE_CURRENT_BINARY_DIR}/buck2-bin)
178+
179+
if(NOT ${BUCK2} STREQUAL "")
180+
list(APPEND resolve_buck2_command --buck2=${BUCK2})
181+
endif()
182+
183+
execute_process(
184+
COMMAND ${resolve_buck2_command}
185+
OUTPUT_VARIABLE resolve_buck2_output
186+
ERROR_VARIABLE resolve_buck2_error
187+
RESULT_VARIABLE resolve_buck2_exit_code
188+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
189+
OUTPUT_STRIP_TRAILING_WHITESPACE)
190+
191+
if(resolve_buck2_exit_code EQUAL 0)
192+
set(BUCK2 ${resolve_buck2_output} PARENT_SCOPE)
193+
message(STATUS "Resolved buck2 as ${resolve_buck2_output}.")
194+
else()
195+
# Wrong buck version used. Stop here to ensure that the user sees
196+
# the error.
197+
message(FATAL_ERROR "Failed to resolve buck2.")
198+
message(FATAL_ERROR ${resolve_buck2_error})
199+
endif()
200+
endfunction()

build/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the BSD-style license found in the
6+
# LICENSE file in the root directory of this source tree.

build/buck_util.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the BSD-style license found in the
6+
# LICENSE file in the root directory of this source tree.
7+
8+
import os
9+
import subprocess
10+
import sys
11+
12+
from typing import Optional, Sequence
13+
14+
15+
# Run buck2 from the same directory (and thus repo) as this script.
16+
BUCK_CWD: str = os.path.dirname(os.path.realpath(__file__))
17+
18+
19+
class Buck2Runner:
20+
def __init__(self, tool_path: str) -> None:
21+
self._path = tool_path
22+
23+
def run(self, args: Sequence[str]) -> list[str]:
24+
"""Runs buck2 with the given args and returns its stdout as a sequence of lines."""
25+
try:
26+
cp: subprocess.CompletedProcess = subprocess.run(
27+
[self._path] + args, capture_output=True, cwd=BUCK_CWD, check=True
28+
)
29+
return [line.strip().decode("utf-8") for line in cp.stdout.splitlines()]
30+
except subprocess.CalledProcessError as ex:
31+
raise RuntimeError(ex.stderr.decode("utf-8")) from ex
32+
33+
34+
def get_buck2_version(path: str) -> Optional[str]:
35+
try:
36+
runner = Buck2Runner(path)
37+
output = runner.run(["--version"])
38+
39+
# Example output:
40+
# buck2 38f7c508bf1b87bcdc816bf56d1b9f2d2411c6be <build-id>
41+
#
42+
# We want the second value.
43+
44+
return output[0].split()[1]
45+
46+
except Exception as e:
47+
print(f"Failed to retrieve buck2 version: {e}.", file=sys.stderr)
48+
return None

build/extract_sources.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
import copy
1010
import os
1111
import re
12-
import subprocess
1312

1413
from enum import Enum
1514
from typing import Any, Optional, Sequence
1615

16+
from buck_util import Buck2Runner
17+
1718
try:
1819
import tomllib # Standard in 3.11 and later
1920
except ModuleNotFoundError:
@@ -65,24 +66,6 @@
6566
]
6667
"""
6768

68-
# Run buck2 from the same directory (and thus repo) as this script.
69-
BUCK_CWD: str = os.path.dirname(os.path.realpath(__file__))
70-
71-
72-
class Buck2Runner:
73-
def __init__(self, tool_path: str) -> None:
74-
self._path = tool_path
75-
76-
def run(self, args: Sequence[str]) -> list[str]:
77-
"""Runs buck2 with the given args and returns its stdout as a sequence of lines."""
78-
try:
79-
cp: subprocess.CompletedProcess = subprocess.run(
80-
[self._path] + args, capture_output=True, cwd=BUCK_CWD, check=True
81-
)
82-
return [line.strip().decode("utf-8") for line in cp.stdout.splitlines()]
83-
except subprocess.CalledProcessError as ex:
84-
raise RuntimeError(ex.stderr.decode("utf-8")) from ex
85-
8669

8770
class Target:
8871
"""Parsed [targets.*] entry from the TOML file.

build/resolve_buck.py

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

0 commit comments

Comments
 (0)