|
@@ -0,0 +1,183 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+
|
|
|
|
|
+"""Updates example module file to use the nightly toolchain release.
|
|
|
|
|
+
|
|
|
|
|
+This script computes the most recent nightly Carbon toolchain release, and
|
|
|
|
|
+updates the example module file with an `archive_override` pointing at it.
|
|
|
|
|
+
|
|
|
|
|
+Usage:
|
|
|
|
|
+ # Within the `examples/bazel` directory:
|
|
|
|
|
+ ./update_module_to_nightly.py
|
|
|
|
|
+
|
|
|
|
|
+For more details about using the Carbon toolchain with Bazel, see the
|
|
|
|
|
+documentation in `examples/bazel/MODULE.bazel`.
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+__copyright__ = """
|
|
|
|
|
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
|
|
|
+Exceptions. See /LICENSE for license information.
|
|
|
|
|
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import re
|
|
|
|
|
+import os
|
|
|
|
|
+import sys
|
|
|
|
|
+import base64
|
|
|
|
|
+import urllib.request
|
|
|
|
|
+import urllib.error
|
|
|
|
|
+import json
|
|
|
|
|
+
|
|
|
|
|
+MODULE_NAME = "carbon_toolchain"
|
|
|
|
|
+MODULE_FILENAME = "MODULE.bazel"
|
|
|
|
|
+DEP_PATTERN = re.compile(
|
|
|
|
|
+ rf'^bazel_dep\s*\(\s*name\s*=\s*"{MODULE_NAME}".*?\)',
|
|
|
|
|
+ re.DOTALL | re.MULTILINE,
|
|
|
|
|
+)
|
|
|
|
|
+OVERRIDE_PATTERN = re.compile(
|
|
|
|
|
+ rf'^archive_override\s*\(\s*module_name\s*=\s*"{MODULE_NAME}".*?\)',
|
|
|
|
|
+ re.DOTALL | re.MULTILINE,
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+# The nightly build starts at 2am UTC, and we give it up to 4 hours to complete.
|
|
|
|
|
+BUFFER_HOURS = 6
|
|
|
|
|
+
|
|
|
|
|
+RELEASES_URL = (
|
|
|
|
|
+ "https://github.com/carbon-language/carbon-lang/releases/download"
|
|
|
|
|
+)
|
|
|
|
|
+RELEASES_API_URL = (
|
|
|
|
|
+ "https://api.github.com/repos/carbon-language/carbon-lang/releases"
|
|
|
|
|
+)
|
|
|
|
|
+API_HEADERS = {
|
|
|
|
|
+ "Accept": "application/vnd.github+json",
|
|
|
|
|
+ "X-GitHub-Api-Version": "2022-11-28",
|
|
|
|
|
+ # GitHub API requires a User-Agent, urllib doesn't send one by default
|
|
|
|
|
+ "User-Agent": "python-urllib",
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def log(msg: str) -> None:
|
|
|
|
|
+ print(f"[update_module_to_nightly] {msg}", file=sys.stderr)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def get_latest_version() -> str:
|
|
|
|
|
+ # Use the 'releases' list endpoint, NOT 'releases/latest'. Using the
|
|
|
|
|
+ # `latest` endpoint only works for full releases, not pre-releases. Carbon's
|
|
|
|
|
+ # nightly releases are classified as pre-releases so we have to get the full
|
|
|
|
|
+ # list and simply take the first one. That does mean we only need the first
|
|
|
|
|
+ # page of results.
|
|
|
|
|
+ url = f"{RELEASES_API_URL}?per_page=1"
|
|
|
|
|
+ req = urllib.request.Request(url, headers=API_HEADERS)
|
|
|
|
|
+ try:
|
|
|
|
|
+ with urllib.request.urlopen(req) as response:
|
|
|
|
|
+ data = json.load(response)
|
|
|
|
|
+ if not data:
|
|
|
|
|
+ log("Error: no releases found for this repository.")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ # The API returns a list sorted by creation date (newest first).
|
|
|
|
|
+ latest_release = data[0]
|
|
|
|
|
+
|
|
|
|
|
+ except urllib.error.HTTPError as e:
|
|
|
|
|
+ log(f"Error: HTTP error {e.code} fetching latest release: {e.reason}")
|
|
|
|
|
+ # It's often useful to print the body for GitHub API errors (e.g. rate
|
|
|
|
|
+ # limit exceeded)
|
|
|
|
|
+ log(e.read().decode("utf-8"))
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ # The release tag starts with `v` followed by the version.
|
|
|
|
|
+ latest_version = str(latest_release["tag_name"])
|
|
|
|
|
+ if not latest_version.startswith("v"):
|
|
|
|
|
+ log(f"Error: malformed release tag name: {latest_version}")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ return latest_version[1:]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def get_digest(version: str, filename: str) -> str:
|
|
|
|
|
+ url = f"{RELEASES_API_URL}/tags/v{version}"
|
|
|
|
|
+ req = urllib.request.Request(url, headers=API_HEADERS)
|
|
|
|
|
+ try:
|
|
|
|
|
+ with urllib.request.urlopen(req) as response:
|
|
|
|
|
+ release_data = json.load(response)
|
|
|
|
|
+ except urllib.error.HTTPError as e:
|
|
|
|
|
+ log(f"Error: unable to find `v{version}`: {e.code}: {e.reason}")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ assets = release_data.get("assets", [])
|
|
|
|
|
+ for asset in assets:
|
|
|
|
|
+ name = str(asset.get("name"))
|
|
|
|
|
+ if name != filename:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ digest = str(asset.get("digest"))
|
|
|
|
|
+ if not digest.startswith("sha256:"):
|
|
|
|
|
+ log(f"Error: found invalid digest for `{filename}`: `{digest}`")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ # Re-encode from the GitHub format to Bazel.
|
|
|
|
|
+ digest = (
|
|
|
|
|
+ "sha256-"
|
|
|
|
|
+ + base64.b64encode(bytes.fromhex(digest[len("sha256:") :])).decode()
|
|
|
|
|
+ )
|
|
|
|
|
+ return digest
|
|
|
|
|
+
|
|
|
|
|
+ log(f"Error: unable to find a digest for `{filename}`")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_override(version: str) -> str:
|
|
|
|
|
+ basename = f"carbon_toolchain-{version}"
|
|
|
|
|
+ digest = get_digest(version, f"{basename}.tar.gz")
|
|
|
|
|
+ return (
|
|
|
|
|
+ f"archive_override(\n"
|
|
|
|
|
+ f' module_name = "{MODULE_NAME}",\n'
|
|
|
|
|
+ f' integrity = "{digest}",\n'
|
|
|
|
|
+ f' strip_prefix = "{basename}/lib/carbon",\n'
|
|
|
|
|
+ f' urls = ["{RELEASES_URL}/v{version}/{basename}.tar.gz"],\n'
|
|
|
|
|
+ f")"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main() -> None:
|
|
|
|
|
+ if not os.path.exists(MODULE_FILENAME):
|
|
|
|
|
+ log(f"Error: `{MODULE_FILENAME}` not found in current directory.")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ with open(MODULE_FILENAME, "r") as f:
|
|
|
|
|
+ content = f.read()
|
|
|
|
|
+
|
|
|
|
|
+ # 1. Verification (Check if dependency exists)
|
|
|
|
|
+ dep_match = DEP_PATTERN.search(content)
|
|
|
|
|
+ if not dep_match:
|
|
|
|
|
+ log(
|
|
|
|
|
+ f"Error: `bazel_dep` for `{MODULE_NAME}` not found in "
|
|
|
|
|
+ f"`{MODULE_FILENAME}`."
|
|
|
|
|
+ )
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ version = get_latest_version()
|
|
|
|
|
+ new_block = generate_override(version)
|
|
|
|
|
+
|
|
|
|
|
+ (new_content, count) = OVERRIDE_PATTERN.subn(new_block, content)
|
|
|
|
|
+ if count > 0:
|
|
|
|
|
+ log("Existing override found, replacing with a fresh one")
|
|
|
|
|
+ else:
|
|
|
|
|
+ log("No existing override found, inserting one")
|
|
|
|
|
+ new_content = (
|
|
|
|
|
+ content[: dep_match.end()]
|
|
|
|
|
+ + "\n\n"
|
|
|
|
|
+ + new_block
|
|
|
|
|
+ + content[dep_match.end() :]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ with open(MODULE_FILENAME, "w") as f:
|
|
|
|
|
+ f.write(new_content)
|
|
|
|
|
+
|
|
|
|
|
+ log(f"Successfully updated `{MODULE_FILENAME}` to version `{version}`")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ try:
|
|
|
|
|
+ main()
|
|
|
|
|
+ except KeyboardInterrupt:
|
|
|
|
|
+ sys.exit(1)
|