Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .github/workflows/test_native_windows_packages_install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT

# Installs and verifies the Windows MSI packages produced by an arbitrary build
# run. Standalone and dispatchable (fork-friendly: MSIs are downloaded from the
# public artifacts prefix, no credentials required), and callable from
# ci/release. Mirrors test_native_linux_packages_install.yml.

name: Test Native Windows Packages Install

on:
workflow_dispatch:
inputs:
artifact_run_id:
description: "Run id whose published Windows MSIs to install and verify"
type: string
required: true
artifact_github_repo:
description: "GitHub repository that owns artifact_run_id"
type: string
default: ROCm/TheRock
packages:
description: "Packages to test, comma-separated (e.g. 'runtime,core')."
type: string
default: runtime
rocm_version:
description: "ROCm version stamped on the MSI (e.g. '10.2.0')."
type: string
required: true
release_type:
description: "Release type selecting the source artifacts bucket."
type: choice
options:
- ci
- dev
default: ci
run_uninstall:
description: "Also uninstall and verify a clean teardown."
type: boolean
default: true
repository:
description: "Repository to checkout. Defaults to github.repository."
type: string
default: ""
ref:
description: "Branch, tag, or SHA to checkout. Defaults to the triggering ref."
type: string
default: ""
workflow_call:
inputs:
artifact_run_id:
type: string
required: true
artifact_github_repo:
type: string
default: ROCm/TheRock
packages:
type: string
default: runtime
rocm_version:
type: string
required: true
release_type:
# Matches the MSI build workflow's default so the test reads from the
# same bucket the producer wrote to.
type: string
default: ci
run_uninstall:
type: boolean
default: true
repository:
type: string
default: ""
ref:
type: string
default: ""

permissions:
contents: read

run-name: Test Windows MSI install (${{ inputs.packages }}, ${{ inputs.rocm_version }})

jobs:
test:
name: Test MSI install
runs-on: windows-2022
defaults:
run:
shell: bash
env:
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
ARTIFACT_GITHUB_REPO: ${{ inputs.artifact_github_repo }}
RELEASE_TYPE: ${{ inputs.release_type }}
PACKAGES: ${{ inputs.packages }}
ROCM_VERSION: ${{ inputs.rocm_version }}
# Download target; the install/uninstall tests consume these files via
# MSI_DIR and never touch the network themselves.
MSI_DIR: ${{ github.workspace }}/msi-packages
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
timeout-minutes: 15
with:
persist-credentials: false
repository: ${{ inputs.repository || github.repository }}
ref: ${{ inputs.ref }}

- name: Set up Python
Comment thread
idass1990 marked this conversation as resolved.
Dismissed
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'

- name: Install Python requirements
run: pip install -r requirements.txt

# Download once; the tests below re-use these files (no network in tests).
- name: Download MSI packages
run: |
python build_tools/packaging/windows/download_msi_packages.py \
--artifact-run-id "${ARTIFACT_RUN_ID}" \
--artifact-github-repo "${ARTIFACT_GITHUB_REPO}" \
--release-type "${RELEASE_TYPE}" \
--packages "${PACKAGES}" \
--dest-dir "${MSI_DIR}"

- name: Install and verify MSI packages
run: |
python -m pytest \
build_tools/packaging/windows/native_windows_package_install_test.py -vv -s

- name: Uninstall and verify teardown
if: ${{ inputs.run_uninstall }}
run: |
python -m pytest \
build_tools/packaging/windows/native_windows_package_uninstall_test.py -vv -s
130 changes: 130 additions & 0 deletions build_tools/packaging/windows/download_msi_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT

"""Download a build run's Windows MSI packages into a local directory.

Separates the download (network) step from the install/uninstall tests, which
consume the already-downloaded files via ``--msi-dir``. CI runs this once and
then runs the tests against the output directory; a developer can do the same
and re-run the tests repeatedly without re-downloading.

The MSIs live at the public artifacts prefix ``{run_id}-windows/packages/msi/``
and are named ``<output_stem>.msi`` per the generator's PACKAGES table, so no S3
listing or credentials are needed (works from forks).

Example::

python download_msi_packages.py \\
--artifact-run-id 36648157930 --release-type nightly \\
--packages runtime --dest-dir ./msis
"""

import argparse
import sys
import urllib.error
import urllib.request
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_msi_wxs import PACKAGES, expected_msi_filename # noqa: E402

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from _therock_utils.workflow_outputs import WorkflowOutputRoot # noqa: E402

PLATFORM = "windows"
DEFAULT_ARTIFACT_GITHUB_REPO = "ROCm/TheRock"


def resolve_msi_prefix_url(
artifact_run_id: str, artifact_github_repo: str, release_type: str
) -> str:
"""Return the public HTTPS URL of the run's msi package prefix.

Resolves the artifacts bucket from the run id + release type without any
GitHub API call (lookup_workflow_run=False).
"""
root = WorkflowOutputRoot.from_workflow_run(
run_id=artifact_run_id,
platform=PLATFORM,
github_repository=artifact_github_repo,
release_type=release_type,
lookup_workflow_run=False,
)
return root.native_windows_packages("msi").https_url


def download_msis(prefix_url: str, packages: list[str], dest_dir: Path) -> list[Path]:
"""Download each package's MSI from the public prefix into ``dest_dir``."""
dest_dir.mkdir(parents=True, exist_ok=True)
downloaded: list[Path] = []
for package in packages:
filename = expected_msi_filename(package)
url = f"{prefix_url.rstrip('/')}/{filename}"
dest = dest_dir / filename
print(f"Downloading {url} -> {dest}")
try:
urllib.request.urlretrieve(url, dest)
Comment thread
idass1990 marked this conversation as resolved.
Resolved
except urllib.error.URLError as e:
raise FileNotFoundError(
f"could not download MSI for package {package!r} from {url}: {e}. "
"The build run may not have produced this package, or its MSIs "
"were not uploaded to the public prefix (fork build runs do not "
"upload)."
) from e
downloaded.append(dest)
return downloaded


def _split_packages(value: str) -> list[str]:
return [p.strip() for p in value.split(",") if p.strip()]


def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(
description="Download a build run's Windows MSI packages to a directory."
)
parser.add_argument(
"--artifact-run-id",
required=True,
help="Run id whose published Windows MSIs to download.",
)
parser.add_argument(
"--artifact-github-repo",
default=DEFAULT_ARTIFACT_GITHUB_REPO,
help=f"Repository that owns the run id. Default: {DEFAULT_ARTIFACT_GITHUB_REPO}.",
)
parser.add_argument(
"--release-type",
required=True,
help="Release type selecting the source artifacts bucket (e.g. ci, nightly).",
)
parser.add_argument(
"--packages",
default="runtime",
help="Comma-separated packages to download (e.g. 'runtime,core').",
)
parser.add_argument(
"--dest-dir",
type=Path,
required=True,
help="Directory to download the MSIs into.",
)
args = parser.parse_args(argv)

packages = _split_packages(args.packages)
if not packages:
parser.error("--packages was empty")
unknown = [p for p in packages if p not in PACKAGES]
if unknown:
parser.error(f"unknown package(s): {', '.join(unknown)}")

prefix_url = resolve_msi_prefix_url(
args.artifact_run_id, args.artifact_github_repo, args.release_type
)
paths = download_msis(prefix_url, packages, args.dest_dir)
print(f"Downloaded {len(paths)} MSI(s) to {args.dest_dir}")


if __name__ == "__main__":
main()
46 changes: 43 additions & 3 deletions build_tools/packaging/windows/generate_msi_wxs.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,48 @@ def discovery_registry_key(major: str, minor: str) -> str:
return f"Software\\AMD\\ROCm\\{major}.{minor}"


def parse_major_minor(version: str) -> tuple[str, str]:
"""Return ``(major, minor)`` from a dotted version like ``10.2.0``.

Raises ``ValueError`` on a version without at least major and minor
components. The MSI install path and discovery key are both scoped to
``major.minor``, so consumers (the generator and the install test) share
this parse to stay consistent.
"""
parts = version.split(".")
if len(parts) < 2 or not parts[0] or not parts[1]:
raise ValueError(f"version must be at least major.minor (got {version!r})")
return parts[0], parts[1]


def expected_msi_filename(package: str) -> str:
"""Return the MSI filename the generator writes for ``package``.

The generator names each package's output ``<output_stem>.msi``; the install
test imports this so it looks for the same filename the build produced,
without duplicating the stem-to-filename rule.
"""
try:
return f"{PACKAGES[package].output_stem}.msi"
except KeyError as e:
raise ValueError(
f"unknown package {package!r}; choose from {', '.join(PACKAGES)}"
) from e


def install_subdir_for(package: str, version: str) -> str:
"""Return the versioned install subdirectory name for ``package``.

Expands the package's ``install_subdir`` template (e.g. ``core-{major}.{minor}``)
the same way resolve_install_layout does, so the install test derives the
install location from the same rule the generator uses.
"""
major, minor = parse_major_minor(version)
return PACKAGES[package].install_subdir.format(
version=version, major=major, minor=minor
)


@dataclass
class PackageInputs:
"""Everything gathered from disk before any WiX XML is emitted.
Expand Down Expand Up @@ -722,9 +764,7 @@ def resolve_package_inputs(

def resolve_install_layout(args: argparse.Namespace, version: str) -> InstallLayout:
"""Resolve the install location and versioned subdirectory name."""
parts = version.split(".")
major = parts[0] if len(parts) > 0 else ""
minor = parts[1] if len(parts) > 1 else ""
major, minor = parse_major_minor(version)
package = PACKAGES[args.package]
subdir_name = package.install_subdir.format(
version=version, major=major, minor=minor
Expand Down
Loading
Loading