mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 13:50:10 +01:00
This replaces black, flake8, and mypy with the more modern and efficient tools `ruff` and `ty` from Astral. Assisted-by: Antigravity with Gemini
184 lines
5.6 KiB
Python
Executable File
184 lines
5.6 KiB
Python
Executable File
#!/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 base64
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
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)
|