目录
1.简介
这两个是 CMake 生成器表达式(Generator Expression) 中的核心关键字,专门用于解决**「构建阶段路径」和「安装后路径」不一致** 的问题,是现代 CMake 导出目标、实现 find_package 可用的核心机制。
一个库在生命周期里有两种完全不同的使用场景,头文件 / 库文件的路径完全不一样:
1.构建阶段 :库还在源码树里,头文件在 MyProject/File/include,库文件在 build/File/
2.安装后 :库被安装到系统 / 指定目录,头文件在 <安装前缀>/include,库文件在 <安装前缀>/lib
如果写死路径,必然会出现「构建时能用、安装后不能用」或者反过来的问题。BUILD_INTERFACE 和 INSTALL_INTERFACE 就是用来分别指定两个场景下的属性值,CMake 会在对应场景自动启用对应的值。
语法,属于生成器表达式,格式固定:
cpp
$<BUILD_INTERFACE:内容>
$<INSTALL_INTERFACE:内容>
最常用在 target_include_directories,也可用于 target_link_libraries、target_compile_definitions 等所有支持 INTERFACE 属性的地方。
2.BUILD_INTERFACE:构建树内生效
生效时机
仅在当前构建树内部生效:
- 同工程内其他目标(子模块之间互相依赖)
export(EXPORT)导出的构建树版本目标文件
作用
告诉依赖本目标的其他目标:构建的时候,去哪里找头文件、链接库、宏定义。
路径特点
通常使用绝对路径变量(CMake 内置变量),指向源码目录或构建目录:
${CMAKE_CURRENT_SOURCE_DIR}/include:当前模块的源码头文件目录${CMAKE_CURRENT_BINARY_DIR}:当前模块的构建目录(比如放生成的头文件)
关键特性
- 只在本地构建树内有效,不会被 install (EXPORT) 写入安装后的目标文件
- 路径是绝对路径,不可迁移
示例:
cpp
target_include_directories(MyProjectFile PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
构建时,所有依赖 MyProjectFile 的目标,都会自动加上 -I/path/to/MyProject/File/include 编译参数。
3.INSTALL_INTERFACE:安装后生效
生效条件
当目标被安装后,外部项目通过 find_package 导入 时生效,仅出现在 install(EXPORT) 生成的安装树目标文件中。
核心作用
告诉外部使用者:安装完成后,从安装根目录去哪里找头文件、链接库。
路径特点
- 永远写相对于
CMAKE_INSTALL_PREFIX的相对路径 ,比如include、lib - 导出时会被 CMake 自动处理,最终和
${_IMPORT_PREFIX}变量拼接 - 安装包整体迁移到任何目录,路径都能自动适配,可迁移
示例:
cpp
target_include_directories(MyProjectFile PUBLIC
$<INSTALL_INTERFACE:include>
)
安装后外部项目导入时,等效于:
cpp
set_target_properties(MyProject::File PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
4.底层原理:导出时的路径转换逻辑
这是双接口最核心的黑盒部分,也是很多人困惑的根源 ------CMake 在处理 install(EXPORT) 时,对接口属性做了完整的「筛选 + 转换」。
完整处理流程(以 install(EXPORT) 为例)
当你执行 install(EXPORT MyProjectFileTargets ...) 时,CMake 内部会做以下步骤:
- 遍历目标的所有 INTERFACE 属性 包括
INTERFACE_INCLUDE_DIRECTORIES、INTERFACE_LINK_LIBRARIES、INTERFACE_COMPILE_DEFINITIONS等。 - 求值并筛选生成器表达式
- 丢弃所有
$<BUILD_INTERFACE:...>分支的内容 - 保留所有
$<INSTALL_INTERFACE:...>分支的内容
- 丢弃所有
- 路径相对化转换 对保留下来的路径,判断是相对路径还是绝对路径:
- 相对路径:保持不变,后续和
${_IMPORT_PREFIX}拼接 - 绝对路径:警告或错误(因为不可迁移)
- 相对路径:保持不变,后续和
- 写入导出文件 把处理后的属性写入
MyProjectFileTargets.cmake,同时自动计算_IMPORT_PREFIX变量。
_IMPORT_PREFIX 是怎么来的
CMake 会根据 xxxTargets.cmake 的安装路径,反向计算出到安装根目录的相对路径 ,作为 _IMPORT_PREFIX 的值。
比如:
- Targets.cmake 安装在:
lib/cmake/MyProject/MyProjectFileTargets.cmake - 从该文件位置往上 3 级就是安装根目录
- 所以
_IMPORT_PREFIX = ${CMAKE_CURRENT_LIST_DIR}/../../..
外部项目 include 这个文件时,_IMPORT_PREFIX 会自动指向当前的安装根目录,实现安装包可迁移。
对比:export() 构建树导出的处理
和 install(EXPORT) 正好相反:
- 保留
BUILD_INTERFACE内容 - 丢弃
INSTALL_INTERFACE内容 - 路径保持绝对路径,不做转换
- 直接写入构建目录下的
xxxTargets.cmake
所以构建树版本的目标文件只能在本机使用,拷贝到其他机器路径就失效了。
5.双接口的完整生命周期
以File 模块为例,看一条定义如何在两个场景下生效。
cpp
target_include_directories(MyProjectFile PUBLIC
# 构建时用:源码目录 + 构建目录
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
# 安装后用:相对安装根目录
$<INSTALL_INTERFACE:include>
)
Transform 编译时会自动加上这两个 include 路径,能找到 File.h 和生成的 Export.h。
场景 1:同工程 Transform 模块依赖 File
构建阶段求值,等效于:
cpp
target_include_directories(MyProjectFile PUBLIC
/path/to/MyProject/File/include
/path/to/MyProject/build/File
)
Transform 编译时会自动加上这两个 include 路径,能找到 File.h 和生成的 Export.h。
场景 2:export () 导出构建树版本
生成的 MyProjectFileTargets.cmake 里:
cpp
set_target_properties(MyProject::File PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES
"/path/to/MyProject/File/include;/path/to/MyProject/build/File"
)
全是绝对路径,只能本机用。
场景 3:install (EXPORT) 导出安装树版本
生成的 MyProjectFileTargets.cmake 里:
cpp
set_target_properties(MyProject::File PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
只有相对路径拼接安装前缀,可迁移。
6.扩展:不止头文件目录
所有带 INTERFACE_ 前缀的目标属性,都可以使用双接口分支,常见的还有:
1.编译宏定义
cpp
target_compile_definitions(MyProjectFile PUBLIC
$<BUILD_INTERFACE:MYPROJECT_BUILDING>
$<INSTALL_INTERFACE:MYPROJECT_USING>
)
- 构建时:定义
MYPROJECT_BUILDING - 安装后:定义
MYPROJECT_USING
2.链接依赖
cpp
target_link_libraries(MyProjectTransform PUBLIC
$<BUILD_INTERFACE:MyProjectFile>
$<INSTALL_INTERFACE:MyProject::File>
)
- 构建时:链接本地目标名
MyProjectFile - 安装后:链接命名空间导入目标
MyProject::File
注:现代 CMake 中,如果两个目标在同一个导出集里,CMake 会自动处理目标名到命名空间的转换,通常不需要手动区分。
3.链接目录
cpp
target_link_directories(MyProjectFile PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/lib>
$<INSTALL_INTERFACE:lib>
)
7.总结
BUILD_INTERFACE 和 INSTALL_INTERFACE 的本质是目标接口属性的双态开关:
- 面向「构建开发」场景,用绝对路径保证本地编译正确
- 面向「安装发布」场景,用相对路径保证安装包可迁移
这也是现代 CMake 组件化库开发的标准范式 ------ 你的 MyProject 工程里同时存在 export() 和 install(EXPORT) 两套导出,正是为了分别对应这两个场景。