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

1327 lines
52 KiB
Python

# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""
Build topology parsing and manipulation for TheRock CI/CD pipeline.
This module provides classes and utilities for parsing BUILD_TOPOLOGY.toml
and computing artifact dependencies for sharded build pipelines.
"""
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set
def get_topology(topology_path: Optional[Path] = None) -> "BuildTopology":
"""Load the BUILD_TOPOLOGY.toml from the default or specified location."""
if topology_path is None:
# Default: BUILD_TOPOLOGY.toml in repository root
# This module is at build_tools/_therock_utils/build_topology.py
script_dir = Path(__file__).parent
repo_root = script_dir.parent.parent
topology_path = repo_root / "BUILD_TOPOLOGY.toml"
if not topology_path.exists():
raise FileNotFoundError(f"BUILD_TOPOLOGY.toml not found at {topology_path}")
return BuildTopology(str(topology_path))
@dataclass
class Submodule:
"""Represents a git submodule with checkout configuration.
This class is designed to be extended with additional fields:
- sparse_checkout: List[str] - paths to include in sparse checkout
- recursive: bool - whether to recursively init submodules
- depth: int - shallow clone depth
"""
name: str
# Future fields for sparse checkout, recursive settings, etc.
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
if isinstance(other, Submodule):
return self.name == other.name
return False
@dataclass
class ExternalGitSource:
"""Represents an externally managed git checkout.
External git sources are not git submodules. They are fetched into ignored
locations under optional-sources/ and pinned by commit hash.
"""
name: str
origin: str
commit: str
path: str
def __hash__(self):
return hash((self.name, self.path))
@dataclass
class SourceSet:
"""Represents a grouping of submodules for partial checkouts."""
name: str
description: str
submodules: List[Submodule] = field(default_factory=list)
external_git_sources: List[ExternalGitSource] = field(default_factory=list)
disable_platforms: List[str] = field(default_factory=list)
path_prefixes: List[str] = field(default_factory=list)
@dataclass
class BuildStage:
"""Represents a build stage (CI/CD pipeline job)."""
name: str
description: str
artifact_groups: List[str]
type: str = "generic" # "generic" or "per-arch"
@dataclass
class ArtifactGroup:
"""Represents a logical grouping of related artifacts."""
name: str
description: str
type: str # "generic" or "per-arch"
artifact_group_deps: List[str] = field(default_factory=list)
source_sets: List[str] = field(default_factory=list)
@dataclass
class Artifact:
"""Represents an individual build output."""
name: str
artifact_group: str
type: str # "target-neutral" or "target-specific"
artifact_deps: List[str] = field(default_factory=list)
platform: Optional[str] = None # e.g., "windows"
feature_name: Optional[str] = None # Override default feature name
feature_group: Optional[str] = None # Override default feature group
disable_platforms: List[str] = field(
default_factory=list
) # Platforms where disabled
disable_platforms_if_flags_not_set: Dict[str, str] = field(
default_factory=dict
) # Platforms disabled unless the named build flag is set
python_requires: List[str] = field(
default_factory=list
) # pip install args (e.g., ["-r path/to/req.txt"])
split_databases: List[str] = field(
default_factory=list
) # Database handlers to use when splitting artifacts (e.g., ["rocblas", "hipblaslt"])
test_artifacts: List[str] = field(
default_factory=list
) # Artifacts needed for testing this artifact (e.g., ["core-hiptests"])
class BuildTopology:
"""
Parses and provides operations on BUILD_TOPOLOGY.toml.
This is the main interface for CI/CD pipelines to understand
build dependencies and artifact relationships.
"""
def __init__(self, toml_path: str):
"""
Load and parse BUILD_TOPOLOGY.toml.
Args:
toml_path: Path to BUILD_TOPOLOGY.toml file
"""
self.toml_path = Path(toml_path)
self.source_sets: Dict[str, SourceSet] = {}
self.build_stages: Dict[str, BuildStage] = {}
self.artifact_groups: Dict[str, ArtifactGroup] = {}
self.artifacts: Dict[str, Artifact] = {}
self._load_topology()
def _load_topology(self):
"""Load and parse the TOML file."""
# Python version compatibility for TOML parsing
try:
import tomllib
except ModuleNotFoundError:
# Python <= 3.10 compatibility (requires install of 'tomli' package)
import tomli as tomllib
with open(self.toml_path, "rb") as f:
data = tomllib.load(f)
# Parse source sets
for set_name, set_data in data.get("source_sets", {}).items():
# Convert submodule names to Submodule objects
submodule_names = set_data.get("submodules", [])
submodules = [Submodule(name=name) for name in submodule_names]
external_git_sources = [
ExternalGitSource(
name=source_data.get("name", ""),
origin=source_data.get("origin", ""),
commit=source_data.get("commit", ""),
path=source_data.get("path", ""),
)
for source_data in set_data.get("external_git_sources", [])
]
self.source_sets[set_name] = SourceSet(
name=set_name,
description=set_data.get("description", ""),
submodules=submodules,
external_git_sources=external_git_sources,
disable_platforms=set_data.get("disable_platforms", []),
path_prefixes=set_data.get("path_prefixes", []),
)
# Parse build stages
for stage_name, stage_data in data.get("build_stages", {}).items():
self.build_stages[stage_name] = BuildStage(
name=stage_name,
description=stage_data.get("description", ""),
artifact_groups=stage_data.get("artifact_groups", []),
type=stage_data.get("type", "generic"),
)
# Parse artifact groups
for group_name, group_data in data.get("artifact_groups", {}).items():
self.artifact_groups[group_name] = ArtifactGroup(
name=group_name,
description=group_data.get("description", ""),
type=group_data.get("type", "generic"),
artifact_group_deps=group_data.get("artifact_group_deps", []),
source_sets=group_data.get("source_sets", []),
)
# Parse artifacts
for artifact_name, artifact_data in data.get("artifacts", {}).items():
python_requires = artifact_data.get("python_requires", [])
if python_requires and not isinstance(python_requires, list):
raise ValueError(
f"Artifact '{artifact_name}' python_requires must be a list, "
f"got {type(python_requires).__name__}"
)
disable_platforms_if_flags_not_set = artifact_data.get(
"disable_platforms_if_flags_not_set", {}
)
if disable_platforms_if_flags_not_set and not isinstance(
disable_platforms_if_flags_not_set, dict
):
raise ValueError(
f"Artifact '{artifact_name}' disable_platforms_if_flags_not_set "
f"must be a table, got {type(disable_platforms_if_flags_not_set).__name__}"
)
self.artifacts[artifact_name] = Artifact(
name=artifact_name,
artifact_group=artifact_data.get("artifact_group", ""),
type=artifact_data.get("type", "target-neutral"),
artifact_deps=artifact_data.get("artifact_deps", []),
platform=artifact_data.get("platform"),
feature_name=artifact_data.get("feature_name"),
feature_group=artifact_data.get("feature_group"),
disable_platforms=artifact_data.get("disable_platforms", []),
disable_platforms_if_flags_not_set=disable_platforms_if_flags_not_set,
python_requires=python_requires,
split_databases=artifact_data.get("split_databases", []),
test_artifacts=artifact_data.get("test_artifacts", []),
)
def get_build_stages(self) -> List[BuildStage]:
"""Get all build stages."""
return list(self.build_stages.values())
def get_artifact_groups(self) -> List[ArtifactGroup]:
"""Get all artifact groups."""
return list(self.artifact_groups.values())
def get_artifacts(self) -> List[Artifact]:
"""Get all artifacts."""
return list(self.artifacts.values())
def is_artifact_disabled_on_platform(
self,
artifact: Artifact,
platform_name: str,
enabled_flags: Optional[Set[str]] = None,
) -> bool:
"""Return whether an artifact is disabled for a platform and flag set."""
if not platform_name:
return False
if platform_name in artifact.disable_platforms:
return True
required_flag = artifact.disable_platforms_if_flags_not_set.get(platform_name)
if not required_flag:
return False
enabled_flags = enabled_flags or set()
return required_flag not in enabled_flags
def get_artifact_feature_name(self, artifact: Artifact) -> str:
"""Get the effective feature name for an artifact."""
if artifact.feature_name:
return artifact.feature_name
# Default rule: uppercase and replace - with _
return artifact.name.upper().replace("-", "_")
def get_artifact_feature_group(self, artifact: Artifact) -> str:
"""Get the effective feature group for an artifact."""
if artifact.feature_group:
return artifact.feature_group
# Default rule: uppercase artifact_group and replace - with _
return artifact.artifact_group.upper().replace("-", "_")
def get_artifacts_in_group(self, group_name: str) -> List[Artifact]:
"""Get all artifacts belonging to a specific artifact group."""
return [a for a in self.artifacts.values() if a.artifact_group == group_name]
def get_inbound_artifacts(self, build_stage: str) -> Set[str]:
"""
Get all artifacts needed by a build stage from previous stages.
This is the key method for CI/CD pipelines to determine what
artifacts need to be fetched from S3 before building.
Args:
build_stage: Name of the build stage
Returns:
Set of artifact names that this stage depends on
"""
if build_stage not in self.build_stages:
raise ValueError(f"Build stage '{build_stage}' not found")
stage = self.build_stages[build_stage]
inbound_artifacts = set()
# Get all artifact groups this stage contains
stage_groups = set(stage.artifact_groups)
# For each artifact group in this stage, collect its dependencies
for group_name in stage_groups:
if group_name not in self.artifact_groups:
continue
group = self.artifact_groups[group_name]
# Get all artifacts from dependent groups (transitively)
for dep_group_name in group.artifact_group_deps:
dep_artifacts = self.get_artifacts_in_group(dep_group_name)
for artifact in dep_artifacts:
inbound_artifacts.add(artifact.name)
self._collect_transitive_artifact_deps(
artifact.name, inbound_artifacts
)
# Also collect direct artifact dependencies from artifacts in this stage
# This includes transitive artifact dependencies
for artifact in self.artifacts.values():
if artifact.artifact_group in stage_groups:
# Add direct dependencies
for dep_name in artifact.artifact_deps:
inbound_artifacts.add(dep_name)
# Also add transitive dependencies
self._collect_transitive_artifact_deps(dep_name, inbound_artifacts)
# Remove artifacts that are produced by this stage itself
produced = self.get_produced_artifacts(build_stage)
inbound_artifacts -= produced
return inbound_artifacts
def _collect_transitive_artifact_deps(
self, artifact_name: str, collected: Set[str]
):
"""
Recursively collect all transitive dependencies of an artifact.
Args:
artifact_name: Name of the artifact to get dependencies for
collected: Set to add dependencies to (modified in place)
"""
if artifact_name not in self.artifacts:
return
artifact = self.artifacts[artifact_name]
for dep_name in artifact.artifact_deps:
if dep_name not in collected:
# Add to collected set BEFORE recursing to prevent revisiting
# the same node in diamond dependency patterns
collected.add(dep_name)
self._collect_transitive_artifact_deps(dep_name, collected)
def get_produced_artifacts(self, build_stage: str) -> Set[str]:
"""
Get all artifacts produced by a build stage.
Args:
build_stage: Name of the build stage
Returns:
Set of artifact names produced by this stage
"""
if build_stage not in self.build_stages:
raise ValueError(f"Build stage '{build_stage}' not found")
stage = self.build_stages[build_stage]
produced_artifacts = set()
# Collect all artifacts from the groups in this stage
for group_name in stage.artifact_groups:
artifacts_in_group = self.get_artifacts_in_group(group_name)
produced_artifacts.update(a.name for a in artifacts_in_group)
return produced_artifacts
def _validate_naming_conventions(self) -> List[str]:
"""
Validate naming conventions for all topology entities.
Conventions:
- Entity names (stages, groups, artifacts): lowercase with hyphens
- feature_name: UPPERCASE with underscores
- feature_group: UPPERCASE with underscores
- type values: lowercase
- platform values: lowercase
Returns:
List of validation error messages
"""
import re
errors = []
# Pattern for entity names: lowercase letters, numbers, and hyphens
entity_pattern = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
# Pattern for feature names/groups: uppercase letters, numbers, and underscores
feature_pattern = re.compile(r"^[A-Z0-9]+(_[A-Z0-9]+)*$")
# Valid type values
valid_stage_types = {"generic", "per-arch"}
valid_artifact_types = {"target-neutral", "target-specific"}
valid_platforms = {"windows", "linux"}
# Validate build stage names and types
for stage_name, stage in self.build_stages.items():
if not entity_pattern.match(stage_name):
errors.append(
f"Build stage '{stage_name}' should be lowercase-with-hyphens"
)
if stage.type not in valid_stage_types:
errors.append(
f"Build stage '{stage_name}' has invalid type '{stage.type}' "
f"(expected: {valid_stage_types})"
)
# Validate artifact group names and types
for group_name, group in self.artifact_groups.items():
if not entity_pattern.match(group_name):
errors.append(
f"Artifact group '{group_name}' should be lowercase-with-hyphens"
)
if group.type not in valid_stage_types:
errors.append(
f"Artifact group '{group_name}' has invalid type '{group.type}' "
f"(expected: {valid_stage_types})"
)
# Validate artifact names, types, and feature overrides
for artifact_name, artifact in self.artifacts.items():
if not entity_pattern.match(artifact_name):
errors.append(
f"Artifact '{artifact_name}' should be lowercase-with-hyphens"
)
if artifact.type not in valid_artifact_types:
errors.append(
f"Artifact '{artifact_name}' has invalid type '{artifact.type}' "
f"(expected: {valid_artifact_types})"
)
if artifact.feature_name and not feature_pattern.match(
artifact.feature_name
):
errors.append(
f"Artifact '{artifact_name}' feature_name '{artifact.feature_name}' "
f"should be UPPERCASE_WITH_UNDERSCORES"
)
if artifact.feature_group and not feature_pattern.match(
artifact.feature_group
):
errors.append(
f"Artifact '{artifact_name}' feature_group '{artifact.feature_group}' "
f"should be UPPERCASE_WITH_UNDERSCORES"
)
if artifact.platform and artifact.platform not in valid_platforms:
errors.append(
f"Artifact '{artifact_name}' has invalid platform '{artifact.platform}' "
f"(expected: {valid_platforms})"
)
for platform in artifact.disable_platforms:
if platform not in valid_platforms:
errors.append(
f"Artifact '{artifact_name}' has invalid disable_platform '{platform}' "
f"(expected: {valid_platforms})"
)
for platform, flag in artifact.disable_platforms_if_flags_not_set.items():
if platform not in valid_platforms:
errors.append(
f"Artifact '{artifact_name}' has invalid conditional disable_platform '{platform}' "
f"(expected: {valid_platforms})"
)
if not isinstance(flag, str) or not feature_pattern.match(flag):
errors.append(
f"Artifact '{artifact_name}' disable_platforms_if_flags_not_set "
f"entry for '{platform}' should be UPPERCASE_WITH_UNDERSCORES"
)
# Validate source set disable_platforms
for source_set_name, source_set in self.source_sets.items():
if not entity_pattern.match(source_set_name):
errors.append(
f"Source set '{source_set_name}' should be lowercase-with-hyphens"
)
for platform in source_set.disable_platforms:
if platform not in valid_platforms:
errors.append(
f"Source set '{source_set_name}' has invalid disable_platform '{platform}' "
f"(expected: {valid_platforms})"
)
for source in source_set.external_git_sources:
if not source.name:
errors.append(
f"Source set '{source_set_name}' has external git source with missing name"
)
elif not entity_pattern.match(source.name):
errors.append(
f"External git source '{source.name}' should be lowercase-with-hyphens"
)
if not source.origin:
errors.append(
f"External git source '{source.name}' in source set "
f"'{source_set_name}' has missing origin"
)
if not source.commit:
errors.append(
f"External git source '{source.name}' in source set "
f"'{source_set_name}' has missing commit"
)
if not source.path:
errors.append(
f"External git source '{source.name}' in source set "
f"'{source_set_name}' has missing path"
)
else:
path = Path(source.path)
if path.is_absolute():
errors.append(
f"External git source '{source.name}' path '{source.path}' "
"must be relative"
)
path_parts = path.parts
if (
not path_parts
or path_parts[0] != "optional-sources"
or ".." in path_parts
or len(path_parts) < 2
):
errors.append(
f"External git source '{source.name}' path '{source.path}' "
"must be under optional-sources/"
)
for prefix in source_set.path_prefixes:
if not prefix:
errors.append(
f"Source set '{source_set_name}' has an empty path_prefix"
)
continue
prefix_path = Path(prefix)
if prefix_path.is_absolute():
errors.append(
f"Source set '{source_set_name}' path_prefix '{prefix}' "
"must be relative"
)
if ".." in prefix_path.parts:
errors.append(
f"Source set '{source_set_name}' path_prefix '{prefix}' "
"must not contain '..'"
)
return errors
def validate_topology(self) -> List[str]:
"""
Validate topology for cycles, missing references, naming conventions, etc.
Returns:
List of validation error messages (empty if valid)
"""
errors = []
# Validate naming conventions
errors.extend(self._validate_naming_conventions())
# Check for missing artifact group references in stages
for stage in self.build_stages.values():
for group_name in stage.artifact_groups:
if group_name not in self.artifact_groups:
errors.append(
f"Stage '{stage.name}' references unknown artifact group '{group_name}'"
)
# Check for missing artifact group references in dependencies
for group in self.artifact_groups.values():
for dep_name in group.artifact_group_deps:
if dep_name not in self.artifact_groups:
errors.append(
f"Artifact group '{group.name}' depends on unknown group '{dep_name}'"
)
for source_set_name in group.source_sets:
if source_set_name not in self.source_sets:
errors.append(
f"Artifact group '{group.name}' references unknown source set "
f"'{source_set_name}'"
)
# Check for missing artifact references
for artifact in self.artifacts.values():
# Check artifact group reference
if (
artifact.artifact_group
and artifact.artifact_group not in self.artifact_groups
):
errors.append(
f"Artifact '{artifact.name}' references unknown group '{artifact.artifact_group}'"
)
# Check artifact dependencies
for dep_name in artifact.artifact_deps:
if dep_name not in self.artifacts:
errors.append(
f"Artifact '{artifact.name}' depends on unknown artifact '{dep_name}'"
)
# Check for circular dependencies in artifact groups
visited = set()
rec_stack = set()
def has_cycle(group_name: str) -> bool:
visited.add(group_name)
rec_stack.add(group_name)
if group_name in self.artifact_groups:
for dep_name in self.artifact_groups[group_name].artifact_group_deps:
if dep_name not in visited:
if has_cycle(dep_name):
return True
elif dep_name in rec_stack:
errors.append(
f"Circular dependency detected involving artifact group '{dep_name}'"
)
return True
rec_stack.remove(group_name)
return False
for group_name in self.artifact_groups:
if group_name not in visited:
has_cycle(group_name)
# Check for circular dependencies in artifacts
visited_artifacts = set()
rec_stack_artifacts = set()
def has_artifact_cycle(artifact_name: str) -> bool:
visited_artifacts.add(artifact_name)
rec_stack_artifacts.add(artifact_name)
if artifact_name in self.artifacts:
for dep_name in self.artifacts[artifact_name].artifact_deps:
if dep_name not in visited_artifacts:
if has_artifact_cycle(dep_name):
return True
elif dep_name in rec_stack_artifacts:
errors.append(
f"Circular dependency detected involving artifact '{dep_name}'"
)
return True
rec_stack_artifacts.remove(artifact_name)
return False
for artifact_name in self.artifacts:
if artifact_name not in visited_artifacts:
has_artifact_cycle(artifact_name)
# Check for conflicting external source paths.
external_sources_by_path: Dict[str, str] = {}
for source_set in self.source_sets.values():
for source in source_set.external_git_sources:
previous_source_set = external_sources_by_path.get(source.path)
if previous_source_set and previous_source_set != source_set.name:
errors.append(
f"External git source path '{source.path}' is used by both "
f"source sets '{previous_source_set}' and '{source_set.name}'"
)
else:
external_sources_by_path[source.path] = source_set.name
# Check for conflicting submodule ownership.
submodule_owner: Dict[str, str] = {}
for source_set in self.source_sets.values():
for submodule in source_set.submodules:
previous_source_set = submodule_owner.get(submodule.name)
if previous_source_set and previous_source_set != source_set.name:
errors.append(
f"Submodule '{submodule.name}' is used by both "
f"source sets '{previous_source_set}' and '{source_set.name}'"
)
else:
submodule_owner[submodule.name] = source_set.name
# Check for conflicting path prefix ownership.
path_prefix_owner: Dict[str, str] = {}
for source_set in self.source_sets.values():
for prefix in source_set.path_prefixes:
normalized_prefix = prefix.rstrip("/")
previous_source_set = path_prefix_owner.get(normalized_prefix)
if previous_source_set and previous_source_set != source_set.name:
errors.append(
f"Path prefix '{normalized_prefix}' is used by both "
f"source sets '{previous_source_set}' and '{source_set.name}'"
)
else:
path_prefix_owner[normalized_prefix] = source_set.name
return errors
def get_dependency_graph(self) -> Dict:
"""
Generate full dependency graph for visualization.
Returns:
Dictionary representation of the dependency graph
"""
graph = {"build_stages": {}, "artifact_groups": {}, "artifacts": {}}
# Build stages graph
for stage in self.build_stages.values():
graph["build_stages"][stage.name] = {
"type": stage.type,
"artifact_groups": stage.artifact_groups,
"inbound_artifacts": list(self.get_inbound_artifacts(stage.name)),
"produced_artifacts": list(self.get_produced_artifacts(stage.name)),
}
# Artifact groups graph
for group in self.artifact_groups.values():
graph["artifact_groups"][group.name] = {
"type": group.type,
"depends_on": group.artifact_group_deps,
"artifacts": [a.name for a in self.get_artifacts_in_group(group.name)],
}
# Artifacts graph
for artifact in self.artifacts.values():
graph["artifacts"][artifact.name] = {
"type": artifact.type,
"artifact_group": artifact.artifact_group,
"depends_on": artifact.artifact_deps,
"platform": artifact.platform,
}
return graph
def get_build_order(self) -> List[str]:
"""
Get the build order for stages based on dependencies.
Returns:
List of build stage names in order they should be built
"""
# Build a dependency graph for stages based on artifact groups
stage_deps = {}
for stage_name, stage in self.build_stages.items():
deps = set()
for group_name in stage.artifact_groups:
if group_name in self.artifact_groups:
group = self.artifact_groups[group_name]
# Find which stages produce the dependent groups
for dep_group in group.artifact_group_deps:
for other_stage_name, other_stage in self.build_stages.items():
if dep_group in other_stage.artifact_groups:
deps.add(other_stage_name)
stage_deps[stage_name] = deps
# Topological sort
visited = set()
order = []
def visit(stage_name: str):
if stage_name in visited:
return
visited.add(stage_name)
for dep in stage_deps.get(stage_name, set()):
visit(dep)
order.append(stage_name)
for stage_name in self.build_stages:
visit(stage_name)
return order
def get_source_set_to_artifact_groups(self) -> Dict[str, List[str]]:
"""
Get a reverse index from source set names to artifact group names.
Returns:
Dictionary mapping source set names to artifact group names that
reference them. Known source sets with no artifact group references
are included with an empty list.
"""
artifact_groups_by_source_set = {
source_set_name: [] for source_set_name in self.source_sets
}
for group in self.artifact_groups.values():
for source_set_name in group.source_sets:
artifact_groups_by_source_set.setdefault(source_set_name, []).append(
group.name
)
return artifact_groups_by_source_set
def get_artifact_group_to_artifacts(self) -> Dict[str, List[str]]:
"""
Get an index from artifact group names to artifact names.
Returns:
Dictionary mapping artifact group names to artifact names in that
group. Known artifact groups with no artifacts are included with an
empty list.
"""
artifacts_by_group = {group_name: [] for group_name in self.artifact_groups}
for artifact in self.artifacts.values():
artifacts_by_group.setdefault(artifact.artifact_group, []).append(
artifact.name
)
return artifacts_by_group
def get_artifact_group_to_build_stages(self) -> Dict[str, List[str]]:
"""
Get a reverse index from artifact group names to producer build stages.
Returns:
Dictionary mapping artifact group names to build stage names that
list the group. Known artifact groups with no producing stage are
included with an empty list.
"""
stages_by_group = {group_name: [] for group_name in self.artifact_groups}
for stage in self.build_stages.values():
for group_name in stage.artifact_groups:
stages_by_group.setdefault(group_name, []).append(stage.name)
return stages_by_group
def get_artifact_to_producer_stages(self) -> Dict[str, List[str]]:
"""
Get an index from artifact names to producer build stages.
Returns:
Dictionary mapping artifact names to build stage names that produce
their artifact group. Artifacts in unmapped groups are included with
an empty list.
"""
stages_by_group = self.get_artifact_group_to_build_stages()
return {
artifact.name: list(stages_by_group.get(artifact.artifact_group, []))
for artifact in self.artifacts.values()
}
def get_stage_to_source_sets(
self, platform: Optional[str] = None
) -> Dict[str, List[str]]:
"""
Get an index from build stages to source set names.
Args:
platform: Current platform (e.g., "linux", "windows"). If provided,
source_sets with this platform in disable_platforms are skipped.
Returns:
Dictionary mapping build stage names to source set names used by the
stage's artifact groups.
"""
return {
stage_name: [
source_set.name
for source_set in self.get_source_sets_for_stage(
stage_name, platform=platform
)
]
for stage_name in self.build_stages
}
def get_source_set_to_stages(
self, platform: Optional[str] = None
) -> Dict[str, List[str]]:
"""
Get a reverse index from source set names to build stages.
Args:
platform: Current platform (e.g., "linux", "windows"). If provided,
source_sets with this platform in disable_platforms are skipped.
Returns:
Dictionary mapping source set names to build stage names that use
them through artifact groups. Known source sets with no build stage
usage are included with an empty list.
"""
stages_by_source_set = {
source_set_name: [] for source_set_name in self.source_sets
}
for stage_name, source_set_names in self.get_stage_to_source_sets(
platform=platform
).items():
for source_set_name in source_set_names:
stages_by_source_set.setdefault(source_set_name, []).append(stage_name)
return stages_by_source_set
def get_submodule_to_source_set(self) -> Dict[str, str]:
"""
Get a reverse index from submodule names to source set names.
"""
mapping: Dict[str, str] = {}
for source_set in self.source_sets.values():
for submodule in source_set.submodules:
mapping[submodule.name] = source_set.name
return mapping
def get_source_sets(self) -> List[SourceSet]:
"""Get all source sets."""
return list(self.source_sets.values())
def get_source_set_for_submodule(
self, submodule_name: str, platform: Optional[str] = None
) -> Optional[SourceSet]:
"""
Return the first source set whose submodules include `submodule_name`.
Args:
submodule_name: Name of the git submodule.
platform: Optional platform filter.
Returns:
Matching SourceSet, or None if no match is found.
"""
for source_set in self.source_sets.values():
if platform and platform in source_set.disable_platforms:
continue
for submodule in source_set.submodules:
if submodule.name == submodule_name:
return source_set
return None
def get_source_set_for_path(
self, path: str, platform: Optional[str] = None
) -> Optional[SourceSet]:
"""
Return the first source set whose path_prefixes match the given path.
"""
normalized_path = path.strip().lstrip("./")
for source_set in self.source_sets.values():
if platform and platform in source_set.disable_platforms:
continue
for prefix in source_set.path_prefixes:
prefix = prefix.rstrip("/")
if normalized_path == prefix or normalized_path.startswith(
prefix + "/"
):
return source_set
return None
def get_source_sets_for_submodules(
self, submodule_names: List[str], platform: Optional[str] = None
) -> List[SourceSet]:
"""
Return deduplicated source sets for a list of submodule names.
"""
source_sets_by_name: Dict[str, SourceSet] = {}
for submodule_name in submodule_names:
source_set = self.get_source_set_for_submodule(
submodule_name, platform=platform
)
if source_set and source_set.name not in source_sets_by_name:
source_sets_by_name[source_set.name] = source_set
return list(source_sets_by_name.values())
def get_submodules_for_source_set(self, source_set_name: str) -> List[Submodule]:
"""
Get the submodules for a specific source set.
Args:
source_set_name: Name of the source set
Returns:
List of Submodule objects
"""
if source_set_name not in self.source_sets:
raise ValueError(f"Source set '{source_set_name}' not found")
return self.source_sets[source_set_name].submodules
def get_external_git_sources_for_source_set(
self, source_set_name: str
) -> List[ExternalGitSource]:
"""
Get the external git sources for a specific source set.
Args:
source_set_name: Name of the source set
Returns:
List of ExternalGitSource objects
"""
if source_set_name not in self.source_sets:
raise ValueError(f"Source set '{source_set_name}' not found")
return self.source_sets[source_set_name].external_git_sources
def get_source_sets_for_stage(
self, build_stage: str, platform: Optional[str] = None
) -> List[SourceSet]:
"""
Get all source sets needed to build a specific stage.
Args:
build_stage: Name of the build stage
platform: Current platform (e.g., "linux", "windows"). If provided,
source_sets with this platform in disable_platforms are skipped.
Returns:
List of SourceSet objects needed for this stage
"""
if build_stage not in self.build_stages:
raise ValueError(f"Build stage '{build_stage}' not found")
stage = self.build_stages[build_stage]
source_sets_by_name: Dict[str, SourceSet] = {}
for group_name in stage.artifact_groups:
if group_name not in self.artifact_groups:
continue
group = self.artifact_groups[group_name]
for source_set_name in group.source_sets:
if source_set_name in self.source_sets:
source_set = self.source_sets[source_set_name]
if platform and platform in source_set.disable_platforms:
continue
if source_set.name not in source_sets_by_name:
source_sets_by_name[source_set.name] = source_set
return list(source_sets_by_name.values())
def get_submodules_for_stage(
self, build_stage: str, platform: Optional[str] = None
) -> List[Submodule]:
"""
Get all submodules needed to build a specific stage.
This collects source_sets from all artifact_groups in the stage,
deduplicating by submodule name. When sparse checkout is added,
this will need to merge specs for the same submodule.
Args:
build_stage: Name of the build stage
platform: Current platform (e.g., "linux", "windows"). If provided,
source_sets with this platform in disable_platforms are skipped.
Returns:
List of Submodule objects needed for this stage
"""
# Use dict to dedupe by name while preserving order
submodules_by_name: Dict[str, Submodule] = {}
for source_set in self.get_source_sets_for_stage(
build_stage, platform=platform
):
for submodule in source_set.submodules:
# TODO: When adding sparse_checkout, merge specs here
if submodule.name not in submodules_by_name:
submodules_by_name[submodule.name] = submodule
return list(submodules_by_name.values())
def get_external_git_sources_for_stage(
self, build_stage: str, platform: Optional[str] = None
) -> List[ExternalGitSource]:
"""
Get all external git sources needed to build a specific stage.
Args:
build_stage: Name of the build stage
platform: Current platform (e.g., "linux", "windows"). If provided,
source_sets with this platform in disable_platforms are skipped.
Returns:
List of ExternalGitSource objects needed for this stage
"""
sources_by_path: Dict[str, ExternalGitSource] = {}
for source_set in self.get_source_sets_for_stage(
build_stage, platform=platform
):
for source in source_set.external_git_sources:
if source.path not in sources_by_path:
sources_by_path[source.path] = source
return list(sources_by_path.values())
def get_all_submodules(self) -> List[Submodule]:
"""
Get all submodules defined across all source sets.
Returns:
List of all Submodule objects (deduplicated by name)
"""
submodules_by_name: Dict[str, Submodule] = {}
for source_set in self.source_sets.values():
for submodule in source_set.submodules:
if submodule.name not in submodules_by_name:
submodules_by_name[submodule.name] = submodule
return list(submodules_by_name.values())
def get_all_external_git_sources(self) -> List[ExternalGitSource]:
"""
Get all external git sources defined across all source sets.
Returns:
List of all ExternalGitSource objects (deduplicated by path)
"""
sources_by_path: Dict[str, ExternalGitSource] = {}
for source_set in self.source_sets.values():
for source in source_set.external_git_sources:
if source.path not in sources_by_path:
sources_by_path[source.path] = source
return list(sources_by_path.values())
def get_python_requires_for_stage(self, build_stage: str) -> List[str]:
"""
Get all python_requires for artifacts produced by a build stage.
Collects python_requires from all artifacts in the stage's artifact groups,
returning them as a deduplicated list suitable for passing to pip install.
Args:
build_stage: Name of the build stage
Returns:
List of pip install arguments (e.g., ["-r path/to/req.txt", "package"])
"""
if build_stage not in self.build_stages:
raise ValueError(f"Build stage '{build_stage}' not found")
stage = self.build_stages[build_stage]
seen: Set[str] = set()
requires: List[str] = []
# Collect python_requires from artifacts in this stage's groups
for group_name in stage.artifact_groups:
for artifact in self.get_artifacts_in_group(group_name):
for req in artifact.python_requires:
if req not in seen:
seen.add(req)
requires.append(req)
return requires
def load_subproject_manifest(
self, manifest_path: Optional[Path] = None
) -> Optional[Dict[str, List[str]]]:
"""Load artifact_subprojects.json from manifest_path or build_tools/."""
if manifest_path is None:
manifest_path = (
self.toml_path.parent / "build_tools" / "artifact_subprojects.json"
)
if not manifest_path.exists():
return None
with manifest_path.open() as f:
return json.load(f)
def _load_json_manifest(self, filename: str) -> Optional[Dict]:
"""Load a JSON manifest file from build_tools/."""
manifest_path = self.toml_path.parent / "build_tools" / filename
if not manifest_path.exists():
return None
with manifest_path.open() as f:
return json.load(f)
def _load_project_mappings(self) -> Optional[Dict]:
"""Load project_mappings.json from build_tools/."""
return self._load_json_manifest("project_mappings.json")
def get_subproject_to_feature_map(
self, build_dir: Optional[Path] = None
) -> Dict[str, str]:
"""Map subproject names directly to feature names."""
feature_map: Dict[str, str] = {}
mappings = self._load_project_mappings()
if mappings and "subproject_features" in mappings:
for subproject, feature in mappings["subproject_features"].items():
feature_map[subproject.lower()] = feature
return feature_map
def get_alias_to_artifact_map(
self, build_dir: Optional[Path] = None
) -> Dict[str, str]:
"""Map subproject/artifact names to artifact names."""
alias_map: Dict[str, str] = {}
manifest = None
if build_dir:
build_manifest = build_dir / "artifact_subprojects.json"
if build_manifest.exists():
manifest = self.load_subproject_manifest(build_manifest)
if manifest is None:
manifest = self.load_subproject_manifest()
for artifact in self.artifacts.values():
alias_map[artifact.name.lower()] = artifact.name
if manifest and artifact.name in manifest:
for alias in manifest[artifact.name]:
alias_lower = alias.lower()
if alias_lower not in alias_map or alias_lower == artifact.name:
alias_map[alias_lower] = artifact.name
for db_name in artifact.split_databases:
alias_map[db_name.lower()] = artifact.name
# Include rocm-systems project mappings (e.g., "hip" -> "core-hip")
mappings = self._load_project_mappings()
if mappings and "rocm_systems_projects" in mappings:
for project_name, artifact_name in mappings[
"rocm_systems_projects"
].items():
project_lower = project_name.lower()
if project_lower not in alias_map:
alias_map[project_lower] = artifact_name
return alias_map
def resolve_project_to_artifact(
self, project_name: str, build_dir: Optional[Path] = None
) -> Optional[str]:
"""Resolve a project name to its artifact name."""
return self.get_alias_to_artifact_map(build_dir).get(project_name.lower())
def resolve_projects_to_features(
self,
project_names: List[str],
platform_name: str = "",
build_dir: Optional[Path] = None,
) -> Set[str]:
"""Resolve project names to CMake feature names."""
features: Set[str] = set()
feature_map = self.get_subproject_to_feature_map(build_dir)
alias_map = self.get_alias_to_artifact_map(build_dir)
for project in project_names:
project_lower = project.lower()
# First check direct subproject -> feature mapping
if project_lower in feature_map:
features.add(feature_map[project_lower])
continue
# Fall back to artifact mapping
artifact_name = alias_map.get(project_lower)
if artifact_name and artifact_name in self.artifacts:
artifact = self.artifacts[artifact_name]
if platform_name and platform_name in artifact.disable_platforms:
continue
features.add(self.get_artifact_feature_name(artifact))
return features
def get_stage_for_artifact(self, artifact_name: str) -> Optional[str]:
"""Get the build stage that produces a given artifact."""
if artifact_name not in self.artifacts:
return None
artifact = self.artifacts[artifact_name]
artifact_group = artifact.artifact_group
for stage in self.build_stages.values():
if artifact_group in stage.artifact_groups:
return stage.name
return None
def get_stages_for_projects(
self,
project_names: List[str],
build_dir: Optional[Path] = None,
) -> Set[str]:
"""Get build stages required to build the given projects."""
# Resolve projects to artifacts
alias_map = self.get_alias_to_artifact_map(build_dir)
required_artifacts: Set[str] = set()
for project in project_names:
artifact_name = alias_map.get(project.lower())
if artifact_name:
required_artifacts.add(artifact_name)
# Also include test_artifacts for each required artifact
artifacts_to_process = list(required_artifacts)
while artifacts_to_process:
artifact_name = artifacts_to_process.pop()
if artifact_name not in self.artifacts:
continue
artifact = self.artifacts[artifact_name]
for test_artifact in artifact.test_artifacts:
if test_artifact not in required_artifacts:
required_artifacts.add(test_artifact)
artifacts_to_process.append(test_artifact)
# Get stages that produce these artifacts
required_stages: Set[str] = set()
for artifact_name in required_artifacts:
stage_name = self.get_stage_for_artifact(artifact_name)
if stage_name:
required_stages.add(stage_name)
# Include dependent stages (stages that produce artifacts we depend on)
# Walk the dependency chain
stages_to_check = list(required_stages)
while stages_to_check:
stage_name = stages_to_check.pop()
if stage_name not in self.build_stages:
continue
stage = self.build_stages[stage_name]
# Get all artifacts in this stage's groups
for group_name in stage.artifact_groups:
for artifact in self.get_artifacts_in_group(group_name):
# Check artifact dependencies
for dep_artifact_name in artifact.artifact_deps:
dep_stage = self.get_stage_for_artifact(dep_artifact_name)
if dep_stage and dep_stage not in required_stages:
required_stages.add(dep_stage)
stages_to_check.append(dep_stage)
return required_stages
def get_all_stage_names(self) -> Set[str]:
"""Get all build stage names."""
return set(self.build_stages.keys())