如何合并静态库
用C++写一个SDK给用户使用,一般来说都是采取打包好的动态库、静态库加头文件的方式提供。SDK内部实现往往是有不少第三方依赖,如果是静态库的形式,SDK和第三方库的编译产物都是多个独立的静态库,比如sdk.a、liba.a和libb.a等等,不利于项目集成。本文以Cmake构建方式为例,讲述如何合并多个静态库的方法。
合并静态库的工具
一般来说,C++工具链都会内置合并工具的功能,根据平台的不同,其工具依次为:
- Linux下,使用
ar
来合并静态库。 - macOS下,使用
libtool
来合并静态库。 - Windows下,使用
lib.exe
来合并静态库。
其合并命令行格式如下所示:
- Linux下的
ar
。
1# 旧版本ar命令不一定支持一步到位合并静态库,这时候可能需要先把.a文件拆回多个.o文件或者使用.mri文件辅助
2ar -rcs target.a deps0.a deps1.a
- macOS下的
libtool
。
1libtool -static -o target.a deps0.a deps1.a
- 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
就可以了,这里就不重复叙述了。