blob: 0a1a3f010c782286e8153b672f811d2bff816cec [file] [log] [blame]
#!/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.
"""Create a directory that contains a Bazel binary, its extracted install_base
files as well as a wrapper script named 'bazel'."""
import argparse
import datetime
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import urllib.request
import zipfile
_SCRIPT_DIR = os.path.dirname(__file__)
_INFRA_3PP_GIT_URL = "https://fuchsia.googlesource.com/infra/3pp"
_INFRA_3PP_GIT_COMMIT = "70a9ea9b301f158a76c1cd31ecac359d2cbaf515"
class OutputTree(object):
def __init__(self, dst_dir, exist_ok=False):
self._dst_dir = os.path.abspath(dst_dir)
self._exist_ok = exist_ok
os.makedirs(dst_dir, exist_ok=exist_ok)
def add_file(self, src_path, dst_path):
copy_dst = os.path.join(self._dst_dir, dst_path)
def do_copy():
shutil.copy2(src_path, copy_dst)
try:
do_copy()
except PermissionError as e:
os.unlink(copy_dst)
do_copy()
def add_tree(self, src_path, dst_path):
copy_dst = os.path.join(self._dst_dir, dst_path)
def do_copy():
shutil.copytree(
src_path,
copy_dst,
symlinks=True,
dirs_exist_ok=self._exist_ok,
)
try:
do_copy()
except shutil.Error:
# NOTE: On error, shutil.copytree() raises an shutil.Error
# with a list of reasons, and filterting on PermissionError is
# really hard, so just try again after removing the tree.
shutil.rmtree(copy_dst)
do_copy()
def close(self):
pass
class OutputArchive(object):
def __init__(self, output_path):
self._zip = zipfile.ZipFile(
output_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
)
def add_file(self, src_path, dst_path):
self._zip.write(src_path, dst_path)
def add_tree(self, src_path, dst_path):
src_path = os.path.abspath(src_path)
for root, subdirs, filenames in os.walk(src_path):
dst_subdir = os.path.join(dst_path, os.path.relpath(root, src_path))
for filename in filenames:
dst_path = os.path.join(dst_subdir, filename)
src_path = os.path.join(root, filename)
self._zip.write(src_path, dst_path)
def close(self):
self._zip.close()
def git_clone_commit(git_url, git_commit, dst_dir):
def git_cmd(args):
subprocess.check_call(["git", "-C", dst_dir] + args)
git_cmd(["init"])
git_cmd(["remote", "add", "origin", git_url])
git_cmd(["fetch", "origin", git_commit])
git_cmd(["reset", "--hard", git_commit])
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 main():
parser = argparse.ArgumentParser(description=__doc__)
bazel_group = parser.add_mutually_exclusive_group(required=True)
bazel_group.add_argument(
"--bazel-version", help="Bazel version to download."
)
bazel_group.add_argument("--bazel", help="Path to local Bazel binary.")
parser.add_argument(
"--force",
action="store_true",
default=False,
help="Force installation if install_dir already exists.",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--install-dir", metavar="INSTALL_DIR", help="Install directory."
)
group.add_argument(
"--output-archive",
metavar="OUTPUT_ARCHIVE",
help="Generate zip archive.",
)
args = parser.parse_args()
bazel_download_dir = None
if args.bazel_version:
bazel_download_dir = tempfile.TemporaryDirectory()
bazel_bin = os.path.join(
bazel_download_dir.name, "bazel-" + args.bazel_version
)
url = get_bazel_download_url(args.bazel_version)
print("Downloading %s" % url)
urllib.request.urlretrieve(url, bazel_bin)
os.chmod(bazel_bin, 0o750)
args.bazel = bazel_bin
if not os.path.exists(args.bazel):
return parser.error("File does not exist: %s" % args.bazel)
if not os.path.isfile(args.bazel):
return parser.error("Not a regular file: %s" % args.bazel)
if args.install_dir:
if os.path.exists(args.install_dir) and not args.force:
return parser.error(
"Install directory already exists, use --force."
)
elif args.output_archive:
if args.output_archive[-4:] != ".zip":
return parser.error("Output archive path must end in '.zip'.")
else:
# Should not happen.
assert False, "Invalid command-line arguments!"
# In many installation, 'bazel' is a wrapper script, not a real
# binary.
with open(args.bazel, "rb") as f:
header = f.read(2)
if header == b"#!":
return parser.error(
"This is a wrapper script, not a real executable: %s"
% args.bazel
)
# Create temporary directory to perform extraction.
with tempfile.TemporaryDirectory() as tmpdirname:
# Download infra-3pp scripts.
infra_3pp_dir = os.path.join(tmpdirname, "fuchsia-infra-3pp")
os.makedirs(infra_3pp_dir)
git_clone_commit(
_INFRA_3PP_GIT_URL, _INFRA_3PP_GIT_COMMIT, infra_3pp_dir
)
# Extract everything under a temporary 'install' directory.
temp_install_dir = os.path.join(tmpdirname, "install")
os.makedirs(temp_install_dir)
output_user_root = os.path.join(tmpdirname, "output_user_root")
# Extract Bazel version from binary.
version_out = subprocess.check_output(
[args.bazel, "--output_user_root", output_user_root, "version"],
text=True,
)
bazel_version = None
version_prefix = "Build label: "
for line in version_out.splitlines():
if line.startswith(version_prefix):
bazel_version = line[len(version_prefix) :]
if not bazel_version:
print(
"ERROR: Could not find version of: %s" % args.bazel,
file=sys.stderr,
)
print(
"The `version` command returned:\n%s\n" % version_out,
file=sys.stderr,
)
return 1
print("Found Bazel version: [%s]" % bazel_version)
# Run the install script.
env = os.environ.copy()
env["_3PP_VERSION"] = bazel_version
env["_BAZEL_BIN"] = os.path.abspath(args.bazel)
subprocess.check_call(
[
os.path.join(infra_3pp_dir, "bazel", "install.sh"),
temp_install_dir,
],
env=env,
)
# Copy to the install dir or the output archive
if args.install_dir:
print(
"\nCopying to install directory: %s" % args.install_dir,
end="",
flush=True,
)
out = OutputTree(args.install_dir, args.force)
else:
print(
"\nCopying to output archive: %s" % args.output_archive,
end="",
flush=True,
)
out = OutputArchive(args.output_archive)
out.add_file(args.bazel, "bazel-real")
out.add_file(os.path.join(temp_install_dir, "bazel"), "bazel")
out.add_file(
os.path.join(temp_install_dir, "README.fuchsia"), "README.fuchsia"
)
out.add_file(os.path.join(temp_install_dir, "LICENSE"), "LICENSE")
out.add_tree(
os.path.join(temp_install_dir, "install_base"), "install_base"
)
out.close()
print("\nDone.")
return 0
if __name__ == "__main__":
sys.exit(main())