Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

409 lines
13 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Sets up a Python venv and optionally installs rocm packages into it.
* https://docs.python.org/3/library/venv.html
* https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#create-and-use-virtual-environments
There are a few modes this can be used in:
* Minimally, this is equivalent to `python -m venv .venv`:
```
python setup_venv.py .venv
```
* To install the latest nightly rocm packages into the venv:
```
python setup_venv.py .venv --packages rocm[libraries,devel,device-all] --index-name nightly
```
This is roughly equivalent to:
```
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install rocm[libraries,devel,device-all] --index-url=https://rocm.nightlies.amd.com/whl-multi-arch/
deactivate
```
Some automated workflows using this script to install packages can run shortly
after other workflows uploads those packages. In these cases server-side index
generation may not have completed yet and installs can fail for 1-3 minutes.
Package installs are retried by default to cover that transient window. This
behavior can be adjusted with the `--install-retry-timeout-seconds` and
`--install-retry-wait-between-seconds` arguments.
See https://github.com/ROCm/TheRock/issues/5455 for more details.
"""
import argparse
from pathlib import Path
import platform
import shlex
import shutil
import subprocess
import sys
import time
from github_actions.github_actions_api import *
is_windows = platform.system() == "Windows"
ROCM_INDEX_URLS_MAP = {
"stable": "https://repo.amd.com/rocm/whl-multi-arch/",
"prerelease": "https://rocm.prereleases.amd.com/whl-multi-arch/",
"nightly": "https://rocm.nightlies.amd.com/whl-multi-arch/",
"dev": "https://rocm.devreleases.amd.com/whl-multi-arch/",
}
def log(*args, **kwargs):
print(*args, **kwargs)
sys.stdout.flush()
def run_command(args: list[str | Path], cwd: Path = Path.cwd()):
args = [str(arg) for arg in args]
log(f"++ Exec [{cwd}]$ {shlex.join(args)}")
subprocess.check_call(args, cwd=str(cwd), stdin=subprocess.DEVNULL)
def run_command_with_retries(
args: list[str | Path],
cwd: Path = Path.cwd(),
retry_timeout_seconds: int = 0,
retry_wait_between_seconds: int = 0,
):
deadline = time.monotonic() + retry_timeout_seconds
attempt = 1
while True:
try:
run_command(args, cwd)
return
except subprocess.CalledProcessError as e:
if (
retry_timeout_seconds == 0
or time.monotonic() + retry_wait_between_seconds > deadline
):
log(
"Command failed after "
f"{attempt} attempt(s); no retry time remains"
)
raise
log(
"Command failed "
f"(attempt {attempt}, exit code {e.returncode}); "
f"retrying in {retry_wait_between_seconds}s..."
)
time.sleep(retry_wait_between_seconds)
attempt += 1
def find_venv_python_exe(venv_path: Path) -> Path | None:
"""Finds the python executable under |venv_path|, if one exists."""
paths = [venv_path / "bin" / "python", venv_path / "Scripts" / "python.exe"]
for p in paths:
if p.exists():
return p
return None
def create_venv(venv_dir: Path, use_uv: bool = False):
"""Creates a Python venv at |venv_dir|.
No-op if venv_dir is already an initialized venv (has a python executable).
"""
log(f"Creating venv at '{venv_dir}'")
# Log some other variations of the path too.
try:
venv_dir_relative = venv_dir.relative_to(Path.cwd())
except ValueError:
venv_dir_relative = venv_dir
venv_dir_resolved = venv_dir.resolve()
log(f" Dir relative to CWD: '{venv_dir_relative}'")
log(f" Dir fully resolved : '{venv_dir_resolved}'")
log("")
# Create with 'python -m venv' as needed.
python_exe = find_venv_python_exe(venv_dir_resolved)
if python_exe:
log(f" Found existing python executable at '{python_exe}', skipping creation")
log(" Run again with --clean to clear the existing directory instead")
return
if use_uv:
run_command(["uv", "venv", str(venv_dir_resolved)])
else:
run_command([sys.executable, "-m", "venv", str(venv_dir_resolved)])
def update_venv(venv_dir: Path, use_uv: bool = False):
if use_uv:
# No updates needed.
return
# pip logs warnings about wanting to update, so we'll do that for it.
# Keep setuptools/wheel available so artifact-only installs of ROCm sdists
# can use --no-build-isolation without resolving build dependencies from
# the package index being tested.
log("")
python_exe = find_venv_python_exe(venv_dir)
run_command(
[
str(python_exe),
"-m",
"pip",
"install",
"--upgrade",
"pip",
"setuptools",
"wheel",
]
)
def activate_venv_in_gha(venv_dir: Path):
"""Activates the venv in venv_dir for future GitHub Actions workflow steps.
This is a (useful) hack that modifies the PATH and VIRTUAL_ENV env vars
rather than call the platform-specific 'activate' scripts.
"""
log("")
log(f"Activating venv for future GitHub Actions workflow steps")
gha_warn_if_not_running_on_ci()
# See https://docs.python.org/3/library/venv.html#how-venvs-work.
#
# The usual way to activate a venv is to run the platform-specific command:
# POSIX bash : `source <venv>/bin/activate`
# Windows cmd.exe : `<venv>\Scripts\activate.bat`
# Windows powershell : `<venv>\Scripts\Activate.ps1`
# etc.
#
# What these scripts actually do is a combination of setting environment
# variables, which we can't normally do (persistently) from a Python script.
# However, in the context of a GitHub Actions workflow, we *can* set
# environment variables (and job outputs, and step summaries, etc.) using
# https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions.
if is_windows:
gha_add_to_path(venv_dir / "Scripts")
else:
gha_add_to_path(venv_dir / "bin")
gha_set_env({"VIRTUAL_ENV": venv_dir})
def install_packages_into_venv(
venv_dir: Path,
packages: list[str],
use_uv: bool = False,
index_url: str | None = None,
index_name: str | None = None,
find_links: str | None = None,
pre: bool = False,
disable_cache: bool = False,
install_retry_timeout_seconds: int = 0,
install_retry_wait_between_seconds: int = 0,
):
"""Installs packages into venv_dir using the provided options.
Args:
venv_dir: The venv to install into
packages: The list of packages to install
use_uv: True to use 'uv', uses 'pip' otherwise
index_url: URL for '--index-url' command argument
index_name: Shorthand for an index_url (e.g. 'nightly')
find_links: URL for '--find-links' command argument
pre: Allow pre-release packages (pip: --pre, uv: --prerelease=allow)
disable_cache: Disable package cache (pip: --no-cache-dir, uv: --no-cache)
install_retry_timeout_seconds: Maximum retry window for the install command
install_retry_wait_between_seconds: Delay between package install retries
"""
log("")
venv_python_exe = find_venv_python_exe(venv_dir)
assert venv_python_exe is not None, f"No python executable found in {venv_dir}"
pip_install_cmd = (
[str(venv_python_exe), "-m", "pip", "install"]
if not use_uv
else ["uv", "pip", "install", "--python", str(venv_python_exe)]
)
if index_url and index_name:
raise ValueError("Can't set both index_url and index_name")
if index_name:
# Look up known index name.
index_url = ROCM_INDEX_URLS_MAP[index_name]
if index_url == "":
pip_install_cmd.append("--no-index")
pip_install_cmd.append("--no-build-isolation")
elif index_url:
pip_install_cmd.append(f"--index-url={index_url}")
if find_links:
pip_install_cmd.append(f"--find-links={find_links}")
if pre:
pip_install_cmd.append("--prerelease=allow" if use_uv else "--pre")
if disable_cache:
pip_install_cmd.append("--no-cache" if use_uv else "--no-cache-dir")
pip_install_cmd.extend(packages)
run_command_with_retries(
pip_install_cmd,
retry_timeout_seconds=install_retry_timeout_seconds,
retry_wait_between_seconds=install_retry_wait_between_seconds,
)
def log_venv_activate_instructions(venv_dir: Path):
"""Logs platform-specific instructions for activating a venv."""
log("")
log(f"Setup complete at '{venv_dir}'! Activate the venv with:")
if is_windows:
log(f" {venv_dir}\\Scripts\\activate.bat")
else:
log(f" source {venv_dir}/bin/activate")
def run(args: argparse.Namespace):
venv_dir = args.venv_dir
use_uv = args.use_uv
if args.clean and venv_dir.exists():
log(f"Clearing existing venv_dir '{venv_dir}'")
shutil.rmtree(venv_dir)
create_venv(venv_dir, use_uv)
update_venv(venv_dir, use_uv)
if args.packages:
install_packages_into_venv(
venv_dir=venv_dir,
packages=args.packages.split(),
use_uv=use_uv,
index_url=args.index_url,
index_name=args.index_name,
find_links=args.find_links,
pre=args.pre,
disable_cache=args.disable_cache,
install_retry_timeout_seconds=args.install_retry_timeout_seconds,
install_retry_wait_between_seconds=args.install_retry_wait_between_seconds,
)
if args.activate_in_future_github_actions_steps:
activate_venv_in_gha(venv_dir)
else:
log_venv_activate_instructions(venv_dir)
def main(argv: list[str]):
p = argparse.ArgumentParser("setup_venv.py")
p.add_argument(
"venv_dir",
type=Path,
help="Directory in which to create the venv, such as '.venv'",
)
general_options = p.add_argument_group("General options")
general_options.add_argument(
"--clean",
action=argparse.BooleanOptionalAction,
help="If the venv directory already exists, clear it and start fresh",
)
general_options.add_argument(
"--pre",
action=argparse.BooleanOptionalAction,
help="Allow installing pre-release packages",
)
general_options.add_argument(
"--disable-cache",
action=argparse.BooleanOptionalAction,
help="Disable the pip/uv package cache",
)
general_options.add_argument(
"--activate-in-future-github-actions-steps",
action=argparse.BooleanOptionalAction,
help="Attempts to activate the venv persistently when running in a GitHub Action. This is less reliable than running the official activate command",
)
general_options.add_argument(
"--use-uv",
action=argparse.BooleanOptionalAction,
help="Uses uv instead of pip/venv, see more at: https://docs.astral.sh/uv/",
)
general_options.add_argument(
"--install-retry-timeout-seconds",
type=int,
default=180,
help=(
"Maximum wall-clock time to keep retrying failed package install "
"commands in seconds. This does not timeout an in-progress pip "
"command. (default: 180; 0 disables retries)"
),
)
general_options.add_argument(
"--install-retry-wait-between-seconds",
type=int,
default=15,
help="Seconds to wait between failed package install attempts (default: 15)",
)
install_options = p.add_argument_group("Install options")
# TODO(#1036): Other flags or helper scripts to help map between versions,
# git commits/refs, workflow runs, etc.
# I'd like a shorthand for "install packages from commit abcde"
# Maybe use find_artifacts_for_commit.py
install_options.add_argument(
"--packages",
type=str,
help="Packages to install, including any extras or explicit versions (e.g. 'rocm[libraries,devel]==1.0')",
)
# TODO(#1036): add "auto" mode here that infers the index from the version?
install_options.add_argument(
"--index-url",
type=str,
help="Package index URL for pip --index-url",
)
install_options.add_argument(
"--index-name",
type=str,
choices=["stable", "prerelease", "nightly", "dev"],
help="Shorthand for a named index",
)
install_options.add_argument(
"--find-links",
type=str,
help="Package location URL for pip --find-links (compatible with --index-url)",
)
args = p.parse_args(argv)
if args.venv_dir.exists() and not args.venv_dir.is_dir():
p.error(f"venv_dir '{args.venv_dir}' exists and is not a directory")
if args.install_retry_timeout_seconds < 0:
p.error("--install-retry-timeout-seconds must be non-negative")
if (
args.install_retry_timeout_seconds > 0
and args.install_retry_wait_between_seconds <= 0
):
p.error(
"--install-retry-wait-between-seconds must be positive when retries are enabled"
)
run(args)
if __name__ == "__main__":
main(sys.argv[1:])