如何合并静态库
用C++写一个SDK给用户使用,一般来说都是采取打包好的动态库、静态库加头文件的方式提供。SDK内部实现往往是有不少第三方依赖,如果是静态库的形式,SDK和第三方库的编译产物都是多个独立的静态库,比如sdk.a、liba.a和libb.a等等,不利于项目集成。本文以Cmake构建方式为例,讲述如何合并多个静态库的方法。
合并静态库的工具
一般来说,C++工具链都会内置合并工具的功能,根据平台的不同,其工具依次为:
- Linux下,使用
ar
来合并静态库。 - macOS下,使用
libtool
来合并静态库。 - Windows下,使用
lib.exe
来合并静态库。
其合并命令行格式如下所示:
- Linux下的
ar
。
# 旧版本ar命令不一定支持一步到位合并静态库,这时候可能需要先把.a文件拆回多个.o文件或者使用.mri文件辅助
ar -rcs target.a deps0.a deps1.a
- macOS下的
libtool
。
libtool -static -o target.a deps0.a deps1.a
- Windows下的
lib.exe
。
lib /NOLOGO /OUT:target.lib deps0.lib deps1.lib
使用Cmake脚本编译时自动合并静态库
如果每次编译都要手动敲命令来执行合并操作,实在太麻烦了,工作中往往使用脚本来自动处理。以我写的一个cmake函数脚本为例,每次编译时会自动生成合并静态库的target
,读者可以参考使用。
该脚本已经托管到github上,其地址是【kesco/static-libraries-combiner】。
function(combine_static_libraries base_lib target_lib_name)
list(APPEND static_libs ${base_lib})
function(get_lib_deps_recursively target)
set(link_libs LINK_LIBRARIES)
get_target_property(target_type ${target} TYPE)
if (${target_type} STREQUAL "INTERFACE_LIBRARY")
set(link_libs INTERFACE_LINK_LIBRARIES)
endif()
get_target_property(public_dependencies ${target} ${link_libs})
foreach(dependency IN LISTS public_dependencies)
if(TARGET ${dependency})
get_target_property(alias ${dependency} ALIASED_TARGET)
if (TARGET ${alias})
set(dependency ${alias})
endif()
get_target_property(_type ${dependency} TYPE)
if (${_type} STREQUAL "STATIC_LIBRARY")
list(APPEND static_libs ${dependency})
endif()
get_property(library_already_added
GLOBAL PROPERTY _${base_lib}_static_bundle_${dependency})
if (NOT library_already_added)
set_property(GLOBAL PROPERTY _${base_lib}_static_bundle_${dependency} ON)
get_lib_deps_recursively(${dependency})
endif()
endif()
endforeach()
set(static_libs ${static_libs} PARENT_SCOPE)
endfunction()
get_lib_deps_recursively(${base_lib})
list(REMOVE_DUPLICATES static_libs)
set(target_lib_path
${CMAKE_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${target_lib_name}${CMAKE_STATIC_LIBRARY_SUFFIX})
if (CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|GNU)$")
file(WRITE ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in
"CREATE ${target_lib_path}\n" )
foreach(tgt IN LISTS static_libs)
file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in
"ADDLIB $<TARGET_FILE:${tgt}>\n")
endforeach()
file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in "SAVE\n")
file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in "END\n")
file(GENERATE
OUTPUT ${CMAKE_BINARY_DIR}/${target_lib_name}.ar
INPUT ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in)
set(ar_tool ${CMAKE_AR})
if (CMAKE_INTERPROCEDURAL_OPTIMIZATION)
set(ar_tool ${CMAKE_CXX_COMPILER_AR})
endif()
add_custom_command(
COMMAND ${ar_tool} -M < ${CMAKE_BINARY_DIR}/${target_lib_name}.ar
OUTPUT ${target_lib_path}
COMMENT "Packing ${target_lib_name}"
VERBATIM)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")
find_program(ar_tool libtool)
foreach(tgt IN LISTS static_libs)
list(APPEND static_lib_paths $<TARGET_FILE:${tgt}>)
endforeach()
add_custom_command(
COMMAND ${ar_tool} -static -o ${target_lib_path} ${static_lib_paths}
OUTPUT ${target_lib_path}
COMMENT "Packing ${target_lib_name}"
VERBATIM)
elseif(MSVC)
find_program(ar_tool lib)
foreach(tgt IN LISTS static_libs)
list(APPEND static_lib_paths $<TARGET_FILE:${tgt}>)
endforeach()
add_custom_command(
COMMAND ${ar_tool} /NOLOGO /OUT:${target_lib_path} ${static_lib_paths}
OUTPUT ${target_lib_path}
COMMENT "Packing ${target_lib_name}"
VERBATIM)
else()
message(FATAL_ERROR "Unknown compiler!")
endif()
set(custom_target_name combine_universal_lib_for_${base_lib})
add_custom_target(${custom_target_name} ALL DEPENDS ${target_lib_path})
add_dependencies(${custom_target_name} ${base_lib})
add_library(${target_lib_name} STATIC IMPORTED GLOBAL)
set_target_properties(${target_lib_name}
PROPERTIES
IMPORTED_LOCATION ${target_lib_path}
INTERFACE_INCLUDE_DIRECTORIES $<TARGET_PROPERTY:${base_lib},INTERFACE_INCLUDE_DIRECTORIES>)
add_dependencies(${target_lib_name} ${custom_target_name})
endfunction()
使用方法也很简单,在CMakeLists.txt
上简单引用声明即可。
# 引入该函数脚本
include(${CMAKE_SOURCE_DIR}/../combiner.cmake)
# 声明合并的静态库Target
combine_static_libraries(${TARGET_LIB} ${TARGET_LIB}_Bundle)
# 在cmake别的target上链接该合并静态库
target_link_libraries(${EXECUTABLE} ${TARGET_LIB}_Bundle)
对合并静态库进一步处理
给用户提供静态库产物,往往也希望隐藏下内部实现的符号位,避免用户反编译一眼看出代码底细。笔者推荐一个用Rust写的好工具armerge,在合并静态库的同时可以去除暴露的符号位,调用命令也很简单:
armerge --keep-symbols '^libfoo_' --output libfoo_merged.a libfoo.a libcrypto.a
想在Cmake上自动处理,只需要把上面的Cmake函数的add_custom_command
部分命令调用改成armerge
就可以了,这里就不重复叙述了。