CMake: 修改共享库时出现冗余链接

9
我正在处理一个由几十个共享库组成的项目,每个库都有许多相关的单元测试。许多库还依赖于其他库,因为某些特定功能的库将使用更常见库中的代码。最后当然还有依赖库的生产可执行文件。
毫无疑问,一些核心通用库的API(头文件)的更改应该触发几乎整个系统的重编译。但通常只有实现的更改,只编译修改的.cxx文件,并且理论上只需要链接修改的库-由于动态链接,不需要重新链接其他任何内容。但是CMake仍然会这样做:在重新链接库之后,它会重新链接所有与该库相关联的单元测试。然后它会重新链接该库依赖树中的所有库及其单元测试。最后,它会重新链接生产可执行文件。由于项目规模大,这需要很多宝贵的时间。
我使用基于此最小示例(为简洁起见已删除注释并将库更改为共享)的简单项目重现了这种行为。我的系统是Ubuntu 16,Intel PC,我的CMake版本为3.5.1。
从空目录开始,创建这些文件:
CMakeLists.txt
cmake_minimum_required (VERSION 2.8.11)
project (HELLO)
add_subdirectory (Hello)
add_subdirectory (Demo)

Demo/CMakeLists.txt

add_executable (helloDemo demo.cxx)
target_link_libraries (helloDemo LINK_PUBLIC Hello)

Demo/demo.cxx

#include "hello.h"
int main() { hello(); }

Hello/CMakeLists.txt

add_library (Hello SHARED hello.cxx)
target_include_directories (Hello PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

Hello/hello.h

void hello();

Hello/hello.cxx

#include <stdio.h>
void hello() { printf("hello!\n"); }

现在运行以下命令:

mkdir build
cd build
cmake ../
make

您现在可以执行 Demo/helloDemo 命令并查看 hello!

现在,使用 touch 命令编辑 Hello/hello.cxx 文件并再次使用 make 命令。您将看到 helloDemo 可执行文件被重新链接 ("Linking CXX executable helloDemo")。即使修改 hello.cxx 以打印不同的字符串,重新链接的可执行文件仍然是二进制相同的,因此重新链接是不必要的。

有没有一种方法可以防止这些冗余的构建操作呢?


重复的(但旧且呈现不佳):https://dev59.com/92HVa4cB1Zd3GeqPoqOp - Unapiedra
@Unapiedra 不同意;你链接到的问题是为什么依赖于链接的库不能并行构建。而我在这里问的是为什么需要执行链接操作。 - itaych
2个回答

8
事实证明,答案在LINK_DEPENDS_NO_SHARED属性中。在我的例子中,只需要将以下行添加到Demo/CMakeLists.txt文件中即可:
set_target_properties(helloDemo PROPERTIES LINK_DEPENDS_NO_SHARED true)

当依赖项是共享库时,这将防止helloDemo在其依赖项更新时重新链接。
在更复杂的系统中,一些库还依赖于其他库,将此设置添加到它们的配置中也很有用。
感谢CMake邮件列表的Craig Scott提供的帮助,存档在this link

我相信现在这应该是被接受的答案。 - dbn
@dbn 不是已经有了吗?也许我的视力不如从前,但我很确定我看到了一个绿色的复选标记。 - itaych
非常抱歉,你是对的。我被排序顺序所迷惑了。 - dbn
1
在定义任何相关目标之前,可以将CMAKE_LINK_DEPENDS_NO_SHARED设置为TRUE。这可能是更简单的选项。 - Alex Reinking

1

概要

  • 没有合适的解决方案。
  • 修补CMake可以得到一个工作的解决方案,但这些更改很可能会引入错误。
  • Bazel没有同样的问题(已测试),并且对于您特定的用例可能会更快。

一段旅程

使用Ninja生成器,生成的build.ninja文件(运行cmake -G Ninja ..)有以下部分。这个部分清楚地显示了问题所在:CMake对Hello/libHello.dylib添加了一个隐式依赖关系,但是只需要一个仅限顺序的依赖关系。

完整的部分如下,但请先阅读下面的解释,并向右滚动:

#############################################
# Link the executable Demo/helloDemo

build Demo/helloDemo: CXX_EXECUTABLE_LINKER__helloDemo Demo/CMakeFiles/helloDemo.dir/demo.cxx.o | Hello/libHello.dylib || Hello/libHello.dylib
  LINK_LIBRARIES = -Wl,-rpath,/Users/myuser/devel/misc/stackoverflow/q50084885/ninja/Hello     Hello/libHello.dylib
  OBJECT_DIR = Demo/CMakeFiles/helloDemo.dir
  POST_BUILD = :
  PRE_LINK = :
  TARGET_FILE = Demo/helloDemo
  TARGET_PDB = helloDemo.dbg

我在macOS上,对于Linux,请将所有*.dylib视为*.so

请注意第一个非注释行:build Demo/helloDemo: ...。Ninja语法如下:build <output>:<rule> <input> | <implicit input> || <order-only-pre-requisite>

<rule>CXX_EXECUTABLE_LINKER_helloDemo,而Hello/libHelly.dylib既是隐式输入,也是仅须满足的前提条件。

手动编辑生成的build.ninja文件并删除隐式输入,但不删除仅须满足的前提条件即可解决问题!

修补CMake

使用以下补丁来修补v3.11.1(针对此特定示例有效)。然而,这是在没有深入了解CMake源代码的情况下完成的,单元测试失败。(其中一个失败的测试是BuildDepends,只有在应用补丁后才会失败!)

diff --git a/Source/cmNinjaTargetGenerator.cxx b/Source/cmNinjaTargetGenerator.cxx
index f4faf47a2..bdbf6b948 100644
--- a/Source/cmNinjaTargetGenerator.cxx
+++ b/Source/cmNinjaTargetGenerator.cxx
@@ -239,7 +239,8 @@ cmNinjaDeps cmNinjaTargetGenerator::ComputeLinkDeps() const
 {
   // Static libraries never depend on other targets for linking.
   if (this->GeneratorTarget->GetType() == cmStateEnums::STATIC_LIBRARY ||
-      this->GeneratorTarget->GetType() == cmStateEnums::OBJECT_LIBRARY) {
+      this->GeneratorTarget->GetType() == cmStateEnums::OBJECT_LIBRARY ||
+      this->GeneratorTarget->GetType() == cmStateEnums::EXECUTABLE ) {
     return cmNinjaDeps();
   }

这个补丁会生成代码,与我说的手动更改完全相同。因此,这似乎是有效的。
进一步参考:
试图消除构建顺序依赖关系
在这里,问题类似于我们的问题:当编译目标X时,使用对象O和库依赖项L,无需等待L被构建后再编译对象O。
  • https://cmake.org/Bug/view.php?id=14726#c35023

  • https://cmake.org/Bug/view.php?id=13799

    target_link_libraries会在目标和其依赖项之间添加传递性链接依赖项和构建顺序依赖项。在许多情况下,构建顺序依赖项是不必要的,并且会导致受限制的叠加依赖图,从而限制了构建的并行执行。

    [Brad King]: FYI, 我们默认具有这些依赖关系的原因是因为库的构建规则可能具有自定义命令来生成头文件或源代码,然后被链接到它的目标使用。即使对于非链接目标(如静态库),也适用。此外,在单个目标内,编译和链接步骤之间没有目标级别的排序依赖关系。因此,任何链接(共享库和可执行文件)的目标必须具有其链接依赖项的顺序依赖关系。

    当然,如果项目作者愿意负责,我认为没有理由不提供跳过这些依赖关系的选项,至少对于静态库而言。一种方法是添加一个目标属性来覆盖目标级别的排序依赖关系。这样,可以使静态库不依赖任何内容或依赖其实现依赖项的子集:

  • https://cmake.org/pipermail/cmake-developers/2014-June/010708.html

    我的观点是,在构建a.cc.o时没有等待构建b.cc.o和prog.cc.o; 它们可以与a.cc.o同时构建。

    因此,我想知道为什么在CMake处理此过程时会将libA.so添加为仅顺序依赖项到b.cc.o?

更多参考资料


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