FEATURED · 精选文章

从手写Makefile到CMakeLists.txt:构建系统核心概念与实战指南

发布时间 / 2026/8/7 1:57:54
来源 / 创域科博编辑部
栏目 / 资讯中心
从手写Makefile到CMakeLists.txt:构建系统核心概念与实战指南 1. 从“手写Makefile”到“CMakeLists.txt”为什么我们需要构建系统如果你是从零开始学习C/C或者一直在一个简单的IDE比如Dev-C、Code::Blocks里写单文件程序那么“构建”这个概念对你来说可能很模糊。不就是点一下“编译运行”按钮吗但当你开始接触稍微复杂一点的项目比如一个项目里有十几个.c/.cpp文件还依赖了第三方库像OpenCV、Boost、Qt你就会发现事情没那么简单了。想象一下你手动用g命令去编译g main.cpp utils.cpp network.cpp -I./include -L./lib -lopencv_core -lopencv_highgui -o myapp。这还只是三个文件。如果文件增加到50个或者你需要为Windows、macOS、Linux分别编译又或者你想编译成调试版带符号信息和发布版优化过的手动敲命令会变成一场噩梦。这就是“构建系统”要解决的问题它是一套规则和工具用来描述源代码如何转换成最终的可执行文件或库并自动化这个过程。早期程序员们用Makefile。它定义了目标target、依赖dependencies和规则rules。但Makefile的语法比较晦涩跨平台处理比如Windows上的路径分隔符是\而Unix是/也很麻烦。更重要的是Makefile本身不检测你系统上有什么编译器、库装在哪里。于是CMake应运而生。CMake不是一个编译器也不是一个构建工具。它是一个构建系统生成器。它的核心思想是“配置”和“生成”。你编写一个名为CMakeLists.txt的、相对高级和易读的配置文件描述你的项目结构、目标、依赖关系。然后CMake会根据你当前的平台Windows、macOS、Linux和指定的编译器GCC、Clang、MSVC生成对应平台的原生构建系统文件。在Linux/macOS上它通常生成Makefile在Windows上可以生成Visual Studio的.sln/.vcxproj文件也可以生成Ninja、Xcode等项目的文件。所以CMakeLists.txt是你的项目与具体构建环境之间的抽象层和接口。你只需要维护这一份CMakeLists.txt就能在各种环境下构建你的项目。这极大地提高了项目的可移植性和协作效率。接下来我们就深入这个CMakeLists.txt文件看看里面到底有哪些核心指令和“生存必备”的用法。2. CMakeLists.txt 的骨架一个最小化可用的模板拆解一个最基本的CMakeLists.txt文件通常位于你项目的根目录。我们从一个最简单的“Hello World”项目开始逐行解析其构成和背后的逻辑。# 1. 指定CMake的最低版本要求 cmake_minimum_required(VERSION 3.10) # 2. 定义项目名称、版本和使用的编程语言 project(MyHelloWorld VERSION 1.0.0 LANGUAGES CXX) # 3. 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 4. 添加可执行文件目标 add_executable(hello_world main.cpp) # 5. 可选更复杂的设置如包含目录、链接库等 # target_include_directories(hello_world PRIVATE ./include) # target_link_libraries(hello_world PRIVATE some_library)现在我们来详细拆解每一部分2.1cmake_minimum_required设定兼容性基线这是CMakeLists.txt的第一行必须是第一行。它告诉CMake运行此脚本所需的最低版本。为什么重要策略行为CMake的版本迭代会引入新特性也会改变某些旧指令的默认行为称为“策略”。指定版本后CMake会启用与该版本兼容的策略集确保你的脚本在不同机器上的CMake环境下行为一致。如果你用了高版本CMake的新语法比如target_sources但在旧机器上运行CMake会在这里报错而不是产生不可预知的构建错误。写法VERSION关键字后的版本号通常建议写你开发时使用的、或团队约定好的版本。3.10是一个比较通用且支持现代特性的起点。2.2project项目的身份标识这条指令定义了项目的元信息。项目名(MyHelloWorld)这个名字会被用于一些内置变量例如PROJECT_NAME也会作为生成的Visual Studio解决方案名。版本(VERSION 1.0.0)定义主、次、修订版本号。它会自动设置变量如PROJECT_VERSION,MyHelloWorld_VERSION注意前缀是项目名。这在打包或生成版本头文件时很有用。语言(LANGUAGES CXX)这里CXX代表C。如果你写C程序就用C如果同时有C和C就写C CXX。声明语言会告诉CMake去查找对应的编译器gcc/g,clang/clang,cl.exe等并设置一系列相关变量如CMAKE_C_COMPILER,CMAKE_CXX_COMPILER。注意project()指令必须放在cmake_minimum_required()之后其他大多数指令之前。因为它会重置很多CMake内置变量。2.3set与变量管理项目的“控制面板”set指令是CMake里最基础的变量操作指令。变量是CMake脚本的“内存”用于存储路径、标志、开关等信息。设置普通变量set(MY_VAR “some_value”)。变量名通常大写值可以是字符串、列表。设置缓存变量set(MY_CACHE_VAR “default_value” CACHE STRING “A description for the user”)。这种变量会保存在CMake缓存即生成的CMakeCache.txt文件中用户可以通过cmake-gui或-D命令行参数如-DMY_CACHE_VARanother_value来修改它。STRING是类型还可以是BOOL,PATH,FILEPATH。设置环境变量set(ENV{PATH} “/some/new/path:$ENV{PATH}”)。在上面的模板中我们用它来设置C标准set(CMAKE_CXX_STANDARD 11)指定编译时使用C11标准。set(CMAKE_CXX_STANDARD_REQUIRED ON)这是一个强硬要求。如果编译器不支持C11CMake将报错并停止。如果设为OFFCMake会尝试使用C11但如果不支持则回退到编译器默认标准。set(CMAKE_CXX_EXTENSIONS OFF)禁用编译器扩展如GNU的-stdgnu11使用严格的标准模式-stdc11。这能提高代码在不同编译器间的可移植性。2.4add_executable与add_library定义构建目标这是CMake的核心告诉CMake你要生成什么。add_executable(name [source1] [source2] ...)定义一个名为name的可执行文件目标。源文件列表可以逐个列出也可以用变量表示。# 方式一直接列出 add_executable(my_app main.cpp foo.cpp bar.cpp) # 方式二使用变量更清晰 set(APP_SOURCES main.cpp foo.cpp bar.cpp) add_executable(my_app ${APP_SOURCES})add_library(name [STATIC | SHARED | MODULE] [source1] ...)定义一个库目标。STATIC静态库.a或.lib代码在链接时被复制到可执行文件中。SHARED动态库/共享库.so或.dll代码在运行时被加载。MODULE一种特殊的动态库通常不被直接链接而是运行时通过dlopen等方式加载如插件。如果不指定类型可以通过全局变量BUILD_SHARED_LIBS来控制默认是静态还是动态。目标hello_world,my_app是CMake现代用法的核心。后续几乎所有针对该目标的设置如头文件路径、链接库、编译选项都通过target_xxx()系列命令来完成这比旧的、全局设置的方式更清晰、更不容易出错。3. 现代CMake的核心范式面向目标Target的配置如果你搜索老的CMake教程可能会看到很多这样的指令include_directories(),link_directories(),add_definitions()。这些是全局命令它们会影响之后定义的所有目标。在现代CMake大致指CMake 3.0中更推荐使用**面向目标Target-Oriented**的命令。这样做的好处是依赖关系明确属性不会意外“污染”其他目标项目结构更清晰。3.1target_include_directories告诉编译器头文件在哪假设你的项目结构如下my_project/ ├── CMakeLists.txt ├── include/ │ └── mylib.h ├── src/ │ ├── mylib.cpp │ └── main.cpp └── thirdparty/ └── some_lib/ └── include/ └── some_header.h你需要让编译器在编译main.cpp和mylib.cpp时能找到mylib.h和some_header.h。旧式全局不推荐include_directories(./include ./thirdparty/some_lib/include) add_executable(my_app src/main.cpp src/mylib.cpp)这会将这两个目录添加到所有后续目标的头文件搜索路径中。现代面向目标推荐# 先定义库目标 add_library(mylib STATIC src/mylib.cpp) # 为 mylib 目标添加其**私有**的头文件路径实现自身需要的 target_include_directories(mylib PRIVATE ./include) # 如果 mylib 的头文件mylib.h需要被外部使用则用 PUBLIC # target_include_directories(mylib PUBLIC ./include) # 定义可执行文件 add_executable(my_app src/main.cpp) # 为 my_app 目标添加它需要的头文件路径 target_include_directories(my_app PRIVATE ./thirdparty/some_lib/include) # 链接库并自动获取库的 PUBLIC/INTERFACE 头文件路径 target_link_libraries(my_app PRIVATE mylib)这里的关键是PRIVATE、PUBLIC、INTERFACE这三个可见性关键字PRIVATE属性仅用于构建当前目标本身。比如mylib在实现时需要用到的头文件但它的头文件mylib.h并不需要暴露给链接它的my_app。PUBLIC属性既用于构建当前目标也传递给任何链接了当前目标的其他目标。最典型的例子是一个库的公共头文件目录。如果mylib的./include目录用PUBLIC声明那么当my_app链接mylib时./include也会自动添加到my_app的编译搜索路径中。INTERFACE属性不用于构建当前目标但会传递给链接了当前目标的其他目标。常用于“接口库”仅包含头文件的库即Header-only Library或定义纯接口。3.2target_link_libraries管理依赖关系这是配置依赖的核心命令。它不仅仅是在链接器命令里加一个-lmylib更重要的是它会传递属性。target_link_libraries(my_app PRIVATE mylib some_third_party_lib)它会将my_app与mylib和some_third_party_lib链接起来。如果mylib通过PUBLIC或INTERFACE设置了头文件路径、编译定义、链接选项等属性这些属性会自动传递给my_app。这就是“依赖传递”极大地简化了配置。同样使用PRIVATE/PUBLIC/INTERFACE来指定依赖的可见性。PRIVATE意味着这个依赖只是my_app实现所需任何链接my_app的其他目标不会自动获得这个依赖。3.3target_compile_definitions与target_compile_options精细化的编译控制target_compile_definitions(target PRIVATE|PUBLIC|INTERFACE definition ...)添加预处理器宏定义。# 为 mylib 定义一个私有宏用于其内部调试 target_compile_definitions(mylib PRIVATE ENABLE_DEBUG_LOGGING1) # 为 my_app 定义一个公共宏这个宏也会影响所有链接它的目标如果用了PUBLIC target_compile_definitions(my_app PUBLIC VERSION\1.0.0\)target_compile_options(target PRIVATE|PUBLIC|INTERFACE option ...)添加特定的编译器标志。# 为 my_app 添加严格的警告标志GCC/Clang if(CMAKE_CXX_COMPILER_ID MATCHES GNU|Clang) target_compile_options(my_app PRIVATE -Wall -Wextra -Werror) elseif(MSVC) target_compile_options(my_app PRIVATE /W4 /WX) endif()使用target_compile_options要非常小心因为不同编译器的选项完全不同。通常建议优先使用target_compile_features来要求语言特性或者使用像CMAKE_CXX_FLAGS这样的变量进行全局设置如果确实需要。4. 项目管理与高级指令让CMakeLists.txt更强大当项目规模增长单一的CMakeLists.txt会变得臃肿。CMake提供了子目录管理、条件判断、查找包等高级功能来应对。4.1add_subdirectory与 项目模块化这是管理大型项目的基石。你可以将不同模块放在子目录中每个子目录有自己的CMakeLists.txt。my_big_project/ ├── CMakeLists.txt # 根目录主项目文件 ├── app/ │ ├── CMakeLists.txt # 定义可执行文件 │ └── src/ ├── corelib/ │ ├── CMakeLists.txt # 定义核心静态库 │ ├── include/ │ └── src/ └── utils/ ├── CMakeLists.txt # 定义工具类动态库 ├── include/ └── src/在根目录的CMakeLists.txt中cmake_minimum_required(VERSION 3.10) project(BigProject) add_subdirectory(corelib) # 这会执行 ./corelib/CMakeLists.txt add_subdirectory(utils) # 这会执行 ./utils/CMakeLists.txt add_subdirectory(app) # 最后构建可执行文件它可以链接前面的库在app/CMakeLists.txt中你可以直接链接在corelib和utils中定义的目标add_executable(big_app main.cpp) target_link_libraries(big_app PRIVATE corelib utils) # corelib和utils是子目录中add_library定义的目标名add_subdirectory会为子目录创建一个新的作用域但父目录定义的变量默认对子目录可见除非使用set(... PARENT_SCOPE)修改父作用域变量。子目录中定义的目标在父目录中可以直接使用。4.2find_package寻找外部依赖的“标准姿势”几乎所有的现代C项目都会依赖第三方库。find_package是CMake用来查找这些外部库的官方机制。它背后可能对应着一个FindPackageName.cmake模块文件模块模式或者一个PackageNameConfig.cmake文件配置模式更现代。# 查找OpenCV库要求版本至少4.5.0并导入所有组件 find_package(OpenCV 4.5.0 REQUIRED COMPONENTS core highgui imgproc) # 如果找到会定义 OpenCV_FOUND, OpenCV_INCLUDE_DIRS, OpenCV_LIBRARIES 等变量 # 现代用法是直接链接导入的目标Target if(OpenCV_FOUND) # 旧式用法不推荐 # include_directories(${OpenCV_INCLUDE_DIRS}) # target_link_libraries(my_app ${OpenCV_LIBRARIES}) # 现代用法推荐 target_link_libraries(my_app PRIVATE ${OpenCV_LIBRARIES}) # OpenCV的包脚本通常已经为目标设置了所有必要属性 # 更现代的OpenCV包会直接提供OpenCV::core等目标 # target_link_libraries(my_app PRIVATE OpenCV::core OpenCV::highgui OpenCV::imgproc) endif()REQUIRED如果没找到包则报错并停止配置。非常有用能及早发现问题。COMPONENTS指定需要该包的哪些组件例如OpenCV的core,highgui是不同组件。find_package成功的关键在于CMake能否找到对应的.cmake文件。这些文件可能由库的开发者提供如果库本身用CMake构建并安装了或者由系统如Linux的包管理器提供或者你自己手动设置CMAKE_PREFIX_PATH变量指向库的安装路径。踩坑实录find_package找不到包怎么办确认包已安装在Ubuntu上可能需要安装libopencv-dev而不仅仅是opencv。设置CMAKE_PREFIX_PATH如果你将库安装到了非标准路径如/home/me/libs/opencv在运行cmake时指定cmake -DCMAKE_PREFIX_PATH/home/me/libs/opencv ..。手动指定路径最后手段使用find_path和find_library手动查找但这是下策因为失去了包提供的目标属性和依赖传递。4.3 条件判断与变量操作让脚本更智能CMake脚本本身也是一门编程语言支持条件、循环和字符串操作。条件判断if(),elseif(),else(),endif()# 判断编译器 if(CMAKE_CXX_COMPILER_ID STREQUAL GNU) message(STATUS Using GCC compiler) set(COMPILER_FLAGS “-O2”) elseif(CMAKE_CXX_COMPILER_ID MATCHES Clang) message(STATUS Using Clang compiler) set(COMPILER_FLAGS “-O2”) elseif(MSVC) message(STATUS Using MSVC compiler) set(COMPILER_FLAGS “/O2”) else() message(WARNING “Unknown compiler, optimization flags may not be set.”) endif() # 判断平台 if(WIN32) # Windows specific settings add_definitions(-DWIN32_LEAN_AND_MEAN) elseif(APPLE) # macOS specific settings find_library(COREFOUNDATION CoreFoundation) elseif(UNIX AND NOT APPLE) # Linux specific settings find_package(Threads REQUIRED) target_link_libraries(my_app PRIVATE Threads::Threads) endif() # 判断变量是否存在或为真 if(DEFINED ENV{HOME}) message(STATUS “Home directory is $ENV{HOME}”) endif() if(MY_OPTION) # 如果MY_OPTION被设置为ON, YES, TRUE, Y, 或非0数字则为真 # ... endif()循环foreach(),while()# 遍历一个文件列表 set(SOURCE_FILES main.cpp foo.cpp bar.cpp) foreach(src_file IN LISTS SOURCE_FILES) message(“Processing source file: ${src_file}”) # 可以对每个文件做一些操作 endforeach() # 遍历数字范围 foreach(i RANGE 1 5) message(“i ${i}”) endforeach()字符串与列表操作# 字符串操作 set(ROOT_PATH “/usr/local”) set(INCLUDE_PATH “${ROOT_PATH}/include”) # 拼接字符串 # 列表操作 set(MY_LIST a b c) list(APPEND MY_LIST d e) # MY_LIST 变成 a;b;c;d;e list(REMOVE_ITEM MY_LIST b) # 移除 b list(LENGTH MY_LIST LIST_LEN) # 获取长度5. 实战配置案例一个包含第三方库和单元测试的中型项目让我们综合运用以上知识为一个假设的“图像处理工具”项目编写一个相对完整的CMakeLists.txt。项目结构如下image_tool/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── main.cpp │ ├── image_processor.cpp │ └── image_processor.h ├── libs/ │ └── my_math/ │ ├── CMakeLists.txt │ ├── include/my_math.h │ └── src/my_math.cpp ├── tests/ │ ├── CMakeLists.txt │ └── test_image_processor.cpp └── thirdparty/ (假设我们下载了 spdlog 头文件库放在这里) └── spdlog/5.1 根目录 CMakeLists.txt# ./image_tool/CMakeLists.txt cmake_minimum_required(VERSION 3.14) # 使用稍高的版本以支持更好的测试功能 project(ImageTool VERSION 0.1.0 LANGUAGES CXX) # 设置C标准为17并要求必须支持 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 全局编译选项谨慎使用 if(MSVC) # MSVC: 提高警告等级并将警告视为错误 add_compile_options(/W4 /WX) else() # GCC/Clang: 提高警告等级并将警告视为错误 add_compile_options(-Wall -Wextra -Wpedantic -Werror) endif() # 设置输出目录让生成的文件更整齐可选 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 包含我们自己编写的库模块 add_subdirectory(libs/my_math) # 包含主程序源代码目录 add_subdirectory(src) # 如果我们在构建时启用了测试则包含测试目录 option(BUILD_TESTING “Build the testing tree” ON) # 定义一个选项默认ON if(BUILD_TESTING) enable_testing() # 启用CMake的测试功能 add_subdirectory(tests) endif() # 安装规则简单示例 install(TARGETS image_tool DESTINATION bin) # 将可执行文件安装到 bin 目录 install(DIRECTORY ${CMAKE_SOURCE_DIR}/configs/ DESTINATION etc/image_tool) # 安装配置文件5.2 库模块 CMakeLists.txt# ./image_tool/libs/my_math/CMakeLists.txt # 定义一个静态库 my_math add_library(my_math STATIC src/my_math.cpp) # 设置该库的头文件目录。因为 my_math.h 需要被外部如src/使用所以用 PUBLIC target_include_directories(my_math PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include # 构建时使用 $INSTALL_INTERFACE:include # 安装后用户使用时的路径 ) # 设置该库的编译属性 target_compile_definitions(my_math PRIVATE MY_MATH_BUILD) # 内部使用的宏 # 可以在这里链接这个库自己的依赖如果有的话 # find_package(...) # target_link_libraries(my_math PRIVATE ...)5.3 主程序 CMakeLists.txt# ./image_tool/src/CMakeLists.txt # 查找第三方依赖 - OpenCV (假设我们需要) find_package(OpenCV 4.5 REQUIRED COMPONENTS core imgproc) # 查找或包含头文件库 - spdlog (我们放在thirdparty里假设是header-only) # 因为spdlog是纯头文件库我们创建一个接口目标INTERFACE来代表它 add_library(spdlog INTERFACE) target_include_directories(spdlog INTERFACE ${CMAKE_SOURCE_DIR}/thirdparty/spdlog/include) # 可以为spdlog添加一些编译定义比如开启格式化检查如果spdlog支持 target_compile_definitions(spdlog INTERFACE SPDLOG_COMPILED_LIB) # 假设我们编译了spdlog库否则对于header-only不需要这行 # 定义主可执行文件 set(APP_SOURCES main.cpp image_processor.cpp) add_executable(image_tool ${APP_SOURCES}) # 为可执行文件添加头文件搜索路径 # 1. 当前目录src/可能有一些私有头文件 target_include_directories(image_tool PRIVATE .) # 2. 来自 my_math 库的公共头文件路径通过链接自动传递无需手动添加 # 3. 来自 spdlog 接口目标的头文件路径通过链接自动传递 # 链接所有依赖 target_link_libraries(image_tool PRIVATE my_math # 我们自己的数学库 OpenCV::core # 现代OpenCV提供的目标 OpenCV::imgproc spdlog # 我们的接口目标 ) # 如果OpenCV包没有提供现代目标则使用旧变量不推荐但可能遇到 # target_include_directories(image_tool PRIVATE ${OpenCV_INCLUDE_DIRS}) # target_link_libraries(image_tool PRIVATE ${OpenCV_LIBRARIES})5.4 测试目录 CMakeLists.txt# ./image_tool/tests/CMakeLists.txt # 查找测试框架这里以 GoogleTest 为例需提前通过 find_package 或 FetchContent 获取 # 假设我们使用 CMake 自带的 FetchContent 从网络获取 Googletest include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.12.1 ) FetchContent_MakeAvailable(googletest) # 定义一个测试可执行文件 add_executable(test_image_processor test_image_processor.cpp) # 测试程序需要链接主程序中的模块如 image_processor 的代码和 gtest # 注意这里为了测试我们可能需要访问 image_processor 的内部函数。 # 更好的做法是将 image_processor 也做成一个库静态或动态然后主程序和测试程序都链接它。 # 这里假设我们把 image_processor 的源码直接加入测试目标简单演示。 target_sources(test_image_processor PRIVATE ../src/image_processor.cpp ../src/image_processor.h) target_include_directories(test_image_processor PRIVATE ../src) target_link_libraries(test_image_processor PRIVATE my_math gtest_main) # 将该测试添加到 CTest add_test(NAME ImageProcessorTest COMMAND test_image_processor)5.5 构建与使用在项目根目录image_tool/下mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease # 或 Debug cmake --build . -j4 # 开始构建使用4个并行任务构建完成后可执行文件在build/bin/image_tool库文件在build/lib/可以运行测试ctest或./bin/test_image_processor这个案例展示了如何组织一个多目录项目如何管理内部库和外部依赖以及如何集成单元测试。关键在于使用add_subdirectory分解项目使用target_xxx()命令精确控制每个目标的属性并通过target_link_libraries自动处理依赖传递。6. 常见“坑点”与调试技巧即使理解了所有指令在实际使用CMake时还是会遇到各种问题。下面是一些高频“坑点”和解决方法。6.1 路径问题绝对路径与相对路径CMake中有几个关键的路径变量混淆它们会导致文件找不到。CMAKE_SOURCE_DIR最顶层CMakeLists.txt所在的目录即你运行cmake命令的源目录。在整个构建树中这个值是不变的。CMAKE_BINARY_DIR最顶层的构建输出目录即你运行cmake命令的build目录。在整个构建树中这个值是不变的。CMAKE_CURRENT_SOURCE_DIR当前正在处理的CMakeLists.txt所在的目录。CMAKE_CURRENT_BINARY_DIR对应于CMAKE_CURRENT_SOURCE_DIR的当前构建输出目录。PROJECT_SOURCE_DIR最近一次调用project()命令的CMakeLists.txt所在的目录。PROJECT_BINARY_DIR对应于PROJECT_SOURCE_DIR的构建输出目录。黄金法则在CMakeLists.txt中引用源代码文件时使用${CMAKE_CURRENT_SOURCE_DIR}作为基准的相对路径。例如${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp。在CMakeLists.txt中设置输出路径或引用构建生成的文件时使用${CMAKE_CURRENT_BINARY_DIR}。尽量避免使用绝对路径以保证项目的可移植性。6.2 变量作用域与传递普通变量在函数function或子目录add_subdirectory内部设置的变量默认是局部变量不会影响父作用域。缓存变量设置时加上CACHE关键字会持久化到CMakeCache.txt在整个CMake运行期间全局可见。父作用域变量在子目录或函数中想修改父作用域的变量需要使用set(... PARENT_SCOPE)。一个常见错误是在add_subdirectory的子CMakeLists.txt中设置了一个变量然后期望在父CMakeLists.txt中使用它结果发现是空的。这时就需要用PARENT_SCOPE。6.3find_package失败排查这是新手最常遇到的问题。按照以下步骤排查确认包已安装在终端尝试pkg-config --libs opencv4Linux或检查对应安装路径。查看CMake搜索路径CMake有一系列搜索前缀。运行cmake --help-variable CMAKE_PREFIX_PATH查看。最常用的方法是手动指定cmake -DCMAKE_PREFIX_PATH/your/custom/install/path ..。检查包是否提供CMake支持不是所有库都提供了PackageNameConfig.cmake。有些古老的库只提供.pc文件供pkg-config用或干脆什么都没有。对于后者你可能需要自己写FindXXX.cmake模块或者直接用find_path和find_library。使用find_package的调试模式在运行CMake时加上--debug-find参数会输出详细的查找过程cmake --debug-find ..。6.4 生成器表达式Generator Expressions这是CMake中一个强大但有点晦涩的特性用于在生成构建系统时而不是在配置时计算一些条件相关的值。它们被广泛用于现代target_xxx()命令中。语法以$...形式出现。常见用途条件编译target_compile_definitions(myapp PRIVATE $$CONFIG:Debug:DEBUG_MODE1)表示仅在Debug配置下定义DEBUG_MODE宏。区分构建接口和安装接口如前文例子中的$BUILD_INTERFACE:...和$INSTALL_INTERFACE:...。获取目标属性$TARGET_PROPERTY:target,property。刚开始可以不用深究但看到$...要知道这是生成器表达式它的值在配置阶段是看不到的只有在生成Makefile或.vcxproj时才会被确定。6.5 调试CMake脚本message()是你的好朋友在脚本中插入message(STATUS “Variable MY_VAR ${MY_VAR}”)来打印变量值。STATUS是普通信息WARNING是黄色警告FATAL_ERROR会停止配置。查看生成的构建文件去build/目录下看看生成的Makefile或.vcxproj文件里面包含了CMake最终生成的所有编译和链接命令这是验证CMake配置是否正确的最直接方式。使用CMake GUI工具cmake-gui可以图形化地查看和修改缓存变量对于调试复杂的变量交互很有帮助。掌握CMake的过程就是不断踩坑和填坑的过程。从最简单的单文件项目开始逐步添加目录、库、外部依赖和测试每次只改变一点并确保能成功构建是学习CMake最稳妥的方法。当你熟悉了这些核心指令和模式后你会发现它不再是障碍而是管理复杂C/C项目的得力助手。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻