Skip to content

Commit 16e1aab

Browse files
committed
Rust: Add script to perform the fixed steps of updating rust-analyzer
1 parent f192711 commit 16e1aab

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import shlex
5+
import subprocess
6+
import sys
7+
import tomllib
8+
import urllib.request
9+
from pathlib import Path
10+
11+
12+
REPO_ROOT = Path(__file__).resolve().parents[2]
13+
RUST_EXTRACTOR_MANIFEST = REPO_ROOT / "rust/extractor/Cargo.toml"
14+
15+
# The files that mention the fixed Rust toolchain version
16+
TOOLCHAIN_RS = REPO_ROOT / "rust/extractor/src/toolchain.rs"
17+
INTEGRATION_TEST_CONFTEST = REPO_ROOT / "rust/ql/integration-tests/conftest.py"
18+
QL_TEST_SETUP = REPO_ROOT / "rust/ql/test/setup.sh"
19+
20+
21+
def print_step(number: int, description: str) -> None:
22+
print(f"\nStep {number}: {description}", flush=True)
23+
24+
25+
def run(*command: str) -> None:
26+
print(f"+ {shlex.join(command)}", flush=True)
27+
subprocess.run(command, cwd=REPO_ROOT, check=True)
28+
29+
30+
def run_codegen() -> None:
31+
try:
32+
run("bazel", "run", "//rust/codegen")
33+
except subprocess.CalledProcessError as error:
34+
print(
35+
"\nCodegen failed. Carry out the instructions from step 4 in rust/updating-rust-analyzer.md manually.",
36+
file=sys.stderr,
37+
flush=True,
38+
)
39+
raise SystemExit(error.returncode) from None
40+
41+
42+
def get_rust_analyzer_version() -> str:
43+
"""Get the minimum version of all ra_ap dependencies from `Cargo.toml`."""
44+
dependencies = tomllib.loads(RUST_EXTRACTOR_MANIFEST.read_text())["dependencies"]
45+
return min(
46+
(
47+
version
48+
for name, version in dependencies.items()
49+
if name.startswith("ra_ap_")
50+
),
51+
key=lambda value: tuple(map(int, value.split("."))),
52+
)
53+
54+
55+
def fetch(url: str) -> bytes:
56+
request = urllib.request.Request(
57+
url,
58+
headers={"User-Agent": "github/codeql rust-analyzer updater"},
59+
)
60+
with urllib.request.urlopen(request) as response:
61+
return response.read()
62+
63+
64+
def get_compatible_rust_toolchain(rust_analyzer_version: str) -> str:
65+
"""Get the latest Rust toolchain that precedes our version of rust-analyzer."""
66+
# Get the release date of the rust-analyzer version
67+
crate_url = f"https://crates.io/api/v1/crates/ra_ap_syntax/{rust_analyzer_version}"
68+
crate = json.loads(fetch(crate_url))
69+
rust_analyzer_release = crate["version"]["created_at"].split("T")[0]
70+
71+
# `manifests.txt` is a list of all toolchains. The one we're interested looks likes
72+
# ```
73+
# static.rust-lang.org/dist/YYYY-MM-DD/channel-rust-stable.toml
74+
# ```
75+
# where `YYYY-MM-DD` is the last that is earlier than the rust-analyzer release.
76+
rust_manifest = next(
77+
manifest
78+
for manifest in reversed(
79+
fetch("https://static.rust-lang.org/manifests.txt").decode().splitlines()
80+
)
81+
if manifest.endswith("/channel-rust-stable.toml")
82+
and manifest.split("/")[2] <= rust_analyzer_release
83+
)
84+
manifest = tomllib.loads(fetch(f"https://{rust_manifest}").decode())
85+
# The version looks like `version = "0.99.0 (797e8a9bc 2026-08-05)"` - we only want the first part.
86+
return manifest["pkg"]["rust"]["version"].split()[0]
87+
88+
89+
def update_fixed_rust_toolchain(version: str) -> None:
90+
"""Change the fixed toolchain in the places where it's hardcoded"""
91+
toolchain_rs = TOOLCHAIN_RS.read_text()
92+
prefix = 'const FIXED_RUST_TOOLCHAIN: &str = "'
93+
old_version = toolchain_rs.split(prefix, 1)[1].split('"', 1)[0]
94+
TOOLCHAIN_RS.write_text(
95+
toolchain_rs.replace(
96+
f'{prefix}{old_version}"',
97+
f'{prefix}{version}"',
98+
)
99+
)
100+
101+
for test_setup in (INTEGRATION_TEST_CONFTEST, QL_TEST_SETUP):
102+
contents = test_setup.read_text()
103+
test_setup.write_text(
104+
contents.replace(
105+
f"rustup toolchain install {old_version} ",
106+
f"rustup toolchain install {version} ",
107+
)
108+
)
109+
110+
111+
def align_rust_analyzer_versions() -> None:
112+
"""Align ra_ap dependencies and update the lockfile when versions differ.
113+
114+
rust-analyzer crates may be published gradually, so this selects the newest
115+
version that is available for every ra_ap dependency.
116+
"""
117+
manifest = RUST_EXTRACTOR_MANIFEST.read_text()
118+
dependencies = {
119+
name: version
120+
for name, version in tomllib.loads(manifest)["dependencies"].items()
121+
if name.startswith("ra_ap_")
122+
}
123+
if len(set(dependencies.values())) == 1:
124+
# All the `ra_ap_` dependencies agree
125+
return
126+
127+
version = get_rust_analyzer_version()
128+
129+
for name, old_version in dependencies.items():
130+
manifest = manifest.replace(
131+
f'{name} = "{old_version}"',
132+
f'{name} = "{version}"',
133+
)
134+
RUST_EXTRACTOR_MANIFEST.write_text(manifest)
135+
run("cargo", "update")
136+
137+
138+
def commit_all(title: str) -> None:
139+
run("git", "add", "--all")
140+
run("git", "commit", "--no-verify", "--allow-empty", "-m", title)
141+
142+
143+
def main() -> None:
144+
status = subprocess.run(
145+
["git", "status", "--porcelain"],
146+
cwd=REPO_ROOT,
147+
check=True,
148+
capture_output=True,
149+
text=True,
150+
).stdout
151+
if status:
152+
raise RuntimeError("the working tree must be clean")
153+
154+
print_step(1, "Update dependencies")
155+
old_rust_analyzer_version = get_rust_analyzer_version()
156+
run("cargo", "upgrade", "--incompatible", "--pinned")
157+
new_rust_analyzer_version = get_rust_analyzer_version()
158+
if new_rust_analyzer_version == old_rust_analyzer_version:
159+
run("git", "restore", ".")
160+
print("No new rust-analyzer version available.")
161+
return
162+
163+
align_rust_analyzer_versions()
164+
commit_all("Cargo: Upgrade dependencies")
165+
166+
print_step(2, "Update the fixed Rust toolchain used by the extractor")
167+
rust_toolchain = get_compatible_rust_toolchain(new_rust_analyzer_version)
168+
update_fixed_rust_toolchain(rust_toolchain)
169+
commit_all("Rust: Update fixed toolchain")
170+
171+
print_step(3, "Regenerate vendored bazel files")
172+
run("misc/bazel/3rdparty/update_tree_sitter_extractors_deps.sh")
173+
commit_all("Bazel: Regenerate vendored cargo dependencies")
174+
175+
print_step(4, "Run codegen")
176+
run_codegen()
177+
commit_all("Rust: Run codegen")
178+
179+
print_step(5, "Try compiling")
180+
run("bazel", "run", "//rust:install")
181+
182+
183+
if __name__ == "__main__":
184+
main()

0 commit comments

Comments
 (0)