Skip to content

Commit

Permalink
Rewrite bundle_static to be much more efficient. (#8386)
Browse files Browse the repository at this point in the history
The `bundle_static` function now detects the private static dependencies
on the given target (in our case, always Halide) and uses the platform
librarian tool to merge static dependencies into a static library. It
picks which tool to use by checking, in order:

* When targeting Windows, it looks for `lib.exe`.
* When targeting macOS, it checks if `libtool` is the Apple libtool.
* Whether `ar` is GNU ar and if so, generates an MRI script.
* Otherwise, a `FATAL_ERROR` is issued.

To mark a static library for bundling, we link privately and use
the `$<BUILD_LOCAL_INTERFACE:...>` generator expression. This prevents
it from being exported, too.

The generator expression that implements this logic is quite complex.
It involves meta-programming generator expressions during evaluation
and then evaluating them. Even so, this saves a considerable amount of
time unpacking LLVM into a temporary directory and adding the objects
to the link line (the previous approach).
  • Loading branch information
alexreinking authored Aug 12, 2024
1 parent 3cdeb53 commit 6dc2b3e
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 203 deletions.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ include(CheckCXXSymbolExists)

# Make our custom helpers available throughout the project via include().
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake)
include(BundleStatic)
include(HalideFeatures)
include(HalideGeneratorHelpers)
include(HalidePackageConfigHelpers)
Expand Down Expand Up @@ -213,6 +214,8 @@ Halide_feature(Halide_BUNDLE_STATIC "Bundle Halide's static dependencies" OFF AD
Halide_feature(Halide_ENABLE_EXCEPTIONS "Enable exceptions in Halide" ON)
Halide_feature(Halide_ENABLE_RTTI "Enable RTTI in Halide" ON
DEPENDS LLVM_ENABLE_RTTI)
Halide_feature(Halide_BUNDLE_STATIC "Bundle Halide's static dependencies" OFF ADVANCED
DEPENDS NOT BUILD_SHARED_LIBS)

Halide_feature(WITH_AUTOSCHEDULERS "Build the Halide autoschedulers" ON
DEPENDS BUILD_SHARED_LIBS)
Expand Down
2 changes: 1 addition & 1 deletion CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
"binaryDir": "static-Release",
"cacheVariables": {
"BUILD_SHARED_LIBS": "NO",
"Halide_BUNDLE_LLVM": "YES"
"Halide_BUNDLE_STATIC": "YES"
}
},
{
Expand Down
16 changes: 8 additions & 8 deletions README_cmake.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,14 @@ Halide reads and understands several options that can configure the build. The
following are the most consequential and control how Halide is actually
compiled.

| Option | Default | Description |
|------------------------------------------|-----------------------|------------------------------------------------------------------------------------------------------------------|
| [`BUILD_SHARED_LIBS`][build_shared_libs] | `ON` | Standard CMake variable that chooses whether to build as a static or shared library. |
| `Halide_BUNDLE_LLVM` | `OFF` | When building Halide as a static library, unpack the LLVM static libraries and add those objects to libHalide.a. |
| `Halide_LLVM_SHARED_LIBS` | `OFF` | Link to the shared version of LLVM. Not available on Windows. |
| `Halide_ENABLE_RTTI` | _inherited from LLVM_ | Enable RTTI when building Halide. Recommended to be set to `ON` |
| `Halide_ENABLE_EXCEPTIONS` | `ON` | Enable exceptions when building Halide |
| `Halide_TARGET` | _empty_ | The default target triple to use for `add_halide_library` (and the generator tests, by extension) |
| Option | Default | Description |
|------------------------------------------|-----------------------|---------------------------------------------------------------------------------------------------|
| [`BUILD_SHARED_LIBS`][build_shared_libs] | `ON` | Standard CMake variable that chooses whether to build as a static or shared library. |
| `Halide_BUNDLE_STATIC` | `OFF` | When building Halide as a static library, merge static library dependencies into libHalide.a. |
| `Halide_LLVM_SHARED_LIBS` | `OFF` | Link to the shared version of LLVM. Not available on Windows. |
| `Halide_ENABLE_RTTI` | _inherited from LLVM_ | Enable RTTI when building Halide. Recommended to be set to `ON` |
| `Halide_ENABLE_EXCEPTIONS` | `ON` | Enable exceptions when building Halide |
| `Halide_TARGET` | _empty_ | The default target triple to use for `add_halide_library` (and the generator tests, by extension) |

The following options are _advanced_ and should not be required in typical workflows. Generally, these are used by
Halide's own CI infrastructure, or as escape hatches for third-party packagers.
Expand Down
302 changes: 112 additions & 190 deletions cmake/BundleStatic.cmake
Original file line number Diff line number Diff line change
@@ -1,201 +1,123 @@
cmake_minimum_required(VERSION 3.28)

##
# This module provides a utility for bundling a set of IMPORTED
# STATIC libraries together as a merged INTERFACE library that,
# due to CMake Issue #15415, requires manual propagation to its
# linkees, unfortunately.
#
# This is useful when a STATIC library produced by your project
# depends privately on some 3rd-party STATIC libraries that are
# tricky to distribute or for end-users to build. CMake handles
# this by assuming that imported libraries will be easy to find
# in an end-user's environment so a simple find_dependency call
# in the package config will suffice. Unfortunately, things are
# not so simple. Some libraries (eg. LLVM) can be built in many
# different configurations, and dependents can be built against
# one fixed configuration. If we have LLVM -> X -> Y where X is
# my library and Y is some other user's library, then Y must be
# very careful to build LLVM in _exactly_ the same way as X was
# configured to use. While this might be acceptable in a super-
# build, it fails when we want to release binary packages of X.
##

# All of the IMPORTED_ and INTERFACE_ properties should be accounted for below.
# https://cmake.org/cmake/help/v3.22/manual/cmake-properties.7.html#properties-on-targets

# Irrelevant properties:
# IMPORTED_IMPLIB(_<CONFIG>) # shared-only
# IMPORTED_LIBNAME(_<CONFIG>) # interface-only
# IMPORTED_LINK_DEPENDENT_LIBRARIES(_<CONFIG>) # shared-only
# IMPORTED_LINK_INTERFACE_LIBRARIES(_<CONFIG>) # deprecated
# IMPORTED_LINK_INTERFACE_MULTIPLICITY(_<CONFIG>) # static-only. irrelevant when all objects listed.
# IMPORTED_NO_SONAME(_<CONFIG>) # shared-only
# IMPORTED_SONAME(_<CONFIG>) # shared-only

function(bundle_static TARGET)
set(options)
set(oneValueArgs)
set(multiValueArgs LIBRARIES)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

set(interfaceLib ${TARGET})
set(objectLib ${interfaceLib}.obj)

add_library(${objectLib} OBJECT IMPORTED)
set_target_properties(${objectLib} PROPERTIES IMPORTED_GLOBAL TRUE)

target_sources(${interfaceLib} INTERFACE $<BUILD_INTERFACE:$<TARGET_OBJECTS:${objectLib}>>)

set(queue ${ARG_LIBRARIES})
while (queue)
list(POP_FRONT queue lib)
if (VISITED_${lib})
continue()
endif ()
set(VISITED_${lib} TRUE)

if (NOT TARGET ${lib})
target_link_libraries(${interfaceLib} INTERFACE ${lib})
continue()
endif ()

get_property(isImported TARGET ${lib} PROPERTY IMPORTED)
get_property(type TARGET ${lib} PROPERTY TYPE)

if (NOT isImported OR NOT "${type}" STREQUAL "STATIC_LIBRARY")
target_link_libraries(${interfaceLib} INTERFACE ${lib})
continue()
endif ()

transfer_same(PROPERTIES INTERFACE_POSITION_INDEPENDENT_CODE
FROM ${lib} TO ${interfaceLib})

transfer_append(PROPERTIES
INTERFACE_AUTOUIC_OPTIONS
INTERFACE_COMPILE_DEFINITIONS
INTERFACE_COMPILE_FEATURES
INTERFACE_COMPILE_OPTIONS
INTERFACE_INCLUDE_DIRECTORIES
INTERFACE_LINK_DEPENDS
INTERFACE_LINK_DIRECTORIES
INTERFACE_LINK_OPTIONS
INTERFACE_PRECOMPILE_HEADERS
INTERFACE_SOURCES
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES
FROM ${lib} TO ${interfaceLib})

transfer_same(PROPERTIES IMPORTED_COMMON_LANGUAGE_RUNTIME
FROM ${lib} TO ${objectLib})

transfer_locations(FROM ${lib} TO ${objectLib})

get_property(deps TARGET ${lib} PROPERTY INTERFACE_LINK_LIBRARIES)
list(APPEND queue ${deps})
endwhile ()
# Merge all the static library dependencies of TARGET into the library as a
# POST_BUILD step.

function(_bundle_static_replace VAR BEFORE AFTER)
string(REPLACE "$<" "$\\\\<" AFTER "${AFTER}")
string(REPLACE ">" "$<ANGLE-R>" AFTER "${AFTER}")
string(REPLACE "," "$<COMMA>" AFTER "${AFTER}")
string(REPLACE ";" "$<SEMICOLON>" AFTER "${AFTER}")
set("${VAR}" "$<LIST:TRANSFORM,${${VAR}},REPLACE,${BEFORE},${AFTER}>")
set("${VAR}" "$<LIST:TRANSFORM,${${VAR}},REPLACE,\\\\<,<>")
set("${VAR}" "$<GENEX_EVAL:${${VAR}}>" PARENT_SCOPE)
endfunction()

function(transfer_same)
set(options)
set(oneValueArgs FROM TO PROPERTIES)
set(multiValueArgs)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

foreach (p IN LISTS ARG_PROPERTIES)
get_property(fromSet TARGET ${ARG_FROM} PROPERTY ${p} SET)
if (NOT fromSet)
continue()
endif ()
get_property(fromVal TARGET ${ARG_FROM} PROPERTY ${p})

get_property(toSet TARGET ${ARG_TO} PROPERTY ${p} SET)
if (NOT toSet)
set_property(TARGET ${ARG_TO} PROPERTY ${p} ${fromVal})
endif ()

get_property(toVal TARGET ${ARG_TO} PROPERTY ${p})
if (NOT "${fromVal}" STREQUAL "${toVal}")
message(WARNING "Property ${p} does not agree between ${ARG_FROM} [${fromVal}] and ${ARG_TO} [${toVal}]")
endif ()
endforeach ()
function(_bundle_static_check_output VAR)
execute_process(COMMAND ${ARGN} OUTPUT_VARIABLE "${VAR}" RESULT_VARIABLE "_${VAR}" ERROR_QUIET)
if (_${VAR})
set("${VAR}" "")
endif ()
set("${VAR}" "${${VAR}}" PARENT_SCOPE)
endfunction()

function(transfer_append)
set(options)
set(oneValueArgs FROM TO PROPERTIES)
set(multiValueArgs)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

foreach (p IN LISTS ARG_PROPERTIES)
get_property(fromSet TARGET ${ARG_FROM} PROPERTY ${p} SET)
if (fromSet)
get_property(fromVal TARGET ${ARG_FROM} PROPERTY ${p})
set_property(TARGET ${ARG_TO} APPEND PROPERTY ${p} ${fromVal})
endif ()
endforeach ()
function(_bundle_static_is_apple_libtool result item)
_bundle_static_check_output(version_info "${item}" -V)
if (version_info MATCHES "Apple, Inc.")
set(result 1 PARENT_SCOPE)
else ()
set(result 0 PARENT_SCOPE)
endif ()
endfunction()

function(transfer_locations)
set(options)
set(oneValueArgs FROM TO)
set(multiValueArgs)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

get_property(configs TARGET ${ARG_FROM} PROPERTY IMPORTED_CONFIGURATIONS)
foreach (cfg IN LISTS configs ITEMS "")
if (cfg)
string(TOUPPER "_${cfg}" cfg)
endif ()

get_property(lib TARGET ${ARG_FROM} PROPERTY "IMPORTED_LOCATION${cfg}")
if (lib)
cmake_path(GET lib STEM stage)
set(stage "${CMAKE_CURRENT_BINARY_DIR}/${stage}.obj")

if (NOT EXISTS "${stage}")
file(MAKE_DIRECTORY "${stage}")
if (MSVC)
execute_process(COMMAND "${CMAKE_AR}" /NOLOGO /LIST "${lib}"
WORKING_DIRECTORY "${stage}"
OUTPUT_VARIABLE objsInLib)

# Process the output to a list of internal objects
string(STRIP "${objsInLib}" objsInLib)
string(REGEX REPLACE "(\r|\n)+" ";" objsInLib "${objsInLib}")
list(TRANSFORM objsInLib STRIP)

foreach (obj IN LISTS objsInLib)
execute_process(COMMAND "${CMAKE_AR}" /NOLOGO "/EXTRACT:${obj}" "${lib}"
WORKING_DIRECTORY "${stage}")
endforeach ()
else ()
execute_process(COMMAND "${CMAKE_AR}" -x "${lib}"
WORKING_DIRECTORY "${stage}"
RESULT_VARIABLE error)
endif ()
endif ()

get_property(languages TARGET ${ARG_FROM} PROPERTY "IMPORTED_LINK_INTERFACE_LANGUAGES${cfg}")
if (NOT languages)
get_property(languages TARGET ${ARG_FROM} PROPERTY "IMPORTED_LINK_INTERFACE_LANGUAGES")
endif ()

message(VERBOSE "Transferring ${languages}[${cfg}] objects from ${lib} to ${ARG_TO}")

set(globs "")
foreach (lang IN LISTS languages)
if (DEFINED "CMAKE_${lang}_OUTPUT_EXTENSION")
list(APPEND globs "${stage}/*${CMAKE_${lang}_OUTPUT_EXTENSION}")
endif ()
endforeach ()

file(GLOB_RECURSE objects ${globs})

foreach (obj IN LISTS objects)
message(VERBOSE "... ${obj}")
endforeach ()

set_property(TARGET ${ARG_TO} APPEND PROPERTY "IMPORTED_OBJECTS${cfg}" ${objects})
endif ()
function(bundle_static TARGET)
get_property(type TARGET "${TARGET}" PROPERTY TYPE)
if (NOT type STREQUAL "STATIC_LIBRARY")
return()
endif ()

# The following code is quite subtle. First, it "recursively" (up to a depth
# limit) expands all the INTERFACE_LINK_LIBRARIES of the TARGET. Once the
# full set of library dependencies has been determined, it filters just
# the static libraries and replaces them with their on-disk locations.

# Start with the $<LINK_ONLY:$<BUILD_LOCAL_INTERFACE:...>> dependencies of
# the target. These are the privately-linked static and interface libraries
# that the user intends to delete upon export.
set(cmd "$<TARGET_PROPERTY:${TARGET},INTERFACE_LINK_LIBRARIES>")
set(cmd "$<FILTER:${cmd},INCLUDE,LINK_ONLY:..BUILD_LOCAL_INTERFACE>")

# Repeatedly expand and flatten: T ~> T, T.INTERFACE_LINK_LIBRARIES
foreach (i RANGE 5)
_bundle_static_replace(
cmd "(.+)" "\\1;$<$<TARGET_EXISTS:\\1>:$<TARGET_PROPERTY:\\1,INTERFACE_LINK_LIBRARIES>>"
)
set(cmd "$<LIST:REMOVE_DUPLICATES,$<GENEX_EVAL:${cmd}>>")
endforeach ()

# Rewrite T ~> T^T.TYPE -- we use ^ as a delimiter
_bundle_static_replace(cmd "(.+)" "\\1^$<TARGET_PROPERTY:\\1,TYPE>")
set(cmd "$<GENEX_EVAL:${cmd}>")

# Select exactly the set of static libraries
set(cmd "$<FILTER:${cmd},INCLUDE,\\^STATIC_LIBRARY$>")

# Rewrite T^... ~> $<TARGET_FILE:T>
_bundle_static_replace(cmd "^([^^]+)\\^.+$" "$<TARGET_FILE:\\1>")

# Rename the target to target.tmp
add_custom_command(
TARGET "${TARGET}" POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E rename "$<TARGET_FILE:${TARGET}>" "$<TARGET_FILE:${TARGET}>.tmp"
VERBATIM
)

# Finally merge everything together using the platform tool.
find_program(LIB lib.exe HINTS "${CMAKE_AR}")
if (WIN32 AND LIB)
add_custom_command(
TARGET "${TARGET}" POST_BUILD
COMMAND "${LIB}" "/out:$<TARGET_FILE:${TARGET}>" "$<TARGET_FILE:${TARGET}>.tmp" "${cmd}"
COMMAND_EXPAND_LISTS
VERBATIM
)
return()
endif ()

find_program(LIBTOOL libtool VALIDATOR _bundle_static_is_apple_libtool)
if (APPLE AND LIBTOOL)
add_custom_command(
TARGET "${TARGET}" POST_BUILD
COMMAND "${LIBTOOL}" -static -o "$<TARGET_FILE:${TARGET}>" "$<TARGET_FILE:${TARGET}>.tmp" "${cmd}"
COMMAND_EXPAND_LISTS
VERBATIM
)
return()
endif ()

_bundle_static_check_output(version_info "${CMAKE_AR}" V)
if (version_info MATCHES "GNU")
string(CONFIGURE [[
create $<TARGET_FILE:@TARGET@>
addlib $<TARGET_FILE:@TARGET@>.tmp
$<LIST:JOIN,$<LIST:TRANSFORM,@cmd@,PREPEND,addlib >,
>
save
end
]] mri_script)
string(REGEX REPLACE "(^|\n) +" "\\1" mri_script "${mri_script}")

file(GENERATE OUTPUT "fuse-${TARGET}.mri"
CONTENT "${mri_script}" TARGET "${TARGET}")

add_custom_command(
TARGET "${TARGET}" POST_BUILD
COMMAND "${CMAKE_AR}" -M < "${CMAKE_CURRENT_BINARY_DIR}/fuse-${TARGET}.mri"
VERBATIM
)
return()
endif ()

message(FATAL_ERROR "bundle_static_libs not implemented for the present toolchain")
endfunction()
Loading

0 comments on commit 6dc2b3e

Please sign in to comment.