问题 立即构建工具,以便稍后在相同的CMake运行中使用


我有一个有趣的鸡蛋问题和一个潜在的解决方案(请参阅我发布的答案),但该解决方案以不寻常的方式使用CMake。欢迎提供更好的替代方案或评论。

问题:

该问题的简单版本可以描述为具有以下特征的单个CMake项目:

  1. 其中一个构建目标是命令行可执行文件,我将调用它 mycomp,其来源是在 mycompdir 并且无法对该目录的内容进行任何修改。
  2. 该项目包含文本文件(我会称之为 foo.my 和 bar.my)哪个需要 mycomp 运行它们来生成一组C ++源代码和标题以及一些 CMakeLists.txt 定义从这些源构建的库的文件。
  3. 同一项目中的其他构建目标需要链接到生成的库定义的库 CMakeLists.txt 文件。这些其他目标也有来源 #include 一些生成的标题。

你可以想到 mycomp 就像编译器和步骤2中的文本文件一样,是某种源文件。这提出了一个问题,因为CMake需要 CMakeLists.txt 配置时的文件,但是 mycomp 在构建时间之前不可用,因此在第一次运行时无法创建 CMakeLists.txt 文件足够早。

NON-解答:

通常情况下,基于ExternalProject的超级排列安排将是一个潜在的解决方案,但上面是我正在处理的实际项目的相当简化,我没有自由将构建分成不同的部分或执行其他大型规模重组工作。


3641
2018-03-18 12:46


起源



答案:


问题的关键是需要 mycomp 在运行CMake时可用,以便生成 CMakeLists.txt 可以创建文件,然后使用 add_subdirectory()。实现这一目标的可能方法是使用 execute_process() 从主构建运行嵌套的cmake-and-build。嵌套的cmake-and-build将使用与顶级CMake运行完全相同的源和二进制目录(除非交叉编译)。主顶层的一般结构 CMakeLists.txt 会是这样的:

# Usual CMakeLists.txt setup stuff goes here...

if(EARLY_BUILD)
    # This is the nested build and we will only be asked to
    # build the mycomp target (see (c) below)
    add_subdirectory(mycompdir)

    # End immediately, we don't want anything else in the nested build
    return()
endif()

# This is the main build, setup and execute the nested build
# to ensure the mycomp executable exists before continuing

# (a) When cross compiling, we cannot re-use the same binary dir
#     because the host and target are different architectures
if(CMAKE_CROSSCOMPILING)
    set(workdir "${CMAKE_BINARY_DIR}/host")
    execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory "${workdir}")
else()
    set(workdir "${CMAKE_BINARY_DIR}")
endif()

# (b) Nested CMake run. May need more -D... options than shown here.
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}"
                        -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
                        -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
                        -DEARLY_BUILD=ON
                        ${CMAKE_SOURCE_DIR}
               WORKING_DIRECTORY "${workdir}")

# (c) Build just mycomp in the nested build. Don't specify a --config
#     because we cannot know what config the developer will be using
#     at this point. For non-multi-config generators, we've already
#     specified CMAKE_BUILD_TYPE above in (b).
execute_process(COMMAND ${CMAKE_COMMAND} --build . --target mycomp
                WORKING_DIRECTORY "${workdir}")

# (d) We want everything from mycompdir in our main build,
#     not just the mycomp target
add_subdirectory(mycompdir)

# (e) Run mycomp on the sources to generate a CMakeLists.txt in the
#     ${CMAKE_BINARY_DIR}/foobar directory. Note that because we want
#     to support cross compiling, working out the location of the
#     executable is a bit more tricky. We cannot know whether the user
#     wants debug or release build types for multi-config generators
#     so we have to choose one. We cannot query the target properties
#     because they are only known at generate time, which is after here.
#     Best we can do is hardcode some basic logic.
if(MSVC)
    set(mycompsuffix "Debug/mycomp.exe")
elseif(CMAKE_GENERATOR STREQUAL "Xcode")
    set(mycompsuffix "Debug/mycomp")
else()
    set(mycompsuffix "mycomp")
endif()
set(mycomp_EXECUTABLE "${workdir}/mycompdir/${mycompsuffix}")
execute_process(COMMAND "${mycomp_EXECUTABLE}" -outdir foobar ${CMAKE_SOURCE_DIR}/foo.my ${CMAKE_SOURCE_DIR}/bar.my)

# (f) Now pull that generated CMakeLists.txt into the main build.
#     It will create a CMake library target called foobar.
add_subdirectory(${CMAKE_BINARY_DIR}/foobar ${CMAKE_BINARY_DIR}/foobar-build)

# (g) Another target which links to the foobar library
#     and includes headers from there
add_executable(gumby gumby.cpp)
target_link_libraries(gumby PUBLIC foobar)
target_include_directories(gumby PUBLIC foobar)

如果我们不重用(b)和(c)中的相同二进制目录,就像我们用于主构建一样,我们最终会建立 mycomp 两次,我们显然想避免。对于交叉编译,我们无法避免这种情况,所以在这种情况下我们构建了 mycomp 工具在一个单独的二进制目录中。

我已经尝试了上述方法,实际上它似乎在真实世界项目中起作用,提出了原始问题,至少对于Unix Makefile,Ninja,Xcode(OS X和iOS)和Visual Studio生成器。这种方法的一部分吸引力在于它只需要将适量的代码添加到顶层 CMakeLists.txt 文件。然而,有一些观察应该做出:

  • 如果编译器或链接器命令 mycomp 它的来源在嵌套构建和主构建之间有任何不同之处 mycomp 目标最终在(d)第二次重建。如果没有差异, mycomp 只有在不进行交叉编译时才会构建一次,这正是我们想要的。
  • 我认为没有简单的方法将完全相同的参数传递给CMake的嵌套调用(b),因为传递给顶级CMake运行(基本上描述的问题) 这里)。读 CMakeCache.txt 不是一个选项,因为它不会在第一次调用时存在,并且它不会从当前运行中提供任何新的或更改的参数。我能做的最好的事情是设置我认为可能会被使用的CMake变量,这可能会影响编译器和链接器命令 mycomp。这可以通过添加越来越多的变量来解决,因为我遇到了我发现我需要的变量,但这并不理想。
  • 当重新使用相同的二进制目录时,我们依赖于CMake没有开始将其任何文件写入二进制目录,直到生成阶段(好吧,至少直到(c)的构建完成之后)。对于测试的发电机,似乎我们没事,但我不知道是否 所有 发电机 所有 平台也遵循这种行为(我无法测试每一个组合以找出!)。这是让我最关心的部分。如果任何人都可以通过推理和/或证据确认这对所有发电机和平台都是安全的,那么这将是有价值的(如果你想作为一个单独的答案来解决这个问题,那么值得投票)。

更新: 在对CMake熟悉不同程度的工作人员的许多实际项目中使用上述策略后,可以进行一些观察。

  • 使嵌套构建重用与主构建相同的构建目录有时会导致问题。具体来说,如果用户在嵌套构建完成之后但在主构建之前杀死了CMake运行,那么 CMakeCache.txt 文件留下 EARLY_BUILD 设置 ON。然后,这使得所有后续的CMake运行都像嵌套的构建,因此主要构建基本上会丢失,直到 CMakeCache.txt 手动删除文件。在其中一个项目的某处可能出现错误 CMakeLists.txt 文件也可能导致类似的情况(未经证实)。在其自己的单独构建目录中执行嵌套构建虽然没有这样的问题但效果很好。

  • 嵌套的构建应该是 发布 而不是 调试。如果没有重新使用与主构建相同的构建目录(现在我建议使用),我们不再关心尝试避免两次编译同一个文件,所以不妨 mycomp 尽可能快。

  • 使用ccache 因此,使用不同设置重建某些文件两次所产生的任何成本都会降至最低。实际上,我们发现使用ccache通常会使嵌套构建非常快,因为与主构建相比,它几乎没有变化。

  • 嵌套的构建可能需要 CMAKE_BUILD_WITH_INSTALL_RPATH 设置 FALSE 在某些平台上,以便任何库 mycomp 无需设置环境变量等即可找到需求。


10
2018-03-18 12:46