侧边栏壁纸
博主头像
柳絮飘 - AIGC, LLM大模型以及C++分享网站

学习,让淘汰成为一个借口!

  • 累计撰写 5 篇文章
  • 累计创建 3 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

其实我不懂CMake--简单入门

杜凌霄
2025-05-21 / 0 评论 / 2 点赞 / 44 阅读 / 0 字

假设我们在目录chp1下面有一个简单的文件main.cpp

 #include <iostream>
 ​
 int main()
 {
     std::cout<<"你好"<<std::endl;
 ​
     return 0;
 }

我们同时在chp1下面写一个文件CMakeLists.txt,内容如下

 cmake_minimum_required(VERSION 3.20)
 project(chp1)
 ​
 add_executable(chp1 main.cpp)

然后我们打开一个terminal,切换到chp1目录,就可以编译生成了。首先输入下面命令:

 cmake -B build -S .

会有如下输出信息

 -- The C compiler identification is AppleClang 14.0.3.14030022
 -- The CXX compiler identification is AppleClang 14.0.3.14030022
 -- Detecting C compiler ABI info
 -- Detecting C compiler ABI info - done
 -- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
 -- Detecting C compile features
 -- Detecting C compile features - done
 -- Detecting CXX compiler ABI info
 -- Detecting CXX compiler ABI info - done
 -- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
 -- Detecting CXX compile features
 -- Detecting CXX compile features - done
 -- Configuring done
 -- Generating done
 -- Build files have been written to: /xxx/chp1/chp1-1/build

这时就会在chp1下面新增加出一个build目录,这个目录里面有一些编译信息

然后在输入下面命令进行编译

 cmake --build ./build

命令行有如下信息输出

 [ 50%] Building CXX object CMakeFiles/chp1.dir/main.cpp.o
 [100%] Linking CXX executable chp1
 [100%] Built target chp1

这样就可以顺利在build目录生成出可执行文件

执行文件,可以有对应的输出

 chp1-1 % ./build/chp1 
 你好

这样,我们就使用cmake完成了第一个c++工程的编译。

一、cmake版本管理

项目指定CMake版本行为的详细信息是使用 cmake_minimum_required() 命令。这应该是CMakeLists.txt文件的第 一行,这样项目的需求就会放在在其他事情之前。

这个命令做了两件事:

  • 指定了项目所需的CMake的最低版本。如果CMakeLists.txt文件使用的CMake版本比指定的版本低,将 立即停止构建,并出现错误报告。

  • 强制设置将CMake行为匹配到对应版本。

cmake_minimum_required() 命令很简单:

 cmake_minimum_required(VERSION major.minor[.patch[.tweak]])

VERSION 关键字必须出现,提供的版本详细信息必须有 major.minor。大多数项目中, patch 和 tweak 没有必 要,因为新特性通常只在 minor 版本更新中出现。只有当修复某个错误时,项目 才应该指定 patch 部分。3.x的CMake使用了 tweak ,但项目不需要进行指定。

一般建议指定3.20以上。

二、创建一个project

每个CMake项目都应该包含一个 project() 命令,它应该在 cmake_minimum_required()之后出现。

该命令常见的 选项如下所示:

 project(projectName 
        [VERSION major[.minor[.patch[.tweak]]]] 
        [LANGUAGES languageName ...]
        )

项目名称是必需的,只能是字母、数字、下划线(_)和连字符(-),字母和下划线比较常用。由于不允许使用空 格,所以项目名称不必用引号括起来。这个名字用于项目的生成器(如Xcode和Visual Studio),也可以运用于 各种项目的其他部分,比如默认打包名称和文档元数据,项目名称等等。 projectNameproject() 命令惟一 强制性参数。 VERSION 只在CMake 3.0之后的版本支持。与 projectName 一样,CMake使用版本细节来填充变量并作为默认元数据,除此之外版本信息没有任何意义。LANGUAGES 参数定义了应该为项目启用的编程语言,支持的语言包括 C 、 CXX 、 Fortran 、 ASM 、 Java、Object-C、Object-C++ 等。 如果指定了多种语言,请用空格分隔。如果没有 语言选项,CMake将默认为 C 和 CXX 。

三、构建可执行文件

add_executable() 命令告诉CMake为一组源文件创建一个可执行文件。

此命令的形式为:

 add_executable(targetName [WIN32] [MACOSX_BUNDLE]
                [EXCLUDE_FROM_ALL]
                source1 [source2 ...]
                )
  • WIN32

    在Windows平台上构建可执行文件时,此选项表示CMake将该可执行文件构建为Windows GUI应用程序。实 际上,会使用 WinMain() 创建入口,而不是 main() ,并且会添加链接选项 /SUBSYSTEM:WINDOWS 。其他平台会忽 略 WIN32 选项。

  • MACOSX_BUNDLE

    它会引导CMake在苹果平台上构建应用程序包。与选项名称不同,不仅适用于macOS,也适用于其他苹果平 台,如iOS。此选项的确切效果在不同平台间略有不同。例如,在macOS上,app bundle有一个非常特定的 目录结构,而iOS的目录结构是扁平的。CMake还会生成一个 Info.plist文件。非Apple平台上,会忽略 MACOSX_BUNDLE 关键字。

  • EXCLUDE_FROM_ALL

    项目定义了许多目标,但默认情况下只需要构建一部分。当在构建没有指定目标时,将构建默认的 ALL 目标 (根据使用CMake生成器的不同,名称略有不同,比如:Xcode的 ALL_BUILD )。如果可执行文件用 EXCLUDE_FROM_ALL 定义,就不会包含在默认的 ALL 目标中。然后,只有当构建命令显式地请求该可执行文 件,或者作为默认构建对其有依赖时,可执行文件才会构建。

调用add_executable将创建一个可执行文件,可以在CMake项目中作为 targetName 引用。这个名称可以包含字母、数字、下划 线和连字符。构建项目时,在构建目录中创建一个与平台相关名称的可执行文件。

通过使用不同的名称多次调用 add_executable(),可以在 CMakeLists.txt文件中定义多个可执行文件。

四、生成工程

 cmake [<options>] -B <path-to-build> [-S <path-to-source>]

从cmake 3.13版本开始,我们更加推荐使用这种方法。直接用-B指定工程生成的目录,用-S指定工程根目录,也就是顶层CMakeLists.txt所在的地方。

我们上面的例子中使用

 cmake -B build -S .

也就是使用当前目录作为源码目录,在当前目录下生成build目录作为生成的工程所在的目录。

五、编译工程

 cmake --build <dir> [<options>] [-- <build-tool-options>]

我们前面的例子使用

 cmake --build ./build

也就是指定当前目录下的build目录为生成所在的工程的目录。

看得出生成工程和编译工程是非常简单的。里面的难点在于如何处理生成程序的类型(Debug, Release),这部分的解释我们后续文章会做详细解释。

如果经常编程序的同学就会问一个问题:编译出来的程序到底是Debug版本还是Release版本?

其实在Cmake内部是通过CMAKE_BUILD_TYPE来标识到底现在编译的是什么类型。我们把CMakeLists.txt修改一下

 cmake_minimum_required(VERSION 3.20)
 ​
 project(chp1)
 ​
 if(CMAKE_BUILD_TYPE MATCHES Debug)
     add_definitions(-DDEBUG)
 else()
     add_definitions(-DRELEASE)
 endif()
 ​
 add_executable(chp1 main.cpp)

也就是

  • 当类型为Debug的时候添加宏DEBUG

  • 当类型为Release的时候添加宏RELEASE

同时我们把main.cpp修改为

 #include <iostream>
 ​
 int main()
 {
 #ifdef DEBUG
     std::cout << "Debug version" << std::endl;
 #elif defined(RELEASE)
     std::cout << "Release version" << std::endl;
 #else
     std::cout << "Other version" << std::endl;
 #endif
     return 0;
 }

编译运行

 chp1-2 % cmake -B build -S .                              
 ...
 chp1-2 % cmake --build ./build
 ...
 chp1-2 % ./build/chp1         
 Release version

可以看出默认情况下生成的是Release版本。

对于cmake来说,从CMakeLists.txt生成工程文件的时候,是可以传入编译类型的,比如下面方式

 chp1-2 % cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
 ...
 chp1-2 % cmake --build ./build                       
 [ 50%] Building CXX object CMakeFiles/chp1.dir/main.cpp.o
 [100%] Linking CXX executable chp1
 [100%] Built target chp1
 chp1-2 % ./build/chp1                                
 Debug version

也就是生成工程文件的时候传入了Debug版本,那么最终生成出了Debug版本的程序。

上面我们没有单独选择生成器,cmake默认生成了Unix Makefiles。我们可以使用cmake --help来查看cmake可以有多少种generator。

 chp1 % cmake --help
 ...
 ​
 Generators
 ​
 The following generators are available on this platform (* marks default):
 * Unix Makefiles               = Generates standard UNIX makefiles.
   Ninja                        = Generates build.ninja files.
   Ninja Multi-Config           = Generates build-<Config>.ninja files.
   Watcom WMake                 = Generates Watcom WMake makefiles.
   Xcode                        = Generate Xcode project files.
   CodeBlocks - Ninja           = Generates CodeBlocks project files.
   CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
   CodeLite - Ninja             = Generates CodeLite project files.
   CodeLite - Unix Makefiles    = Generates CodeLite project files.
   Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
   Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
   Kate - Ninja                 = Generates Kate project files.
   Kate - Unix Makefiles        = Generates Kate project files.
   Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
   Sublime Text 2 - Unix Makefiles
                                = Generates Sublime Text 2 project files.
 ​

这是我的mac笔记本上支持的generator。查看官方文档我们可以看到

CMake Generators

Command-Line Build Tool Generators

These generators support command-line build tools. In order to use them, one must launch CMake from a command-line prompt whose environment is already configured for the chosen compiler and build tool.

Makefile Generators

Ninja Generators

IDE Build Tool Generators

These generators support Integrated Development Environment (IDE) project files. Since the IDEs configure their own environment one may launch CMake from any environment.

Visual Studio Generators
Other Generators

Extra Generators

Deprecated since version 3.27: Support for "Extra Generators" is deprecated and will be removed from a future version of CMake. IDEs may use the cmake-file-api(7) to view CMake-generated project build trees.

Some of the CMake Generators listed in the cmake(1) command-line tool --help output may have variants that specify an extra generator for an auxiliary IDE tool. Such generator names have the form <extra-generator> - <main-generator>. The following extra generators are known to CMake.

其实我们比较常用generator主要有:

  • Visual Studio

  • Xcode

  • Ninja

  • Unix Makefiles

generator还会引入一个新的概念:multi-config。这个概念我们以前其实经常用到,比如在Visual Studio里面,我们可以选择Debug还是Release编译,在Xcode里面也可以配置。也就是说,vs和xcode的工程都同时支持多种编译配置。事实上,以下三种generator都支持multi-config:

  • Ninja Multi-Config

  • Visual Studio Generators

  • Xcode

那我们使用一下Xcode,看上面的代码是否能正常检测编译类型。

 chp1-2 % cmake -B build -S . -GXcode           
 ...
 chp1-2 % cmake --build ./build

在build目录下面生成了一个Debug目录,里面生成了可执行文件。

 chp1-2 % ./build/Debug/chp1 
 Debug version

我们删除build目录,尝试传入编译类型

 chp1-2 % cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -GXcode
 ...
 chp1-2 % cmake --build ./build     
 ...

依然只是在build目录下面生成了一个Debug目录,并且在Debug目录里面生成了可执行文件。

 chp1-2 % ./build/Debug/chp1 
 Release version

出现了很诡异的现象,就是在Debug的目录生成的可执行程序,打印出了Release版本的信息。

其实,在使用multi-config的generator的情况下,生成的程序类型不是由CMAKE_BUILD_TYPE控制的,而是由CMAKE_CONFIGURATION_TYPES控制的,会忽略掉CMAKE_BUILD_TYPE的值

我们使用下面命令试一下

 cmake -B build -S . -GXcode

然后查看build目录下面的CMakeCache.txt,会有两个发现

  • 没有CMAKE_BUILD_TYPE相关内容

  • CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;MinSizeRel;RelWithDebInfo

而使用

 cmake -B build -S .

的时候,build目录下面的CMakeCache.txt只有

 //Choose the type of build, options are: None Debug Release RelWithDebInfo
 // MinSizeRel ...
 CMAKE_BUILD_TYPE:STRING=

也就是说:

  • 使用非multi-config的generator的时候,是CMAKE_BUILD_TYPE控制程序生成的类型,默认是NoneCMAKE_CONFIGURATION_TYPES不生效;

  • 使用multi-config的generator的时候,CMAKE_CONFIGURATION_TYPES不生效,程序生成类型完全是CMAKE_BUILD_TYPE控制。

需要把CMakeLists.txt修改成下面这样才可以

 cmake_minimum_required(VERSION 3.20)
 ​
 project(chp1)
 ​
 # if(CMAKE_BUILD_TYPE MATCHES Debug)
 # add_definitions(-DDEBUG)
 # else()
 # add_definitions(-DRELEASE)
 # endif()
 add_executable(chp1 main.cpp)
 ​
 # Add this compile definition for debug builds, this same logic works for
 # target_compile_options, target_link_options, etc.
 target_compile_definitions(chp1 PRIVATE
     $<$<CONFIG:Debug>:DEBUG=1>
     $<$<CONFIG:Release>:RELEASE=1>
 )

$<$<xxx>:yyy>这种表达式我们后面会详细介绍,这里只需要知道上面的意思就是

  • 如果CMAKE_CONFIGURATION_TYPESDebug,那么定义DEBUG=1

  • 如果CMAKE_CONFIGURATION_TYPESRelease,那么定义RELEASE=1

  • 否则什么都不定义

对于multi-config的generator,可以在生成工程阶段使用CMAKE_CONFIGURATION_TYPES来配置默认的生成类型,也可以编译的时候使用--config来执行到底选用Debug还是Release

我们先来测试一下使用CMAKE_CONFIGURATION_TYPES来控制。

  1. 首先测试默认config的情况

     chp1-2 % cmake -B build -S . -GXcode -DCMAKE_CONFIGURATION_TYPES="Release;Debug"  
     ...
     chp1-2 % cmake --build ./build
     ...
     chp1-2 % ./build/Debug/chp1 
     Debug version

    在build目录下面生成了Debug目录,并没有Release目录。也就是说,默认config选择了Debug版本。但是这个默认的值对于不同的generator是不一样的。Ninja Multi-Config也是一个multi-config的generator,我们可以测试一下

     chp1-2 % cmake -B build -S . -G"Ninja Multi-Config" -DCMAKE_CONFIGURATION_TYPES="Release;Debug"
     ...
     chp1-2 % cmake --build ./build
     [2/2] Linking CXX executable Release/chp1
     chp1-2 % ./build/Release/chp1 
     Release version

    也就是说,用除了generator之外同样的配置去生成工程,Xcode在不指定config的情况下默认生成的是Debug版本而Ninja Multi-Config生成的确是Release版本。这说明我们不能依靠默认的config去生成我们想要的版本,需要用--config显示指定

  2. 测试使用config修改配置

     chp1-2 % cmake -B build -S . -GXcode -DCMAKE_CONFIGURATION_TYPES="Release;Debug"  
     ...
     chp1-2 % cmake --build ./build --config Release
     ...
     chp1-2 % ./build/Release/chp1 
     Release version

    也比较符合预期,在build目录下面只生成了Release目录,在该目录下面生成了可执行程序。

    继续执行

     chp1-2 % cmake --build ./build --config Debug
     ...
     chp1-2 % ./build/Debug/chp1 
     Debug version

    因为该工程支持multi-config,所以可以切换编译目标。

  3. 我们再测试只指定一个CMAKE_CONFIGURATION_TYPES的情况

     chp1-2 % cmake -B build -S . -GXcode -DCMAKE_CONFIGURATION_TYPES="Release"      
     ...
     chp1-2 % cmake --build ./build --config Debug
     ...
     chp1-2 % ./build/Release/chp1 
     Release version
     chp1-2 % cmake --build ./build --config Release
     ...
     chp1-2 % ./build/Release/chp1 
     Release version

    也就是说,在显式指定只有一种configuration的情况下,无论我们使用--config如何切换,得到的都只会有一种生成的程序类型。这个其实也很好理解,--config只能从给定的config中去选择,既然只指定了一个,那么怎么选择也没有办法选择其它的类型。

  4. 最后我们再测试一下使用CMAKE_CONFIGURATION_TYPES控制single-config的情况

     chp1-2 % cmake -B build -S . -G"Unix Makefiles" -DCMAKE_CONFIGURATION_TYPES="Release;Debug" -DCMAKE_BUILD_TYPE="Release"
     ...
     CMake Warning:
       Manually-specified variables were not used by the project:
     ​
         CMAKE_CONFIGURATION_TYPES
     ​
     ​

    这跟在multi-config使用CMAKE_BUILD_TYPE传参一样,cmake警告说CMAKE_CONFIGURATION_TYPES没有被工程使用。实际上,DCMAKE_CONFIGURATION_TYPES只能在multi-config中使用,CMAKE_BUILD_TYPE只能在single-config中使用。

multi-config的总结:

  • multi-config的数量是由CMAKE_CONFIGURATION_TYPES决定的,默认情况下有Debug, Release, RelWithDebInfo, MinSizeRel,用户可以通过-DCMAKE_CONFIGURATION_TYPES=xxx显式指定需要的类型;

  • 用户可以通过--config显示指定需要的类型,但是指定的类型只能在CMAKE_CONFIGURATION_TYPES范围内才会生效;

  • 不同generator在不使用--config显式指定生成类型的时候,不同的generator表现是不一致的,尽量避免这种情况;

  • CMAKE_CONFIGURATION_TYPES只能在multi-config中使用,就像CMAKE_BUILD_TYPE只能在single-config中使用一样。

六、总结

single-config意味着工程只可能生成出一种类型的程序,类型在工程生成的时候就已经确定了;multi-config意味着cmake生成的工程是有能力生成多种类型的程序,具体生成的类型可以通过--config在编译的时候进行选择。multi-config未来也许有更多的类型,但是目前就只有Visual StudioXCode以及Ninja multi-config是这种类型,其它的都是single-config类型。

综合single-configmulti-config的情况,我们有如下结论:

  1. cmake同时支持single-configmulti-config类型;

  2. single-config类型最后生成的程序的类型是通过CMAKE_BUILD_TYPE在生成工程的时候控制的;而multi-config是通过CMAKE_CONFIGURATION_TYPES在生成工程的时候以及--config xxx在编译的时候选择来共同控制的;

  3. multi-config的默认config对于不同的generator是不一致的,最佳做法是显式指定。

我们再使用single-config的时候是使用

 if(CMAKE_BUILD_TYPE MATCHES Debug)
     add_definitions(-DDEBUG)
 else()
    add_definitions(-DRELEASE)
 endif()

来给最终的程序添加需要的宏。在使用multi-config的时候是通过

 target_compile_definitions(chp1 PRIVATE
     $<$<CONFIG:Debug>:DEBUG=1>
     $<$<CONFIG:Release>:RELEASE=1>
 )

来给最终的程序添加需要的宏。

那如何添加宏才能使得无论在single-config还是multi-config下都生效呢?

其实CMAKE给我们提供了一个只读的GENERATOR_IS_MULTI_CONFIG来帮助判断,这样我们就可以把上面加宏的代码修改为:

 if(GENERATOR_IS_MULTI_CONFIG)
     target_compile_definitions(chp1 PRIVATE
         $<$<CONFIG:Debug>:DEBUG=1>
         $<$<CONFIG:Release>:RELEASE=1>
     )
 else()
     if(CMAKE_BUILD_TYPE MATCHES Debug)
         add_definitions(-DDEBUG)
     else()
         add_definitions(-DRELEASE)
     endif()
 endif()

也就是在使用multi-config的时候使用Config判断,否则使用CMAKE_BUILD_TYPE判断。

除此之外,cmake保证在编译Release的版本的时候会加入NDEBUG宏,我们也可以在代码里面直接使用这个宏来判断,这也是一个非常准确的判断条件。

七、

结语

使用cmake生成和编译工程的时候一定要注意区分single-configmulti-config,因为两种不同的config中用来控制最终程序的版本的条件是不一样的,不注意的话很容易造成编译出来的程序的效率不符合预期。

2
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区