# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This file handles logic relating to building "fat" static libraries, that is, ones which contain other static libraries. # Idiomatically, CMake would prefer we ship all static libraries as part of our release and ship a file which specifies the linkages. # However, for various reasons, this is both inadvisable and annoying. Instead, we would prefer to ship one static library containing all dependencies. # # To do that, we need rules to bundle static libraries into other static libraries. Hence, this class. define_property(TARGET PROPERTY BUNDLED_LIBRARY_TEMPLATE_PATH BRIEF_DOCS "File path to the template script which will be written to when calling target_bundle_libraries." ) define_property(TARGET PROPERTY BUNDLE_LIBRARIES BRIEF_DOCS "The list of libraries that have been bundled into this target by target_bundle_libraries." ) define_property(TARGET PROPERTY BUNDLE_LIBRARY_KNOWN_TYPE BRIEF_DOCS "Fallback type used when a target's TYPE is UNKNOWN_LIBRARY." ) define_property(TARGET PROPERTY BUNDLE_VISITED_INTERFACES BRIEF_DOCS "Interface libraries already traversed by __bundleRecursiveDeps for this target." ) # Internal helper to prefix all messages with "[target_bundle_libraries]: ". # # \param mode The message mode to be passed to message(...) # \param argn The message contents. macro(__bundleMessage mode) message(${mode} "[target_bundle_libraries]: " ${ARGN}) endmacro() # Illegal genex magicâ„¢ # Given a string containing a generator expression (var), edits the variable in-place to escape any generator expression literals. # The escaped generator expression is then suitable for use within a LIST:TRANSFORM replacement block. # This more or less allows for mapping lists to generator expressions, which can be recursively evaluated. function(escape_generator_expression var) string(REPLACE ">" "__ANGLE_R__" ${var} "${${var}}") string(REPLACE "$" "$<1:$>" ${var} "${${var}}") string(REPLACE "," "$" ${var} "${${var}}") string(REPLACE "__ANGLE_R__" "$" ${var} "${${var}}") return(PROPAGATE ${var}) endfunction() # Recursively unwraps alias targets until finding the real target. Non-targets are returned verbatim. # We need to ensure that the generated .mri script contains a deduplicated list of bundled targets # so we need to resolve aliases, otherwise we may end up with multiple entries for the same target. # # \param target_name The name of the target to unwrap. # \param result_var The variable to store the unwrapped target name in. function(unwrapAlias target_name result_var) # Check cache first (keyed on the raw input string). get_property(_cache_set GLOBAL PROPERTY _UNWRAP_ALIAS_CACHE_${target_name} SET) if(_cache_set) get_property(_cached GLOBAL PROPERTY _UNWRAP_ALIAS_CACHE_${target_name}) set(${result_var} ${_cached} PARENT_SCOPE) return() endif() # First, try to unwrap common generator expressions that may wrap the target name. string(REGEX MATCH "\\$" _ ${target_name}) if(TARGET ${CMAKE_MATCH_1}) set(target_name ${CMAKE_MATCH_1}) endif() string(REGEX MATCH "\\$" _ ${target_name}) if(TARGET ${CMAKE_MATCH_1}) set(target_name ${CMAKE_MATCH_1}) endif() if(TARGET ${target_name}) get_target_property(aliased_target ${target_name} ALIASED_TARGET) if(aliased_target) # Recursively unwrap in case there are multiple levels unwrapAlias(${aliased_target} unwrapped) set(_result ${unwrapped}) else() # Not an alias, return the original name set(_result ${target_name}) endif() else() # Not a target at all, return the original name set(_result ${target_name}) endif() set_property(GLOBAL PROPERTY _UNWRAP_ALIAS_CACHE_${target_name} ${_result}) set(${result_var} ${_result} PARENT_SCOPE) endfunction() # Internal function to retrieve the type of library for a given target. # This function will fallback to the value of BUNDLE_LIBRARY_KNOWN_TYPE if it encounters an UNKNOWN_LIBRARY. # # \param lib A target to evaluate the type for. # \param outVar The output variable to store the type name in. function(__get_lib_type lib outVar) get_target_property(libType ${lib} TYPE) if (${libType} STREQUAL UNKNOWN_LIBRARY) get_target_property(knownType ${lib} BUNDLE_LIBRARY_KNOWN_TYPE) if (NOT ${knownType} STREQUAL "knownType-NOTFOUND") set(libType ${knownType}) endif() __bundleMessage(DEBUG "Using known type of unknown library ${lib}: ${knownType}") endif() set(${outVar} ${libType} PARENT_SCOPE) endfunction() # This is an internal-only function called the first time that target_bundle_libraries is called. # It "registers" a target for bundling by creating the base template file and populating the target property BUNDLED_LIBRARY_TEMPLATE_PATH. # Additionally, it registers the file generation logic for making the "final" script, as well as the custom command for running it after the build. # # \param lib The mainLib from target_bundle_libraries. # \param templatePath The file path to the template file that will be created. function(__registerTargetForBundling lib templatePath) if(MSVC) set(scriptPath $/archive-${lib}.bat) set(template "/OUT:\"$\" \"$\"\n") # Windows-syntax version of the same logic from the linux build below. # Main differences is that windows does not have `addlib`, and uses `\n` instead of `\n`. Additionally, the values need to be quoted to account for spaces in file paths. set(replaceExpr "\"$,$,\\1>\"") escape_generator_expression(replaceExpr) string(APPEND template "$,REPLACE,(.+),${replaceExpr}>,\n>>\n") file(WRITE ${templatePath} ${template}) file(GENERATE OUTPUT ${scriptPath} INPUT ${templatePath} ) add_custom_command(TARGET ${lib} POST_BUILD COMMAND ${CMAKE_AR} /NOLOGO @\"${scriptPath}\" COMMAND ${CMAKE_COMMAND} -E echo "Bundled $> static libraries into target ${lib}. Script: ${scriptPath}" WORKING_DIRECTORY $ ) else() set(scriptPath $/archive-${lib}.mri) set(template "create $\n") string(APPEND template "addlib $\n") # Expand BUNDLE_LIBRARIES into the appropriate chain of addlib commands needed. # BUNDLE_LIBRARIES will contain either (a) target names or (b) absolute file paths to libraries to include. # This first part will disambiguate between (a) and (b) by evaluating (a) to `addlib $` and (b) to `addlib [[filepath]]` set(replaceExpr "addlib $,$,\\1>") escape_generator_expression(replaceExpr) # The second part maps every element in BUNDLE_LIBRARIES to `replaceExpr` and evaluates the resulting replacement, which produces the final .mri file. string(APPEND template "$,REPLACE,(.+),${replaceExpr}>,\n>>\n") string(APPEND template "save\n") string(APPEND template "end\n") file(WRITE ${templatePath} ${template}) file(GENERATE OUTPUT ${scriptPath} INPUT ${templatePath} ) add_custom_command(TARGET ${lib} POST_BUILD COMMAND ${CMAKE_AR} -M < ${scriptPath} COMMAND ${CMAKE_RANLIB} $ COMMAND ${CMAKE_COMMAND} -E echo "Bundled $> static libraries into target ${lib}. Script: ${scriptPath}" WORKING_DIRECTORY $ ) endif() set_target_properties(${lib} PROPERTIES BUNDLED_LIBRARY_TEMPLATE_PATH ${templatePath} ) endfunction() # Subcomponent of target_bundle_libraries which is responsible for walking the provided depLibs and recursively calling target_bundle_libraries. # This macro must only be used within target_bundle_libraries. # # \param mainLib The current main library from target_bundle_libraries # \param linkVis The link visibility from target_bundle_libraries # \param argn The dependencies to walk. Usually the INTERFACE_LINK_LIBRARIES of a target currently being bundled. function(__bundleRecursiveDeps mainLib linkVis) if(ARGN) get_target_property(bundledLibs ${mainLib} BUNDLE_LIBRARIES) foreach(dep IN LISTS ARGN) unwrapAlias(${dep} dep) if(${dep} IN_LIST bundledLibs) continue() # Skip bundling of already-bundled libs to avoid many invocations of the same warnings. endif() if(TARGET ${dep}) __get_lib_type(${dep} depType) if (${depType} STREQUAL STATIC_LIBRARY) target_bundle_libraries(${mainLib} ${linkVis} ${dep}) elseif(${depType} STREQUAL INTERFACE_LIBRARY) # For interface libraries, we want to add all of the static libraries they may be pointing to, without the library itself (since it is not a static). # Skip if we've already traversed this interface lib's deps for mainLib to avoid re-walking shared transitive subtrees. get_target_property(_visited ${mainLib} BUNDLE_VISITED_INTERFACES) if(NOT _visited) set(_visited "") endif() if(${dep} IN_LIST _visited) continue() endif() set_property(TARGET ${mainLib} APPEND PROPERTY BUNDLE_VISITED_INTERFACES ${dep}) get_target_property(interfaceLibs ${dep} INTERFACE_LINK_LIBRARIES) __bundleRecursiveDeps(${mainLib} ${linkVis} ${interfaceLibs}) elseif(${depType} STREQUAL SHARED_LIBRARY) # Skip SO's, since static libraries at the SO-boundary should not be bundled into the target mainLib. else() __bundleMessage(DEBUG "Skipping unhandled dependency ${dep} (a dependency of ${bundledLib}) with type ${depType} in ${mainLib}") endif() else() __bundleMessage(DEBUG "Failed to recursively bundle dependency ${dep} (a dependency of ${bundledLib}) in ${mainLib}") endif() endforEach() endif() endfunction() # This function acts as a replacement target_link_libraries, instead linking # one or more "bundled" static libraries into a "main" static library. # The bundled libs are embedded inside the main lib during the post-build step. # CMake interface properties for definitions, etc. are propagated using the specified visibility. # # This function is recursive. Any static dependencies of any bundled library will also be bundled into the mainLib. # # \param mainLib The main library that other libraries are to be bundled into. # \param linkVis The link visiblity for interface properties. One of PRIVATE or PUBLIC. # Using PRIVATE hides all interface properties of bundled libraries from consumers of this library. # Using PUBLIC will share interface properties with dependents. # \param argn Variadic arguments - The list of targets to be linked into the mainLib. function(target_bundle_libraries mainLib linkVis) if (NOT ${linkVis} STREQUAL PUBLIC AND NOT ${linkVis} STREQUAL PRIVATE) __bundleMessage(FATAL_ERROR "Error: Called target_bundle_libraries with unknown visibility ${linkVis}. Must be either \"PUBLIC\" or \"PRIVATE\".") endif() # TODO: Allow direct insertion of absolute file paths when CMAKE_LINK_LIBRARIES_ONLY_TARGETS is off, instead of always forcing targets. if (NOT TARGET ${mainLib}) __bundleMessage(FATAL_ERROR "Error: Called target_bundle_libraries for target ${mainLib}, but no target with that name is known.") endif() __get_lib_type(${mainLib} mainLibType) if (NOT mainLibType STREQUAL STATIC_LIBRARY) __bundleMessage(FATAL_ERROR "Error: Called target_bundle_libraries for target ${mainLib}, but it is not a static library.") endif() get_target_property(isMainLibImported ${mainLib} IMPORTED) if(isMainLibImported) __bundleMessage(FATAL_ERROR "Error: Called target_bundle_libraries for target ${mainLib}, but it is an imported target.") endif() if(NOT ARGN) __bundleMessage(WARNING "Called target_bundle_libraries with no libraries for target ${mainLib}") endif() __bundleMessage(DEBUG "Bundling ${ARGN} into ${mainLib}") get_target_property(templatePath ${mainLib} BUNDLED_LIBRARY_TEMPLATE_PATH) if(NOT EXISTS ${templatePath}) if(MSVC) set(templatePath ${PROJECT_BINARY_DIR}/archive-${mainLib}.bat.template) else() set(templatePath ${PROJECT_BINARY_DIR}/archive-${mainLib}.mri.template) endif() __bundleMessage(STATUS "Registering default script path ${templatePath} for target ${mainLib}") __registerTargetForBundling(${mainLib} ${templatePath}) endif() get_target_property(bundledLibs ${mainLib} BUNDLE_LIBRARIES) if (NOT bundledLibs) set(bundledLibs "") endif() foreach(bundledLib IN LISTS ARGN) unwrapAlias(${bundledLib} bundledLib) __get_lib_type(${bundledLib} bundledLibType) if(${bundledLibType} STREQUAL INTERFACE_LIBRARY) # Interface libraries are not bundled, since they do not contain any static libraries. # Their dependencies will be bundled recursively in the next step. continue() endif() if (NOT ${bundledLibType} STREQUAL STATIC_LIBRARY AND NOT ${bundledLibType} STREQUAL OBJECT_LIBRARY) __bundleMessage(FATAL_ERROR "Attempted to bundle ${bundledLibType} library ${bundledLib} into target ${mainLib} (only static and object libraries may be bundled)") endif() if (${bundledLibType} STREQUAL STATIC_LIBRARY) list(APPEND bundledLibs ${bundledLib}) else() # Exclude object libs from the BUNDLE_LIBRARIES property as they get added into the static lib using normal means. endif() endforeach() list(REMOVE_DUPLICATES bundledLibs) set_target_properties(${mainLib} PROPERTIES BUNDLE_LIBRARIES "${bundledLibs}" ) foreach(bundledLib IN LISTS ARGN) unwrapAlias(${bundledLib} bundledLib) __get_lib_type(${bundledLib} bundledLibType) # Recursively bundle all static dependencies of each lib to be bundled. get_target_property(depLibs ${bundledLib} LINK_LIBRARIES) __bundleRecursiveDeps(${mainLib} ${linkVis} ${depLibs}) # Since we want the main library to be linkable standalone, we need both the LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES bundled in. # Otherwise, private static dependencies may be lost. get_target_property(depLibs ${bundledLib} INTERFACE_LINK_LIBRARIES) __bundleRecursiveDeps(${mainLib} ${linkVis} ${depLibs}) # Use `target_link_libraries` to propagate INTERFACE definitions from bundled libs # BUILD_LOCAL_INTERFACE prevents clients from seeing this internal link relationship # COMPILE_ONLY prevents the bundled lib from appearing in the link command redundantly if (${bundledLibType} STREQUAL STATIC_LIBRARY OR ${bundledLibType} STREQUAL INTERFACE_LIBRARY) target_link_libraries(${mainLib} ${linkVis} $> ) else() # Include Object Libraries as full libraries, since they do not get added by the bundling stage. # To do this without breaking the link dependency logic, we need to steal the target objects and link the lib as local + compile only. target_link_libraries(${mainLib} ${linkVis} $> ) target_sources(${mainLib} PRIVATE $) endif() # require that bundled libs are built before we try to bundle them add_dependencies(${mainLib} ${bundledLib}) endforeach() if(NOT EXISTS ${templatePath}) __bundleMessage(FATAL_ERROR "Template file ${templatePath} for target ${mainLib} does not exist.") endif() endfunction()