让CMake在不使用包装脚本的情况下构建源代码

32

我正在尝试使 CMake 构建到一个名为 "build" 的目录中,例如 project/build,其中 CMakeLists.txt 位于 project/ 中。

我知道我可以这样做:

mkdir build
cd build
cmake ../

但是那太麻烦了。我可以把它放在脚本中并调用它,但是这样就很难为CMake提供不同的参数(比如-G“MSYS Makefiles”),或者我需要在每个平台上编辑此文件。

最好的情况下,我想在主要的CMakeLists.txt中做一些像SET(CMAKE_OUTPUT_DIR build)的事情。请告诉我是否可能,如果可能,请说明如何操作?或者有其他的基于外部构建的方法可以轻松指定不同的参数吗?


1
我和你有相同的项目架构。我的“build”目录总是在这里。我个人认为输入“cmake ..”并不是什么大问题。 - Offirmo
4个回答

62
CMake 3.13或更高版本支持命令行选项-S-B,分别用于指定源目录和二进制目录。
cmake -S . -B build -G "MSYS Makefiles"

这将在当前文件夹中查找CMakeLists.txt并创建一个build文件夹(如果它还不存在)。
对于较旧版本的CMake,您可以使用未记录的CMake选项-H-B来指定调用cmake时的源和二进制目录:
cmake -H. -Bbuild -G "MSYS Makefiles"

请注意,选项和目录路径之间不得有空格字符。

12
不错的选择。但Max说“给cmake提供不同的参数很不愉快”。他希望将其放入CMakeList中。 - Offirmo
@Offirmo 实际上,Max 说的是相反的。他说通过文件提供论点是不愉快的,因为这样文件就必须在每个平台上进行编辑。但这假设文件已经提交到 Git 并与其他开发人员共享。如果没有共享(或者只共享了像在我的回答中那样的共同部分),那么这就不是问题。 - undefined

8

我最近找到的一个解决方案是将“外部构建”概念与Makefile包装器结合起来。

在我的顶层CMakeLists.txt文件中,我包含以下内容以防止内部构建:

if ( ${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR} )
    message( FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt." )
endif()

然后,我创建一个顶层的Makefile,并包含以下内容:

# -----------------------------------------------------------------------------
# CMake project wrapper Makefile ----------------------------------------------
# -----------------------------------------------------------------------------

SHELL := /bin/bash
RM    := rm -rf
MKDIR := mkdir -p

all: ./build/Makefile
    @ $(MAKE) -C build

./build/Makefile:
    @  ($(MKDIR) build > /dev/null)
    @  (cd build > /dev/null 2>&1 && cmake ..)

distclean:
    @  ($(MKDIR) build > /dev/null)
    @  (cd build > /dev/null 2>&1 && cmake .. > /dev/null 2>&1)
    @- $(MAKE) --silent -C build clean || true
    @- $(RM) ./build/Makefile
    @- $(RM) ./build/src
    @- $(RM) ./build/test
    @- $(RM) ./build/CMake*
    @- $(RM) ./build/cmake.*
    @- $(RM) ./build/*.cmake
    @- $(RM) ./build/*.txt

ifeq ($(findstring distclean,$(MAKECMDGOALS)),)
    $(MAKECMDGOALS): ./build/Makefile
    @ $(MAKE) -C build $(MAKECMDGOALS)
endif

默认目标all可通过键入make来调用,并调用目标./build/Makefile
目标./build/Makefile的第一件事是使用$(MKDIR)创建build目录,其中$(MKDIR)mkdir -p的变量。目录build是我们将执行外部源生成构建的地方。我们提供参数-p以确保mkdir不会因为尝试创建可能已经存在的目录而报错。
目标./build/Makefile的第二件事是更改目录到build目录并调用cmake
回到all目标,我们调用$(MAKE) -C build,其中$(MAKE)是自动生成的make Makefile变量。 make -C在做任何事情之前都会更改目录。因此,使用$(MAKE) -C build等同于执行cd build; make
总之,使用make allmake调用此Makefile包装器相当于执行:
mkdir build
cd build
cmake ..
make 

目标distclean会调用cmake ..,然后是make -C build clean,最后会从build目录中删除所有内容。我相信这正是您在问题中要求的。
Makefile的最后一部分评估用户提供的目标是否为distclean。如果不是,则在调用之前将更改目录到build。这非常强大,因为用户可以键入例如make clean,而Makefile将将其转换为等效的cd build; make clean
总之,这个Makefile包装器与必须在源外构建的CMake配置相结合,使得用户永远不必与命令cmake交互。此解决方案还提供了一种优雅的方法来从build目录中删除所有CMake输出文件。
顺便说一下,在Makefile中,我们使用前缀@来抑制来自shell命令的输出,并使用前缀@-来忽略shell命令的错误。当使用rm作为distclean目标的一部分时,如果文件不存在(可能已经使用rm -rf build命令行删除或根本没有生成),则该命令将返回错误。此返回错误将强制我们的Makefile退出。我们使用前缀@-来防止这种情况发生。如果文件已被删除,则是可以接受的;我们希望我们的Makefile继续并删除其余文件。
还要注意的一件事:如果您使用变量数量的CMake变量构建项目,例如cmake .. -DSOMEBUILDSUSETHIS:STRING="foo" -DSOMEOTHERBUILDSUSETHISTOO:STRING="bar",则此Makefile可能无法正常工作。此Makefile假定您以一致的方式调用CMake,即通过键入cmake ..或提供cmake一致数量的参数(您可以将其包含在Makefile中)。
最后,请给予信用。此Makefile包装器是从C++ Application Project Template提供的Makefile进行改编的。
此答案最初发布在此处。我认为它也适用于您的情况。

4
我认为这通常是一个不错的解决方案。但它使用了一个包装脚本。而原帖作者要求一个“不需要包装脚本”的解决方案。它也不能在Windows上运行。 - hko
1
这可能只是我的个人想法,但我不喜欢用特定的构建系统(如Make)包装一个构建系统生成器。 - Rich von Lehe
一个跨平台的解决方案是使用CMake编写包装脚本。请参考我的回答 - undefined

5

根据之前的回答,我编写了以下模块,您可以包含它以强制执行外部构建。

set(DEFAULT_OUT_OF_SOURCE_FOLDER "cmake_output")

if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
    message(WARNING "In-source builds not allowed. CMake will now be run with arguments:
        cmake -H. -B${DEFAULT_OUT_OF_SOURCE_FOLDER}
")

    # Run CMake with out of source flag
    execute_process(
            COMMAND ${CMAKE_COMMAND} -H. -B${DEFAULT_OUT_OF_SOURCE_FOLDER}
            WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})

    # Cause fatal error to stop the script from further execution
    message(FATAL_ERROR "CMake has been ran to create an out of source build.
This error prevents CMake from running an in-source build.")
endif ()

这个方法是可行的,不过我已经注意到两个缺点:

  • 如果用户懒惰并且只运行cmake .,他们将始终看到一个FATAL_ERROR。我找不到其他阻止CMake执行任何其他操作并提前退出的方法。
  • 传递给原始调用cmake的任何命令行参数都不会传递给"源代码外构建调用"。

欢迎提出改进此模块的建议。


避免致命错误的一种方法是使用else()而不是endif(),并将endif()放置在原始cmake代码的末尾。 - hko
@hko 这是正确的,但考虑到我将其放在一个单独的、可重用的模块中,并由 CMakeLists.txt 文件包含,你的建议并不适用。 - Arno Moonen

1
诀窍是用CMake编写你的包装脚本!
Bash脚本或Makefile包装需要开发人员在系统上安装Bash或Make,但使用CMake编写的脚本将在所有平台上运行,而不会引入任何新的依赖项。
我们可以利用CMake的-P选项,它将:
将给定的CMake文件处理为以CMake语言编写的脚本。不执行配置或生成步骤,也不修改缓存。
在脚本内部,我们将使用file()命令创建构建目录,然后使用execute_process()运行CMake,这次使用生成构建系统所需的选项。由于选项在脚本中指定,这样可以节省每次在命令行上手动输入它们的时间。
在项目文件夹中创建一个名为build.cmake的新文本文件,并粘贴以下代码:
#!/usr/bin/env -S cmake -P

# Set default values for project variables
set(SOURCE_PATH "${CMAKE_CURRENT_LIST_DIR}") # directory of this script
set(BUILD_PATH "${SOURCE_PATH}/build")

set(CPUS 16) # number of logical processor cores (for parallel builds)

# Set generator for each platform
if(WIN32)
    set(GENERATOR "MSYS Makefiles")
elseif(APPLE)
    set(GENERATOR "Xcode")
else()
    set(GENERATOR "Ninja")
endif()

# Allow developers to override the above variables if they want
if(EXISTS "${SOURCE_PATH}/overrides.cmake")
    include("${SOURCE_PATH}/overrides.cmake")
endif()

# Configure
if(NOT EXISTS "${BUILD_PATH}/CMakeCache.txt")
    file(MAKE_DIRECTORY "${BUILD_PATH}")
    execute_process(
        # CMAKE_ARGS on next line must not be quoted
        COMMAND "${CMAKE_COMMAND}" -S "${SOURCE_PATH}" -B . -G "${GENERATOR}" ${CMAKE_ARGS}
        WORKING_DIRECTORY "${BUILD_PATH}"
        RESULT_VARIABLE EXIT_STATUS
    )
    if(NOT "${EXIT_STATUS}" EQUAL "0")
        file(REMOVE "${BUILD_PATH}/CMakeCache.txt") # force CMake to run again next time
        message(FATAL_ERROR "CMake failed with status ${EXIT_STATUS}.")
    endif()
endif()

# Build
execute_process(
    COMMAND "${CMAKE_COMMAND}" --build . --parallel "${CPUS}"
    WORKING_DIRECTORY "${BUILD_PATH}"
    RESULT_VARIABLE EXIT_STATUS
)
if(NOT "${EXIT_STATUS}" EQUAL "0")
    message(FATAL_ERROR "${GENERATOR} failed with status ${EXIT_STATUS}.")
endif()

重要提示:文件build.cmake必须使用Unix风格的LF(\n)行终止符,以使shebang行正常工作。在某些系统上,需要使用env的-S选项,以允许在shebang行上指定多个参数。
现在,您只需在命令行上运行以下命令即可构建项目:
# From any Unix shell (Linux, macOS, WSL, Git Bash)
./build.cmake

# CMD or PowerShell
cmake -P build.cmake

太好了,但是项目变量都是硬编码到build.cmake中的,所以对于所有开发人员来说都是相同的。如果我想用不同的变量集构建项目怎么办?
没问题!只需在与build.cmake相同的目录中创建另一个名为overrides.cmake的文件。在overrides.cmake中指定你需要的值,它们将优先于build.cmake中的值。
例如,在overrides.cmake中你可能会有:
set(CPUS 32) # override default CPUS set in build.cmake
set(GENERATOR "Visual Studio 17 2022") # use different generator

# Pass some options to CMake
set(CMAKE_ARGS
    "-DFIRST_OPTION=Value 1"
    "-DSECOND_OPTION=Value 2"
)

set(ENV{PATH} "C:/Tools;$ENV{PATH}") # add a directory to PATH

重要提示:必须将文件overrides.cmake添加到您的项目的.gitignore中,以允许每个开发人员指定自己的覆盖内容。
看看MuseScore的build.cmake开发者文档,获取更多想法,比如如何读取处理通过命令行传递给脚本的参数,以及如何使用MSVC作为编译器。

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