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() 与 install(EXPORT) 核心逻辑 库项目 (mylib) add_library(mylib ...) export() MyLibConfig.cmake (构建目录中) 应用项目 (myapp) find_package(MyLib) CMAKE_PREFIX_PATH 指向构建目录 install(EXPORT) MyLibConfig.cmake (安装目录中) 发布后 用户 使用 export() 路径 install(EXPORT) 路径

避坑指南

最后,我分享几个实际开发中容易踩的坑:

  • 路径硬编码问题export()生成的.cmake文件中,目标的路径是绝对路径。如果你把构建目录删了或移动了,导出文件就失效了。解决办法是:每次重新构建后,重新执行export()
  • 命名空间冲突:如果多个库都导出了同名的目标(比如都叫core),链接时就会冲突。我建议始终使用NAMESPACE,比如MyLib::,这样目标名就变成了MyLib::core,不会冲突。
  • 调试与Release混用export()导出的目标默认使用当前构建配置。如果你用Debug构建导出了,另一个项目用Release构建引用,可能会链接到错误的库。解决办法是:在导出时使用BUILD_INTERFACEINSTALL_INTERFACE生成器表达式,区分不同配置。

我的建议:对于团队内部的多项目协作,优先使用export()。它比install(EXPORT)更轻量,修改代码后反馈更快。等代码稳定了,再考虑用install(EXPORT)生成正式的发布包。

好了,关于export()install(EXPORT)的区别与用法,就讲到这里。记住一句话:开发用export,发布用install。这样你的CMake项目既灵活又规范。

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