| #!/usr/bin/env python3 | |
| # Copyright 2026 The IREE Authors | |
| # | |
| # Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| # See https://llvm.org/LICENSE.txt for license information. | |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| """Builds precompiled AMDGPU HAL builtin device code objects. | |
| This script intentionally runs outside the normal runtime build. It preserves | |
| the existing in-tree/out-of-tree LLVM tool flows while allowing the runtime | |
| build to consume checked-in code objects without depending on LLVM. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import ast | |
| import os | |
| from pathlib import Path | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| from typing import Iterable, Sequence | |
| import amdgpu_target_map | |
| TARGET_TRIPLE = "amdgcn-amd-amdhsa" | |
| DEFAULT_TARGET_SELECTIONS = amdgpu_target_map.DEFAULT_TARGET_SELECTIONS | |
| DEVICE_BINARY_TARGET_ENV = "IREE_HAL_AMDGPU_DEVICE_BINARY_TARGETS" | |
| DEVICE_PACKAGE_PATH = Path("runtime/src/iree/hal/drivers/amdgpu/device") | |
| DEVICE_SOURCE_LIST_PATH = DEVICE_PACKAGE_PATH / "device_bitcode_sources.bzl" | |
| DEVICE_SOURCE_LIST_VARIABLE = "IREE_HAL_AMDGPU_DEVICE_BITCODE_SRCS" | |
| class Toolchain: | |
| def __init__( | |
| self, | |
| *, | |
| clang: Path, | |
| llvm_link: Path, | |
| lld: Path, | |
| llvm_objcopy: Path, | |
| clang_resource_include: Path, | |
| ): | |
| self.clang = clang | |
| self.llvm_link = llvm_link | |
| self.lld = lld | |
| self.llvm_objcopy = llvm_objcopy | |
| self.clang_resource_include = clang_resource_include | |
| def eprint(*args): | |
| print(*args, file=sys.stderr) | |
| def repo_root_from_script() -> Path: | |
| current = Path(__file__).resolve() | |
| for parent in [current.parent, *current.parents]: | |
| if (parent / "runtime" / "src" / "iree").is_dir(): | |
| return parent | |
| raise RuntimeError("could not find repository root from script location") | |
| def literal_string_list_from_ast( | |
| node: ast.AST, *, path: Path, variable_name: str | |
| ) -> list[str]: | |
| try: | |
| values = ast.literal_eval(node) | |
| except (SyntaxError, ValueError) as exc: | |
| raise RuntimeError( | |
| f"{path}: {variable_name} must be a literal string list" | |
| ) from exc | |
| if not isinstance(values, list): | |
| raise RuntimeError(f"{path}: {variable_name} must be a literal string list") | |
| if not all(isinstance(value, str) for value in values): | |
| raise RuntimeError(f"{path}: {variable_name} must contain only strings") | |
| return list(values) | |
| def load_device_source_paths(repo_root: Path) -> list[Path]: | |
| source_list_path = repo_root / DEVICE_SOURCE_LIST_PATH | |
| package_path = repo_root / DEVICE_PACKAGE_PATH | |
| module = ast.parse(source_list_path.read_text(), filename=str(source_list_path)) | |
| source_names = None | |
| for node in module.body: | |
| if not isinstance(node, ast.Assign): | |
| continue | |
| if any( | |
| isinstance(target, ast.Name) and target.id == DEVICE_SOURCE_LIST_VARIABLE | |
| for target in node.targets | |
| ): | |
| source_names = literal_string_list_from_ast( | |
| node.value, | |
| path=source_list_path, | |
| variable_name=DEVICE_SOURCE_LIST_VARIABLE, | |
| ) | |
| break | |
| if source_names is None: | |
| raise RuntimeError( | |
| f"{source_list_path}: missing {DEVICE_SOURCE_LIST_VARIABLE} assignment" | |
| ) | |
| source_paths: list[Path] = [] | |
| for source_name in source_names: | |
| source_path = Path(source_name) | |
| if source_path.is_absolute() or ".." in source_path.parts: | |
| raise RuntimeError( | |
| f"{source_list_path}: {DEVICE_SOURCE_LIST_VARIABLE} entry must be " | |
| f"package-relative: {source_name}" | |
| ) | |
| if source_path.suffix != ".c": | |
| raise RuntimeError( | |
| f"{source_list_path}: {DEVICE_SOURCE_LIST_VARIABLE} entry is " | |
| f"not a C source file: {source_name}" | |
| ) | |
| absolute_source_path = package_path / source_path | |
| if not absolute_source_path.is_file(): | |
| raise RuntimeError( | |
| f"{source_list_path}: {DEVICE_SOURCE_LIST_VARIABLE} entry " | |
| f"does not exist: {source_name}" | |
| ) | |
| source_paths.append(absolute_source_path) | |
| return source_paths | |
| def split_env_list(value: str | None) -> list[str]: | |
| if not value: | |
| return [] | |
| return [part for part in re.split(r"[,;:\s]+", value) if part] | |
| def append_unique(values: list[Path], new_values: Iterable[Path]): | |
| seen = {value.resolve() if value.exists() else value for value in values} | |
| for value in new_values: | |
| key = value.resolve() if value.exists() else value | |
| if key not in seen: | |
| values.append(value) | |
| seen.add(key) | |
| def executable_path(path: Path) -> Path | None: | |
| if path.is_file() and os.access(path, os.X_OK): | |
| return path | |
| return None | |
| def candidate_tool_dirs(args: argparse.Namespace) -> list[Path]: | |
| dirs: list[Path] = [] | |
| explicit_tool_dirs = [] | |
| for value in args.tool_dir or []: | |
| explicit_tool_dirs.extend(split_env_list(value)) | |
| append_unique(dirs, [Path(value) for value in explicit_tool_dirs]) | |
| explicit_rocm_roots = [] | |
| for value in args.rocm_path or []: | |
| explicit_rocm_roots.extend(Path(part) for part in split_env_list(value)) | |
| for root in explicit_rocm_roots: | |
| append_unique(dirs, rocm_tool_dirs(root)) | |
| tool_dir_envs = ( | |
| "IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_LLVM_TOOLS_DIR", | |
| "IREE_HOST_BIN_DIR", | |
| "IREE_HOST_TOOLS", | |
| "IREE_HOST_TOOLS_DIR", | |
| "IREE_BINARY_DIR", | |
| "IREE_LLVM_BINARY_DIR", | |
| "IREE_LLVM_TOOLS_DIR", | |
| "IREE_LLVM_TOOL_DIR", | |
| "LLVM_TOOLS_BINARY_DIR", | |
| "LLVM_BINARY_DIR", | |
| ) | |
| for env_var in tool_dir_envs: | |
| for value in split_env_list(os.environ.get(env_var)): | |
| path = Path(value) | |
| append_unique(dirs, [path.parent if path.is_file() else path]) | |
| expanded_dirs = [] | |
| for directory in dirs: | |
| expanded_dirs.append(directory) | |
| expanded_dirs.append(directory / "bin") | |
| expanded_dirs.append(directory / "llvm-project" / "bin") | |
| expanded_dirs.append(directory / "llvm" / "bin") | |
| expanded_dirs.append(directory / "lib" / "llvm" / "bin") | |
| dirs = [] | |
| append_unique(dirs, expanded_dirs) | |
| for root in candidate_rocm_roots(args, include_explicit=False): | |
| append_unique(dirs, rocm_tool_dirs(root)) | |
| path_dirs = [Path(value) for value in os.environ.get("PATH", "").split(os.pathsep)] | |
| append_unique(dirs, path_dirs) | |
| return dirs | |
| def candidate_rocm_roots( | |
| args: argparse.Namespace, *, include_explicit: bool = True | |
| ) -> list[Path]: | |
| roots: list[Path] = [] | |
| if include_explicit: | |
| for value in args.rocm_path or []: | |
| roots.extend(Path(part) for part in split_env_list(value)) | |
| for env_var in ( | |
| "IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_ROCM_PATH", | |
| "IREE_ROCM_PATH", | |
| "ROCM_PATH", | |
| "ROCM_ROOT", | |
| "ROCM_HOME", | |
| "HIP_PATH", | |
| ): | |
| for value in split_env_list(os.environ.get(env_var)): | |
| path = Path(value) | |
| roots.append(path) | |
| if env_var == "HIP_PATH" and path.name == "hip": | |
| roots.append(path.parent) | |
| if hipcc_path := shutil.which("hipcc"): | |
| roots.append(Path(hipcc_path).resolve().parent.parent) | |
| if Path("/opt/rocm").exists(): | |
| roots.append(Path("/opt/rocm")) | |
| result: list[Path] = [] | |
| append_unique(result, roots) | |
| return result | |
| def rocm_tool_dirs(root: Path) -> list[Path]: | |
| return [ | |
| root / "llvm" / "bin", | |
| root / "lib" / "llvm" / "bin", | |
| root / "bin", | |
| ] | |
| def find_tool( | |
| *, | |
| explicit_path: str | None, | |
| env_vars: Sequence[str], | |
| names: Sequence[str], | |
| tool_dirs: Sequence[Path], | |
| additional_candidates: Sequence[Path] = (), | |
| description: str, | |
| ) -> Path: | |
| candidates: list[Path] = [] | |
| if explicit_path: | |
| candidates.append(Path(explicit_path)) | |
| for env_var in env_vars: | |
| value = os.environ.get(env_var) | |
| if value: | |
| candidates.append(Path(value)) | |
| candidates.extend(additional_candidates) | |
| for directory in tool_dirs: | |
| for name in names: | |
| candidates.append(directory / name) | |
| for name in names: | |
| found = shutil.which(name) | |
| if found: | |
| candidates.append(Path(found)) | |
| for candidate in candidates: | |
| if executable := executable_path(candidate): | |
| return executable.resolve() | |
| env_list = ", ".join(env_vars) | |
| name_list = ", ".join(names) | |
| raise RuntimeError( | |
| f"could not find {description}; tried explicit flags, env vars " | |
| f"[{env_list}], tool dirs, and PATH names [{name_list}]" | |
| ) | |
| def run_capture(args: Sequence[str]) -> str: | |
| result = subprocess.run( | |
| args, | |
| check=True, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| ) | |
| return result.stdout.strip() | |
| def clang_tool_candidates(clang: Path, names: Sequence[str]) -> list[Path]: | |
| candidates: list[Path] = [] | |
| for name in names: | |
| try: | |
| printed_name = run_capture([str(clang), f"--print-prog-name={name}"]) | |
| except subprocess.CalledProcessError: | |
| continue | |
| if not printed_name: | |
| continue | |
| path = Path(printed_name) | |
| if path.is_absolute(): | |
| append_unique(candidates, [path]) | |
| else: | |
| append_unique(candidates, [clang.parent / path, path]) | |
| return candidates | |
| def detect_clang_resource_include(clang: Path, explicit_path: str | None) -> Path: | |
| if explicit_path: | |
| path = Path(explicit_path) | |
| elif os.environ.get("IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_CLANG_RESOURCE_INCLUDE"): | |
| path = Path( | |
| os.environ["IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_CLANG_RESOURCE_INCLUDE"] | |
| ) | |
| elif os.environ.get("IREE_CLANG_BUILTIN_HEADERS_PATH"): | |
| path = Path(os.environ["IREE_CLANG_BUILTIN_HEADERS_PATH"]) | |
| else: | |
| resource_dir = Path(run_capture([str(clang), "-print-resource-dir"])) | |
| path = resource_dir / "include" | |
| if not path.is_dir(): | |
| raise RuntimeError(f"clang resource include directory not found: {path}") | |
| return path.resolve() | |
| def detect_toolchain(args: argparse.Namespace) -> Toolchain: | |
| dirs = candidate_tool_dirs(args) | |
| clang = find_tool( | |
| explicit_path=args.clang, | |
| env_vars=( | |
| "IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_CLANG_BINARY", | |
| "IREE_CLANG_BINARY", | |
| "CLANG", | |
| ), | |
| names=("clang", "amdclang", "clang-22"), | |
| tool_dirs=dirs, | |
| description="clang with AMDGPU support", | |
| ) | |
| llvm_link = find_tool( | |
| explicit_path=args.llvm_link, | |
| env_vars=( | |
| "IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_LLVM_LINK_BINARY", | |
| "IREE_LLVM_LINK_BINARY", | |
| "LLVM_LINK", | |
| ), | |
| names=("llvm-link",), | |
| tool_dirs=dirs, | |
| additional_candidates=clang_tool_candidates(clang, ("llvm-link",)), | |
| description="llvm-link", | |
| ) | |
| lld = find_tool( | |
| explicit_path=args.lld, | |
| env_vars=( | |
| "IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_LLD_BINARY", | |
| "IREE_LLD_BINARY", | |
| "LLD", | |
| "LD_LLD", | |
| ), | |
| names=("lld", "ld.lld"), | |
| tool_dirs=dirs, | |
| additional_candidates=clang_tool_candidates(clang, ("lld", "ld.lld")), | |
| description="lld", | |
| ) | |
| llvm_objcopy = find_tool( | |
| explicit_path=args.llvm_objcopy, | |
| env_vars=( | |
| "IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_LLVM_OBJCOPY_BINARY", | |
| "IREE_LLVM_OBJCOPY_BINARY", | |
| "LLVM_OBJCOPY", | |
| ), | |
| names=("llvm-objcopy",), | |
| tool_dirs=dirs, | |
| additional_candidates=clang_tool_candidates(clang, ("llvm-objcopy",)), | |
| description="llvm-objcopy", | |
| ) | |
| return Toolchain( | |
| clang=clang, | |
| llvm_link=llvm_link, | |
| lld=lld, | |
| llvm_objcopy=llvm_objcopy, | |
| clang_resource_include=detect_clang_resource_include( | |
| clang, args.clang_resource_include | |
| ), | |
| ) | |
| def expand_target_selections(selections: Sequence[str]) -> list[str]: | |
| code_object_targets = [] | |
| all_exact_targets = set(amdgpu_target_map.exact_targets()) | |
| all_code_object_targets = set(amdgpu_target_map.code_object_targets()) | |
| exact_to_code_object = dict(amdgpu_target_map.EXACT_TARGET_CODE_OBJECTS) | |
| family_map = { | |
| family: amdgpu_target_map.family_targets(targets) | |
| for family, targets in amdgpu_target_map.TARGET_FAMILIES | |
| } | |
| def append_code_object(target: str): | |
| if target not in code_object_targets: | |
| code_object_targets.append(target) | |
| for selection in selections: | |
| if selection in all_code_object_targets: | |
| append_code_object(selection) | |
| elif selection in all_exact_targets: | |
| append_code_object(exact_to_code_object[selection]) | |
| elif selection in family_map: | |
| for exact_target in family_map[selection]: | |
| append_code_object(exact_to_code_object[exact_target]) | |
| else: | |
| available = sorted( | |
| all_code_object_targets | all_exact_targets | set(family_map) | |
| ) | |
| raise RuntimeError( | |
| f"unknown AMDGPU target selector '{selection}'. " | |
| f"Available selectors include: {', '.join(available)}" | |
| ) | |
| return code_object_targets | |
| def default_target_selections() -> tuple[str, ...]: | |
| env_value = os.environ.get(DEVICE_BINARY_TARGET_ENV) | |
| if env_value: | |
| return tuple(split_env_list(env_value)) | |
| return DEFAULT_TARGET_SELECTIONS | |
| def run_command(command: Sequence[str], *, verbose: bool, dry_run: bool): | |
| if verbose or dry_run: | |
| print(" ".join(command)) | |
| if dry_run: | |
| return | |
| subprocess.run(command, check=True) | |
| def compile_source( | |
| *, | |
| source_path: Path, | |
| output_path: Path, | |
| arch: str, | |
| repo_root: Path, | |
| binary_root: Path | None, | |
| toolchain: Toolchain, | |
| extra_copts: Sequence[str], | |
| verbose: bool, | |
| dry_run: bool, | |
| ): | |
| include_args = [ | |
| "-isystem", | |
| str(toolchain.clang_resource_include), | |
| "-I", | |
| str(repo_root / "runtime/src"), | |
| ] | |
| if binary_root: | |
| include_args.extend(["-I", str(binary_root / "runtime/src")]) | |
| command = [ | |
| str(toolchain.clang), | |
| "-x", | |
| "c", | |
| "-std=c23", | |
| "-Xclang", | |
| "-finclude-default-header", | |
| "-nogpulib", | |
| "-fno-short-wchar", | |
| "-target", | |
| TARGET_TRIPLE, | |
| f"-march={arch}", | |
| "-fgpu-rdc", | |
| *include_args, | |
| "-Wno-gnu-pointer-arith", | |
| "-fno-ident", | |
| "-fvisibility=hidden", | |
| "-O3", | |
| *extra_copts, | |
| "-c", | |
| "-emit-llvm", | |
| "-o", | |
| str(output_path), | |
| str(source_path), | |
| ] | |
| run_command(command, verbose=verbose, dry_run=dry_run) | |
| def link_bitcode( | |
| *, | |
| input_paths: Sequence[Path], | |
| output_path: Path, | |
| toolchain: Toolchain, | |
| verbose: bool, | |
| dry_run: bool, | |
| ): | |
| command = [str(toolchain.llvm_link), *map(str, input_paths), "-o", str(output_path)] | |
| run_command(command, verbose=verbose, dry_run=dry_run) | |
| def internalize_bitcode( | |
| *, | |
| input_path: Path, | |
| output_path: Path, | |
| toolchain: Toolchain, | |
| verbose: bool, | |
| dry_run: bool, | |
| ): | |
| command = [ | |
| str(toolchain.llvm_link), | |
| "-internalize", | |
| "-only-needed", | |
| str(input_path), | |
| "-o", | |
| str(output_path), | |
| ] | |
| run_command(command, verbose=verbose, dry_run=dry_run) | |
| def write_local_all_version_script(path: Path): | |
| path.write_text("{\n local:\n *;\n};\n") | |
| def link_code_object( | |
| *, | |
| input_path: Path, | |
| output_path: Path, | |
| arch: str, | |
| version_script: Path | None, | |
| toolchain: Toolchain, | |
| linkopts: Sequence[str], | |
| verbose: bool, | |
| dry_run: bool, | |
| ): | |
| command = [ | |
| str(toolchain.lld), | |
| "-flavor", | |
| "gnu", | |
| "-m", | |
| "elf64_amdgpu", | |
| "--build-id=none", | |
| "--no-undefined", | |
| "-shared", | |
| f"-plugin-opt=mcpu={arch}", | |
| "-plugin-opt=O3", | |
| "--lto-CGO3", | |
| "--no-whole-archive", | |
| "--gc-sections", | |
| "--strip-debug", | |
| "--discard-all", | |
| "--discard-locals", | |
| *linkopts, | |
| ] | |
| if version_script: | |
| command.append(f"--version-script={version_script}") | |
| command.extend([str(input_path), "-o", str(output_path)]) | |
| run_command(command, verbose=verbose, dry_run=dry_run) | |
| def minimize_code_object( | |
| *, | |
| input_path: Path, | |
| output_path: Path, | |
| toolchain: Toolchain, | |
| verbose: bool, | |
| dry_run: bool, | |
| ): | |
| command = [ | |
| str(toolchain.llvm_objcopy), | |
| "-R", | |
| ".comment", | |
| "-R", | |
| ".AMDGPU.gpr_maximums", | |
| "--discard-all", | |
| "-N", | |
| "_DYNAMIC", | |
| ] | |
| command.extend([str(input_path), str(output_path)]) | |
| run_command(command, verbose=verbose, dry_run=dry_run) | |
| def build_target( | |
| *, | |
| arch: str, | |
| source_paths: Sequence[Path], | |
| repo_root: Path, | |
| binary_root: Path | None, | |
| output_dir: Path, | |
| toolchain: Toolchain, | |
| minimize: bool, | |
| keep_intermediates: bool, | |
| extra_copts: Sequence[str], | |
| linkopts: Sequence[str], | |
| verbose: bool, | |
| dry_run: bool, | |
| ) -> dict: | |
| output_name = f"{TARGET_TRIPLE}--{arch}.so" | |
| output_path = output_dir / output_name | |
| if dry_run: | |
| work_dir = output_dir / f"{output_name}.work" | |
| work_dir.mkdir(parents=True, exist_ok=True) | |
| temp_dir_context = None | |
| elif keep_intermediates: | |
| work_dir = output_dir / f"{output_name}.work" | |
| if work_dir.exists(): | |
| shutil.rmtree(work_dir) | |
| work_dir.mkdir(parents=True) | |
| temp_dir_context = None | |
| else: | |
| temp_dir_context = tempfile.TemporaryDirectory( | |
| prefix=f"{output_name}.", dir=output_dir | |
| ) | |
| work_dir = Path(temp_dir_context.name) | |
| try: | |
| object_paths = [] | |
| for index, source_path in enumerate(source_paths): | |
| object_path = work_dir / f"{index}_{source_path.stem}.bc" | |
| compile_source( | |
| source_path=source_path, | |
| output_path=object_path, | |
| arch=arch, | |
| repo_root=repo_root, | |
| binary_root=binary_root, | |
| toolchain=toolchain, | |
| extra_copts=extra_copts, | |
| verbose=verbose, | |
| dry_run=dry_run, | |
| ) | |
| object_paths.append(object_path) | |
| archive_path = work_dir / f"{arch}.archive.bc" | |
| link_bitcode( | |
| input_paths=object_paths, | |
| output_path=archive_path, | |
| toolchain=toolchain, | |
| verbose=verbose, | |
| dry_run=dry_run, | |
| ) | |
| linked_bitcode_path = work_dir / f"{arch}.linked.bc" | |
| internalize_bitcode( | |
| input_path=archive_path, | |
| output_path=linked_bitcode_path, | |
| toolchain=toolchain, | |
| verbose=verbose, | |
| dry_run=dry_run, | |
| ) | |
| version_script = None | |
| linked_code_object_path = output_path | |
| if minimize: | |
| version_script = work_dir / "local-all.version" | |
| if not dry_run: | |
| write_local_all_version_script(version_script) | |
| linked_code_object_path = work_dir / output_name | |
| link_code_object( | |
| input_path=linked_bitcode_path, | |
| output_path=linked_code_object_path, | |
| arch=arch, | |
| version_script=version_script, | |
| toolchain=toolchain, | |
| linkopts=linkopts, | |
| verbose=verbose, | |
| dry_run=dry_run, | |
| ) | |
| if minimize: | |
| minimize_code_object( | |
| input_path=linked_code_object_path, | |
| output_path=output_path, | |
| toolchain=toolchain, | |
| verbose=verbose, | |
| dry_run=dry_run, | |
| ) | |
| if output_path.exists(): | |
| output_path.chmod(0o644) | |
| size = output_path.stat().st_size if output_path.exists() else 0 | |
| return { | |
| "target": arch, | |
| "path": output_name, | |
| "size": size, | |
| } | |
| finally: | |
| if temp_dir_context is not None: | |
| temp_dir_context.cleanup() | |
| def supported_amdgpu_processors(clang: Path) -> set[str]: | |
| try: | |
| output = run_capture( | |
| [str(clang), "-target", TARGET_TRIPLE, "--print-supported-cpus"] | |
| ) | |
| except subprocess.CalledProcessError: | |
| return set() | |
| processors = set() | |
| for line in output.splitlines(): | |
| candidate = line.strip().split(" ", 1)[0] | |
| if candidate.startswith("gfx") or candidate in ("generic", "generic-hsa"): | |
| processors.add(candidate) | |
| return processors | |
| def validate_toolchain_targets( | |
| toolchain: Toolchain, code_object_targets: Sequence[str] | |
| ): | |
| supported_processors = supported_amdgpu_processors(toolchain.clang) | |
| if not supported_processors: | |
| return | |
| unsupported_targets = [ | |
| target for target in code_object_targets if target not in supported_processors | |
| ] | |
| if unsupported_targets: | |
| raise RuntimeError( | |
| "selected AMDGPU code-object target(s) are not reported by clang: " | |
| f"{', '.join(unsupported_targets)}. Point --rocm-path/--tool-dir at " | |
| "a newer LLVM/ROCm toolchain or choose a smaller --targets set." | |
| ) | |
| def parse_args(argv: Sequence[str]) -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="Build precompiled AMDGPU HAL builtin device binaries." | |
| ) | |
| parser.add_argument( | |
| "--repo-root", | |
| type=Path, | |
| default=repo_root_from_script(), | |
| help="IREE repository root containing runtime/src. Defaults to auto-detection.", | |
| ) | |
| default_binary_root = ( | |
| Path(os.environ["IREE_BINARY_DIR"]) | |
| if os.environ.get("IREE_BINARY_DIR") | |
| else None | |
| ) | |
| parser.add_argument( | |
| "--binary-root", | |
| type=Path, | |
| default=default_binary_root, | |
| help="Optional build tree root to add as a generated include root.", | |
| ) | |
| parser.add_argument( | |
| "--output-dir", | |
| type=Path, | |
| required=True, | |
| help="Directory to receive generated code objects.", | |
| ) | |
| parser.add_argument( | |
| "--targets", | |
| default=",".join(default_target_selections()), | |
| help=( | |
| "Comma/space separated exact targets, code-object targets, or families. " | |
| f"Defaults to ${DEVICE_BINARY_TARGET_ENV} or " | |
| f"{','.join(DEFAULT_TARGET_SELECTIONS)}." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--all-targets", | |
| action="store_true", | |
| help="Build every known code-object target.", | |
| ) | |
| parser.add_argument( | |
| "--tool-dir", | |
| action="append", | |
| help=( | |
| "Directory containing clang, llvm-link, lld, and llvm-objcopy. " | |
| "May be repeated." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--rocm-path", | |
| action="append", | |
| help=( | |
| "ROCm installation root. May be repeated. If omitted, the script " | |
| "checks IREE_HAL_AMDGPU_DEVICE_TOOLCHAIN_ROCM_PATH, IREE_ROCM_PATH, " | |
| "ROCM_PATH, ROCM_ROOT, ROCM_HOME, HIP_PATH, hipcc on PATH, and " | |
| "/opt/rocm." | |
| ), | |
| ) | |
| parser.add_argument("--clang", help="Explicit clang/amdclang executable.") | |
| parser.add_argument("--llvm-link", help="Explicit llvm-link executable.") | |
| parser.add_argument("--lld", help="Explicit lld or ld.lld executable.") | |
| parser.add_argument("--llvm-objcopy", help="Explicit llvm-objcopy executable.") | |
| parser.add_argument( | |
| "--clang-resource-include", | |
| help=( | |
| "Explicit clang resource include directory. Defaults to " | |
| "IREE_CLANG_BUILTIN_HEADERS_PATH or clang -print-resource-dir/include." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--minimize", | |
| dest="minimize", | |
| action=argparse.BooleanOptionalAction, | |
| default=True, | |
| help=( | |
| "Keep only the exported .kd symbol surface in regular symbol tables " | |
| "after linking. Enabled by default." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--keep-intermediates", | |
| action="store_true", | |
| help="Keep per-target bitcode and intermediate code objects next to outputs.", | |
| ) | |
| parser.add_argument( | |
| "--copt", | |
| action="append", | |
| default=[], | |
| help="Additional clang compile option. May be repeated.", | |
| ) | |
| parser.add_argument( | |
| "--linkopt", | |
| action="append", | |
| default=[], | |
| help="Additional lld link option. May be repeated.", | |
| ) | |
| parser.add_argument( | |
| "--dry-run", | |
| action="store_true", | |
| help="Print commands without executing them.", | |
| ) | |
| parser.add_argument( | |
| "--verbose", | |
| action="store_true", | |
| help="Print commands as they execute.", | |
| ) | |
| return parser.parse_args(argv) | |
| def main(argv: Sequence[str]) -> int: | |
| args = parse_args(argv) | |
| repo_root = args.repo_root.resolve() | |
| output_dir = args.output_dir.resolve() | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| selections = ( | |
| amdgpu_target_map.code_object_targets() | |
| if args.all_targets | |
| else split_env_list(args.targets) | |
| ) | |
| code_object_targets = expand_target_selections(selections) | |
| source_paths = load_device_source_paths(repo_root) | |
| toolchain = detect_toolchain(args) | |
| validate_toolchain_targets(toolchain, code_object_targets) | |
| eprint("AMDGPU device binary generator") | |
| eprint(f" repo root: {repo_root}") | |
| eprint(f" output dir: {output_dir}") | |
| eprint(f" targets: {', '.join(code_object_targets)}") | |
| eprint( | |
| " sources: " | |
| + ", ".join(str(path.relative_to(repo_root)) for path in source_paths) | |
| ) | |
| eprint(f" clang: {toolchain.clang}") | |
| eprint(f" llvm-link: {toolchain.llvm_link}") | |
| eprint(f" lld: {toolchain.lld}") | |
| eprint(f" llvm-objcopy: {toolchain.llvm_objcopy}") | |
| eprint(f" clang resource include: {toolchain.clang_resource_include}") | |
| outputs = [] | |
| for arch in code_object_targets: | |
| eprint(f"Building {arch}...") | |
| outputs.append( | |
| build_target( | |
| arch=arch, | |
| source_paths=source_paths, | |
| repo_root=repo_root, | |
| binary_root=args.binary_root.resolve() if args.binary_root else None, | |
| output_dir=output_dir, | |
| toolchain=toolchain, | |
| minimize=args.minimize, | |
| keep_intermediates=args.keep_intermediates, | |
| extra_copts=args.copt, | |
| linkopts=args.linkopt, | |
| verbose=args.verbose, | |
| dry_run=args.dry_run, | |
| ) | |
| ) | |
| if not args.dry_run: | |
| total_size = sum(output["size"] for output in outputs) | |
| for output in outputs: | |
| eprint(f" wrote {output['path']}: {output['size']} bytes") | |
| eprint(f"Total code-object size: {total_size} bytes") | |
| return 0 | |
| def run_cli(argv: Sequence[str]) -> int: | |
| try: | |
| return main(argv) | |
| except RuntimeError as exc: | |
| eprint(f"error: {exc}") | |
| return 1 | |
| except subprocess.CalledProcessError as exc: | |
| if exc.returncode < 0: | |
| detail = f"signal {-exc.returncode}" | |
| else: | |
| detail = f"exit code {exc.returncode}" | |
| eprint(f"error: command failed with {detail}: {' '.join(exc.cmd)}") | |
| return 1 | |
| if __name__ == "__main__": | |
| sys.exit(run_cli(sys.argv[1:])) |