Skip to content

Commit b52307f

Browse files
committed
Add a script to help update to a new CTK version
1 parent 73da391 commit b52307f

1 file changed

Lines changed: 209 additions & 0 deletions

File tree

toolshed/update_ctk.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python
2+
3+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE
5+
6+
import argparse
7+
import json
8+
import re
9+
import subprocess
10+
import sys
11+
import tarfile
12+
import tempfile
13+
import venv
14+
from pathlib import Path
15+
from urllib.request import urlopen
16+
17+
# Example URL of an HTML directory listing
18+
CONTENT_URL = "https://developer.download.nvidia.com/compute/cuda/redist"
19+
20+
21+
CYBIND_GENERATED_LIBRARIES = [
22+
("cufile", "libcufile", "cufile"),
23+
("nvvm", "libnvvm", "nvvm"),
24+
("nvjitlink", "libnvjitlink", "nvJitLink"),
25+
]
26+
27+
28+
def fetch_headers(version: str, library_name: str, dest_dir: Path):
29+
def tar_filter(members):
30+
for tarinfo in members:
31+
name = Path(tarinfo.name)
32+
parts = name.parts
33+
try:
34+
idx = parts.index("include")
35+
except ValueError:
36+
continue
37+
tarinfo.name = str(Path(*parts[idx + 1 :]))
38+
yield tarinfo
39+
40+
output_dir = dest_dir / Path(version)
41+
if output_dir.exists():
42+
print(f"Skipping header download for {library_name} {version}, already exists")
43+
return
44+
45+
output_dir.mkdir()
46+
47+
json_url = f"{CONTENT_URL}/redistrib_{version}.json"
48+
with urlopen(json_url) as resp: # noqa: S310
49+
content = json.loads(resp.read().decode("utf-8"))
50+
if library := content.get(library_name):
51+
archive_url = f"{CONTENT_URL}/{library['linux-x86_64']['relative_path']}"
52+
print(f"Fetching package {archive_url}")
53+
with tempfile.NamedTemporaryFile() as tmp:
54+
tmppath = Path(tmp.name)
55+
56+
with tmppath.open("wb") as f, urlopen(archive_url) as resp: # noqa: S310
57+
f.write(resp.read())
58+
59+
with tarfile.open(tmppath, "r:xz") as tar:
60+
tar.extractall( # noqa: S202
61+
members=tar_filter(tar.getmembers()),
62+
path=output_dir,
63+
filter="fully_trusted",
64+
)
65+
else:
66+
print(f"No {library_name} in version {version}")
67+
68+
69+
def update_config(version: str, config_path: Path) -> None:
70+
# This is pretty brittle, but will be better when/if we move all the config to YAML
71+
72+
out = []
73+
in_version_section = False
74+
with config_path.open() as f:
75+
for line in f:
76+
if line.strip() == "'versions': [":
77+
in_version_section = True
78+
if in_version_section and line.strip() == "],":
79+
out.append(f" ('{version}', ),\n")
80+
in_version_section = False
81+
out.append(line)
82+
83+
with config_path.open("w") as f:
84+
f.write("".join(out))
85+
86+
87+
def run_cybind(cybind_repo: Path, cuda_python_repo: Path, libraries: list[str]) -> None:
88+
with tempfile.TemporaryDirectory() as tempdir:
89+
tempdir_path = Path(tempdir)
90+
91+
venv.create(tempdir_path, with_pip=True)
92+
subprocess.check_call( # noqa: S603
93+
[
94+
str(tempdir_path / "bin" / "python"),
95+
"-m",
96+
"pip",
97+
"install",
98+
str(cybind_repo),
99+
]
100+
)
101+
try:
102+
subprocess.check_call( # noqa: S603
103+
[
104+
str(tempdir_path / "bin" / "python"),
105+
"-m",
106+
"cybind",
107+
"--generate",
108+
*libraries,
109+
"--output-dir",
110+
str(cuda_python_repo / "cuda_bindings"),
111+
]
112+
)
113+
except subprocess.CalledProcessError:
114+
print("Error running cybind.")
115+
print("This probably indicates an issue introduced with the new headers.")
116+
print("If necessary, you can edit the headers and re-run this script.")
117+
return 1
118+
119+
120+
def update_version_file(version: str, version_path: Path, is_prev: bool) -> str:
121+
if is_prev:
122+
key = "prev_build"
123+
else:
124+
key = "build"
125+
126+
with version_path.open() as f:
127+
content = json.load(f)
128+
existing_version = content["cuda"][key]["version"]
129+
content["cuda"][key]["version"] = version
130+
131+
with version_path.open("w") as f:
132+
content = json.dump(content, f, indent=2)
133+
# json.dump doesn't add a trailing newline
134+
f.write("\n")
135+
136+
return existing_version
137+
138+
139+
def update_matrix(existing_version: str, new_version: str, matrix_path: Path) -> None:
140+
# It would be less brittle to update using JSON here, but that messes up the formatting
141+
142+
with matrix_path.open() as f:
143+
content = f.read()
144+
145+
content = re.sub(rf'"CUDA_VER": "{existing_version}"', f'"CUDA_VER": "{new_version}"', content)
146+
147+
with matrix_path.open("w") as f:
148+
f.write(content)
149+
150+
151+
def main(version: str, cuda_python_repo: Path, cybind_repo: Path, is_prev: bool):
152+
cybind_headers_path = cybind_repo / "assets" / "headers"
153+
cybind_config_path = cybind_repo / "assets" / "configs"
154+
155+
for libname, distname, subdir in CYBIND_GENERATED_LIBRARIES:
156+
fetch_headers(version, distname, cybind_headers_path / subdir)
157+
update_config(version, cybind_config_path / f"config_{libname}.py")
158+
159+
existing_version = update_version_file(version, cuda_python_repo / "ci" / "versions.json", is_prev)
160+
update_matrix(existing_version, version, cuda_python_repo / "ci" / "test-matrix.json")
161+
162+
# Do this last, because, if anything, it's the thing that's likely to fail
163+
if run_cybind(cybind_repo, cuda_python_repo, [x[0] for x in CYBIND_GENERATED_LIBRARIES]):
164+
sys.exit(1)
165+
166+
167+
if __name__ == "__main__":
168+
parser = argparse.ArgumentParser(description="Update cuda-python for a new version of the CTK")
169+
parser.add_argument(
170+
"--cybind-repo",
171+
type=Path,
172+
help="Path to a checkout of cybind (default: ../cybind relative to cuda-python)",
173+
)
174+
parser.add_argument(
175+
"--is-prev",
176+
action="store_true",
177+
help="When given, update the previous, not latest version",
178+
)
179+
parser.add_argument(
180+
"version",
181+
type=str,
182+
help="Version to move to",
183+
)
184+
args = parser.parse_args()
185+
186+
cuda_python_repo = Path(__file__).parents[1]
187+
188+
if args.cybind_repo is None:
189+
args.cybind_repo = cuda_python_repo.parent / "cybind"
190+
191+
print("Before running this script, you need to:")
192+
print(" - Create a new branch in this repo based on upstream/main")
193+
print(f" - Create a new branch in a cybind checkout at {args.cybind_repo} based on upstream/main")
194+
print()
195+
print(f"This will add CTK {args.version} as the {'previous' if args.is_prev else 'latest'} version.")
196+
print("Proceed? [y/N]")
197+
resp = input().strip().lower()
198+
if resp != "y":
199+
print("Aborting")
200+
201+
main(args.version, cuda_python_repo, args.cybind_repo, args.is_prev)
202+
203+
print("Remaining manual steps:")
204+
print("- Add a changelog entry:")
205+
print(
206+
f"* Updated the ``cuda.bindings.runtime`` module to statically link "
207+
f"against the CUDA Runtime library from CUDA Toolkit {args.version}."
208+
)
209+
print("- Inspect the changes to this repo and cybind, commit and submit PRs.")

0 commit comments

Comments
 (0)