Files
TheRock/build_tools/fetch_artifacts.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

417 lines
14 KiB
Python

#!/usr/bin/env python
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Fetches artifacts from S3.
The install_rocm_from_artifacts.py script builds on top of this script to both
download artifacts then unpack them into a usable install directory.
Example usage (using https://github.com/ROCm/TheRock/actions/runs/15685736080):
pip install boto3
python build_tools/fetch_artifacts.py \
--run-id 15685736080 --artifact-group gfx110X-all --output-dir ~/.therock/artifacts_15685736080
Download all artifacts for all GPU architectures:
python build_tools/fetch_artifacts.py \
--run-id 15685736080 --output-dir ~/.therock/artifacts_15685736080
Include/exclude regular expressions can be given to control what is downloaded:
python build_tools/fetch_artifacts.py \
--run-id 15685736080 --artifact-group gfx110X-all --output-dir ~/.therock/artifacts_15685736080 \
amd-llvm base 'core-(hip|runtime)' sysdeps \
--exclude _dbg_
This will process artifacts that match any of the include patterns and do not
match any of the exclude patterns.
Note this module will respect:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
if and only if all are specified in the environment to connect with S3.
If unspecified, we will create an anonymous boto file that can only acccess public artifacts.
"""
import argparse
import concurrent.futures
from pathlib import Path
import platform
import re
import shutil
import sys
from _therock_utils.archive_util import open_archive_for_read
from _therock_utils.artifact_backend import ArtifactBackend, S3Backend
from _therock_utils.os_util import rmtree_with_retry
from _therock_utils.artifacts import (
ArtifactName,
ArtifactPopulator,
)
from _therock_utils.workflow_outputs import WorkflowOutputRoot
from artifact_manager import DownloadRequest, download_artifact
# TODO(geomin12): switch out logging library
def log(*args, **kwargs):
print(*args, **kwargs)
sys.stdout.flush()
def _get_base_arch(target: str) -> str:
"""Strip xnack/other suffixes: 'gfx942:xnack+' -> 'gfx942'."""
if not target:
return ""
base = target.split(":")[0]
return base if base else target
def _matches_target(artifact_target: str, requested_targets: set[str]) -> bool:
"""Match if the artifact's base arch equals any requested target's base arch."""
if not artifact_target:
return False
requested_bases = {_get_base_arch(t) for t in requested_targets}
return _get_base_arch(artifact_target) in requested_bases
def list_artifacts_for_group(
backend: ArtifactBackend,
artifact_group: str | None,
amdgpu_targets: list[str] | None = None,
) -> set[str]:
"""Lists artifacts from backend, filtered by artifact_group and/or individual targets.
Inclusive matching: accepts both family-named archives (mono-arch pipeline)
and individual-target archives (split/kpack pipeline). Whichever naming
convention is present in the bucket will be matched.
Base architecture matching: requesting a base arch (e.g., "gfx942") will
also match variants with suffixes (e.g., "gfx942:xnack+", "gfx942:xnack-").
Args:
backend: ArtifactBackend instance configured for the target run
artifact_group: GPU family to filter by (e.g., "gfx94X-dcgpu"). If None
and amdgpu_targets is also empty, downloads all artifacts.
amdgpu_targets: Individual GPU targets to also match (e.g., ["gfx942"]).
Returns:
Set of artifact filenames matching any of the target families or "generic".
If both artifact_group and amdgpu_targets are None/empty, returns all artifacts.
"""
log(f"Retrieving artifacts from '{backend.base_uri}'")
# Get all artifacts from backend
all_artifacts = backend.list_artifacts()
# If no filtering criteria specified, return all artifacts
if not artifact_group and not amdgpu_targets:
log("No artifact group or targets specified - downloading all artifacts")
return all_artifacts
# Build inclusive set of target families to match
targets_to_match: set[str] = {"generic"}
if artifact_group:
targets_to_match.add(artifact_group)
if amdgpu_targets:
targets_to_match.update(amdgpu_targets)
log(f"Matching artifact target families: {sorted(targets_to_match)}")
# Use structured ArtifactName parsing for reliable matching
data = set()
for filename in all_artifacts:
an = ArtifactName.from_filename(filename)
if an and _matches_target(an.target_family, targets_to_match):
data.add(filename)
if not data:
log(
f"Found no artifacts matching {sorted(targets_to_match)} "
f"at '{backend.base_uri}'"
)
return data
def filter_artifacts(
artifacts: set[str], includes: list[str], excludes: list[str]
) -> set[str]:
"""Filters artifacts based on include and exclude regex lists"""
def _should_include(artifact_name: str) -> bool:
if not includes and not excludes:
return True
# If includes, then one include must match.
if includes:
for include in includes:
pattern = re.compile(include)
if pattern.search(artifact_name):
break
else:
return False
# If excludes, then no excludes must match.
if excludes:
for exclude in excludes:
pattern = re.compile(exclude)
if pattern.search(artifact_name):
return False
# Included and not excluded.
return True
return {a for a in artifacts if _should_include(a)}
def get_postprocess_mode(args) -> str | None:
"""Returns 'extract', 'flatten' or None (default is 'extract')."""
if args.flatten:
return "flatten"
if args.no_extract:
return None
return "extract"
def extract_artifact(
archive_path: Path, *, delete_archive: bool, postprocess_mode: str
):
"""Extracts and postprocesses an artifact from an archive file in-place.
Args:
archive_path: Path to the archive (e.g. `amd-llvm_lib_generic.tar.xz`)
delete_archive: True to delete the archive after extraction
postprocess_mode: Either 'flatten' or 'extract'
* 'flatten' merges artifacts into a single "dist/" directory
* 'extract' puts each artifact in a dir (e.g. `amd-llvm_lib_generic/`)
"""
# Get (for example) 'amd-llvm_lib_generic' from '/path/to/amd-llvm_lib_generic.tar.xz'
# We can't just use .stem since that only removes the last extension.
# 1. .name gets us 'amd-llvm_lib_generic.tar.xz'
# 2. .partition('.') gets (before, sep, after), discard all but 'before'
archive_file = archive_path
artifact_name, *_ = archive_file.name.partition(".")
if postprocess_mode == "extract":
output_dir = archive_file.parent / artifact_name
if output_dir.exists():
rmtree_with_retry(output_dir)
with open_archive_for_read(archive_file) as tf:
log(f"++ Extracting '{archive_file.name}' to '{artifact_name}'")
tf.extractall(archive_file.parent / artifact_name, filter="tar")
elif postprocess_mode == "flatten":
output_dir = archive_file.parent
log(f"++ Flattening '{archive_file.name}' to '{artifact_name}'")
flattener = ArtifactPopulator(
output_path=output_dir, verbose=True, flatten=True
)
flattener(archive_file)
else:
raise AssertionError(f"Unhandled postprocess_mode = {postprocess_mode}")
if delete_archive:
archive_file.unlink()
def run(args):
run_github_repo = args.run_github_repo
run_id = args.run_id
artifact_group = args.artifact_group
output_dir = args.output_dir
output_root = WorkflowOutputRoot.from_workflow_run(
run_id=run_id,
platform=args.platform,
github_repository=run_github_repo,
lookup_workflow_run=True,
)
backend = S3Backend(output_root=output_root)
# Parse individual GPU targets (comma-separated string to list).
amdgpu_targets = (
[t.strip() for t in args.amdgpu_targets.split(",") if t.strip()]
if args.amdgpu_targets
else []
)
# Lookup which artifacts exist in the bucket.
# Note: this currently does not check that all requested artifacts
# (via include patterns) do exist, so this may silently fail to fetch
# expected files.
available_artifacts = list_artifacts_for_group(
backend=backend,
artifact_group=artifact_group,
amdgpu_targets=amdgpu_targets,
)
if not available_artifacts:
log(f"No matching artifacts for {run_id} exist. Exiting...")
sys.exit(1)
# Include/exclude filtering.
filtered_artifacts = filter_artifacts(
available_artifacts, args.include, args.exclude
)
if not filtered_artifacts:
log(f"Filtering artifacts for {run_id} resulted in an empty set. Exiting...")
sys.exit(1)
download_requests = [
DownloadRequest(
artifact_key=artifact,
dest_path=output_dir / artifact,
backend=backend,
)
for artifact in sorted(filtered_artifacts)
]
download_summary = "\n ".join(
[f"{req.backend.base_uri}/{req.artifact_key}" for req in download_requests]
)
log(f"\nFiltered artifacts to download:\n {download_summary}\n")
if args.dry_run:
log("Skipping downloads since --dry-run was set")
return
output_dir.mkdir(parents=True, exist_ok=True)
# Download and extract in parallel.
with concurrent.futures.ThreadPoolExecutor(
max_workers=args.download_concurrency
) as download_executor:
download_futures = [
download_executor.submit(download_artifact, req)
for req in download_requests
]
postprocess_mode = get_postprocess_mode(args)
if not postprocess_mode:
# No postprocessing to do, wait on downloads then return.
[f.result() for f in download_futures]
return
with concurrent.futures.ThreadPoolExecutor(
max_workers=args.extract_concurrency
) as extract_executor:
extract_futures: list[concurrent.futures.Future] = []
for download_future in concurrent.futures.as_completed(download_futures):
# download_artifact returns Optional[Path] - None on failure
download_result = download_future.result(timeout=60)
if download_result is None:
continue
extract_futures.append(
extract_executor.submit(
extract_artifact,
download_result,
delete_archive=args.delete_after_extract,
postprocess_mode=postprocess_mode,
)
)
[f.result() for f in extract_futures]
def main(argv):
parser = argparse.ArgumentParser(prog="fetch_artifacts")
filter_group = parser.add_argument_group("Artifact filtering")
filter_group.add_argument(
"include",
nargs="*",
help="Regular expression patterns of artifacts to include: "
"if supplied one pattern must match for an artifact to be included",
)
filter_group.add_argument(
"--exclude",
nargs="*",
help="Regular expression patterns of artifacts to exclude",
)
filter_group.add_argument(
"--platform",
type=str,
choices=["linux", "windows"],
default=platform.system().lower(),
help="Platform to download artifacts for (matches artifact folder name suffixes in S3)",
)
filter_group.add_argument(
"--artifact-group",
type=str,
help="Artifact group to fetch (e.g., 'gfx110X-all'). If omitted along with --amdgpu-targets, downloads all artifacts.",
)
filter_group.add_argument(
"--amdgpu-targets",
type=str,
default="",
help="Comma-separated individual GPU targets for fetching split artifacts (e.g. 'gfx942')",
)
parser.add_argument(
"--download-concurrency",
type=int,
default=10,
help="Number of concurrent download jobs to execute at once",
)
parser.add_argument(
"--extract-concurrency",
type=int,
help="Number of extract jobs to execute at once (defaults to python VM defaults for CPU tasks)",
)
parser.add_argument(
"--run-github-repo",
type=str,
help="GitHub repository for --run-id in 'owner/repo' format (e.g. 'ROCm/TheRock'). Defaults to GITHUB_REPOSITORY env var or 'ROCm/TheRock'",
)
parser.add_argument(
"--run-id",
type=str,
required=True,
help="GitHub run ID to retrieve artifacts from",
)
parser.add_argument(
"--output-dir",
type=Path,
default="build/artifacts",
help="Output path for fetched artifacts, defaults to `build/artifacts/` as in source builds",
)
parser.add_argument(
"--dry-run",
default=False,
help="If set, will only log which artifacts would be fetched without downloading or extracting",
action=argparse.BooleanOptionalAction,
)
postprocess_group = parser.add_argument_group("Postprocessing")
postprocess_p = postprocess_group.add_mutually_exclusive_group()
postprocess_p.add_argument(
"--no-extract",
default=False,
action="store_true",
help="Do no extraction or flattening",
)
postprocess_p.add_argument(
"--extract",
default=False,
action="store_true",
help="Extract files after fetching them",
)
postprocess_p.add_argument(
"--flatten",
default=False,
action="store_true",
help="Flattens artifacts after fetching them",
)
postprocess_group.add_argument(
"--delete-after-extract",
default=True,
action=argparse.BooleanOptionalAction,
help="Delete archive files after extraction",
)
args = parser.parse_args(argv)
run(args)
if __name__ == "__main__":
main(sys.argv[1:])