Skip to content

Commit 1df317b

Browse files
ScottToddclaude
andauthored
Refactor setup_venv.py, including adding --find-links support (ROCm#3242)
## Motivation Progress on ROCm#1559. I'm splitting this off from ROCm#3182. In ROCm#3136, I'm using [`indexer.py`](https://github.com/ROCm/TheRock/blob/main/third-party/indexer/indexer.py) to generate an index page for a CI workflow run that can be installed from using `--find-links` (and _not_ `--index-url`). For example: https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/index.html. For workflows and developers to be able to conveniently create Python venvs and install those packages, this setup_venv.py script needed support for `--find-links` too. ## Technical Details Key points about the refactoring: 1. Changed scraping signature / behavior to returns the union of all subdirs instead of a distinct list of subdirs per package index. If someone tries to install a package from an index that doesn't have that package, that will be a later error. * before: `scrape_subdirs() -> dict[str, set[str]] | set[str] | None` * after: `_scrape_rocm_index_subdirs() -> set[str] | None` 2. Reworked branching between `pip` and `uv` to make it more clear what is being used where and why 3. Streamlined `run()` to emphasize the `create_venv` -> `update_venv` -> `install_packages_into_venv` flow 4. Allowed setting `--packages` without `--index-url` or `--index-name` (to install from the default pypi index) 5. Added new `--find-links` option (can be used together with `--index-url` as they are compatible/complimentary) Documentation for reference: * https://pip.pypa.io/en/stable/cli/pip_install/ * https://docs.astral.sh/uv/pip/packages/ ## Test Plan * CI usage in some existing workflows * New unit tests for helper functions ## Test Result Manual testing: <details><summary>With `--find-links` and pip</summary> <p> ``` D:\scratch\therock λ python D:\projects\TheRock\build_tools/setup_venv.py test.venv --packages rocm[libraries]==7.12.0.dev0 --find-links=https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/index.html --clean Clearing existing venv_dir 'test.venv' Creating venv at 'test.venv' Dir relative to CWD: 'test.venv' Dir fully resolved : 'D:\scratch\therock\test.venv' ++ Exec [D:\scratch\therock]$ 'C:\Users\Nod-Shark16\AppData\Local\Programs\Python\Python313\python.exe' -m venv 'D:\scratch\therock\test.venv' ++ Exec [D:\scratch\therock]$ 'test.venv\Scripts\python.exe' -m pip install --upgrade pip Requirement already satisfied: pip in d:\scratch\therock\test.venv\lib\site-packages (25.1.1) Collecting pip Using cached pip-26.0-py3-none-any.whl.metadata (4.7 kB) Using cached pip-26.0-py3-none-any.whl (1.8 MB) Installing collected packages: pip Attempting uninstall: pip Found existing installation: pip 25.1.1 Uninstalling pip-25.1.1: Successfully uninstalled pip-25.1.1 Successfully installed pip-26.0 ++ Exec [D:\scratch\therock]$ 'test.venv\Scripts\python.exe' -m pip install --find-links=https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/index.html 'rocm[libraries]==7.12.0.dev0' Looking in links: https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/index.html Collecting rocm==7.12.0.dev0 (from rocm[libraries]==7.12.0.dev0) Using cached rocm-7.12.0.dev0-py3-none-any.whl Collecting rocm-sdk-core==7.12.0.dev0 (from rocm==7.12.0.dev0->rocm[libraries]==7.12.0.dev0) Using cached https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/rocm_sdk_core-7.12.0.dev0-py3-none-win_amd64.whl (654.1 MB) Collecting rocm-sdk-libraries-gfx110X-all==7.12.0.dev0 (from rocm[libraries]==7.12.0.dev0) Using cached https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/rocm_sdk_libraries_gfx110x_all-7.12.0.dev0-py3-none-win_amd64.whl (224.4 MB) Installing collected packages: rocm-sdk-libraries-gfx110X-all, rocm-sdk-core, rocm Successfully installed rocm-7.12.0.dev0 rocm-sdk-core-7.12.0.dev0 rocm-sdk-libraries-gfx110X-all-7.12.0.dev0 Setup complete at 'test.venv'! Activate the venv with: test.venv\Scripts\activate.bat ``` </p> </details> <details><summary>With `--find-links` and uv</summary> <p> ``` λ python D:\projects\TheRock\build_tools/setup_venv.py test_uv.venv --packages rocm[libraries]==7.12.0.dev0 --find-links=https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/index.html --clean --use-uv --pre Clearing existing venv_dir 'test_uv.venv' Creating venv at 'test_uv.venv' Dir relative to CWD: 'test_uv.venv' Dir fully resolved : 'D:\scratch\therock\test_uv.venv' ++ Exec [D:\scratch\therock]$ uv venv 'D:\scratch\therock\test_uv.venv' Using CPython 3.13.5 interpreter at: C:\Users\Nod-Shark16\AppData\Local\Programs\Python\Python313\python.exe Creating virtual environment at: test_uv.venv Activate with: test_uv.venv\Scripts\activate ++ Exec [D:\scratch\therock]$ uv pip install --python 'test_uv.venv\Scripts\python.exe' --find-links=https://therock-artifacts-testing.s3.amazonaws.com/21440027240-windows/python/gfx110X-all/index.html --prerelease=allow 'rocm[libraries]==7.12.0.dev0' Using Python 3.13.5 environment at: test_uv.venv Resolved 3 packages in 162ms ░░░░░░░░░░░░░░░░░░░░ [0/3] Installing wheels... warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance. If the cache and target directories are on different filesystems, hardlinking may not be supported. If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning. Installed 3 packages in 1.09s + rocm==7.12.0.dev0 + rocm-sdk-core==7.12.0.dev0 + rocm-sdk-libraries-gfx110x-all==7.12.0.dev0 Setup complete at 'test_uv.venv'! Activate the venv with: test_uv.venv\Scripts\activate.bat ``` </p> </details> ## Submission Checklist - [x] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1f9fc2e commit 1df317b

File tree

2 files changed

+310
-116
lines changed

2 files changed

+310
-116
lines changed

build_tools/setup_venv.py

Lines changed: 153 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,13 @@
4040
import sys
4141
import re
4242

43-
try:
44-
import requests
45-
except ImportError:
46-
requests = None
47-
4843
from github_actions.github_actions_utils import *
4944

50-
GFX_TARGET_REGEX = r'(gfx(?:\d{2,3}X|\d{3,4})(?:-[^<"/]*)?)</a>'
51-
5245
is_windows = platform.system() == "Windows"
5346

54-
INDEX_URLS_MAP = {
47+
ROCM_INDEX_URLS_MAP = {
48+
"stable": "https://repo.amd.com/rocm/whl/",
49+
"prerelease": "https://rocm.prereleases.amd.com/whl",
5550
"nightly": "https://rocm.nightlies.amd.com/v2",
5651
"dev": "https://rocm.devreleases.amd.com/v2",
5752
}
@@ -68,22 +63,20 @@ def run_command(args: list[str | Path], cwd: Path = Path.cwd()):
6863
subprocess.check_call(args, cwd=str(cwd), stdin=subprocess.DEVNULL)
6964

7065

71-
def get_system_py_command(use_uv: bool) -> list[str]:
72-
return ["uv"] if use_uv else [sys.executable, "-m"]
73-
74-
75-
def find_venv_python(venv_path: Path) -> Path | None:
66+
def find_venv_python_exe(venv_path: Path) -> Path | None:
67+
"""Finds the python executable under |venv_path|, if one exists."""
7668
paths = [venv_path / "bin" / "python", venv_path / "Scripts" / "python.exe"]
7769
for p in paths:
7870
if p.exists():
7971
return p
8072
return None
8173

8274

83-
def create_venv(venv_dir: Path, py_cmd: list[str] | None = None):
84-
if not py_cmd:
85-
py_cmd = get_system_py_command(use_uv=False)
75+
def create_venv(venv_dir: Path, use_uv: bool = False):
76+
"""Creates a Python venv at |venv_dir|.
8677
78+
No-op if venv_dir is already an initialized venv (has a python executable).
79+
"""
8780
log(f"Creating venv at '{venv_dir}'")
8881

8982
# Log some other variations of the path too.
@@ -92,43 +85,41 @@ def create_venv(venv_dir: Path, py_cmd: list[str] | None = None):
9285
except ValueError:
9386
venv_dir_relative = venv_dir
9487
venv_dir_resolved = venv_dir.resolve()
95-
log(f" Relative dir: '{venv_dir_relative}'")
96-
log(f" Resolved dir: '{venv_dir_resolved}'")
88+
log(f" Dir relative to CWD: '{venv_dir_relative}'")
89+
log(f" Dir fully resolved : '{venv_dir_resolved}'")
9790
log("")
9891

9992
# Create with 'python -m venv' as needed.
100-
python_exe = find_venv_python(venv_dir_resolved)
93+
python_exe = find_venv_python_exe(venv_dir_resolved)
10194
if python_exe:
10295
log(f" Found existing python executable at '{python_exe}', skipping creation")
10396
log(" Run again with --clean to clear the existing directory instead")
104-
else:
105-
run_command(py_cmd + ["venv", str(venv_dir_resolved)])
106-
97+
return
10798

108-
def upgrade_pip(python_exe: Path):
109-
log("")
110-
run_command([str(python_exe), "-m", "pip", "install", "--upgrade", "pip"])
99+
if use_uv:
100+
run_command(["uv", "venv", str(venv_dir_resolved)])
101+
else:
102+
run_command([sys.executable, "-m", "venv", str(venv_dir_resolved)])
111103

112104

113-
def install_packages(args: argparse.Namespace, py_cmd: list[str] | None):
114-
if not py_cmd:
115-
py_cmd = get_system_py_command(use_uv=False)
105+
def update_venv(venv_dir: Path, use_uv: bool = False):
106+
if use_uv:
107+
# No updates needed.
108+
return
116109

110+
# pip logs warnings about wanting to update, so we'll do that for it.
117111
log("")
112+
python_exe = find_venv_python_exe(venv_dir)
113+
run_command([str(python_exe), "-m", "pip", "install", "--upgrade", "pip"])
118114

119-
if args.index_name:
120-
index_url = INDEX_URLS_MAP[args.index_name]
121-
else:
122-
index_url = args.index_url
123-
index_url = index_url.rstrip("/") + "/" + args.index_subdir.strip("/")
124115

125-
command = py_cmd + [f"--index-url={index_url}", args.packages]
126-
if args.disable_cache:
127-
command.append("--no-cache-dir")
128-
run_command(command)
116+
def activate_venv_in_gha(venv_dir: Path):
117+
"""Activates the venv in venv_dir for future GitHub Actions workflow steps.
129118
119+
This is a (useful) hack that modifies the PATH and VIRTUAL_ENV env vars
120+
rather than call the platform-specific 'activate' scripts.
121+
"""
130122

131-
def activate_venv_in_gha(venv_dir: Path):
132123
log("")
133124
log(f"Activating venv for future GitHub Actions workflow steps")
134125
gha_warn_if_not_running_on_ci()
@@ -154,41 +145,70 @@ def activate_venv_in_gha(venv_dir: Path):
154145
gha_set_env({"VIRTUAL_ENV": venv_dir})
155146

156147

157-
def scrape_subdirs() -> dict[str, set[str]] | set[str] | None:
158-
if not requests:
159-
return None
148+
def install_packages_into_venv(
149+
venv_dir: Path,
150+
packages: list[str],
151+
use_uv: bool = False,
152+
index_url: str | None = None,
153+
index_name: str | None = None,
154+
index_subdir: str | None = None,
155+
find_links: str | None = None,
156+
pre: bool = False,
157+
disable_cache: bool = False,
158+
):
159+
"""Installs packages into venv_dir using the provided options.
160+
161+
Args:
162+
venv_dir: The venv to install into
163+
packages: The list of packages to install
164+
use_uv: True to use 'uv', uses 'pip' otherwise
165+
index_url: URL for '--index-url' command argument
166+
index_name: Shorthand for a base index_url (e.g. 'nightly')
167+
index_subdir: Subdirectory for 'index_url' or 'index_name'
168+
find_links: URL for '--find-links' command argument
169+
pre: Allow pre-release packages (pip: --pre, uv: --prerelease=allow)
170+
disable_cache: Disable package cache (pip: --no-cache-dir, uv: --no-cache)
171+
"""
172+
log("")
160173

161-
index_subdirs: dict[str, set[str]] | set[str] = dict()
174+
venv_python_exe = find_venv_python_exe(venv_dir)
175+
assert venv_python_exe is not None, f"No python executable found in {venv_dir}"
176+
pip_install_cmd = (
177+
[str(venv_python_exe), "-m", "pip", "install"]
178+
if not use_uv
179+
else ["uv", "pip", "install", "--python", str(venv_python_exe)]
180+
)
162181

163-
def scrape_subdirs_from_index(index_url: str) -> set[str]:
164-
index_url = index_url.rstrip("/") + "/"
165-
try:
166-
response = requests.get(index_url)
167-
response.raise_for_status()
168-
except Exception as e:
169-
print(
170-
f"[ERROR]: fetching subdirs from index url: {index_url} failed with: {e}"
171-
)
172-
return set()
182+
if index_url and index_name:
183+
raise ValueError("Can't set both index_url and index_name")
173184

174-
# matches the text inside the <a></a> elements to find all gfx targets, then puts returns them in a set
175-
html = response.text
176-
matches = re.findall(GFX_TARGET_REGEX, html)
177-
return set(matches)
185+
if index_name:
186+
# Look up known index name.
187+
index_url = ROCM_INDEX_URLS_MAP[index_name]
178188

179-
# for every index url in the map fetches the subdirs and puts them in a dict with the index_name being the key
180-
for index_name, index_url in INDEX_URLS_MAP.items():
181-
index_subdirs[index_name] = scrape_subdirs_from_index(index_url)
189+
if index_url:
190+
# Join index with subdir.
191+
if index_subdir:
192+
index_url = f"{index_url.rstrip('/')}/{index_subdir.strip('/')}"
182193

183-
# compares the first set of dirs to the rest of them, then, if they are all equal, returns a singular set instead of a dictionary
184-
subdirs_sets = list(index_subdirs.values())
185-
if all(s == subdirs_sets[0] for s in subdirs_sets[1:]):
186-
index_subdirs = subdirs_sets[0]
194+
pip_install_cmd.append(f"--index-url={index_url}")
187195

188-
return index_subdirs
196+
if find_links:
197+
pip_install_cmd.append(f"--find-links={find_links}")
189198

199+
if pre:
200+
pip_install_cmd.append("--prerelease=allow" if use_uv else "--pre")
190201

191-
def log_activate_instructions(venv_dir: Path):
202+
if disable_cache:
203+
pip_install_cmd.append("--no-cache" if use_uv else "--no-cache-dir")
204+
205+
pip_install_cmd.extend(packages)
206+
207+
run_command(pip_install_cmd)
208+
209+
210+
def log_venv_activate_instructions(venv_dir: Path):
211+
"""Logs platform-specific instructions for activating a venv."""
192212
log("")
193213
log(f"Setup complete at '{venv_dir}'! Activate the venv with:")
194214
if is_windows:
@@ -199,29 +219,60 @@ def log_activate_instructions(venv_dir: Path):
199219

200220
def run(args: argparse.Namespace):
201221
venv_dir = args.venv_dir
202-
py_cmd = get_system_py_command(use_uv=args.use_uv)
222+
use_uv = args.use_uv
203223

204224
if args.clean and venv_dir.exists():
205225
log(f"Clearing existing venv_dir '{venv_dir}'")
206226
shutil.rmtree(venv_dir)
207227

208-
create_venv(venv_dir, py_cmd)
209-
210-
# if not using uv, replace the python exe in py_cmd with the venv python
211-
python_exe = find_venv_python(venv_dir)
212-
if not args.use_uv:
213-
py_cmd = [str(python_exe), "-m", "pip", "install"]
214-
upgrade_pip(python_exe)
215-
else:
216-
py_cmd = ["uv", "pip", "install", "--python", str(python_exe)]
228+
create_venv(venv_dir, use_uv)
229+
update_venv(venv_dir, use_uv)
217230

218231
if args.packages:
219-
install_packages(args, py_cmd)
232+
install_packages_into_venv(
233+
venv_dir=venv_dir,
234+
packages=args.packages.split(),
235+
use_uv=use_uv,
236+
index_url=args.index_url,
237+
index_subdir=args.index_subdir,
238+
index_name=args.index_name,
239+
find_links=args.find_links,
240+
pre=args.pre,
241+
disable_cache=args.disable_cache,
242+
)
220243

221244
if args.activate_in_future_github_actions_steps:
222245
activate_venv_in_gha(venv_dir)
223246
else:
224-
log_activate_instructions(venv_dir)
247+
log_venv_activate_instructions(venv_dir)
248+
249+
250+
GFX_TARGET_REGEX = r'(gfx(?:\d{2,3}X|\d{3,4})(?:-[^<"/]*)?)</a>'
251+
252+
253+
def _scrape_rocm_index_subdirs() -> set[str] | None:
254+
"""Scrapes available subdirs from all known indexes, returns union of all."""
255+
try:
256+
import requests
257+
except ImportError:
258+
return
259+
260+
all_subdirs: set[str] = set()
261+
262+
for index_url in ROCM_INDEX_URLS_MAP.values():
263+
index_url = index_url.rstrip("/") + "/"
264+
try:
265+
response = requests.get(index_url)
266+
response.raise_for_status()
267+
except Exception as e:
268+
print(f"[ERROR]: fetching subdirs from {index_url} failed: {e}")
269+
continue
270+
271+
# Extract gfx targets from <a> elements.
272+
matches = re.findall(GFX_TARGET_REGEX, response.text)
273+
all_subdirs.update(matches)
274+
275+
return all_subdirs if all_subdirs else None
225276

226277

227278
def main(argv: list[str]):
@@ -238,10 +289,15 @@ def main(argv: list[str]):
238289
action=argparse.BooleanOptionalAction,
239290
help="If the venv directory already exists, clear it and start fresh",
240291
)
292+
general_options.add_argument(
293+
"--pre",
294+
action=argparse.BooleanOptionalAction,
295+
help="Allow installing pre-release packages",
296+
)
241297
general_options.add_argument(
242298
"--disable-cache",
243299
action=argparse.BooleanOptionalAction,
244-
help="Disables the pip cache through the --no-cache-dir option",
300+
help="Disable the pip/uv package cache",
245301
)
246302
general_options.add_argument(
247303
"--activate-in-future-github-actions-steps",
@@ -259,65 +315,46 @@ def main(argv: list[str]):
259315
# TODO(#1036): Other flags or helper scripts to help map between versions,
260316
# git commits/refs, workflow runs, etc.
261317
# I'd like a shorthand for "install packages from commit abcde"
318+
# Maybe use find_artifacts_for_commit.py
262319
install_options.add_argument(
263320
"--packages",
264321
type=str,
265-
help="List of packages to install, including any extras or explicit versions",
322+
help="Packages to install, including any extras or explicit versions (e.g. 'rocm[libraries,devel]==1.0')",
266323
)
267-
268-
index_group = install_options.add_mutually_exclusive_group()
269324
# TODO(#1036): add "auto" mode here that infers the index from the version?
270-
# TODO(#1036): Default to nightly?
271-
index_group.add_argument(
325+
install_options.add_argument(
326+
"--index-url",
327+
type=str,
328+
help="Package index URL for pip --index-url (complete URL, or base URL with --index-subdir)",
329+
)
330+
install_options.add_argument(
272331
"--index-name",
273332
type=str,
274-
choices=["nightly", "dev"],
275-
help="Shorthand name for an index to use with 'pip install --index-url='",
333+
choices=["stable", "prerelease", "nightly", "dev"],
334+
help="Shorthand for a named index (requires --index-subdir)",
276335
)
277-
index_group.add_argument(
278-
"--index-url",
336+
install_options.add_argument(
337+
"--find-links",
279338
type=str,
280-
help="Full URL for a release index to use with 'pip install --index-url='",
339+
help="Package location URL for pip --find-links (compatible with --index-url)",
281340
)
282341

283-
subdirs: dict[str, set[str]] | set[str] | None = scrape_subdirs()
284-
all_subdir_sets_congruent = isinstance(subdirs, set)
285-
286-
index_subdir_help = "Index subdirectory"
287-
if not all_subdir_sets_congruent and subdirs:
288-
index_subdir_help += ". Available options per index: " + str(subdirs)
289-
elif not subdirs:
290-
index_subdir_help += ", such as 'gfx110X-all'"
291-
else:
292-
index_subdir_help += "."
293-
342+
# Scrape available subdirs for --index-subdir choices.
343+
available_subdirs = _scrape_rocm_index_subdirs()
294344
install_options.add_argument(
295345
"--index-subdir",
296346
"--index-subdirectory",
297347
type=str,
298-
help=index_subdir_help,
299-
choices=subdirs if all_subdir_sets_congruent else None,
348+
help="Index subdirectory, such as 'gfx110X-all'",
349+
choices=available_subdirs,
300350
)
301351

302352
args = p.parse_args(argv)
303353

304-
# Validate arguments.
305354
if args.venv_dir.exists() and not args.venv_dir.is_dir():
306355
p.error(f"venv_dir '{args.venv_dir}' exists and is not a directory")
307-
if args.packages and not (args.index_name or args.index_url):
308-
p.error("If --packages is set, one of --index-name or --index-url must be set")
309-
if args.packages and not args.index_subdir:
310-
if subdirs and not all_subdir_sets_congruent:
311-
if not args.index_name:
312-
p.error(
313-
f"If --packages is set, --index-subdir must be set from the following list: {subdirs}"
314-
)
315-
else:
316-
p.error(
317-
f"If --packages is set, --index-subdir must be set from the following list: {subdirs[args.index_name]}"
318-
)
319-
else:
320-
p.error("If --packages is set, --index-subdir must be set")
356+
if args.index_name and not args.index_subdir:
357+
p.error("--index-subdir must be set when using --index-name")
321358

322359
run(args)
323360

0 commit comments

Comments
 (0)