17、导出目标:export与install(EXPORT)。如何将构建树中的目标导出,供其他项目直接引用,无需安装。
说实话,很多C++开发者做了好几年项目,对CMake的理解还停留在“写个CMakeLists.txt能编译就行”。但一旦涉及到多项目协作、库的复用,问题就来了——我编译好的库,怎么给别人用?
大部分人第一反应是:make install,把头文件和库文件拷贝到系统目录。这当然可以,但不够灵活。你想想看,如果我只是在本地调试两个项目,每次改完库代码都要install一遍,多麻烦?
今天我要讲的,就是CMake里一个非常实用的功能——导出构建树中的目标。说白了,就是让另一个项目直接引用你还没安装的库,省掉install那一步。
为什么需要导出目标?
先说说我自己的经历。几年前我在做一个嵌入式中间件项目,底层库和上层应用分在两个仓库里。每次改底层接口,都要重新编译、安装,再切到应用项目里编译。一天下来,光等编译就浪费半小时。
后来我发现了export()命令。它能把当前构建树中的目标信息导出成一个文件,其他项目通过find_package()就能直接找到。不需要安装,不需要拷贝文件。说白了,就是在构建树里搭了一座桥。
核心命令:export()
export()命令的作用是:把当前CMake构建中的目标(比如一个静态库、动态库、可执行文件)的使用规范导出到一个文件中。这个文件可以被其他项目通过find_package()找到。
基本语法如下:
export(TARGETS <target-list>
FILE <filename>
[NAMESPACE <namespace>])
TARGETS:要导出的目标列表,比如你的库目标。FILE:导出的文件名,通常以Config.cmake结尾。NAMESPACE:给目标加一个命名空间前缀,避免名字冲突。
举个例子。假设我有一个库叫mylib:
add_library(mylib STATIC src/mylib.cpp)
target_include_directories(mylib PUBLIC include)
export(TARGETS mylib
FILE MyLibConfig.cmake
NAMESPACE MyLib::)
执行这个export()后,会在当前构建目录下生成一个MyLibConfig.cmake文件。其他项目只要知道这个文件的位置,就能通过find_package(MyLib)找到mylib目标。
关键点:export()导出的目标,引用的是构建树中的产物(比如.a或.so文件),而不是安装后的文件。这意味着你改了库代码,重新编译后,引用项目立刻就能用新版本。
如何让其他项目找到导出的文件?
光导出还不够,你得告诉其他项目去哪里找这个MyLibConfig.cmake。CMake提供了两种方式:
方式一:通过CMAKE_PREFIX_PATH指定路径
在引用项目的CMakeLists.txt中,设置CMAKE_PREFIX_PATH指向导出文件所在的目录:
list(APPEND CMAKE_PREFIX_PATH "/path/to/mylib/build")
find_package(MyLib REQUIRED)
target_link_libraries(myapp PRIVATE MyLib::mylib)
这种方式最直接,适合本地开发调试。
方式二:通过环境变量或CMake缓存
你也可以把路径写到CMakeCache.txt里,或者设置环境变量MyLib_DIR指向导出文件所在目录。CMake在find_package()时会自动搜索这些位置。
我的习惯:在项目根目录放一个CMakePresets.json,把依赖库的构建路径写进去。这样团队成员拉下代码后,直接cmake --preset=debug就能用,不用手动配路径。
与install(EXPORT)的区别
很多初学者会把export()和install(EXPORT)搞混。我简单说一下区别:
| 特性 | export() | install(EXPORT) |
|---|---|---|
| 目标位置 | 构建树中的产物 | 安装后的产物 |
| 是否需要install | 不需要 | 需要先执行make install |
| 适用场景 | 本地开发、调试 | 发布、部署、给第三方使用 |
| 生成的文件位置 | 构建目录下 | 安装目录下 |
说白了,export()是给开发者自己用的,install(EXPORT)是给用户用的。我个人的做法是:开发阶段用export(),发布时再用install(EXPORT)生成正式的包。
实战:一个完整的例子
下面我写一个完整的示例,展示如何导出目标并让另一个项目引用。
库项目:mylib
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(mylib VERSION 1.0.0)
add_library(mylib STATIC src/mylib.cpp)
target_include_directories(mylib PUBLIC include)
# 导出目标
export(TARGETS mylib
FILE MyLibConfig.cmake
NAMESPACE MyLib::)
# 同时支持安装(可选)
install(TARGETS mylib
EXPORT MyLibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include)
install(EXPORT MyLibTargets
FILE MyLibConfig.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib)
应用项目:myapp
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(myapp)
# 指定库的构建路径
list(APPEND CMAKE_PREFIX_PATH "/path/to/mylib/build")
find_package(MyLib REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE MyLib::mylib)
这样,你只需要先编译mylib项目,然后编译myapp时,CMake会自动找到mylib的构建产物并链接。改了mylib代码后,重新编译mylib,再编译myapp就能用上新版本。
注意:export()导出的文件路径是硬编码在生成的.cmake文件中的。如果你移动了构建目录,需要重新执行export()。我曾经因为重命名了构建目录,导致另一个项目找了半天找不到库,最后发现是路径变了。
知识体系结构图
下面我用一张SVG图来总结本章的核心逻辑:
避坑指南
最后,我分享几个实际开发中容易踩的坑:
- 路径硬编码问题:
export()生成的.cmake文件中,目标的路径是绝对路径。如果你把构建目录删了或移动了,导出文件就失效了。解决办法是:每次重新构建后,重新执行export()。 - 命名空间冲突:如果多个库都导出了同名的目标(比如都叫
core),链接时就会冲突。我建议始终使用NAMESPACE,比如MyLib::,这样目标名就变成了MyLib::core,不会冲突。 - 调试与Release混用:
export()导出的目标默认使用当前构建配置。如果你用Debug构建导出了,另一个项目用Release构建引用,可能会链接到错误的库。解决办法是:在导出时使用BUILD_INTERFACE和INSTALL_INTERFACE生成器表达式,区分不同配置。
我的建议:对于团队内部的多项目协作,优先使用export()。它比install(EXPORT)更轻量,修改代码后反馈更快。等代码稳定了,再考虑用install(EXPORT)生成正式的发布包。
好了,关于export()和install(EXPORT)的区别与用法,就讲到这里。记住一句话:开发用export,发布用install。这样你的CMake项目既灵活又规范。