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

434 lines
15 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""CLI tool for managing the 'prebuilt' state of project components.
In TheRock terminology, 'prebuilt' refers to a project component that has had
its staging install provided externally and it will not be configured/built
locally (but will still be available for dependents). Where such prebuilts
come from can vary:
* From a prior build invocation locally.
* From a central CI server.
* From a current build invocation where we just want to mark the project as
not involved in the build any longer.
Where such prebuilts come from is presently outside of the scope of this
utility, but it may be expanded to pull automatically from the CI, etc, in the
future.
Basic Usage
-----------
The most basic usage of the tool is to enable/disable sub-projects from the
build after having bootstrapped (built once or obtained artifacts from elsewhere).
Two subcommands are provided for this: "enable" and "disable". Both take arguments:
* List of regular expressions to explicitly include (default to all).
* `--exclude` + list of regular expressions to exclude (default to none).
For example, if after an initial boostrap, you only want to iterate on
`ROCR-Runtime`, and you don't intend to change the public API and therefore
don't care to build dependents, run:
python ./build_tools/buildctl.py enable ROCR
If you do want to do codevelopment with clr, you could also do:
python ./build_tools/buildctl.py enable ROCR clr
To reset to building everything, just run the `enable` command with no arguments.
Similar usage can be made with `disable`. Let's say you would like to avoid
spurious builds of some math libraries:
python ./build_tools/buildctl.py disable hipBLASLt rocSOLVER
A report will be printed and if any changes to project state were made,
"Reconfiguring..." will be printed and a CMake reconfigure of TheRock will be
done to pick up the changes.
Bootstrap Usage
---------------
The `bootstrap` subcommand populates a build directory with artifacts from a
prior build, creating .prebuilt markers so CMake skips building those components:
python ./build_tools/buildctl.py bootstrap \
--build-dir build \
--artifact-dir /path/to/artifacts
When --stage is provided, artifacts are filtered to only those needed as inbound
dependencies for that stage (using BUILD_TOPOLOGY.toml):
python ./build_tools/buildctl.py bootstrap \
--build-dir build \
--artifact-dir /path/to/artifacts \
--stage math-libs \
--target-families gfx94X-dcgpu
What is going on under the covers
---------------------------------
Under the covers, the build system operates off of `stage/` subdirectories in
each project's tree. This represents the result of `cmake --install` of the
sub-project. If there is an adjacent file called `stage.prebuilt`, then the
build system will just trust that the `stage/` directory contents are correct,
skip build/install of it, and just use the `stage.prebuilt` file as an up to
date check (so if you touch this file, it will invalidate all dependents,
forcing them to rebuild). You can manage these files yourself with `find`,
`touch`, and `rm` but it is tedious. This tool aims to handle common workflows
without filesystem hacking.
"""
import argparse
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
from typing import Optional, Set
from _therock_utils.artifacts import ArtifactPopulator, ArtifactName
def do_enable_disable(args: argparse.Namespace, enable_mode: bool):
build_dir = resolve_build_dir(args)
stage_dirs = find_stage_dirs(build_dir)
selection = filter_selection(args, stage_dirs)
changed = False
print("Projects marked with an 'X' will be build enabled:")
for rp, include in selection:
stage_dir = build_dir / Path(rp).as_posix()
prebuilt_file = stage_dir.with_name(stage_dir.name + ".prebuilt")
if not is_valid_stage_dir(stage_dir):
action = (False, "(EMPTY)")
elif include:
action = (True, "")
else:
action = (False, "")
action_enable, message = action
if message == "(EMPTY)":
continue
if not enable_mode:
action_enable = False
if action_enable:
if prebuilt_file.exists():
prebuilt_file.unlink()
changed = True
else:
if not prebuilt_file.exists():
prebuilt_file.touch()
changed = True
print(f"[{'X' if action_enable else ' '}] {rp} {message}")
if changed or args.force_reconfigure:
reconfigure(build_dir)
def resolve_build_dir(args: argparse.Namespace) -> Path:
build_dir: Path | None = args.build_dir
if build_dir is None:
build_dir = Path(__file__).resolve().parent.parent / "build"
if not build_dir.exists() or not build_dir.is_dir():
raise CLIError(
f"Build directory {build_dir} not found: specify with --build-dir"
)
return build_dir
# The build system creates marker files named ".{something}stage.marker" for
# every stage directory in the build tree. This returns relative paths to all
# such stage directories.
def find_stage_dirs(build_dir: Path) -> list[str]:
PREFIX = "."
SUFFIX = ".marker"
results: list[Path] = list()
for current_dir, dirs, files in os.walk(build_dir.absolute()):
for file in files:
if file.startswith(PREFIX) and file.endswith(f"stage{SUFFIX}"):
stage_dir_name = file[len(PREFIX) : -len(SUFFIX)]
results.append(Path(current_dir) / stage_dir_name)
# Prevent os.walk from recursing into subdirectories of this match
try:
index = dirs.index(stage_dir_name)
except ValueError:
...
else:
del dirs[index]
relative_results = [d.relative_to(build_dir).as_posix() for d in results]
relative_results.sort()
return relative_results
# Applies filter arguments to a list of relative paths returning a list of
# (relpath, include).
def filter_selection(
args: argparse.Namespace, relpaths: list[str]
) -> list[tuple[str, bool]]:
def _filter(rp: str) -> bool:
# If any includes, only pass if at least one matches.
if args.include:
for include_regex in args.include:
pattern = re.compile(include_regex)
if pattern.search(rp):
break
else:
return False
# And if no excludes match.
if args.exclude:
for exclude_regex in args.exclude:
pattern = re.compile(exclude_regex)
if pattern.search(rp):
return False
# Otherwise, pass.
return True
return [(rp, _filter(rp)) for rp in relpaths]
def is_valid_stage_dir(stage_dir: Path) -> bool:
# Non existing are invalid.
if not stage_dir.exists():
return False
# Empty stage directories are invalid.
children = list(stage_dir.iterdir())
if not children:
return False
return True
# Runs cmake reconfiguration.
def reconfigure(build_dir: Path):
PREFIX = "CMAKE_COMMAND:INTERNAL="
cache_file = build_dir / "CMakeCache.txt"
cmake_command = None
if not cache_file.exists():
raise CLIError(f"Cannot reconfigure: cache file {cache_file} does not exist")
cache_lines = cache_file.read_text().splitlines()
for cache_line in cache_lines:
if cache_line.startswith(PREFIX):
cmake_command = cache_line[len(PREFIX) :]
break
else:
raise CLIError(
f"Could not find {PREFIX} in {cache_file}: Cannot automatically reconfigure"
)
print("Reconfiguring...", file=sys.stderr)
try:
subprocess.check_output(
[cmake_command, str(build_dir)], stderr=subprocess.STDOUT, text=True
)
except subprocess.CalledProcessError as e:
# Print combined output only if the command fails
print(e.output, end="")
raise CLIError(f"Project reconfigure failed")
################################################################################
# Bootstrap command
################################################################################
def get_inbound_artifact_names(stage: str) -> Set[str]:
"""Get the set of artifact names needed by a build stage."""
from _therock_utils.build_topology import BuildTopology
script_dir = Path(__file__).parent
repo_root = script_dir.parent
topology_path = repo_root / "BUILD_TOPOLOGY.toml"
if not topology_path.exists():
raise CLIError(f"BUILD_TOPOLOGY.toml not found at {topology_path}")
topology = BuildTopology(str(topology_path))
return topology.get_inbound_artifacts(stage)
class BootstrappingPopulator(ArtifactPopulator):
"""ArtifactPopulator that creates .prebuilt markers for bootstrapping."""
def __init__(self, output_path: Path, verbose: bool = False):
super().__init__(output_path=output_path, verbose=verbose, flatten=False)
self.created_markers: list[Path] = []
def on_first_relpath(self, relpath: str):
if not relpath:
return # Skip empty relpaths
full_path = self.output_path / relpath
if full_path.exists():
print(f"CLEANING: {full_path}")
shutil.rmtree(full_path)
# Write the ".prebuilt" marker file
prebuilt_path = full_path.with_name(full_path.name + ".prebuilt")
prebuilt_path.parent.mkdir(parents=True, exist_ok=True)
prebuilt_path.touch()
self.created_markers.append(prebuilt_path)
def on_artifact_dir(self, artifact_dir: Path):
print(f"FLATTENING {artifact_dir.name}")
def on_artifact_archive(self, artifact_archive: Path):
print(f"EXPANDING {artifact_archive.name}")
def do_bootstrap(args: argparse.Namespace):
"""Bootstrap a build directory with artifacts from a prior build."""
build_dir: Path = args.build_dir
artifact_dir: Path = args.artifact_dir
if not artifact_dir.exists():
raise CLIError(f"Artifact directory not found: {artifact_dir}")
# Determine which artifacts to include based on stage
inbound_names: Optional[Set[str]] = None
if args.stage:
inbound_names = get_inbound_artifact_names(args.stage)
print(f"Stage '{args.stage}' requires {len(inbound_names)} inbound artifacts:")
for name in sorted(inbound_names):
print(f" - {name}")
print()
# Determine allowed target families
allowed_families = {"generic"}
if args.target_families:
# Support comma-separated list
for family in args.target_families.split(","):
allowed_families.add(family.strip())
print(f"Allowed target families: {', '.join(sorted(allowed_families))}")
print()
build_dir.mkdir(parents=True, exist_ok=True)
populator = BootstrappingPopulator(output_path=build_dir, verbose=args.verbose)
artifact_names: set[ArtifactName] = set()
processed_count = 0
skipped_count = 0
for entry in artifact_dir.iterdir():
an = ArtifactName.from_path(entry)
if not an:
continue
# Check target family
if an.target_family not in allowed_families:
if args.verbose:
print(
f"SKIP {entry.name}: Target family '{an.target_family}' not in allowed list"
)
skipped_count += 1
continue
# Check if artifact is in the inbound set (when stage filtering)
if inbound_names is not None and an.name not in inbound_names:
if args.verbose:
print(
f"SKIP {entry.name}: Not in inbound artifacts for stage '{args.stage}'"
)
skipped_count += 1
continue
# Skip duplicates
if an in artifact_names:
if args.verbose:
print(f"SKIP {entry.name}: Duplicate")
skipped_count += 1
continue
artifact_names.add(an)
populator(entry)
processed_count += 1
print(f"\nProcessed {processed_count} artifacts, skipped {skipped_count}")
class CLIError(Exception): ...
def main(cl_args: list[str]):
p = argparse.ArgumentParser("buildctl.py", usage="buildctl.py {command} ...")
sub_p = p.add_subparsers(required=True)
def add_common_options(command_p: argparse.ArgumentParser, handler):
command_p.set_defaults(func=handler)
command_p.add_argument(
"--build-dir",
type=Path,
help="Build directory (defaults to project level build/)",
)
def add_selection_options(command_p: argparse.ArgumentParser):
command_p.add_argument(
"--force-reconfigure",
action="store_true",
help="Reconfigure, even if not changed",
)
command_p.add_argument(
"include", nargs="*", help="Regular expressions to include (all if empty)"
)
command_p.add_argument(
"--exclude",
nargs="*",
help="Regular expressions to exclude (none if empty)",
)
# 'enable' command
enable_p = sub_p.add_parser("enable", help="Enable subset of projects as buildable")
add_common_options(enable_p, lambda args: do_enable_disable(args, enable_mode=True))
add_selection_options(enable_p)
# 'disable' command
disable_p = sub_p.add_parser(
"disable", help="Disable subset of projects as prebuilt"
)
add_common_options(
disable_p, lambda args: do_enable_disable(args, enable_mode=False)
)
add_selection_options(disable_p)
# 'bootstrap' command
bootstrap_p = sub_p.add_parser(
"bootstrap",
help="Bootstrap build directory with artifacts from a prior build",
)
bootstrap_p.set_defaults(func=do_bootstrap)
bootstrap_p.add_argument(
"--build-dir",
type=Path,
required=True,
help="Path to the CMake build directory to populate",
)
bootstrap_p.add_argument(
"--artifact-dir",
type=Path,
required=True,
help="Directory from which to source artifacts",
)
bootstrap_p.add_argument(
"--stage",
type=str,
help="Build stage to bootstrap for (uses BUILD_TOPOLOGY.toml to filter artifacts)",
)
bootstrap_p.add_argument(
"--target-families",
type=str,
help="Target families to include (comma-separated, e.g., 'gfx94X-dcgpu,gfx110X-all'). "
"Generic artifacts are always included.",
)
bootstrap_p.add_argument(
"--verbose", action="store_true", help="Print verbose status"
)
args = p.parse_args(cl_args)
try:
args.func(args)
except CLIError as e:
print(f"ERROR: {str(e)}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main(sys.argv[1:])