[cmake] Extract whole archive link as a separate function (#3087)

This commit extracts out as a function the functionality for going
through a target's direct library dependencies and recursively
linking ALWAYSLINK libraries as whole archive. It can be reused
for iree_cc_library so that wen we pack shared libraries we can
make sure ALWAYSLINK markers are respected and we have the necessary
global static objects for registering various functionalities.

This change is mostly NFC, except for exposing whole archive link
in iree_cc_library.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 53aba14..2995809 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -236,6 +236,7 @@
 
 include(iree_macros)
 include(iree_copts)
+include(iree_whole_archive_link)
 include(iree_cc_binary)
 include(iree_cc_library)
 include(iree_cc_test)
diff --git a/build_tools/cmake/iree_cc_binary.cmake b/build_tools/cmake/iree_cc_binary.cmake
index 51d9d68..5e0c58f 100644
--- a/build_tools/cmake/iree_cc_binary.cmake
+++ b/build_tools/cmake/iree_cc_binary.cmake
@@ -151,68 +151,6 @@
           RUNTIME DESTINATION bin)
 endfunction()
 
-# Lists all transitive dependencies of DIRECT_DEPS in TRANSITIVE_DEPS.
-function(_iree_transitive_dependencies DIRECT_DEPS TRANSITIVE_DEPS)
-  set(_TRANSITIVE "")
-
-  foreach(_DEP ${DIRECT_DEPS})
-    _iree_transitive_dependencies_helper(${_DEP} _TRANSITIVE)
-  endforeach(_DEP)
-
-  set(${TRANSITIVE_DEPS} "${_TRANSITIVE}" PARENT_SCOPE)
-endfunction()
-
-# Recursive helper function for _iree_transitive_dependencies.
-# Performs a depth-first search through the dependency graph, appending all
-# dependencies of TARGET to the TRANSITIVE_DEPS list.
-function(_iree_transitive_dependencies_helper TARGET TRANSITIVE_DEPS)
-  if (NOT TARGET "${TARGET}")
-    # Excluded from the project, or invalid name? Just ignore.
-    return()
-  endif()
-
-  # Resolve aliases, canonicalize name formatting.
-  get_target_property(_ALIASED_TARGET ${TARGET} ALIASED_TARGET)
-  if(_ALIASED_TARGET)
-    set(_TARGET_NAME ${_ALIASED_TARGET})
-  else()
-    string(REPLACE "::" "_" _TARGET_NAME ${TARGET})
-  endif()
-
-  set(_RESULT "${${TRANSITIVE_DEPS}}")
-  if (${_TARGET_NAME} IN_LIST _RESULT)
-    # Already visited, ignore.
-    return()
-  endif()
-
-  # Append this target to the list. Dependencies of this target will be added
-  # (if valid and not already visited) in recursive function calls.
-  list(APPEND _RESULT ${_TARGET_NAME})
-
-  # Check for non-target identifiers again after resolving the alias.
-  if (NOT TARGET ${_TARGET_NAME})
-    return()
-  endif()
-
-  # Get the list of direct dependencies for this target.
-  get_target_property(_TARGET_TYPE ${_TARGET_NAME} TYPE)
-  if(NOT ${_TARGET_TYPE} STREQUAL "INTERFACE_LIBRARY")
-    get_target_property(_TARGET_DEPS ${_TARGET_NAME} LINK_LIBRARIES)
-  else()
-    get_target_property(_TARGET_DEPS ${_TARGET_NAME} INTERFACE_LINK_LIBRARIES)
-  endif()
-
-  if(_TARGET_DEPS)
-    # Recurse on each dependency.
-    foreach(_TARGET_DEP ${_TARGET_DEPS})
-      _iree_transitive_dependencies_helper(${_TARGET_DEP} _RESULT)
-    endforeach(_TARGET_DEP)
-  endif()
-
-  # Propagate the augmented list up to the parent scope.
-  set(${TRANSITIVE_DEPS} "${_RESULT}" PARENT_SCOPE)
-endfunction()
-
 # Sets target_link_libraries() on all registered binaries.
 # This must be called after all libraries have been declared.
 function(iree_complete_binary_link_options)
@@ -220,69 +158,6 @@
 
   foreach(_NAME ${_NAMES})
     get_target_property(_DIRECT_DEPS ${_NAME} DIRECT_DEPS)
-
-    # List all dependencies, including transitive dependencies, then split the
-    # dependency list into one for whole archive (ALWAYSLINK) and one for
-    # standard linking (which only links in symbols that are directly used).
-    _iree_transitive_dependencies("${_DIRECT_DEPS}" _TRANSITIVE_DEPS)
-    set(_ALWAYS_LINK_DEPS "")
-    set(_STANDARD_DEPS "")
-    foreach(_DEP ${_TRANSITIVE_DEPS})
-      # Check if _DEP is a library with the ALWAYSLINK property set.
-      set(_DEP_IS_ALWAYSLINK OFF)
-      if (TARGET ${_DEP})
-        get_target_property(_DEP_TYPE ${_DEP} TYPE)
-        if(${_DEP_TYPE} STREQUAL "INTERFACE_LIBRARY")
-          # Can't be ALWAYSLINK since it's an INTERFACE library.
-          # We also can't even query for the property, since it isn't allowlisted.
-        else()
-          get_target_property(_DEP_IS_ALWAYSLINK ${_DEP} ALWAYSLINK)
-        endif()
-      endif()
-
-      # Append to the corresponding list of deps.
-      if(_DEP_IS_ALWAYSLINK)
-        list(APPEND _ALWAYS_LINK_DEPS ${_DEP})
-
-        # For MSVC, also add a `-WHOLEARCHIVE:` version of the dep.
-        # CMake treats -WHOLEARCHIVE[:lib] as a link flag and will not actually
-        # try to link the library in, so we need the flag *and* the dependency.
-        # For macOS, also add a `-Wl,-force_load` version of the dep.
-        if(MSVC)
-          get_target_property(_ALIASED_TARGET ${_DEP} ALIASED_TARGET)
-          if (_ALIASED_TARGET)
-            list(APPEND _ALWAYS_LINK_DEPS "-WHOLEARCHIVE:${_ALIASED_TARGET}")
-          else()
-            list(APPEND _ALWAYS_LINK_DEPS "-WHOLEARCHIVE:${_DEP}")
-          endif()
-        elseif(APPLE)
-          get_target_property(_ALIASED_TARGET ${_DEP} ALIASED_TARGET)
-          if (_ALIASED_TARGET)
-            list(APPEND _ALWAYS_LINK_DEPS "-Wl,-force_load $<TARGET_FILE:${_ALIASED_TARGET}>")
-          else()
-            list(APPEND _ALWAYS_LINK_DEPS "-Wl,-force_load $<TARGET_FILE:${_DEP}>")
-          endif()
-        endif()
-      else()
-        list(APPEND _STANDARD_DEPS ${_DEP})
-      endif()
-    endforeach(_DEP)
-
-    # Call into target_link_libraries with the lists of deps.
-    if(MSVC OR APPLE)
-      target_link_libraries(${_NAME}
-        PUBLIC
-          ${_ALWAYS_LINK_DEPS}
-          ${_STANDARD_DEPS}
-      )
-    else()
-      target_link_libraries(${_NAME}
-        PUBLIC
-          "-Wl,--whole-archive"
-          ${_ALWAYS_LINK_DEPS}
-          "-Wl,--no-whole-archive"
-          ${_STANDARD_DEPS}
-      )
-    endif()
+    iree_whole_archive_link(${_NAME} ${_DIRECT_DEPS})
   endforeach(_NAME)
 endfunction()
diff --git a/build_tools/cmake/iree_cc_library.cmake b/build_tools/cmake/iree_cc_library.cmake
index 848f99b..6f5e51f 100644
--- a/build_tools/cmake/iree_cc_library.cmake
+++ b/build_tools/cmake/iree_cc_library.cmake
@@ -34,6 +34,7 @@
 # Also in IDE, target will appear in IREE folder while non PUBLIC will be in IREE/internal.
 # TESTONLY: When added, this target will only be built if user passes -DIREE_BUILD_TESTS=ON to CMake.
 # SHARED: If set, will compile to a shared object.
+# WHOLEARCHIVE: If set, links all symbols from "ALWAYSLINK" libraries.
 #
 # Note:
 # By default, iree_cc_library will always create a library named iree_${NAME},
@@ -68,7 +69,7 @@
 function(iree_cc_library)
   cmake_parse_arguments(
     _RULE
-    "PUBLIC;ALWAYSLINK;TESTONLY;SHARED"
+    "PUBLIC;ALWAYSLINK;TESTONLY;SHARED;WHOLEARCHIVE"
     "NAME"
     "HDRS;TEXTUAL_HDRS;SRCS;COPTS;DEFINES;LINKOPTS;DATA;DEPS;INCLUDES"
     ${ARGN}
@@ -108,6 +109,9 @@
       add_library(${_NAME} SHARED "")
     else()
       add_library(${_NAME} STATIC "")
+      if (_RULE_WHOLEARCHIVE)
+        message(FATAL_ERROR "WHOLEARCHIVE must be set together with SHARED")
+      endif()
     endif()
 
     target_sources(${_NAME}
@@ -127,13 +131,17 @@
         ${IREE_DEFAULT_COPTS}
     )
 
+  if(_RULE_WHOLEARCHIVE)
+      iree_whole_archive_link(${_NAME} ${_RULE_DEPS})
+    else()
+      target_link_libraries(${_NAME} PUBLIC ${_RULE_DEPS})
+    endif()
     target_link_libraries(${_NAME}
-      PUBLIC
-        ${_RULE_DEPS}
       PRIVATE
         ${_RULE_LINKOPTS}
         ${IREE_DEFAULT_LINKOPTS}
     )
+
     iree_add_data_dependencies(NAME ${_NAME} DATA ${_RULE_DATA})
     target_compile_definitions(${_NAME}
       PUBLIC
diff --git a/build_tools/cmake/iree_whole_archive_link.cmake b/build_tools/cmake/iree_whole_archive_link.cmake
new file mode 100644
index 0000000..e34f900
--- /dev/null
+++ b/build_tools/cmake/iree_whole_archive_link.cmake
@@ -0,0 +1,148 @@
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#      https://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.
+
+# Lists all transitive dependencies of DIRECT_DEPS in TRANSITIVE_DEPS.
+function(_iree_transitive_dependencies DIRECT_DEPS TRANSITIVE_DEPS)
+  set(_TRANSITIVE "")
+
+  foreach(_DEP ${DIRECT_DEPS})
+    _iree_transitive_dependencies_helper(${_DEP} _TRANSITIVE)
+  endforeach(_DEP)
+
+  set(${TRANSITIVE_DEPS} "${_TRANSITIVE}" PARENT_SCOPE)
+endfunction()
+
+# Recursive helper function for _iree_transitive_dependencies.
+# Performs a depth-first search through the dependency graph, appending all
+# dependencies of TARGET to the TRANSITIVE_DEPS list.
+function(_iree_transitive_dependencies_helper TARGET TRANSITIVE_DEPS)
+  if (NOT TARGET "${TARGET}")
+    # Excluded from the project, or invalid name? Just ignore.
+    return()
+  endif()
+
+  # Resolve aliases, canonicalize name formatting.
+  get_target_property(_ALIASED_TARGET ${TARGET} ALIASED_TARGET)
+  if(_ALIASED_TARGET)
+    set(_TARGET_NAME ${_ALIASED_TARGET})
+  else()
+    string(REPLACE "::" "_" _TARGET_NAME ${TARGET})
+  endif()
+
+  set(_RESULT "${${TRANSITIVE_DEPS}}")
+  if (${_TARGET_NAME} IN_LIST _RESULT)
+    # Already visited, ignore.
+    return()
+  endif()
+
+  # Append this target to the list. Dependencies of this target will be added
+  # (if valid and not already visited) in recursive function calls.
+  list(APPEND _RESULT ${_TARGET_NAME})
+
+  # Check for non-target identifiers again after resolving the alias.
+  if (NOT TARGET ${_TARGET_NAME})
+    return()
+  endif()
+
+  # Get the list of direct dependencies for this target.
+  get_target_property(_TARGET_TYPE ${_TARGET_NAME} TYPE)
+  if(NOT ${_TARGET_TYPE} STREQUAL "INTERFACE_LIBRARY")
+    get_target_property(_TARGET_DEPS ${_TARGET_NAME} LINK_LIBRARIES)
+  else()
+    get_target_property(_TARGET_DEPS ${_TARGET_NAME} INTERFACE_LINK_LIBRARIES)
+  endif()
+
+  if(_TARGET_DEPS)
+    # Recurse on each dependency.
+    foreach(_TARGET_DEP ${_TARGET_DEPS})
+      _iree_transitive_dependencies_helper(${_TARGET_DEP} _RESULT)
+    endforeach(_TARGET_DEP)
+  endif()
+
+  # Propagate the augmented list up to the parent scope.
+  set(${TRANSITIVE_DEPS} "${_RESULT}" PARENT_SCOPE)
+endfunction()
+
+# Given the ${TARGET} and the libaries it directly depends on in ${ARGN},
+# properly establish the linking relationship by considering ALWAYSLINK
+# in a recursive manner.
+#
+# All symbols from ALWAYSLINK libraries will be included in ${TARGET},
+# regardless of whether they are directly referenced or not.
+function(iree_whole_archive_link TARGET)
+  # List all dependencies, including transitive dependencies, then split the
+  # dependency list into one for whole archive (ALWAYSLINK) and one for
+  # standard linking (which only links in symbols that are directly used).
+  _iree_transitive_dependencies("${ARGN}" _TRANSITIVE_DEPS)
+  set(_ALWAYS_LINK_DEPS "")
+  set(_STANDARD_DEPS "")
+  foreach(_DEP ${_TRANSITIVE_DEPS})
+    # Check if _DEP is a library with the ALWAYSLINK property set.
+    set(_DEP_IS_ALWAYSLINK OFF)
+    if (TARGET ${_DEP})
+      get_target_property(_DEP_TYPE ${_DEP} TYPE)
+      if(${_DEP_TYPE} STREQUAL "INTERFACE_LIBRARY")
+        # Can't be ALWAYSLINK since it's an INTERFACE library.
+        # We also can't even query for the property, since it isn't allowlisted.
+      else()
+        get_target_property(_DEP_IS_ALWAYSLINK ${_DEP} ALWAYSLINK)
+      endif()
+    endif()
+
+    # Append to the corresponding list of deps.
+    if(_DEP_IS_ALWAYSLINK)
+      list(APPEND _ALWAYS_LINK_DEPS ${_DEP})
+
+      # For MSVC, also add a `-WHOLEARCHIVE:` version of the dep.
+      # CMake treats -WHOLEARCHIVE[:lib] as a link flag and will not actually
+      # try to link the library in, so we need the flag *and* the dependency.
+      # For macOS, also add a `-Wl,-force_load` version of the dep.
+      if(MSVC)
+        get_target_property(_ALIASED_TARGET ${_DEP} ALIASED_TARGET)
+        if (_ALIASED_TARGET)
+          list(APPEND _ALWAYS_LINK_DEPS "-WHOLEARCHIVE:${_ALIASED_TARGET}")
+        else()
+          list(APPEND _ALWAYS_LINK_DEPS "-WHOLEARCHIVE:${_DEP}")
+        endif()
+      elseif(APPLE)
+        get_target_property(_ALIASED_TARGET ${_DEP} ALIASED_TARGET)
+        if (_ALIASED_TARGET)
+          list(APPEND _ALWAYS_LINK_DEPS "-Wl,-force_load $<TARGET_FILE:${_ALIASED_TARGET}>")
+        else()
+          list(APPEND _ALWAYS_LINK_DEPS "-Wl,-force_load $<TARGET_FILE:${_DEP}>")
+        endif()
+      endif()
+    else()
+      list(APPEND _STANDARD_DEPS ${_DEP})
+    endif()
+  endforeach(_DEP)
+
+  # Call into target_link_libraries with the lists of deps.
+  if(MSVC OR APPLE)
+    target_link_libraries(${TARGET}
+      PUBLIC
+        ${_ALWAYS_LINK_DEPS}
+        ${_STANDARD_DEPS}
+    )
+  else()
+    target_link_libraries(${TARGET}
+      PUBLIC
+        "-Wl,--whole-archive"
+        ${_ALWAYS_LINK_DEPS}
+        "-Wl,--no-whole-archive"
+        ${_STANDARD_DEPS}
+    )
+  endif()
+endfunction()
+