blob: 1a343a49b3ea8c729b4b1de00b2f942d620f9aad [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2022 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import html.parser
import json
import os
import re
import urllib.request
# NOTE: There are three potential sources for Bazel binaries:
#
# - The latest stable release (e.g. 5.3.2 currently), available from GitHub,
# and whose version is obtainable from the following API file:
#
# https://api.github.com/repos/bazelbuild/bazel/releases/latest
#
# - The latest pre-release binary, which are no longer available from GitHub.
# Since there is no API to get it, the latest version and corresponding
# download link can be extracted by parsing the HTML content of:
#
# https://releases.bazel.build/rolling.html
#
# - The release candidates, which are _not_ available from GitHub, but from
# releases.bazel.build instead, and which do not have an API file, and
# must be specified with explicit version/candidate name below.
#
# Which version is downloaded depends on the definition of the _BAZEL_VERSION
# configuration variable below, which should be one of the following constants:
_BAZEL_VERSION_LATEST_PRERELEASE = "latest-prerelease"
_BAZEL_VERSION_LATEST_RELEASE = "latest-release"
_BAZEL_VERSION_RELEASE_CANDIDATE = "6.0.0/rc4" # Must be <version>/<candidate>
# This value is used to auto-detect which version to fetch
# by looking at the name of this script's parent directory. which
# can be either "bazel" or "bazel-prerelease". This allows generating
# CIPD packages for both versions using a simple directory symlink.
_BAZEL_VERSION_AUTO_DETECT = "auto-detect"
# Which Bazel binary to download, see notes above.
_BAZEL_VERSION = _BAZEL_VERSION_AUTO_DETECT
# Consistency check.
assert _BAZEL_VERSION in (
_BAZEL_VERSION_AUTO_DETECT,
_BAZEL_VERSION_LATEST_PRERELEASE,
_BAZEL_VERSION_LATEST_RELEASE,
_BAZEL_VERSION_RELEASE_CANDIDATE,
)
class BazelPreReleaseHtmlParser(html.parser.HTMLParser):
"""Parser used to extract the latest Bazel pre-release version from ROLLING_URL
Usage is:
1) Create instance, e.g. parser = BazelPreReleaseHtmlParser()
2) Load input HTML from parser.INPUT_URL
3) Call parser.feed(input_data)
4) Read parser.version value to retrieve latest pre-release version as a string.
"""
# Where to find the input data.
INPUT_URL = "https://releases.bazel.build/rolling.html"
def __init__(self):
super().__init__()
self._version = None
def handle_starttag(self, tag, attrs):
""" "Parse input tags to extract the first prelease version.
Expected format of input is:
<h1>8.0.0</h1>
<ul>
<li><a href="https://releases.bazel.build/8.0.0/rolling/8.0.0-pre.20240128.3/index.html">8.0.0-pre.20240128.3</a> (2024-02-12)</li>
<li><a href="https://releases.bazel.build/8.0.0/rolling/8.0.0-pre.20240108.7/index.html">8.0.0-pre.20240108.7</a> (2024-02-05)</li>
...
</ul>
<h1>7.0.0</h1>
<ul>
<li><a href="https://releases.bazel.build/7.0.0/rolling/7.0.0-pre.20231018.3/index.html">7.0.0-pre.20231018.3</a> (2023-11-07)</li>
<li><a href="https://releases.bazel.build/7.0.0/rolling/7.0.0-pre.20231011.2/index.html">7.0.0-pre.20231011.2</a> (2023-10-23)</li>
<li><a href="https://releases.bazel.build/7.0.0/rolling/7.0.0-pre.20230926.1/index.html">7.0.0-pre.20230926.1</a> (2023-10-06)</li>
...
</ul>
...
"""
if (
self._version is None
and tag == "a"
and len(attrs) == 1
and attrs[0][0] == "href"
):
href = attrs[0][1]
components = href.split("/")
assert (
len(components) > 3 and components[-1] == "index.html"
), f"Unexpected item in {self.INPUT_URL}: tag={tag} attrs={attrs}"
self._version = components[-2]
@property
def version(self):
return self._version
def auto_detect_bazel_version():
"""Auto-detect correct _BAZEL_VERSION value base on current script directory name."""
script_dir_name = os.path.basename(os.path.dirname(__file__))
version = {
"bazel": _BAZEL_VERSION_LATEST_RELEASE,
"bazel-prerelease": _BAZEL_VERSION_LATEST_PRERELEASE,
}.get(script_dir_name)
assert (
version is not None
), f"Unknown name of parent directory {script_dir_name}, cannot auto-detect Bazel version!"
return version
def get_bazel_version():
"""Return _BAZEL_VERSION after optional auto-detection."""
bazel_version = _BAZEL_VERSION
if bazel_version == _BAZEL_VERSION_AUTO_DETECT:
bazel_version = auto_detect_bazel_version()
return bazel_version
def natural_sort_key(item):
"""Convert a version number to a sorting key for natural ordering."""
# Split the input item into a list of words and numbers (ignore everything else).
# E.g. "6.0-pre2022:1" -> ("6", "0", "pre", "2022", "1")
tups = re.findall("[^\W\d_]|\d+", item)
# Convert all numbers to their integer representation in the output list, but
# keep words as is. For example (6, 0, "pre", 2022, 1)
# Python will naturally sort tuple items in ascending order.
return [int(tup) if tup.isdigit() else tup for tup in tups]
def get_release_candidate_version(bazel_version):
"""Return (version, candidate) information from formatted version string."""
version, _, candidate = bazel_version.partition("/")
assert candidate, "Missing release candidate /<suffix> in: " + bazel_version
return version, candidate
def do_latest():
"""Retrieve tag of the latest pre-release to fetch."""
bazel_version = get_bazel_version()
if bazel_version == _BAZEL_VERSION_RELEASE_CANDIDATE:
# A release candidate is only available from releases.bazel.build
# and there is no API file provided to list them, which forces the use
# of hard-coded version + candidate
version, candidate = get_release_candidate_version(_BAZEL_VERSION)
print(f"{version}{candidate}")
elif bazel_version == _BAZEL_VERSION_LATEST_PRERELEASE:
# Prebuilt pre-release binaries are not longer available from GitHub.
# Extract the latest prerelease version from rolling HTML page.
prerelease_parser = BazelPreReleaseHtmlParser()
with urllib.request.urlopen(prerelease_parser.INPUT_URL) as input:
prerelease_parser.feed(input.read().decode("utf-8"))
print(prerelease_parser.version)
elif bazel_version == _BAZEL_VERSION_LATEST_RELEASE:
# This codes get the latest official release tag directly instead.
print(
json.load(
urllib.request.urlopen(
"https://api.github.com/repos/bazelbuild/bazel/releases/latest"
)
)["tag_name"]
)
else:
assert (
False
), f"Invalid or unrecognized value for _BAZEL_VERSION: {bazel_version}"
_PLATFORMS = {
"linux-amd64": "linux-x86_64",
"linux-arm64": "linux-arm64",
"mac-amd64": "darwin-x86_64",
"mac-arm64": "darwin-arm64",
"windows-amd64": "windows-x86_64",
"windows-arm64": "windows-arm64",
}
_EXTENSION = {
"linux": "",
"mac": "",
"windows": ".zip",
}
def get_download_url(version, platform):
if platform not in _PLATFORMS:
raise ValueError("unsupported platform {}".format(platform))
extension = _EXTENSION[platform.split("-")[0]]
bazel_version = _BAZEL_VERSION
if bazel_version == _BAZEL_VERSION_AUTO_DETECT:
bazel_version = auto_detect_bazel_version()
if bazel_version == _BAZEL_VERSION_RELEASE_CANDIDATE:
version, candidate = get_release_candidate_version(_BAZEL_VERSION)
url = (
"https://releases.bazel.build/{version}/{candidate}/"
"bazel-{version}{candidate}-{platform}{extension}".format(
version=version,
candidate=candidate,
platform=_PLATFORMS[platform],
extension=extension,
)
)
elif bazel_version == _BAZEL_VERSION_LATEST_PRERELEASE:
# Example for version == "8.0.0-pre.20240128.3" and platform == "linux-amd64"
# https://releases.bazel.build/8.0.0/rolling/8.0.0-pre.20240128.3/bazel-8.0.0-pre.20240128.3-linux-x86_64
pos = version.find("-pre")
assert pos > 0, f"Unexpected pre-release version: {version}"
url = (
"https://releases.bazel.build/{major_version}/rolling/{version}/"
"bazel-{version}-{platform}{extension}".format(
major_version=version[:pos],
version=version,
platform=_PLATFORMS[platform],
extension=extension,
)
)
else:
url = (
"https://github.com/bazelbuild/bazel/releases/download/{version}/"
"bazel-{version}-{platform}{extension}".format(
version=version,
platform=_PLATFORMS[platform],
extension=extension,
)
)
manifest = {
"url": [url],
"ext": extension,
}
print(json.dumps(manifest))
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers()
latest = sub.add_parser("latest")
latest.set_defaults(func=lambda _opts: do_latest())
download = sub.add_parser("get_url")
download.set_defaults(
func=lambda opts: get_download_url(
os.environ["_3PP_VERSION"], os.environ["_3PP_PLATFORM"]
)
)
opts = ap.parse_args()
opts.func(opts)
if __name__ == "__main__":
main()