Files
TheRock/build_tools/github_actions/upload_python_packages.py
T
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

369 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""
This script uploads built Python packages (wheels, sdists) along with an index
page to S3 or a local directory for testing. Once packages are uploaded, they
can be downloaded directly or via `pip install --find-links {url}` by
developers, users, and test workflows.
Usage:
upload_python_packages.py
--input-packages-dir PACKAGES_DIR
--run-id RUN_ID
[--artifact-group ARTIFACT_GROUP] # Required for single-arch; omit with --multiarch
[--multiarch] # Multi-arch mode: omits artifact_group from upload path
[--output-dir OUTPUT_DIR] # Local output instead of S3
[--bucket BUCKET] # Override bucket selection (defaults to auto-select)
[--dry-run] # Print what would happen without taking action
Modes:
1. S3 upload (default): Uploads to an AWS S3 bucket
2. Local output: With --output-dir, copies files to local directory
3. Dry run: With --dry-run, prints plan without uploading or copying
Output Layout:
{bucket}/{external_repo}{run_id}-{platform}/python/{artifact_group}/
*.whl, *.tar.gz # Wheel and sdist files
index.html # File listing for pip --find-links
For multi-arch builds (--multiarch), artifact_group is omitted:
{bucket}/{external_repo}{run_id}-{platform}/python/
{family}/ # Per-family subdirectory (gfx94X-dcgpu, etc.)
index.html
*.whl, *.tar.gz
Installation:
pip install rocm[libraries,devel] --pre \\
--find-links=https://{bucket}.s3.amazonaws.com/{path}/index.html
"""
import argparse
from pathlib import Path
import platform
import shlex
import subprocess
import sys
_BUILD_TOOLS_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_BUILD_TOOLS_DIR))
sys.path.insert(0, str(_BUILD_TOOLS_DIR / "packaging" / "python"))
from _therock_utils.workflow_outputs import WorkflowOutputRoot
from _therock_utils.storage_location import StorageLocation
from _therock_utils.storage_backend import StorageBackend, create_storage_backend
from generate_local_index import generate_multiarch_indexes
from github_actions_api import (
gha_append_step_summary,
gha_set_output,
)
THEROCK_DIR = Path(__file__).resolve().parent.parent.parent
PLATFORM = platform.system().lower()
LINE_CONTINUATION_CHAR = "^" if PLATFORM == "windows" else "\\"
def log(*args):
print(*args)
sys.stdout.flush()
def run_command(cmd: list[str], cwd: Path = Path.cwd()):
log(f"++ Exec [{cwd}]$ {shlex.join(cmd)}")
subprocess.run(cmd, check=True)
def _make_output_root(
run_id: str, bucket_override: str | None = None
) -> WorkflowOutputRoot:
if bucket_override:
return WorkflowOutputRoot(
bucket=bucket_override, external_repo="", run_id=run_id, platform=PLATFORM
)
return WorkflowOutputRoot.from_workflow_run(run_id=run_id, platform=PLATFORM)
def detect_family_subdirs(dist_dir: Path) -> list[str]:
"""Return GPU-family subdirectory names in dist_dir.
Ignores dot-directories (e.g. ".kpack_staging") which are build staging
artifacts, not GPU families. Their presence must not flip a multi-arch
upload into the legacy per-family layout/summary.
"""
return sorted(
d.name for d in dist_dir.iterdir() if d.is_dir() and not d.name.startswith(".")
)
def generate_index(dist_dir: Path, multiarch: bool = False, dry_run: bool = False):
"""Generates an index.html file listing packages for pip --find-links.
Args:
dist_dir: Directory containing packages to index
multiarch: If True, generate per-family indexes for multi-arch builds
dry_run: If True, print command without executing
"""
if dry_run:
log(f"[DRY RUN] Would generate index in {dist_dir} (multiarch={multiarch})")
return
if multiarch:
has_subdirs = bool(detect_family_subdirs(dist_dir))
if has_subdirs:
log("[INFO] Multi-arch legacy mode: generating per-family indexes")
generate_multiarch_indexes(dist_dir)
else:
# kpack-split flat mode: multiple jobs may append to the same
# prefix (ROCm wheels + pytorch wheels). therock-ci-artifacts
# generates index.html server-side, so a client-side flat index
# would just race with (and briefly shadow) the server view.
log(
"[INFO] kpack-split flat mode: skipping client-side index "
"generation (server-side indexing handles it)"
)
else:
# Single-arch mode: use existing indexer.py for top-level
log("[INFO] Single-arch mode: using indexer.py")
indexer_script = THEROCK_DIR / "third-party" / "indexer" / "indexer.py"
if not indexer_script.is_file():
raise FileNotFoundError(f"Indexer script not found: {indexer_script}")
cmd = [
sys.executable,
str(indexer_script),
str(dist_dir),
"--filter",
"*.whl",
"*.tar.gz",
]
run_command(cmd)
def find_package_files(dist_dir: Path) -> list[Path]:
"""Finds all wheel, sdist, and index files in the dist directory."""
files = []
for pattern in ["*.whl", "*.tar.gz", "index.html"]:
files.extend(dist_dir.glob(pattern))
return sorted(files)
def upload_packages(
dist_dir: Path, packages_loc: StorageLocation, backend: StorageBackend
):
"""Upload package files using the provided backend."""
package_files = find_package_files(dist_dir)
if not package_files:
raise FileNotFoundError(f"No package files found in {dist_dir}")
log(f"[INFO] Found {len(package_files)} top-level package files in {dist_dir}:")
for f in package_files:
log(f" - {f.relative_to(dist_dir)}")
# Log all files that will actually be uploaded (including subdirectories).
all_files = sorted(
f for f in dist_dir.rglob("*") if f.is_file() and not f.is_symlink()
)
log(f"[INFO] Uploading {len(all_files)} total files to {packages_loc.s3_uri}:")
for f in all_files:
log(f" {f.relative_to(dist_dir).as_posix()}")
count = backend.upload_directory(dist_dir, packages_loc)
log(f"[INFO] Uploaded {count} files")
def write_gha_upload_summary(
packages_loc: StorageLocation, families: list[str] | None = None
):
"""Write GitHub Actions summary with pip install instructions.
Args:
packages_loc: Storage location for packages
families: Controls the summary format:
- None: single-arch mode — single index URL
- [] (empty list): kpack-split flat mode — single index URL covering
all per-target wheels
- [...] non-empty: legacy per-family mode — per-family install links
"""
if families:
# Legacy per-family multi-arch
base_url = packages_loc.https_url
family_links = "\n".join(
f"- [{family}]({base_url}/{family}/index.html)" for family in families
)
family_installs = "\n\n".join(
f"```bash\npip install rocm[libraries,devel] --pre {LINE_CONTINUATION_CHAR}\n"
f" --find-links={base_url}/{family}/index.html\n```"
for family in families
)
install_instructions_markdown = f"""ROCm Python packages (multi-arch build)
Per-family indexes:
{family_links}
{family_installs}
"""
elif families is not None:
# kpack-split flat build — single index covers all per-target wheels
index_url = f"{packages_loc.https_url}/index.html"
install_instructions_markdown = f"""[ROCm Python packages (kpack-split)]({index_url})
Replace `<YOUR_TARGET>` with your GPU target (e.g. `gfx942`, `gfx1201`):
```bash
pip install rocm[libraries,devel,device-<YOUR_TARGET>] --pre {LINE_CONTINUATION_CHAR}
--find-links={index_url}
```
"""
else:
# Single-arch: traditional index URL
index_url = f"{packages_loc.https_url}/index.html"
install_instructions_markdown = f"""[ROCm Python packages]({index_url})
```bash
pip install rocm[libraries,devel] --pre {LINE_CONTINUATION_CHAR}
--find-links={index_url}
```
"""
gha_append_step_summary(install_instructions_markdown)
def run(args: argparse.Namespace):
packages_dir = args.input_packages_dir.resolve()
if not packages_dir.is_dir():
raise FileNotFoundError(f"Packages root directory not found: {packages_dir}")
# Convention: build_python_packages.py writes into <packages_dir>/dist/,
# but build_prod_wheels.py (pytorch) writes directly into its output dir.
# Use the dist/ subdirectory when it exists, otherwise use the input dir.
dist_subdir = packages_dir / "dist"
dist_dir = dist_subdir if dist_subdir.is_dir() else packages_dir
log(f"[INFO] Packages directory : {packages_dir}")
log(f"[INFO] Dist [sub]directory : {dist_dir}")
log(f"[INFO] Artifact group : {args.artifact_group}")
log(f"[INFO] Run ID : {args.run_id}")
log(f"[INFO] Platform : {PLATFORM}")
if args.dry_run:
log(f"[INFO] Mode : DRY RUN")
elif args.output_dir:
log(f"[INFO] Mode : Local output to {args.output_dir}")
else:
log(f"[INFO] Mode : S3 upload")
log("")
log("Generating index.html")
log("---------------------")
log(f"[INFO] Multi-arch indexing: {args.multiarch}")
generate_index(dist_dir, multiarch=args.multiarch, dry_run=args.dry_run)
output_root = _make_output_root(args.run_id, bucket_override=args.bucket)
# Multi-arch builds don't need an artifact_group subdirectory: the run_id
# already uniquely identifies the build, and there is only one Python build
# job per multi-arch run.
packages_loc = output_root.python_packages(
"" if args.multiarch else args.artifact_group
)
backend = create_storage_backend(staging_dir=args.output_dir, dry_run=args.dry_run)
log("")
log("Uploading packages")
log("------------------")
upload_packages(dist_dir=dist_dir, packages_loc=packages_loc, backend=backend)
if not args.output_dir:
if args.multiarch:
# Detect flat (kpack-split) vs legacy per-family layout from dist/.
# Dot-directories (e.g. .kpack_staging) are staging artifacts, not
# GPU families, and must not trigger the per-family layout.
family_subdirs = detect_family_subdirs(dist_dir)
if family_subdirs:
# Legacy per-family: return base URL; downstream appends /{family}/index.html
index_url = packages_loc.https_url
kpack_split = "false"
log(
f"[INFO] Multi-arch legacy URL (tests append /{{family}}/index.html): {index_url}"
)
else:
# kpack-split flat: all wheels at top level; return full /index.html URL
index_url = f"{packages_loc.https_url}/index.html"
kpack_split = "true"
log(f"[INFO] kpack-split flat URL: {index_url}")
else:
family_subdirs = None
index_url = f"{packages_loc.https_url}/index.html"
kpack_split = "false"
log(f"[INFO] Single-arch index URL: {index_url}")
log("Set github actions output")
log("-------------------------")
gha_set_output(
{"package_find_links_url": index_url, "kpack_split": kpack_split}
)
log("Write github actions build summary")
log("----------------------------------")
# families: None=single-arch, []=kpack-split flat, [...]=legacy per-family
families = family_subdirs if args.multiarch else None
write_gha_upload_summary(packages_loc, families=families)
log("")
log("[INFO] Done!")
def main():
parser = argparse.ArgumentParser(
description="Upload Python packages to S3 or a local directory"
)
parser.add_argument(
"--input-packages-dir",
type=Path,
required=True,
help="Directory containing built packages (with dist/ subdirectory)",
)
parser.add_argument(
"--artifact-group",
type=str,
default="",
help="Artifact group (e.g., gfx94X-dcgpu). Omit for multi-arch builds.",
)
parser.add_argument(
"--run-id",
type=str,
required=True,
help="Workflow run ID (e.g. 21440027240)",
)
parser.add_argument(
"--output-dir",
type=Path,
default=None,
help="Output to local directory instead of S3",
)
parser.add_argument(
"--bucket",
type=str,
default=None,
help="Override S3 bucket (default: auto-select from workflow run)",
)
parser.add_argument(
"--multiarch",
action="store_true",
help="Multi-arch mode: generate per-family indexes (required for multi-arch builds with family-specific wheels)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would happen without uploading or copying",
)
args = parser.parse_args()
if args.output_dir and args.bucket:
parser.error("--output-dir and --bucket are mutually exclusive")
if not args.multiarch and not args.artifact_group:
parser.error("--artifact-group is required for single-arch builds")
run(args)
if __name__ == "__main__":
main()