2. CMake基础入门:CMakeLists.txt语法、project()与add_executable()、变量与缓存、生成器表达式
好,咱们正式开始CMake的旅程。说实话,我见过太多C++开发者,写代码一把好手,一到构建环节就抓瞎。Makefile写得像天书,跨平台更是噩梦。CMake就是来解决这个问题的。我个人习惯是,任何新项目,第一件事就是写CMakeLists.txt,哪怕只是个玩具项目。这能帮你从一开始就养成好习惯。
2.1 CMakeLists.txt:项目的“宪法”
每个CMake项目,根目录下必须有一个CMakeLists.txt文件。它就是整个构建系统的“宪法”,定义了项目怎么编译、链接、安装。你想想看,没有这个文件,CMake根本不知道从哪下手。
一个最基础的CMakeLists.txt长这样:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
add_executable(my_app main.cpp)
三行,搞定。第一行指定CMake最低版本,第二行定义项目名,第三行生成可执行文件。嗯,这里要注意:cmake_minimum_required一定要写在project()之前,这是规矩。
cmake_minimum_required,结果在新版CMake上编译出一堆莫名其妙的警告。后来排查半天,发现是旧版CMake的某些行为在新版里被废弃了。从那以后,我每条CMakeLists.txt第一行必定是版本声明。
2.2 project() 与 add_executable():项目的骨架
project()命令不只是起个名字那么简单。它会自动定义几个有用的变量,比如PROJECT_NAME、PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR。说白了,它帮你把项目的根目录和构建目录都记下来了。
举个例子:
project(MyAwesomeApp LANGUAGES CXX)
这里我指定了语言是C++。如果你不写,CMake默认会检测C和C++。我个人习惯显式声明,避免歧义。
add_executable()就更直接了——告诉CMake,我要生成一个可执行文件。第一个参数是目标名,第二个参数是源文件。你可以一次列多个源文件:
add_executable(my_app main.cpp utils.cpp network.cpp)
但项目大了以后,这么列会疯掉。我建议用变量来管理源文件列表:
set(SOURCES main.cpp utils.cpp network.cpp)
add_executable(my_app ${SOURCES})
这样清晰多了,对吧?
2.3 变量与缓存:CMake的“内存”
CMake里的变量,说白了就是键值对。用set()设置,用${}取值。但有个坑:变量作用域。CMake的变量是函数作用域的,出了函数就没了。如果你想跨目录共享变量,就得用缓存变量。
缓存变量用set()加CACHE关键字:
set(MY_LIB_PATH "/usr/local/lib" CACHE PATH "Path to my library")
这个变量会被写入CMakeCache.txt,下次运行CMake时仍然存在。用户也可以通过cmake -D来覆盖它。我在项目中经常用这个来让用户自定义安装路径、编译选项等。
option()命令可以创建布尔型的缓存变量,非常适合开关功能:option(USE_OPENCV "Enable OpenCV support" ON)。然后你就可以在代码里用if(USE_OPENCV)来判断了。
2.4 生成器表达式:编译时的“动态决策”
生成器表达式是CMake里最强大的特性之一,但也是最容易被忽略的。它允许你在生成构建系统时(而不是CMake配置时)做出决策。语法是$<...>。
最常见的用法是条件编译:
target_compile_definitions(my_app PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
)
这段代码的意思是:如果当前是Debug配置,就定义DEBUG_MODE宏。你想想看,这比在代码里写#ifdef NDEBUG灵活多了。
另一个常用场景是处理不同平台的源文件:
target_sources(my_app PRIVATE
$<$<PLATFORM_ID:Windows>:win_specific.cpp>
$<$<PLATFORM_ID:Linux>:linux_specific.cpp>
)
这样,Windows上只编译win_specific.cpp,Linux上只编译linux_specific.cpp。干净利落。
CONFIG、PLATFORM_ID、COMPILE_LANGUAGE等构建时的信息。这是普通CMake变量做不到的。
2.5 知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一张地图,随时回来查阅。
2.6 避坑指南与最佳实践
最后,分享几个我踩过的坑:
- 路径问题:CMake里尽量用相对路径,别写死绝对路径。我见过有人把
/home/username/project写死在CMakeLists.txt里,换台机器就编译不过。 - 变量覆盖:普通变量和缓存变量同名时,普通变量优先。这曾经让我困惑了一下午,后来发现是
set()和set(... CACHE ...)混用导致的。 - 生成器表达式的引号:生成器表达式里如果包含空格,一定要加引号。比如
$<$<CONFIG:Debug>:"-DDEBUG=1">,否则CMake会报错。
cmake --build . --target help看看生成的目标列表,确认一切符合预期。这花不了几秒钟,但能避免很多低级错误。
好了,CMake基础入门就讲到这里。记住,CMakeLists.txt是项目的“宪法”,写好了,后续的跨平台编译就成功了一半。下一章我们会深入探讨target_include_directories和target_link_libraries,到时候你会看到,CMake的模块化设计有多优雅。