使用CMake将多个静态库合并为一个

59
我有一个非常相似的问题,与CMake邮件列表上描述的问题(链接)非常相似。我们有一个依赖于许多静态库的项目(所有这些库都是从单独的子模块中构建的源代码,并且每个子模块都有自己的CMakeLists.txt文件来描述每个库的构建过程),我想将它们合并为一个静态库,以供消费者使用。我的库的依赖关系可能会发生变化,我不想给后面的开发人员带来更多负担。最好的解决方案是将所有库捆绑到一个单一的库中。

有趣的是,当将目标设置为mylib并像下面这样使用target_link_libraries命令时,它并不会将所有的静态库组合在一起。

target_link_libraries(mylib a b c d)

然而,奇怪的是,如果我将mylib项目作为可执行项目的子模块,并且仅在顶级可执行CMakeLists.txt文件中链接到mylib,则库似乎已经合并。也就是说,mylib大小为27 MB,而不是当我将目标设置为仅构建mylib时的3MB。
有一些解决方案描述了将库解压缩为对象文件并重新组合(herehere),但当CMake似乎完全能够像上面的示例描述的那样自动合并库时,这似乎非常笨拙。我是否遗漏了某个神奇的命令,或者有没有推荐的优雅方法来制作发布库?

如果您正在使用gcc,并且不需要使您的makefile与编译器无关,则可以尝试--whole-archive选项。 - Karsten Koop
@KarstenKoop 需要同时支持 Apple Clang 和 GCC。 - learnvst
可能是重复的问题 https://dev59.com/fm865IYBdhLWcg3wa980 - n. m.
1
嗯,@n.m.,我本来希望CMake以平台无关的方式完成这个任务,因为这是该工具的全部意义。我下面的答案可行,但由于它的平台依赖性而很糟糕。 - learnvst
我认为你想要学习这个:http://www.mail-archive.com/cmake@cmake.org/msg28670/libutils.cmake。 这是mysql CMake构建的一个片段。他们有自己的宏来合并库。正如你所看到的,当将静态库合并成更大的静态库时,他们会分别处理unix/apple/windows路径。我相信没有真正可移植的方法来做到这一点。 - n. m.
显示剩余3条评论
7个回答

20
给出我能想到的最简单的工作示例:有两个类a和b,其中a依赖于b。

a.h

#ifndef A_H
#define A_H

class aclass
{
public:
    int method(int x, int y);
};

#endif

a.cpp

#include "a.h"
#include "b.h"

int aclass::method(int x, int y) {
    bclass b;
    return x * b.method(x,y);
}

b.h

#ifndef B_H
#define B_H

class bclass
{
public:
    int method(int x, int y);
};

#endif

b.cpp

#include "b.h"

int bclass::method(int x, int y) {
    return x+y;
}

main.cpp

#include "a.h"
#include <iostream>

int main()
{
    aclass a;
    std::cout << a.method(3,4) << std::endl;

    return 0;
}

可以将它们编译成单独的静态库,然后使用自定义目标组合这些静态库。

cmake_minimum_required(VERSION 2.8.7)

add_library(b b.cpp b.h)
add_library(a a.cpp a.h)
add_executable(main main.cpp)

set(C_LIB ${CMAKE_BINARY_DIR}/libcombi.a)

add_custom_target(combined
        COMMAND ar -x $<TARGET_FILE:a>
        COMMAND ar -x $<TARGET_FILE:b>
        COMMAND ar -qcs ${C_LIB} *.o
        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
        DEPENDS a b
        )

add_library(c STATIC IMPORTED GLOBAL)
add_dependencies(c combined)

set_target_properties(c
        PROPERTIES
        IMPORTED_LOCATION ${C_LIB}
        )

target_link_libraries(main c)

使用苹果的libtool版本的自定义目标也可以正常工作...

add_custom_target(combined
        COMMAND libtool -static -o ${C_LIB} $<TARGET_FILE:a> $<TARGET_FILE:b>
        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
        DEPENDS a b
        )

仍然感觉应该有更简洁的方法...


2
这个回答不是关于问题的答案。这个回复是关于合并几个目标文件,而问题是关于合并几个库的。 - ctc chen
1
@ctcchen:看起来 CMakeLists.txt 会生成一个合并的库 libcombi.a。但是我想问learnvst,这真的需要自定义目标吗?难道没有更标准的机制吗? - einpoklum
5
不,他从a.cpp和b.cpp创建了两个库“a”和“b”,然后将它们组合成一个名为“c”的库。 - Jimmy Pettersson
2
谢谢!在Windows上执行相应操作的方法是添加自定义命令"lib.exe /OUT:combi.lib a.lib b.lib"(很好知道CMake不提供任何帮助方法,我们必须手动分别支持每个平台)。 - Top-Master

12
您可以使用此函数来加入任意数量的库。
function(combine_archives output_archive list_of_input_archives)
    set(mri_file ${TEMP_DIR}/${output_archive}.mri)
    set(FULL_OUTPUT_PATH ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/lib${output_archive}.a)
    file(WRITE ${mri_file} "create ${FULL_OUTPUT_PATH}\n")
    FOREACH(in_archive ${list_of_input_archives})
        file(APPEND ${mri_file} "addlib ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/lib${in_archive}.a\n")
    ENDFOREACH()
    file(APPEND ${mri_file} "save\n")
    file(APPEND ${mri_file} "end\n")

    set(output_archive_dummy_file ${TEMP_DIR}/${output_archive}.dummy.cpp)
    add_custom_command(OUTPUT ${output_archive_dummy_file}
                       COMMAND touch ${output_archive_dummy_file}
                       DEPENDS ${list_of_input_archives})

    add_library(${output_archive} STATIC ${output_archive_dummy_file})
    add_custom_command(TARGET ${output_archive}
                       POST_BUILD
                       COMMAND ar -M < ${mri_file})
endfunction(combine_archives)

使用add_custom_command而不是add_custom_target的好处在于只有在需要时才会构建库(及其依赖项),而不是每次都构建。缺点是会打印生成虚拟文件的过程。


虽然这对于合并目标文件很好,但我认为通过添加target_link_libraries调用以获取传递依赖项可以改进它。 - Daniel Jour
1
你能详细说明一下吗? 缺少哪些依赖项? - zbut
3
不适用于 macOS。在 macOS 上,-Mar 命令中不存在。 :( - Adham Zahran
1
这应该是被接受的答案,谢谢!可以轻松修改上面的内容,使用libtool来适用于MacOS,从而使其在所有类Unix操作系统上工作。 - cyrusbehr

9

这并没有直接回答问题,但我发现它很有用:

https://cristianadam.eu/20190501/bundling-together-static-libraries-with-cmake/

首先,定义一个 CMake 函数,该函数将收集目标所需的所有静态库并将它们合并为单个静态库:

add_library(awesome_lib STATIC ...);
bundle_static_library(awesome_lib awesome_lib_bundled)

这是实际函数的复制粘贴:

function(bundle_static_library tgt_name bundled_tgt_name)
  list(APPEND static_libs ${tgt_name})

  function(_recursively_collect_dependencies input_target)
    set(_input_link_libraries LINK_LIBRARIES)
    get_target_property(_input_type ${input_target} TYPE)
    if (${_input_type} STREQUAL "INTERFACE_LIBRARY")
      set(_input_link_libraries INTERFACE_LINK_LIBRARIES)
    endif()
    get_target_property(public_dependencies ${input_target} ${_input_link_libraries})
    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 _${tgt_name}_static_bundle_${dependency})
        if (NOT library_already_added)
          set_property(GLOBAL PROPERTY _${tgt_name}_static_bundle_${dependency} ON)
          _recursively_collect_dependencies(${dependency})
        endif()
      endif()
    endforeach()
    set(static_libs ${static_libs} PARENT_SCOPE)
  endfunction()

  _recursively_collect_dependencies(${tgt_name})

  list(REMOVE_DUPLICATES static_libs)

  set(bundled_tgt_full_name 
    ${CMAKE_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${bundled_tgt_name}${CMAKE_STATIC_LIBRARY_SUFFIX})

  if (CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|GNU)$")
    file(WRITE ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in
      "CREATE ${bundled_tgt_full_name}\n" )
        
    foreach(tgt IN LISTS static_libs)
      file(APPEND ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in
        "ADDLIB $<TARGET_FILE:${tgt}>\n")
    endforeach()
    
    file(APPEND ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in "SAVE\n")
    file(APPEND ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in "END\n")

    file(GENERATE
      OUTPUT ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar
      INPUT ${CMAKE_BINARY_DIR}/${bundled_tgt_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}/${bundled_tgt_name}.ar
      DEPENDS ${static_libs}  
      OUTPUT ${bundled_tgt_full_name}
      COMMENT "Bundling ${bundled_tgt_name}"
      VERBATIM)
  elseif(MSVC)
    find_program(lib_tool lib)

    foreach(tgt IN LISTS static_libs)
      list(APPEND static_libs_full_names $<TARGET_FILE:${tgt}>)
    endforeach()

    add_custom_command(
      COMMAND ${lib_tool} /NOLOGO /OUT:${bundled_tgt_full_name} ${static_libs_full_names}
      DEPENDS ${static_libs}          
      OUTPUT ${bundled_tgt_full_name}
      COMMENT "Bundling ${bundled_tgt_name}"
      VERBATIM)
  else()
    message(FATAL_ERROR "Unknown bundle scenario!")
  endif()

  add_custom_target(bundling_target ALL DEPENDS ${bundled_tgt_full_name})
  add_dependencies(bundling_target ${tgt_name})

  add_library(${bundled_tgt_name} STATIC IMPORTED)
  set_target_properties(${bundled_tgt_name} 
    PROPERTIES 
      IMPORTED_LOCATION ${bundled_tgt_full_name}
      INTERFACE_INCLUDE_DIRECTORIES $<TARGET_PROPERTY:${tgt_name},INTERFACE_INCLUDE_DIRECTORIES>)
  add_dependencies(${bundled_tgt_name} bundling_target)

endfunction()

1
谢谢分享!为了确保目标库在其中一个依赖项发生更改时被生成,可以进行一些小的改进:将以下行添加到“add_custom_command”行中:DEPENDS ${static_libs} - fredvanl
请注意,依赖项必须已经通过cmakeadd_subdirectory(...)加载,否则bundle_static_library将无法获取到依赖项。 - undefined

4
如果你尝试合并的库来自第三方,则需要遵循 learnvst 示例并使用以下代码来处理可能的 .o 文件替换(例如,如果 liba 和 libb 都有一个文件名为 zzz.o)。
## Create static library (by joining the new objects and the dependencies)
ADD_LIBRARY("${PROJECT_NAME}-static" STATIC ${SOURCES})
add_custom_command(OUTPUT lib${PROJECT_NAME}.a
                   COMMAND rm ARGS -f *.o
                   COMMAND ar ARGS -x ${CMAKE_BINARY_DIR}/lib${PROJECT_NAME}-static.a
                   COMMAND rename ARGS 's/^/lib${PROJECT_NAME}-static./g' *.o
                   COMMAND rename ARGS 's/\\.o/.otmp/g' *.o
                   COMMAND ar ARGS -x ${CMAKE_SOURCE_DIR}/lib/a/liba.a
                   COMMAND rename ARGS 's/^/liba./g' *.o
                   COMMAND rename ARGS 's/\\.o/.otmp/g' *.o
                   COMMAND ar ARGS -x ${CMAKE_SOURCE_DIR}/lib/b/libb.a
                   COMMAND rename ARGS 's/^/libb./g' *.o
                   COMMAND rename ARGS 's/\\.o/.otmp/g' *.o
                   COMMAND rename ARGS 's/\\.otmp/.o/g' *.otmp
                   COMMAND ar ARGS -r lib${PROJECT_NAME}.a *.o
                   COMMAND rm ARGS -f *.o
                   DEPENDS "${PROJECT_NAME}-static")

add_custom_target(${PROJECT_NAME} ALL DEPENDS lib${PROJECT_NAME}.a)

否则,如果这些库是你自己的,你应该使用CMake OBJECT库,这是一个非常好的机制来将它们合并。

1
你几乎肯定应该使用PROJECT_BINARY_DIRPROJECT_SOURCE_DIR而不是CMAKE_...变量。越来越流行的是将其他CMake项目导入到“超级构建”中,上面的答案在这种情况下会出现问题。 - Alex Reinking


1
正确的做法不是通过合并静态库来解决问题,而是向用户提供CMake Config文件,其中包含将所有必要部分链接在一起的方式。 可以使用CMake生成这些文件,或生成pkg-config文件,以及可能是其他格式的“告诉我如何链接这个和那个库”的工具。
有些用户可能会对您所链接的库感兴趣,甚至在链接您的库时使用自己的副本/版本相同的库。恰恰是在这种情况下,您的解决方案是可怕的,并且阻止用户集成多个代码片段,因为您决定他们必须绝对使用您的依赖项副本(这就是当您将静态库依赖项组合成一个静态库时所做的事情)。

除非库意图用于不基于CMake的项目。 - Dávid Tóth
...或者这些东西是交错的静态库,其中某些内容指的是newlib中的某些内容,但在应用程序中作为另一个.a使用。ld不允许您这样做-您必须聚合.a文件或类似文件才能到达那里。没有整洁的CMake方式来完成这个任务...下次在口出之前先检查一下他们在做什么... - Svartalf

1

我基于zbut的答案创建了一个解决方案,但它支持从给定目标检索输入库路径,并且还支持像Ninja Multi-Config这样的多配置生成器:

# Combine a list of library targets into a single output archive
# Usage:
# combine_archives(output_archive_name input_target1 input_target2...)
function(combine_archives output_archive)
    # Generate the MRI file for ar to consume.
    # Note that a separate file must be generated for each build configuration.
    set(mri_file ${CMAKE_BINARY_DIR}/$<CONFIG>/${output_archive}.mri)
    set(mri_file_content "create ${CMAKE_BINARY_DIR}/$<CONFIG>/lib${output_archive}.a\n")
    FOREACH(in_target ${ARGN})
        string(APPEND mri_file_content "addlib $<TARGET_FILE:${in_target}>\n")
    ENDFOREACH()
    string(APPEND mri_file_content "save\n")
    string(APPEND mri_file_content "end\n")
    file(GENERATE
            OUTPUT ${mri_file}
            CONTENT ${mri_file_content}
            )

    # Create a dummy file for the combined library
    # This dummy file depends on all the input targets so that the combined library is regenerated if any of them changes.
    set(output_archive_dummy_file ${CMAKE_BINARY_DIR}/${output_archive}.dummy.cpp)
    add_custom_command(OUTPUT ${output_archive_dummy_file}
            COMMAND touch ${output_archive_dummy_file}
            DEPENDS ${ARGN})

    add_library(${output_archive} STATIC ${output_archive_dummy_file})

    # Add a custom command to combine the archives after the static library is "built".
    add_custom_command(TARGET ${output_archive}
            POST_BUILD
            COMMAND ar -M < ${mri_file}
            COMMENT "Combining static libraries for ${output_archive}"
            )
endfunction(combine_archives)

使用以下命令可以从 libTargetA.alibTargetB.a 生成 libTargetC.a:

add_library(TargetA STATIC ...)
add_library(TargetB STATIC ...)
combine_archives(TargetC TargetA TargetB)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接