Kesco的编码世界

如何合并静态库

用C++写一个SDK给用户使用,一般来说都是采取打包好的动态库、静态库加头文件的方式提供。SDK内部实现往往是有不少第三方依赖,如果是静态库的形式,SDK和第三方库的编译产物都是多个独立的静态库,比如sdk.a、liba.a和libb.a等等,不利于项目集成。本文以Cmake构建方式为例,讲述如何合并多个静态库的方法。

合并静态库的工具

一般来说,C++工具链都会内置合并工具的功能,根据平台的不同,其工具依次为:

  1. Linux下,使用ar来合并静态库。
  2. macOS下,使用libtool来合并静态库。
  3. Windows下,使用lib.exe来合并静态库。

其合并命令行格式如下所示:

  1. Linux下的ar
1# 旧版本ar命令不一定支持一步到位合并静态库,这时候可能需要先把.a文件拆回多个.o文件或者使用.mri文件辅助
2ar -rcs target.a deps0.a deps1.a
  1. macOS下的libtool
1libtool -static -o target.a deps0.a deps1.a
  1. Windows下的lib.exe
1lib /NOLOGO /OUT:target.lib deps0.lib deps1.lib

使用Cmake脚本编译时自动合并静态库

如果每次编译都要手动敲命令来执行合并操作,实在太麻烦了,工作中往往使用脚本来自动处理。以我写的一个cmake函数脚本为例,每次编译时会自动生成合并静态库的target,读者可以参考使用。

该脚本已经托管到github上,其地址是【kesco/static-libraries-combiner】

  1function(combine_static_libraries base_lib target_lib_name)
  2  list(APPEND static_libs ${base_lib})
  3
  4  function(get_lib_deps_recursively target)
  5    set(link_libs LINK_LIBRARIES)
  6    get_target_property(target_type ${target} TYPE)
  7    if (${target_type} STREQUAL "INTERFACE_LIBRARY")
  8      set(link_libs INTERFACE_LINK_LIBRARIES)
  9    endif()
 10    get_target_property(public_dependencies ${target} ${link_libs})
 11    foreach(dependency IN LISTS public_dependencies)
 12      if(TARGET ${dependency})
 13        get_target_property(alias ${dependency} ALIASED_TARGET)
 14        if (TARGET ${alias})
 15          set(dependency ${alias})
 16        endif()
 17        get_target_property(_type ${dependency} TYPE)
 18        if (${_type} STREQUAL "STATIC_LIBRARY")
 19          list(APPEND static_libs ${dependency})
 20        endif()
 21
 22        get_property(library_already_added
 23          GLOBAL PROPERTY _${base_lib}_static_bundle_${dependency})
 24        if (NOT library_already_added)
 25          set_property(GLOBAL PROPERTY _${base_lib}_static_bundle_${dependency} ON)
 26          get_lib_deps_recursively(${dependency})
 27        endif()
 28      endif()
 29    endforeach()
 30    set(static_libs ${static_libs} PARENT_SCOPE)
 31  endfunction()
 32
 33  get_lib_deps_recursively(${base_lib})
 34
 35  list(REMOVE_DUPLICATES static_libs)
 36
 37  set(target_lib_path
 38    ${CMAKE_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${target_lib_name}${CMAKE_STATIC_LIBRARY_SUFFIX})
 39
 40  if (CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|GNU)$")
 41    file(WRITE ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in
 42      "CREATE ${target_lib_path}\n" )
 43
 44    foreach(tgt IN LISTS static_libs)
 45      file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in
 46        "ADDLIB $<TARGET_FILE:${tgt}>\n")
 47    endforeach()
 48
 49    file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in "SAVE\n")
 50    file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in "END\n")
 51
 52    file(GENERATE
 53      OUTPUT ${CMAKE_BINARY_DIR}/${target_lib_name}.ar
 54      INPUT ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in)
 55
 56    set(ar_tool ${CMAKE_AR})
 57    if (CMAKE_INTERPROCEDURAL_OPTIMIZATION)
 58      set(ar_tool ${CMAKE_CXX_COMPILER_AR})
 59    endif()
 60
 61    add_custom_command(
 62      COMMAND ${ar_tool} -M < ${CMAKE_BINARY_DIR}/${target_lib_name}.ar
 63      OUTPUT ${target_lib_path}
 64      COMMENT "Packing ${target_lib_name}"
 65      VERBATIM)
 66  elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")
 67    find_program(ar_tool libtool)
 68
 69    foreach(tgt IN LISTS static_libs)
 70      list(APPEND static_lib_paths $<TARGET_FILE:${tgt}>)
 71    endforeach()
 72
 73    add_custom_command(
 74      COMMAND ${ar_tool} -static -o ${target_lib_path} ${static_lib_paths}
 75      OUTPUT ${target_lib_path}
 76      COMMENT "Packing ${target_lib_name}"
 77      VERBATIM)
 78  elseif(MSVC)
 79    find_program(ar_tool lib)
 80
 81    foreach(tgt IN LISTS static_libs)
 82      list(APPEND static_lib_paths $<TARGET_FILE:${tgt}>)
 83    endforeach()
 84
 85    add_custom_command(
 86      COMMAND ${ar_tool} /NOLOGO /OUT:${target_lib_path} ${static_lib_paths}
 87      OUTPUT ${target_lib_path}
 88      COMMENT "Packing ${target_lib_name}"
 89      VERBATIM)
 90  else()
 91    message(FATAL_ERROR "Unknown compiler!")
 92  endif()
 93
 94  set(custom_target_name combine_universal_lib_for_${base_lib})
 95  add_custom_target(${custom_target_name} ALL DEPENDS ${target_lib_path})
 96  add_dependencies(${custom_target_name} ${base_lib})
 97
 98  add_library(${target_lib_name} STATIC IMPORTED GLOBAL)
 99  set_target_properties(${target_lib_name}
100    PROPERTIES
101      IMPORTED_LOCATION ${target_lib_path}
102      INTERFACE_INCLUDE_DIRECTORIES $<TARGET_PROPERTY:${base_lib},INTERFACE_INCLUDE_DIRECTORIES>)
103  add_dependencies(${target_lib_name} ${custom_target_name})
104
105endfunction()

使用方法也很简单,在CMakeLists.txt上简单引用声明即可。

1# 引入该函数脚本
2include(${CMAKE_SOURCE_DIR}/../combiner.cmake)
3
4# 声明合并的静态库Target
5combine_static_libraries(${TARGET_LIB} ${TARGET_LIB}_Bundle)
6
7# 在cmake别的target上链接该合并静态库
8target_link_libraries(${EXECUTABLE} ${TARGET_LIB}_Bundle)

对合并静态库进一步处理

给用户提供静态库产物,往往也希望隐藏下内部实现的符号位,避免用户反编译一眼看出代码底细。笔者推荐一个用Rust写的好工具armerge,在合并静态库的同时可以去除暴露的符号位,调用命令也很简单:

1armerge --keep-symbols '^libfoo_' --output libfoo_merged.a libfoo.a libcrypto.a

想在Cmake上自动处理,只需要把上面的Cmake函数的add_custom_command部分命令调用改成armerge就可以了,这里就不重复叙述了。

#Linux #MacOS #Windows #C++ #CMake