update_module_to_nightly.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #!/usr/bin/env python3
  2. """Updates example module file to use the nightly toolchain release.
  3. This script computes the most recent nightly Carbon toolchain release, and
  4. updates the example module file with an `archive_override` pointing at it.
  5. Usage:
  6. # Within the `examples/bazel` directory:
  7. ./update_module_to_nightly.py
  8. For more details about using the Carbon toolchain with Bazel, see the
  9. documentation in `examples/bazel/MODULE.bazel`.
  10. """
  11. __copyright__ = """
  12. Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  13. Exceptions. See /LICENSE for license information.
  14. SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  15. """
  16. import re
  17. import os
  18. import sys
  19. import base64
  20. import urllib.request
  21. import urllib.error
  22. import json
  23. MODULE_NAME = "carbon_toolchain"
  24. MODULE_FILENAME = "MODULE.bazel"
  25. DEP_PATTERN = re.compile(
  26. rf'^bazel_dep\s*\(\s*name\s*=\s*"{MODULE_NAME}".*?\)',
  27. re.DOTALL | re.MULTILINE,
  28. )
  29. OVERRIDE_PATTERN = re.compile(
  30. rf'^archive_override\s*\(\s*module_name\s*=\s*"{MODULE_NAME}".*?\)',
  31. re.DOTALL | re.MULTILINE,
  32. )
  33. # The nightly build starts at 2am UTC, and we give it up to 4 hours to complete.
  34. BUFFER_HOURS = 6
  35. RELEASES_URL = (
  36. "https://github.com/carbon-language/carbon-lang/releases/download"
  37. )
  38. RELEASES_API_URL = (
  39. "https://api.github.com/repos/carbon-language/carbon-lang/releases"
  40. )
  41. API_HEADERS = {
  42. "Accept": "application/vnd.github+json",
  43. "X-GitHub-Api-Version": "2022-11-28",
  44. # GitHub API requires a User-Agent, urllib doesn't send one by default
  45. "User-Agent": "python-urllib",
  46. }
  47. def log(msg: str) -> None:
  48. print(f"[update_module_to_nightly] {msg}", file=sys.stderr)
  49. def get_latest_version() -> str:
  50. # Use the 'releases' list endpoint, NOT 'releases/latest'. Using the
  51. # `latest` endpoint only works for full releases, not pre-releases. Carbon's
  52. # nightly releases are classified as pre-releases so we have to get the full
  53. # list and simply take the first one. That does mean we only need the first
  54. # page of results.
  55. url = f"{RELEASES_API_URL}?per_page=1"
  56. req = urllib.request.Request(url, headers=API_HEADERS)
  57. try:
  58. with urllib.request.urlopen(req) as response:
  59. data = json.load(response)
  60. if not data:
  61. log("Error: no releases found for this repository.")
  62. sys.exit(1)
  63. # The API returns a list sorted by creation date (newest first).
  64. latest_release = data[0]
  65. except urllib.error.HTTPError as e:
  66. log(f"Error: HTTP error {e.code} fetching latest release: {e.reason}")
  67. # It's often useful to print the body for GitHub API errors (e.g. rate
  68. # limit exceeded)
  69. log(e.read().decode("utf-8"))
  70. sys.exit(1)
  71. # The release tag starts with `v` followed by the version.
  72. latest_version = str(latest_release["tag_name"])
  73. if not latest_version.startswith("v"):
  74. log(f"Error: malformed release tag name: {latest_version}")
  75. sys.exit(1)
  76. return latest_version[1:]
  77. def get_digest(version: str, filename: str) -> str:
  78. url = f"{RELEASES_API_URL}/tags/v{version}"
  79. req = urllib.request.Request(url, headers=API_HEADERS)
  80. try:
  81. with urllib.request.urlopen(req) as response:
  82. release_data = json.load(response)
  83. except urllib.error.HTTPError as e:
  84. log(f"Error: unable to find `v{version}`: {e.code}: {e.reason}")
  85. sys.exit(1)
  86. assets = release_data.get("assets", [])
  87. for asset in assets:
  88. name = str(asset.get("name"))
  89. if name != filename:
  90. continue
  91. digest = str(asset.get("digest"))
  92. if not digest.startswith("sha256:"):
  93. log(f"Error: found invalid digest for `{filename}`: `{digest}`")
  94. sys.exit(1)
  95. # Re-encode from the GitHub format to Bazel.
  96. digest = (
  97. "sha256-"
  98. + base64.b64encode(bytes.fromhex(digest[len("sha256:") :])).decode()
  99. )
  100. return digest
  101. log(f"Error: unable to find a digest for `{filename}`")
  102. sys.exit(1)
  103. def generate_override(version: str) -> str:
  104. basename = f"carbon_toolchain-{version}"
  105. digest = get_digest(version, f"{basename}.tar.gz")
  106. return (
  107. f"archive_override(\n"
  108. f' module_name = "{MODULE_NAME}",\n'
  109. f' integrity = "{digest}",\n'
  110. f' strip_prefix = "{basename}/lib/carbon",\n'
  111. f' urls = ["{RELEASES_URL}/v{version}/{basename}.tar.gz"],\n'
  112. f")"
  113. )
  114. def main() -> None:
  115. if not os.path.exists(MODULE_FILENAME):
  116. log(f"Error: `{MODULE_FILENAME}` not found in current directory.")
  117. sys.exit(1)
  118. with open(MODULE_FILENAME, "r") as f:
  119. content = f.read()
  120. # 1. Verification (Check if dependency exists)
  121. dep_match = DEP_PATTERN.search(content)
  122. if not dep_match:
  123. log(
  124. f"Error: `bazel_dep` for `{MODULE_NAME}` not found in "
  125. f"`{MODULE_FILENAME}`."
  126. )
  127. sys.exit(1)
  128. version = get_latest_version()
  129. new_block = generate_override(version)
  130. new_content, count = OVERRIDE_PATTERN.subn(new_block, content)
  131. if count > 0:
  132. log("Existing override found, replacing with a fresh one")
  133. else:
  134. log("No existing override found, inserting one")
  135. new_content = (
  136. content[: dep_match.end()]
  137. + "\n\n"
  138. + new_block
  139. + content[dep_match.end() :]
  140. )
  141. with open(MODULE_FILENAME, "w") as f:
  142. f.write(new_content)
  143. log(f"Successfully updated `{MODULE_FILENAME}` to version `{version}`")
  144. if __name__ == "__main__":
  145. try:
  146. main()
  147. except KeyboardInterrupt:
  148. sys.exit(1)