| #!/usr/bin/env python3 |
| # Copyright 2022 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Prepare a Fuchsia checkout for 'fx bazel'. This is a temporary measure |
| until all requirements are properly managed with 'jiri'.""" |
| |
| import argparse |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import urllib.request |
| import xml.etree.ElementTree as ET |
| |
| _SCRIPT_DIR = os.path.dirname(__file__) |
| |
| |
| def get_host_platform() -> str: |
| '''Return host platform name, following Fuchsia conventions.''' |
| if sys.platform == 'linux': |
| return 'linux' |
| elif sys.platform == 'darwin': |
| return 'mac' |
| else: |
| return os.uname().sysname |
| |
| |
| def get_host_arch() -> str: |
| '''Return host CPU architecture, following Fuchsia conventions.''' |
| host_arch = os.uname().machine |
| if host_arch == 'x86_64': |
| return 'x64' |
| elif host_arch.startswith(('armv8', 'aarch64')): |
| return 'arm64' |
| else: |
| return host_arch |
| |
| |
| def get_host_tag(): |
| '''Return host tag, following Fuchsia conventions.''' |
| return '%s-%s' % (get_host_platform(), get_host_arch()) |
| |
| |
| def write_file(path, content): |
| with open(path, 'w') as f: |
| f.write(content) |
| |
| |
| def clone_git_branch(git_url, git_branch, dst_dir): |
| subprocess.check_call( |
| ['git', 'clone', '--branch', git_branch, '--depth=1', git_url, dst_dir], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| |
| |
| def clone_git_commit(git_url, git_commit, dst_dir): |
| |
| def git_cmd(args): |
| subprocess.check_call( |
| ['git', '-C', dst_dir] + args, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| |
| git_cmd(['init']) |
| git_cmd(['remote', 'add', 'origin', git_url]) |
| git_cmd(['fetch', 'origin', git_commit]) |
| git_cmd(['reset', '--hard', 'FETCH_HEAD']) |
| |
| |
| def get_bazel_download_url(version: str) -> str: |
| """Return Bazel download URL for a specific version and the current host platform.""" |
| if sys.platform == 'linux': |
| host_os = 'linux' |
| elif sys.platform == 'darwin': |
| host_os = 'darwin' |
| elif sys.platform in ('win32', 'cygwin'): |
| host_os = 'windows' |
| else: |
| host_os = os.uname().sysname |
| |
| host_cpu = os.uname().machine |
| if host_cpu.startswith(('armv8', 'aarch64')): |
| host_cpu = 'arm64' |
| |
| ext = '.exe' if host_os == 'windows' else '' |
| |
| return f'https://github.com/bazelbuild/bazel/releases/download/{version}/bazel-{version}-{host_os}-{host_cpu}{ext}' |
| |
| |
| def get_bazel_version(bazel_launcher): |
| """Return version of a given Bazel binary.""" |
| output = subprocess.check_output( |
| [bazel_launcher, "version"], stderr=subprocess.DEVNULL, text=True) |
| version_prefix = 'Build label: ' |
| for line in output.splitlines(): |
| if line.startswith(version_prefix): |
| return line[len(version_prefix):].strip() |
| return None |
| |
| |
| def ignore_log(message): |
| pass |
| |
| |
| _FALLBACK_BAZEL_VERSION = '5.3.0' |
| |
| |
| def get_jiri_bazel_version(fuchsia_dir, log=ignore_log): |
| # The location of the file that contains the Jiri Bazel package definition |
| prebuilts_file = os.path.join( |
| fuchsia_dir, 'integration', 'fuchsia', 'prebuilts') |
| if not os.path.exists(prebuilts_file): |
| log( |
| 'Could not find Jiri file defining Bazel version at: %s (using fallback version %s)' |
| % (prebuilts_file, _FALLBACK_BAZEL_VERSION)) |
| return _FALLBACK_BAZEL_VERSION |
| |
| version = None |
| prebuilts = ET.parse(prebuilts_file) |
| for package in prebuilts.findall('packages/package'): |
| package_name = package.get('name') |
| if package_name == 'fuchsia/third_party/bazel/${platform}': |
| version = package.get('version') |
| break |
| |
| if not version: |
| log( |
| 'Could not find Bazel package in: %s (using fallback version %s)' % |
| (prebuilts_file, _FALLBACK_BAZEL_VERSION)) |
| return _FALLBACK_BAZEL_VERSION |
| |
| # The package version has a .<patch> sufix which corresponds to |
| # the LUCI recipe patch number, so remove it. |
| pos = version.rfind('.') |
| if pos > 0: |
| version = version[:pos] |
| |
| version_prefix = 'version:2@' |
| if not version.startswith(version_prefix): |
| log( |
| 'Unsupported Bazel version tag (%s), using fallback %s' % |
| (version, _FALLBACK_BAZEL_VERSION)) |
| return _FALLBACK_BAZEL_VERSION |
| |
| return version[len(version_prefix):] |
| |
| |
| class InstallDirectory(object): |
| |
| def __init__(self, path): |
| self._tmp_dir = path + ".tmp" |
| self._dst_dir = path |
| self._old_dir = path + ".old" |
| |
| @property |
| def path(self): |
| return self._tmp_dir |
| |
| @property |
| def final_path(self): |
| return self._dst_dir |
| |
| def __enter__(self): |
| if os.path.exists(self._tmp_dir): |
| shutil.rmtree(self._tmp_dir) |
| os.makedirs(self._tmp_dir) |
| return self |
| |
| def __exit__(self, exc_type, exc_val, exc_tb): |
| if exc_type is not None: |
| # An exception occurred, cleanup temp dir. |
| shutil.rmtree(self._tmp_dir) |
| else: |
| if os.path.exists(self._old_dir): |
| shutil.rmtree(self._old_dir) |
| os.rename(self._dst_dir, self._old_dir) |
| os.rename(self._tmp_dir, self._dst_dir) |
| return False |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| '--fuchsia-dir', help='Path to Fuchsia checkout directory.') |
| parser.add_argument( |
| '--bazel-version', help='Set Bazel binary version to use.') |
| parser.add_argument( |
| '--quiet', action='store_true', help='Disable verbose output.') |
| |
| args = parser.parse_args() |
| |
| def log(message): |
| if not args.quiet: |
| print(message, flush=True) |
| |
| if not args.fuchsia_dir: |
| # Assume this script is under build/bazel/scripts/ |
| args.fuchsia_dir = os.path.abspath( |
| os.path.join(_SCRIPT_DIR, '..', '..', '..')) |
| log('Found Fuchsia dir: %s' % args.fuchsia_dir) |
| |
| if not args.bazel_version: |
| args.bazel_version = get_jiri_bazel_version(args.fuchsia_dir, log=log) |
| if not args.bazel_version: |
| return 1 |
| log('Using default Bazel version: ' + args.bazel_version) |
| |
| # Compare the available Bazel version with the one we need. |
| bazel_install_path = os.path.join( |
| args.fuchsia_dir, 'prebuilt', 'third_party', 'bazel', get_host_tag()) |
| bazel_launcher = os.path.join(bazel_install_path, 'bazel') |
| bazel_update = not os.path.exists(bazel_launcher) |
| if not bazel_update: |
| current_version = get_bazel_version(bazel_launcher) |
| if current_version != args.bazel_version: |
| log( |
| 'Found installed Bazel version %s, updating to %s' % |
| (current_version, args.bazel_version)) |
| bazel_update = True |
| |
| if bazel_update: |
| with tempfile.TemporaryDirectory() as tmpdirname: |
| bazel_bin = os.path.join(tmpdirname, 'bazel-' + args.bazel_version) |
| url = get_bazel_download_url(args.bazel_version) |
| log('Downloading %s' % url) |
| urllib.request.urlretrieve(url, bazel_bin) |
| os.chmod(bazel_bin, 0o750) |
| |
| log('Generating Bazel install at: %s' % bazel_install_path) |
| with InstallDirectory(bazel_install_path) as install: |
| subprocess.check_call( |
| [ |
| sys.executable, |
| '-S', |
| os.path.join(_SCRIPT_DIR, 'generate-bazel-install.py'), |
| '--bazel', |
| bazel_bin, |
| '--install-dir', |
| install.path, |
| '--force', |
| ], |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.PIPE) |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |