| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- #!/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)
|