8、CMake中的路径管理:CMAKE_SOURCE_DIR、CMAKE_BINARY_DIR、PROJECT_SOURCE_DIR、相对路径与绝对路径

路径管理这件事,说大不大,说小不小。但你要是没搞明白,CMakeLists.txt 写起来就会处处碰壁。我见过不少新手,明明代码逻辑没问题,一编译就报“找不到文件”,十有八九是路径写死了。

今天我们就来彻底捋清楚 CMake 里的几个核心路径变量。说白了,就是搞清楚“我在哪”、“我要去哪”、“东西放哪了”这三个问题。

8.1 三个核心路径变量

CMake 提供了几个内置变量,用来表示不同层级的目录位置。最常用的就是这三个:CMAKE_SOURCE_DIRCMAKE_BINARY_DIRPROJECT_SOURCE_DIR

变量名 含义 典型值示例
CMAKE_SOURCE_DIR 顶层 CMakeLists.txt 所在目录(最外层) /home/user/myproject
CMAKE_BINARY_DIR 构建目录的根目录(执行 cmake 命令的地方) /home/user/myproject/build
PROJECT_SOURCE_DIR 最近一次调用 project() 命令的 CMakeLists.txt 所在目录 /home/user/myproject/src

嗯,这里要注意:CMAKE_SOURCE_DIR 是“绝对”的顶层,而 PROJECT_SOURCE_DIR 是“相对”于当前 project 的。如果你只有一个 project() 调用,那它俩通常是一样的。但如果你用了子项目或者嵌套调用,区别就出来了。

核心原则:尽量使用 PROJECT_SOURCE_DIR 而不是 CMAKE_SOURCE_DIR。这样你的子项目被别的项目引用时,路径不会乱掉。

8.2 绝对路径 vs 相对路径

CMake 里写路径,有两种方式:绝对路径和相对路径。绝对路径以 / 开头,相对路径则基于当前 CMakeLists.txt 所在目录。

我个人习惯是:在 CMakeLists.txt 里尽量用相对路径。为什么?因为你的项目可能被拷贝到不同机器上,绝对路径一换环境就废了。

# 不推荐:写死绝对路径
set(MY_HEADERS /home/user/myproject/include/foo.h)

# 推荐:使用相对路径 + 变量
set(MY_HEADERS ${PROJECT_SOURCE_DIR}/include/foo.h)

我在项目中遇到过一个问题:同事把项目从 Linux 迁移到 Windows,所有硬编码的 /home/xxx 路径全炸了。从那以后,我团队里就立了个规矩——CMakeLists.txt 里不允许出现任何硬编码的绝对路径。

8.3 构建路径与源路径分离

CMake 强烈建议你使用“out-of-source”构建,也就是把构建产物放在单独的目录里。这样源目录干干净净,不会混入 .o 文件、可执行文件等。

# 在项目根目录下执行
mkdir build
cd build
cmake ..
make

这时候 CMAKE_BINARY_DIR 就是 /path/to/project/build,而 CMAKE_SOURCE_DIR/path/to/project。两者完全独立。

小技巧:如果你需要在构建目录里生成文件(比如编译后的配置文件),可以用 ${CMAKE_BINARY_DIR} 来定位。千万别往源目录里写东西。

8.4 路径拼接的正确姿势

很多人喜欢用字符串拼接来组合路径,比如:

set(MY_PATH "${PROJECT_SOURCE_DIR}/subdir/file.txt")

这样写大部分情况下没问题,但跨平台时可能翻车。Windows 用反斜杠 \,Linux 用正斜杠 /。CMake 提供了 file(TO_CMAKE_PATH)file(TO_NATIVE_PATH) 来处理这个差异。

更推荐的做法是用 cmake_path() 命令(CMake 3.20+):

cmake_path(APPEND MY_PATH "${PROJECT_SOURCE_DIR}" "subdir" "file.txt")

这样写,路径分隔符会自动适配当前平台。我曾经在 Windows 上被反斜杠坑过一次,调试了半天才发现是路径分隔符的问题。嗯,从那以后我就改用 cmake_path 了。

8.5 路径变量的作用域

这几个路径变量都是全局的,但 PROJECT_SOURCE_DIR 会在每次调用 project() 时被重新赋值。如果你有多个子项目:

# 顶层 CMakeLists.txt
project(MyProject)
add_subdirectory(sub)

# sub/CMakeLists.txt
project(SubProject)
# 此时 PROJECT_SOURCE_DIR 指向 sub/ 目录

这个特性有时候会让人迷惑。我建议你在子项目里用 CMAKE_CURRENT_SOURCE_DIR 来获取当前 CMakeLists.txt 所在目录,这样更稳妥。

注意:CMAKE_CURRENT_SOURCE_DIR 是动态的,它始终指向当前正在处理的 CMakeLists.txt 所在目录。而 PROJECT_SOURCE_DIR 是静态的,只跟最近一次 project() 绑定。

8.6 实战:一个典型项目的路径布局

我们来看一个实际项目的目录结构:

myapp/
├── CMakeLists.txt          # 顶层,project(myapp)
├── src/
│   ├── CMakeLists.txt
│   └── main.cpp
├── lib/
│   ├── CMakeLists.txt
│   └── core.cpp
├── include/
│   └── core.h
└── tests/
    ├── CMakeLists.txt
    └── test_core.cpp

在顶层 CMakeLists.txt 里:

cmake_minimum_required(VERSION 3.20)
project(MyApp)

add_subdirectory(src)
add_subdirectory(lib)
add_subdirectory(tests)

src/CMakeLists.txt 里,我想引用 include/core.h

# 正确做法:用 PROJECT_SOURCE_DIR 定位到顶层
target_include_directories(myapp PRIVATE ${PROJECT_SOURCE_DIR}/include)

# 或者用 CMAKE_CURRENT_SOURCE_DIR 定位到当前目录的上级
target_include_directories(myapp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)

我个人更推荐第一种,因为 PROJECT_SOURCE_DIR 语义清晰,不会因为目录结构调整而失效。

8.7 知识体系图

下面这张图帮你理清这几个路径变量的关系:

CMake 路径变量关系图 CMAKE_SOURCE_DIR 顶层 CMakeLists.txt 所在目录 CMAKE_BINARY_DIR 构建目录根目录 cmake .. PROJECT_SOURCE_DIR 最近 project() 所在目录 CMAKE_CURRENT_SOURCE_DIR 当前 CMakeLists.txt 所在目录 层级关系: CMAKE_SOURCE_DIR 是根 PROJECT_SOURCE_DIR 是子项目根 CMAKE_CURRENT_SOURCE_DIR 是当前文件 最佳实践 优先用 PROJECT_SOURCE_DIR 避免硬编码绝对路径

8.8 避坑指南

最后,分享几个我踩过的坑:

  • 别在 install 路径里用 CMAKE_SOURCE_DIR——安装后的路径跟源目录没关系,应该用 CMAKE_INSTALL_PREFIX
  • 小心 add_subdirectory 里的路径——如果你在子目录里用了 ${CMAKE_SOURCE_DIR},它指向的还是顶层目录,不是子目录。
  • file(GLOB ...) 的路径陷阱——file(GLOB 默认基于当前 CMakeLists.txt 目录,但很多人以为它是基于项目根目录的。

我曾经犯过的错:在子项目的 CMakeLists.txt 里用 ${CMAKE_SOURCE_DIR}/config.h.in 来配置生成文件。结果主项目引用这个子项目时,路径指向了主项目的根目录,而不是子项目的目录。后来改成 ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in 才解决问题。

路径管理说白了就是搞清楚“参照系”。你只要记住:CMAKE_SOURCE_DIR 是全局根,PROJECT_SOURCE_DIR 是当前项目根,CMAKE_CURRENT_SOURCE_DIR 是当前文件所在目录。用对了,你的 CMakeLists.txt 就能在各种环境下稳定运行。


公众号:蓝海资料掘金营,微信 deep3321