Files
TheRock/cmake/therock_artifacts.cmake
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

435 lines
16 KiB
CMake

# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
# therock_artifacts.cmake
# Facilities for bundling artifacts for bootstrapping and subsequent CI/CD
# phases.
# therock_provide_artifact
# This populates directories under build/artifacts representing specific
# subsets of the install tree. See docs/development/artifacts.md for further
# design notes on the subsystem.
#
# While artifacts are the primary output of the build system, it is often
# an aid to development to materialize them all locally into a `distribution`.
# These are directories under build/dist/${ARG_DISTRIBUTION} (default "rocm").
#
# All artifact slices of a distribution should be non-overlapping, populating
# some subset of the install directory tree.
#
# This will produce the following convenience targets:
# - artifact-${slice_name} : Populate the build/artifacts/{qualified_name}
# directory. Added as a dependency of the `therock-artifacts` target.
#
# Convenience targets with a "+expunge" suffix are created to remove corresponding
# files. Invoking the project level "expunge" will depend on all of them.
function(therock_provide_artifact slice_name)
cmake_parse_arguments(PARSE_ARGV 1 ARG
"TARGET_NEUTRAL"
"DESCRIPTOR;DISTRIBUTION"
"COMPONENTS;SUBPROJECT_DEPS"
)
if(NOT ${slice_name} MATCHES "^[A-Za-z][A-Za-z0-9-]*$")
message(FATAL_ERROR
"Artifact slice name '${slice_name}' must start with a letter "
"and may only contain alphanumeric characters and dashes"
)
endif()
# Fail-fast: Check if artifact is defined in topology
if(DEFINED THEROCK_TOPOLOGY_ARTIFACTS)
if(NOT "${slice_name}" IN_LIST THEROCK_TOPOLOGY_ARTIFACTS)
message(FATAL_ERROR
"Artifact '${slice_name}' is not defined in BUILD_TOPOLOGY.toml. "
"All artifacts must be declared in the topology. "
"Valid artifacts are: ${THEROCK_TOPOLOGY_ARTIFACTS}"
)
endif()
endif()
# Determine if this artifact should be split into generic + arch-specific components
set(_should_split FALSE)
if(THEROCK_FLAG_KPACK_SPLIT_ARTIFACTS)
set(_artifact_type "${THEROCK_ARTIFACT_TYPE_${slice_name}}")
if(NOT _artifact_type)
message(FATAL_ERROR
"THEROCK_FLAG_KPACK_SPLIT_ARTIFACTS is enabled but THEROCK_ARTIFACT_TYPE_${slice_name} "
"is not defined. Ensure topology_to_cmake.py has been run."
)
endif()
if("${_artifact_type}" STREQUAL "target-specific")
set(_should_split TRUE)
set(_split_databases "${THEROCK_ARTIFACT_SPLIT_DATABASES_${slice_name}}")
endif()
endif()
# Record artifact→subprojects mapping for manifest generation.
# This allows Python scripts to dynamically discover which subprojects
# belong to each artifact without hardcoding in BUILD_TOPOLOGY.toml.
if(ARG_SUBPROJECT_DEPS)
# Convert list to comma-separated string and store
string(REPLACE ";" "," _subprojects_csv "${ARG_SUBPROJECT_DEPS}")
set_property(GLOBAL APPEND PROPERTY THEROCK_ARTIFACT_SUBPROJECT_MAP
"${slice_name}:${_subprojects_csv}"
)
endif()
# Normalize arguments.
set(_target_name "artifact-${slice_name}")
# Check if targets exist from topology (expected) vs duplicate definition (error)
set(_target_exists FALSE)
if(TARGET "${_target_name}")
# Target exists - check if it's from topology or a duplicate
# If THEROCK_TOPOLOGY_ARTIFACTS is defined, we expect the target to exist
if(DEFINED THEROCK_TOPOLOGY_ARTIFACTS)
set(_target_exists TRUE)
else()
message(FATAL_ERROR "Artifact slice '${slice_name}' provided more than once")
endif()
endif()
if(NOT ARG_DESCRIPTOR)
set(ARG_DESCRIPTOR "artifact.toml")
endif()
cmake_path(ABSOLUTE_PATH ARG_DESCRIPTOR BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
file(SHA256 "${ARG_DESCRIPTOR}" _descriptor_fprint)
if(NOT DEFINED ARG_DISTRIBUTION)
set(ARG_DISTRIBUTION "rocm")
endif()
if(ARG_DISTRIBUTION)
set(_dist_dir "${THEROCK_BINARY_DIR}/dist/${ARG_DISTRIBUTION}")
# First time we see the distribution, set up targets and install.
if(NOT TARGET "dist-${ARG_DISTRIBUTION}")
add_custom_target("dist-${ARG_DISTRIBUTION}" ALL)
add_dependencies(therock-dist "dist-${ARG_DISTRIBUTION}")
# expunge target for the dist
add_custom_target(
"dist-${ARG_DISTRIBUTION}+expunge"
COMMAND
"${CMAKE_COMMAND}" -E rm -rf "${_dist_dir}"
VERBATIM
)
add_dependencies(therock-expunge "dist-${ARG_DISTRIBUTION}+expunge")
# Add install()
install(
DIRECTORY "${_dist_dir}/"
DESTINATION "."
COMPONENT "${ARG_DISTRIBUTION}"
USE_SOURCE_PERMISSIONS
)
add_custom_target(
"install-${ARG_DISTRIBUTION}"
COMMAND
"${CMAKE_COMMAND}"
--install "${THEROCK_BINARY_DIR}"
--component "${ARG_DISTRIBUTION}"
DEPENDS
"dist-${ARG_DISTRIBUTION}"
VERBATIM
)
endif()
endif()
# Determine top-level name.
if(ARG_TARGET_NEUTRAL)
set(_bundle_suffix "_generic")
else()
set(_bundle_suffix "_${THEROCK_AMDGPU_DIST_BUNDLE_NAME}")
endif()
### Generate artifact directories.
# Determine dependencies.
set(_stamp_file_deps)
_therock_cmake_subproject_deps_to_stamp(_stamp_file_deps "stage.stamp" ${ARG_SUBPROJECT_DEPS})
# Compute fingerprint of dependencies.
# TODO: Potentially prime content with some environment/machine state.
set(_fprint_content "ARTIFACT=${slice_name}" "DESCRIPTOR=${_descriptor_fprint}")
set(_fprint_is_valid TRUE)
foreach(_subproject_dep ${ARG_SUBPROJECT_DEPS})
get_target_property(_subproject_fprint "${_subproject_dep}" THEROCK_FPRINT)
if(_subproject_fprint)
list(APPEND _fprint_content "${_subproject_dep}=${_subproject_fprint}")
else()
message(STATUS "Cannot compute fprint for artifact ${slice_name} (no fprint for ${_subproject_dep})")
set(_fprint_is_valid FALSE)
endif()
endforeach()
set(_fprint)
if(_fprint_is_valid)
string(SHA256 _fprint "${_fprint_content}")
endif()
# Write fingerprint files for each component (configure-time).
# TODO: Also write fingerprint files when splitting is enabled.
if(NOT _should_split)
set(_artifacts_dir "${THEROCK_BINARY_DIR}/artifacts")
file(MAKE_DIRECTORY "${_artifacts_dir}")
foreach(_component ${ARG_COMPONENTS})
set(_fprint_file "${_artifacts_dir}/${slice_name}_${_component}${_bundle_suffix}.fprint")
if(_fprint_is_valid)
file(WRITE "${_fprint_file}" "${_fprint}")
elseif(EXISTS "${_fprint_file}")
file(REMOVE "${_fprint_file}")
endif()
endforeach()
endif()
# Populate commands.
set(_fileset_tool "${THEROCK_SOURCE_DIR}/build_tools/fileset_tool.py")
set(_artifact_command
COMMAND "${Python3_EXECUTABLE}" "${_fileset_tool}" artifact
--root-dir "${THEROCK_BINARY_DIR}" --descriptor "${ARG_DESCRIPTOR}"
--artifact-name "${slice_name}"
)
set(_flatten_command_list)
set(_manifest_files)
set(_component_dirs)
# When splitting, populate to artifacts-unsplit/ first, then split to artifacts/
if(_should_split)
set(_artifacts_base_dir "${THEROCK_BINARY_DIR}/artifacts-unsplit")
else()
set(_artifacts_base_dir "${THEROCK_BINARY_DIR}/artifacts")
endif()
foreach(_component ${ARG_COMPONENTS})
set(_component_dir "${_artifacts_base_dir}/${slice_name}_${_component}${_bundle_suffix}")
list(APPEND _component_dirs "${_component_dir}")
set(_manifest_file "${_component_dir}/artifact_manifest.txt")
list(APPEND _manifest_files "${_manifest_file}")
# The 'artifact' command takes an alternating list of component name and
# directory to populate.
list(APPEND _artifact_command
"${_component}"
"${_component_dir}"
)
endforeach()
# Populate the corresponding build/dist/DISTRIBUTION directory.
# Only flatten in the populate command for non-split artifacts.
# Split artifacts get a post-split flatten step below (artifact-flatten-split)
# that reads from split outputs (new inodes) instead of from the
# multiply-aliased unsplit hardlinks.
if(ARG_DISTRIBUTION AND NOT _should_split)
list(APPEND _flatten_command_list
COMMAND "${Python3_EXECUTABLE}" "${_fileset_tool}" artifact-flatten
-o "${_dist_dir}" ${_component_dirs}
)
endif()
add_custom_command(
OUTPUT ${_manifest_files}
COMMENT "Populate artifact ${slice_name}"
${_artifact_command}
${_flatten_command_list}
DEPENDS
${_stamp_file_deps}
"${ARG_DESCRIPTOR}"
"${_fileset_tool}"
VERBATIM
)
# When splitting is enabled, run split_artifacts.py on each component
if(_should_split)
set(_split_tool "${THEROCK_ROCM_SYSTEMS_SOURCE_DIR}/shared/kpack/python/rocm_kpack/tools/split_artifacts.py")
set(_bundler_path "${THEROCK_BINARY_DIR}/compiler/amd-llvm/dist/lib/llvm/bin/clang-offload-bundler")
set(_split_manifest_files)
set(_split_component_dirs)
foreach(_component ${ARG_COMPONENTS})
set(_unsplit_component_dir "${_artifacts_base_dir}/${slice_name}_${_component}${_bundle_suffix}")
set(_unsplit_manifest "${_unsplit_component_dir}/artifact_manifest.txt")
set(_artifact_prefix "${slice_name}_${_component}")
# The split output generic manifest (used as dependency tracking output)
set(_split_generic_dir "${THEROCK_BINARY_DIR}/artifacts/${_artifact_prefix}_generic")
set(_split_manifest "${_split_generic_dir}/artifact_manifest.txt")
list(APPEND _split_manifest_files "${_split_manifest}")
list(APPEND _split_component_dirs "${_split_generic_dir}")
# Build split command arguments
set(_split_command_args
--artifact-dir "${_unsplit_component_dir}"
--output-dir "${THEROCK_BINARY_DIR}/artifacts/"
--artifact-prefix "${_artifact_prefix}"
--clang-offload-bundler "${_bundler_path}"
)
if(_split_databases)
list(APPEND _split_command_args --split-databases ${_split_databases})
endif()
if(THEROCK_AMDGPU_TARGETS AND NOT "${THEROCK_AMDGPU_TARGETS}" STREQUAL "THEROCK_AMDGPU_TARGETS-NOTFOUND")
list(APPEND _split_command_args --gpu-targets ${THEROCK_AMDGPU_TARGETS})
endif()
add_custom_command(
OUTPUT "${_split_manifest}"
COMMENT "Splitting ${_artifact_prefix} into generic and arch-specific artifacts"
COMMAND "${CMAKE_COMMAND}" -E env "PYTHONPATH=${THEROCK_ROCM_SYSTEMS_SOURCE_DIR}/shared/kpack/python"
"${Python3_EXECUTABLE}" "${_split_tool}" ${_split_command_args}
DEPENDS
"${_unsplit_manifest}"
"${_split_tool}"
VERBATIM
)
endforeach()
# Flatten split artifacts to dist/DISTRIBUTION after all splits complete.
# This uses artifact-flatten-split which discovers split output dirs by
# globbing at build time (per-target dir names can't be predicted at
# configure time due to xnack suffixes and clang defaults).
# Reading from split outputs (new inodes via shutil.copy2) instead of
# from artifacts-unsplit/ (hardlinks aliased to stage/) breaks the
# aliasing chain that caused intermittent SIGBUS/truncation in #3447.
if(ARG_DISTRIBUTION)
set(_artifact_prefixes)
foreach(_component ${ARG_COMPONENTS})
list(APPEND _artifact_prefixes "${slice_name}_${_component}")
endforeach()
set(_flatten_stamp "${THEROCK_BINARY_DIR}/artifacts/.flatten-${slice_name}.stamp")
add_custom_command(
OUTPUT "${_flatten_stamp}"
COMMENT "Flatten split artifacts for ${slice_name} to dist/${ARG_DISTRIBUTION}"
COMMAND "${Python3_EXECUTABLE}" "${_fileset_tool}" artifact-flatten-split
-o "${_dist_dir}"
--artifacts-dir "${THEROCK_BINARY_DIR}/artifacts"
${_artifact_prefixes}
COMMAND "${CMAKE_COMMAND}" -E touch "${_flatten_stamp}"
DEPENDS
${_split_manifest_files}
"${_fileset_tool}"
VERBATIM
)
endif()
# IMPORTANT: Redirect downstream targets to depend on split artifacts.
#
# The split command depends on the unsplit manifest, preserving the
# dependency chain: stage.stamp -> unsplit manifest -> split manifest.
#
# The splitter produces multiple outputs (generic + per-arch artifacts)
# but only the generic manifest is tracked here. This means:
# - Ninja will rebuild split artifacts when source changes
# - If someone deletes an arch-specific artifact, ninja won't notice
# (the generic manifest still exists)
# - This is acceptable since arch-specific artifacts are derived outputs
set(_manifest_files ${_split_manifest_files})
set(_component_dirs ${_split_component_dirs})
# Also depend on the flatten stamp so dist/DISTRIBUTION is populated
# before the artifact target is considered complete.
if(ARG_DISTRIBUTION)
list(APPEND _manifest_files "${_flatten_stamp}")
endif()
endif()
# If target exists from topology, create a helper target for file dependencies
if(_target_exists)
# Target already exists from topology - create a helper target for file dependencies
add_custom_target(
"${_target_name}_files"
DEPENDS ${_manifest_files}
)
add_dependencies("${_target_name}" "${_target_name}_files")
else()
# Create new target (fallback for when topology is not loaded)
add_custom_target(
"${_target_name}"
DEPENDS ${_manifest_files}
)
endif()
add_dependencies(therock-artifacts "${_target_name}")
if(ARG_DISTRIBUTION)
add_dependencies("dist-${ARG_DISTRIBUTION}" "${_target_name}")
endif()
# Generate expunge targets.
set(_expunge_paths ${_component_dirs})
if(_should_split AND ARG_DISTRIBUTION)
list(APPEND _expunge_paths "${THEROCK_BINARY_DIR}/artifacts/.flatten-${slice_name}.stamp")
endif()
add_custom_target(
"${_target_name}+expunge"
COMMAND
"${CMAKE_COMMAND}" -E rm -rf ${_expunge_paths}
VERBATIM
)
add_dependencies(therock-expunge "${_target_name}+expunge")
add_dependencies("dist-${ARG_DISTRIBUTION}+expunge" "${_target_name}+expunge")
# For each subproject dep, we add a dependency on its +dist target to also
# trigger overall artifact construction. In this way `ninja myfoo+dist`
# will always populate all related artifacts and distributions. Note that
# this only applies to the convenience +dist target, not the underlying
# stamp-file chain, which is what the core dependency mechanism uses.
if(ARG_DISTRIBUTION)
foreach(subproject_dep ${ARG_SUBPROJECT_DEPS})
set(_subproject_dist_target "${subproject_dep}+dist")
if(NOT TARGET "${_subproject_dist_target}")
message(FATAL_ERROR "Subproject convenience target ${_subproject_dist_target} does not exist")
endif()
add_dependencies("${_subproject_dist_target}" "${_target_name}")
endforeach()
endif()
endfunction()
# therock_write_subproject_manifest
# Writes artifact→subprojects mapping to a JSON file that Python scripts
# can use for dynamic project resolution. This should be called at the end
# of the top-level CMakeLists.txt after all therock_provide_artifact() calls.
#
# Output: ${THEROCK_BINARY_DIR}/artifact_subprojects.json
function(therock_write_subproject_manifest)
get_property(_mappings GLOBAL PROPERTY THEROCK_ARTIFACT_SUBPROJECT_MAP)
if(NOT _mappings)
message(STATUS "No artifact subproject mappings to write")
return()
endif()
# Sort mappings for deterministic output
list(SORT _mappings)
# Build JSON content
set(_json_content "{\n")
set(_first TRUE)
foreach(_mapping ${_mappings})
# Parse "artifact_name:subproject1,subproject2,..."
string(FIND "${_mapping}" ":" _colon_pos)
if(_colon_pos EQUAL -1)
continue()
endif()
string(SUBSTRING "${_mapping}" 0 ${_colon_pos} _artifact_name)
math(EXPR _value_start "${_colon_pos} + 1")
string(SUBSTRING "${_mapping}" ${_value_start} -1 _subprojects_csv)
# Sort subprojects for deterministic output
string(REPLACE "," ";" _subprojects_list "${_subprojects_csv}")
list(SORT _subprojects_list)
string(REPLACE ";" "\", \"" _subprojects_json "${_subprojects_list}")
# Add comma before all entries except the first
if(_first)
set(_first FALSE)
else()
string(APPEND _json_content ",\n")
endif()
string(APPEND _json_content " \"${_artifact_name}\": [\"${_subprojects_json}\"]")
endforeach()
string(APPEND _json_content "\n}\n")
# Write to file
set(_manifest_path "${THEROCK_BINARY_DIR}/artifact_subprojects.json")
file(WRITE "${_manifest_path}" "${_json_content}")
message(STATUS "Wrote artifact subproject manifest to ${_manifest_path}")
endfunction()