目录

一.export函数

1.1.基础示例1

1.1.1.ExportExample

1.1.2.UseMyLib

1.2.export和install(export)的区别

二.configure_package_config_file

2.1.示例一

2.2.示例二

三.install

3.1.安装静态库

3.1.1.修改CMAKE_INSTALL_PREFIX

3.1.2.不修改CMAKE_INSTALL_PREFIX

3.2.install(target)和install(export)

3.2.1.传统的安装使用机制

3.2.2.现代CMake的安装机制

3.3.install综合示例一——安装到系统目录

3.3.1.InstallExample

3.3.2. UseMyLib

3.3.3.正确示例(使用find_package)

3.4.install综合示例二——不安装到系统目录版

3.4.1.代码片段1讲解

3.4.2.代码片段2讲解

3.5.GNUInstallDirs

3.6.DIRECTORY和FILES_MATCHING  PATTERN

四.write_basic_package_version_file

4.1.示例

4.1.1.使用方的CMakeLists.txt(不加版本号)

4.1.2.使用方的CMakeLists.txt(版本号完全匹配)

4.1.3.使用方的CMakeLists.txt(主版本号相同,但是请求的版本比库的版本大)

 4.1.4.使用方的CMakeLists.txt(主版本号相同,但是请求的版本比库的版本小)

 4.1.5.使用方的CMakeLists.txt(主版本号不同,但是请求的版本比库的版本小)

 4.1.6.使用方的CMakeLists.txt(主版本号不同,但是请求的版本比库的版本大)


一.export函数

我们去官网看看:export — CMake 4.1.0 Documentation

这个函数的作用是:将项⽬中的⽬标(如可执⾏⽂件、库)及其相关属性导出到⼀个⽂件中,以便在其他项⽬中使⽤

export(TARGETS <target>... [...])
export(TARGETS <target>... 
       [NAMESPACE <namespace>]
       [APPEND] FILE <filename> 
       [EXPORT_LINK_INTERFACE_LIBRARIES]
       [CXX_MODULES_DIRECTORY <directory>])

选项详解

参数含义说明
<export_name>导出集的名称用于在其他项目中引用此导出集,是必需的标识符
<file_name>导出文件的名称通常命名为<export_name>Config.cmake,这是CMake的标准命名约定
NAMESPACE <namespace>为导出的目标添加命名空间可选参数,用于避免目标名称冲突,例如添加MyLib::前缀
DESTINATION <destination>指定导出文件的安装目录可选参数,定义配置文件安装的位置,通常使用CMake的包管理目录
CONFIGURATIONS <config1>...指定要导出的配置可选参数,可指定一个或多个构建配置(如Debug、Release)进行导出
INCLUDE_INSTALL_DIRS导出的配置文件中包含安装路径可选参数,自动在导出文件中包含安装目录信息,方便其他项目使用

这些参数用于CMake的install(EXPORT)命令,该命令用于创建并安装一个包含所有导出目标的CMake文件,使得其他项目可以使用find_package()命令找到并链接这些目标。

1.1.基础示例1

我们现在就来看看这个export到底是干啥的!

1.1.1.ExportExample

我们先来构建一个库,再将库使用export进行导出。

📂 目录结构

ExportAndUse/
└── ExportExample/       # 库项目
   ├── CMakeLists.txt
   ├── mylib.cpp
   └── mylib.h

📄 ExportExample/mylib.h

#pragma once
void hello();

📄 ExportExample/mylib.cpp

#include "mylib.h"
#include <iostream>

void hello() {
    std::cout << "Hello from MyLib!" << std::endl;
}

📄 ExportExample/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(ExportExample LANGUAGES CXX)

# 创建静态库
add_library(mylib STATIC mylib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# 导出 targets 文件(到 build 目录)
export(TARGETS mylib
       FILE "${CMAKE_BINARY_DIR}/MyLibTargets.cmake")

其实上面的目录结构我们可以复制下面的命令来一键构建出来

rm -rf ExportAndUse && \
mkdir -p ExportAndUse/ExportExample && \

# ==== ExportExample ====
echo 'cmake_minimum_required(VERSION 3.10)
project(ExportExample LANGUAGES CXX)

add_library(mylib STATIC mylib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

export(TARGETS mylib
       FILE "${CMAKE_BINARY_DIR}/MyLibTargets.cmake")' \
> ExportAndUse/ExportExample/CMakeLists.txt && \

echo '#pragma once
void hello();' \
> ExportAndUse/ExportExample/mylib.h && \

echo '#include "mylib.h"
#include <iostream>

void hello() {
    std::cout << "Hello from MyLib!" << std::endl;
}' \
> ExportAndUse/ExportExample/mylib.cpp

接下来我们就来进行构建这个项目

先构建 ExportExample 生成库和 MyLibTargets.cmake:

cd ExportAndUse/ExportExample && \
mkdir build && cd build && \
cmake .. && \
cmake --build .

这样会在 ExportExample/build/ 目录下生成 MyLibTargets.cmake。

我们看看build目录里面有什么

……

我们可以进去看看这个文件里面是什么东西?

我截取了一小部分

我们看看箭头所指向的地方是啥意思

1. 定义导入的库 target

add_library(mylib STATIC IMPORTED)

👉 声明一个 IMPORTED 静态库 target,这个 target 不会自己编译源文件,而是告诉 CMake “这个库文件已经存在,可以直接用”。

2. 设置头文件搜索路径

set_target_properties(mylib PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "/root/cmake/ExportAndUse/ExportExample"
)

👉 指定 target_link_libraries(app PRIVATE mylib) 时,mylib 的 公开头文件路径(INTERFACE_INCLUDE_DIRECTORIES)会传递给 app。

也就是说,#include "mylib.h" 能找到。

3. 指定库文件位置

set_target_properties(mylib PROPERTIES
  IMPORTED_LOCATION_NOCONFIG "/root/cmake/ExportAndUse/ExportExample/build/libmylib.a"
)

👉 告诉 CMake mylib 实际对应的库文件路径在哪里。

这里是静态库:

/root/cmake/ExportAndUse/ExportExample/build/libmylib.a

当 app 链接时,CMake 会自动加上这个库。

换句话说,它就是把 mylib 这个 target 的编译接口信息完整保存下来,这样在另一个工程里只要:

include(path/to/MyLibTargets.cmake) 
target_link_libraries(app PRIVATE mylib)

就能无缝使用这个库了,不需要重新写 include 路径和库路径。

1.1.2.UseMyLib

接下来我们就来通过上面说的那两条语句来链接使用这个mylib库。

我们还是在上面项目的基础之上进行补充,使得整个项目的结构变成下面这样子

ExportAndUse/
├── ExportExample/
│   ├── CMakeLists.txt
│   ├── mylib.cpp
│   └── mylib.h
└── UseMyLib/
    ├── CMakeLists.txt
    └── main.cpp

我们讲讲新添加的文件

 📄 UseMyLib/main.cpp

#include "mylib.h"

int main() {
    hello();
    return 0;
}

📄 UseMyLib/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(UseMyLib LANGUAGES CXX)

# 引入导出的 targets 文件
include(${CMAKE_SOURCE_DIR}/../ExportExample/build/MyLibTargets.cmake)

# 定义可执行文件
add_executable(app main.cpp)

# 链接导入的 mylib
target_link_libraries(app PRIVATE mylib)

include里面接的是MyLibTargets.cmake的具体路径。


我们也可以执行下面这个命令来一键搭建出这个新添加的UseMyLib的目录结构

mkdir -p ExportAndUse/UseMyLib && \
echo 'cmake_minimum_required(VERSION 3.10)
project(UseMyLib LANGUAGES CXX)

include(${CMAKE_SOURCE_DIR}/../ExportExample/build/MyLibTargets.cmake)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)' \
> ExportAndUse/UseMyLib/CMakeLists.txt && \

echo '#include "mylib.h"

int main() {
    hello();
    return 0;
}' \
> ExportAndUse/UseMyLib/main.cpp

……

再构建 UseMyLib:

cd ExportAndUse/UseMyLib && \
mkdir build && cd build && \
cmake ..

我们看看详细的编译过程

cmake --build . -v

在构建main程序的时候去链接了/root/cmake/ExportAndUse/ExportExample/build/libmylib.a这个库。

1.2.export和install(export)的区别

首先,记住这两个命令的终极目的是一样的:生成一个 .cmake 配置文件,这个文件包含了如何“导入”和使用你项目中构建的目标的所有信息(如头文件路径、库文件位置、编译定义、依赖关系等)。

它们最根本的区别在于 生成配置文件的时机、位置以及这些文件的用途和生命周期。


让我们通过一个详细的对比表格来剖析它们的区别:

特性export(EXPORT)install(EXPORT)
核心用途开发阶段、交叉编译软件分发、部署
作用对象构建树 (Build Tree)安装树 (Install Tree)
命令执行时机在构建过程中执行(如 make 时)在安装过程中执行(如 make install 时)
生成文件位置构建目录中(如 build/)安装目录中(如 /usr/local/ 或自定义前缀)
配置文件内容包含指向构建目录中文件的绝对路径包含指向安装目录中文件的相对路径或基于 CMAKE_INSTALL_PREFIX 的路径
生命周期临时性。随构建目录一起被清理或重建。永久性。独立于构建目录和源码目录存在。
适用场景1. 项目A和项目B同时在开发,B需要依赖A的最新构建结果。
2. 交叉编译:为主机平台构建工具,为目标平台构建库,然后让目标平台项目依赖主机平台工具。
1. 将库安装到系统路径,供系统级其他项目使用。
2. 制作软件包(如 deb, rpm)。
3. 将 SDK 分发给他人在其项目中使用。
关联命令export(TARGETS)install(TARGETS), install(FILES)
优点快速、方便,无需安装步骤即可在项目间共享依赖。干净、标准、可移植,符合 FHS 标准,适合分发。
缺点不可移植,路径硬编码,构建目录结构复杂混乱。需要额外的安装步骤。

话不多说来看例子即可

假设我们有一个数学库项目 MyMath。

使用 export(EXPORT) 的工作流程 (用于开发)

在 MyMath/CMakeLists.txt 中:

add_library(MyMath STATIC mymath.cpp)
target_include_directories(MyMath PUBLIC include)

# 定义一个导出集,命名为 MyMathTargets
install(EXPORT MyMathTargets
        FILE MyMathTargets.cmake
        NAMESPACE MyMath::
        DESTINATION lib/cmake/MyMath
)

# ⚠️ 注意:上面的 install(EXPORT) 只定义了安装时要导出的东西,但并没有生成文件。
# 下面的 export(EXPORT) 才会在构建目录中立即生成配置文件。
export(EXPORT MyMathTargets
       FILE "${CMAKE_BINARY_DIR}/MyMathConfig.cmake"
       NAMESPACE MyMath::
)

构建 MyMath:

cd MyMath
mkdir build
cd build
cmake ..
make

我们一执行cmake ../ ,此时,export(EXPORT) 命令执行,在 build/ 目录下生成 MyMathConfig.cmake。

我们一执行cmake../,在这个阶段,CMake 会读取并处理 CMakeLists.txt 文件中的所有命令。当它遇到 export(EXPORT) 命令时,会立即执行它,从而在构建目录中生成那个配置文件(如 MyMathConfig.cmake)。

在另一个项目 MyApp 的 CMakeLists.txt 中依赖它:

find_package(MyMath CONFIG REQUIRED PATHS "/path/to/MyMath/build")
target_link_libraries(MyApp PRIVATE MyMath::MyMath)

MyApp 现在链接的是 MyMath/build/ 目录中的 libMyMath.a。

使用 install(EXPORT) 的工作流程 (用于分发)

在 MyMath/CMakeLists.txt 中:

add_library(MyMath STATIC mymath.cpp)
target_include_directories(MyMath PUBLIC include)

# 安装目标本身
install(TARGETS MyMath
        EXPORT MyMathTargets # 将此目标关联到名为 MyMathTargets 的导出集
        ARCHIVE DESTINATION lib # 安装静态库到 <prefix>/lib
        INCLUDES DESTINATION include # 安装头文件到 <prefix>/include
)

# 安装头文件
install(FILES include/mymath.h DESTINATION include)

# 定义导出集,并指定其安装位置。
# 这会在安装时生成 MyMathTargets.cmake 文件。
install(EXPORT MyMathTargets
        FILE MyMathConfig.cmake # 安装的配置文件名称
        NAMESPACE MyMath::
        DESTINATION lib/cmake/MyMath # 安装到 <prefix>/lib/cmake/MyMath
)

安装 MyMath:

cd MyMath/build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
make
sudo make install

install(EXPORT) 命令在配置阶段并不会立即执行,它只是被CMake记录为一个“待安装”的规则。只有当您显式地执行安装命令(如 make install、ninja install 或 cmake --install .)时,CMake 才会执行所有 install(...) 命令中定义的操作,其中就包括运行 install(EXPORT) 来生成并安装最终的配置文件到指定的安装目录(如 /usr/local/lib/cmake/MyMath/)。

执行完之后,文件被安装到:

  • /usr/local/lib/libMyMath.a
  • /usr/local/include/mymath.h
  • /usr/local/lib/cmake/MyMath/MyMathConfig.cmake

任何项目都可以轻松找到它:

# 因为安装到了标准路径,CMake 会自动查找

find_package(MyMath REQUIRED)

# 或者指定自定义前缀路径

# find_package(MyMath REQUIRED PATHS "/my/custom/prefix")

target_link_libraries(AnyApp PRIVATE MyMath::MyMath)

二.configure_package_config_file

大家可以去官网看看:CMakePackageConfigHelpers — CMake 4.1.0 文档

根据模板⽂件⽣成⾃定义包配置⽂件,可以被其他项⽬使⽤来查找和配置你的包。

configure_package_config_file(
     <input>
     <output>
     [COPYONLY]
     [INSTALL_DESTINATION <dir>]
     [PATH_VARS <var1> [<var2>...]]
     [NO_SET_AND_CHECK_MACRO]
     [NO_CHECK_REQUIRED_COMPONENTS_MACRO]
     [NO_MESSAGE]
 )
参数含义说明
<input>模板文件路径包含变量和逻辑的CMake脚本文件,用于生成最终配置文件
<output>生成的配置文件路径指定configure_file命令输出文件的完整路径
COPYONLY直接复制模板文件可选参数,指定后仅复制文件内容而不进行任何变量替换或配置处理
INSTALL_DESTINATION <dir>生成配置文件的安装目录可选参数,指定配置文件在安装阶段的目标位置
CONFIGURATIONS <config1>...指定要导出的配置可选参数,可指定一个或多个构建配置(如Debug、Release)进行导出
NO_SET_AND_CHECK_MACRO不生成set_and_check宏可选参数,禁用自动生成用于设置变量并验证路径是否存在的宏
NO_CHECK_REQUIRED_COMPONENTS_MACRO不生成check_required_components宏可选参数,禁用自动生成用于检查包所有必需组件是否已找到并正确配置的宏

configure_file命令是CMake中非常重要的功能,它允许开发者创建模板文件(通常包含@VAR@或${VAR}形式的变量),然后在配置阶段将这些变量替换为实际值,生成最终的目标文件。这个命令常用于生成配置文件、头文件或其他需要根据CMake变量动态生成的文件。

使用此命令时,通常会在模板文件中使用特殊的变量占位符,CMake会在配置过程中将这些占位符替换为当前CMake环境中对应变量的值,从而生成适用于特定平台或配置的最终文件。

怎么理解这货呢?

想象一下你要写一张“如何找到我家的说明书”,然后发给你的朋友。

  1. 你先创建一个“模板”说明书(就是 .in 文件)。上面有些空还没填,比如:

    我家住在:@我家地址@
    我家门牌号:@我家门牌@

  2. configure_package_config_file 这个命令,就是帮你自动填上这些空的秘书。

    • 你告诉秘书:我家地址 是“幸福路”,我家门牌 是“123号”。

    • 秘书就会帮你生成一份最终的、完整的说明书:

    我家住在:幸福路
    我家门牌号:123号

  3. 而且这个秘书很聪明!如果你后来把整条“幸福路”搬到了另一个城市,朋友拿着这份说明书,依然能找到你家的新位置。这就是它最厉害的地方——处理路径。


2.1.示例一

我们来做一个最简单的项目,只导出一个版本号。

第1步:创建模板文件 MyConfig.cmake.in

创建一个新文件,就叫 MyConfig.cmake.in,里面只写两行:

@PACKAGE_INIT@ 

set(MY_COOL_LIBRARY_VERSION @MY_VERSION@)
  • @PACKAGE_INIT@:这是一个魔法咒语,你暂时不用管它是什么,只要知道必须把它写在最前面就行了。CMake 会把它变成一堆有用的代码。

  • @MY_VERSION@:这是一个占位符,就像上面例子里的 @我家地址@。我们等下要在 CMake 里告诉秘书,用什么值来替换它。

第2步:创建 CMakeLists.txt

创建一个新的 CMakeLists.txt 文件:

cmake_minimum_required(VERSION 3.10)
project(MyCoolProject)

# 1. 设置我们要替换到模板里的变量的值
set(MY_VERSION "1.0.0") # 告诉秘书,@MY_VERSION@ 这个空,要填成 "1.0.0"

# 2. 请出我们的“秘书”(这个模块包含了我们需要用的命令)
include(CMakePackageConfigHelpers)

# 3. 让秘书开始工作!
configure_package_config_file(
     MyConfig.cmake.in # 【输入】模板文件是哪个?
     MyConfig.cmake # 【输出】最终生成的文件叫什么名字?
     INSTALL_DESTINATION . # 假设这个文件最终就放在安装目录的根下
)

# 4. (可选) 我们马上看看秘书生成的结果
message("配置文件生成完毕!")

第3步:运行并查看结果

  1. 打开终端,进入你放这两个文件的目录。

  2. 运行命令:

    mkdir build && cd build && cmake ../
    
  3. 现在,看看你的顶级目录里,是不是多了一个叫 MyConfig.cmake 的新文件?

  4. 用文本编辑器打开这个 MyConfig.cmake 文件,你会看到类似这样的内容:

####### 这里是一大堆由 @PACKAGE_INIT@ 生成的复杂代码 #######
...
####### 上面是一大堆由 @PACKAGE_INIT@ 生成的复杂代码 #######

set(MY_COOL_LIBRARY_VERSION 1.0.0)

看最后一行! 秘书成功地把模板里的占位符 @MY_VERSION@ 替换成了我们设置的值 1.0.0!

 这有什么用?

现在,如果另一个项目想用你的 MyCoolProject,它就可以这样做:

# 在其他项目的CMakeLists.txt中
find_package(MyCoolProject REQUIRED)
# 找到后,上面生成的 MY_COOL_LIBRARY_VERSION 变量就可以被使用了
message("找到的版本号是: ${MY_COOL_LIBRARY_VERSION}") # 这会打印 1.0.0

我们来看看我们实际运行的情况

cat MyConfig.cmake

我只截取了后半部分

我们发现还真有!!

所以,configure_package_config_file 就是一个自动填空秘书:

  1. 输入:你给它一个带占位符(如 @变量名@)的模板文件 (.in 文件)。

  2. 处理:你告诉它这些占位符应该换成什么值(在 CMakeLists.txt 里设置的变量)。

  3. 输出:它帮你生成一个完整的、没有占位符的最终文件 (.cmake 文件)。

它的主要工作就是把“变量”从你的 CMakeLists.txt 搬运 到给其他人用的 Config.cmake 文件里。

这样理解是不是简单多了?这就是它最核心、最基本的功能。其他的高级功能都是在这个基础上扩展的。

2.2.示例二

那我来一个 最最简单的 configure_package_config_file 示例,只保留核心逻辑,先帮你理解它是怎么工作的。


目录结构

MyLib/
├── CMakeLists.txt
└── MyLibConfig.cmake.in

📄 MyLibConfig.cmake.in

这是一个模板文件,里面只放一句话:

@PACKAGE_INIT@

它会被 configure_package_config_file() 替换成一堆初始化代码,保证安装后的 Config.cmake 能正确找到相关文件。


📄 CMakeLists.txt

最小可运行版本:

cmake_minimum_required(VERSION 3.10)
project(MyLib)

include(CMakePackageConfigHelpers)

# 用模板生成一个 Config.cmake
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/MyLibConfig.cmake.in   # 输入模板
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake      # 输出结果
    INSTALL_DESTINATION lib/cmake/MYLib                # 安装位置
)

# 安装 Config.cmake
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    DESTINATION lib/cmake/MYLib
)

我们直接一键复制下面的命令就可以搭建好我们需要的目录结构

mkdir -p MyLib && cd MyLib && \
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.10)
project(MyLib)

include(CMakePackageConfigHelpers)

# 用模板生成一个 Config.cmake
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/MyLibConfig.cmake.in   # 输入模板
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake      # 输出结果
    INSTALL_DESTINATION lib/cmake/MYLib                # 安装位置
)

# 安装 Config.cmake
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    DESTINATION lib/cmake/MYLib
)
EOF

cat > MyLibConfig.cmake.in <<'EOF'
@PACKAGE_INIT@
EOF

好,接下来我们就来构建项目。

mkdir build && cd build && cmake .. && make

运行效果

安装后目录:

打开这个 MyLibConfig.cmake,你会发现它长这样(CMake 自动生成的):

我不想讲全部,也没有任何意义,我只想讲第一句的含义

get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)

做了什么

  • CMAKE_CURRENT_LIST_DIR 是当前这个 .cmake 文件所在的目录(比如:/path/to/install/lib/cmake/MyLib)。

  • "/../../../" 向上走三层(典型安装布局假设:<prefix>/lib/cmake/MyLib,向上三层回到 <prefix>)。

  • get_filename_component(... ABSOLUTE) 把这个相对路径解析为绝对路径并赋值给 PACKAGE_PREFIX_DIR。

为什么

  • 目的是推断出库的安装前缀(<prefix>),后续可以用它来构造头文件、库文件等的标准路径或作检查。

举例

  • 如果 CMAKE_CURRENT_LIST_DIR = /home/me/install/lib/cmake/MyLib,则 PACKAGE_PREFIX_DIR → /home/me/install。

为什么重要:后续所有库文件(头文件、二进制库文件)的路径都可以基于这个 PACKAGE_PREFIX_DIR 来可靠地构建,例如 {PACKAGE_PREFIX_DIR}/include 或 {PACKAGE_PREFIX_DIR}/lib。

也就是说白了@PACKAGE_INIT@ 是来定位整个库的“安装根目录”(即 PACKAGE_PREFIX_DIR)的,让Config.cmake文件知道这个安装根目录,我们就能准确地找到这个库的所有文件(头文件、库文件等)在哪里。

三.install

这个其实我们在之前讲过,但是那个时候讲的太浅了,我们现在得深入理解一下。

大家可以去官网:install — CMake 4.1.0 Documentation

好的 👍
我给你整理几个 CMake install() 的常见简单例子,涵盖可执行文件、库、头文件和目录的安装。


1. 安装可执行文件

cmake_minimum_required(VERSION 3.10)
project(InstallExample)

add_executable(myapp main.cpp)

# 把可执行文件安装到 bin 目录
install(TARGETS myapp DESTINATION bin)

编译后执行:

cmake --install build --prefix /usr/local

会把 myapp 复制到 /usr/local/bin/。


2. 安装库文件

add_library(mylib SHARED mylib.cpp)

# 安装库到 lib,头文件到 include
install(TARGETS mylib DESTINATION lib)
install(FILES mylib.h DESTINATION include)

会把 libmylib.so 安装到 lib/,头文件 mylib.h 安装到 include/。


3. 安装多个头文件

# 假设有多个头文件在 include/ 目录
install(DIRECTORY include/ DESTINATION include)

这会把 include/ 目录里的所有头文件复制到目标的 include/。


4. 安装配置文件

# 安装一个配置文件到 etc 目录
install(FILES myconfig.conf DESTINATION etc)

5. 安装时重命名

add_executable(mytool tool.cpp)

# 安装时把 mytool 改名成 mytool_cli
install(TARGETS mytool DESTINATION bin RENAME mytool_cli)

3.1.安装静态库

我们来实现一个最精简的 静态库 + 头文件 + install() 的完整 CMake 示例。


📂 目录结构

StaticLibExample/
├── CMakeLists.txt
├── mylib.cpp
└── mylib.h

我们直接一条语句合成好吧

mkdir -p StaticLibExample && cd StaticLibExample && touch CMakeLists.txt mylib.cpp mylib.h

📄 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(StaticLibExample VERSION 1.0 LANGUAGES CXX)

# 创建静态库
add_library(mylib STATIC mylib.cpp)

# 安装规则
# 1. 静态库 -> lib/
install(TARGETS mylib
    ARCHIVE DESTINATION lib
)

# 2. 头文件 -> include/
install(FILES mylib.h DESTINATION include)

大家注意一下:install(FILES mylib.h DESTINATION include)可不是代表着mylib.h被安装到当前目录下面,而实际上CMake会自动在include前面加上CMAKE_INSTALL_PREFIX这个变量里面存储的路径前缀,CMAKE_INSTALL_PREFIX一般是/usr/local/,也就是说mylib.h会被安装到${CMAKE_INSTALL_PREFIX}/include目录里面,也就是/usr/local/include目录里面!!!!

install(TARGETS mylib
    ARCHIVE DESTINATION lib
)也是一样的意思,静态库会被安装到${CMAKE_INSTALL_PREFIX}/lib目录里面,也就是/usr/local/lib里面


📄 mylib.h

#pragma once
#include <string>

std::string hello();

📄 mylib.cpp

#include "mylib.h"

std::string hello() {
    return "Hello from static library!";
}

3.1.1.修改CMAKE_INSTALL_PREFIX

CMAKE_INSTALL_PREFIX是什么?

CMAKE_INSTALL_PREFIX 是 CMake 中最重要且最常用的变量之一,它决定了当你运行 make install 时,你的软件会被安装到哪个目录。

CMAKE_INSTALL_PREFIX 就是你软件的"安装目的地"或"新家地址"。

当你执行 make install 时,CMake 会把所有需要安装的文件(可执行文件、库文件、头文件等)复制到以 CMAKE_INSTALL_PREFIX 为根目录的相应子目录中。

CMake 会为不同平台设置不同的默认值:

  • Linux/macOS: /usr/local

  • Windows: C:/Program Files/${PROJECT_NAME}

你可以在项目的build母下通过以下命令查看当前项目的默认值:

cmake -L | grep CMAKE_INSTALL_PREFIX

说明 当前工程的安装前缀 是 /usr/local。

这就是默认值。

所以如果你写了:

install(FILES mylib.h DESTINATION include) 
install(TARGETS mylib ARCHIVE DESTINATION lib)

那么安装的实际路径就是:

  • 头文件 → /usr/local/include/mylib.h

  • 静态库 → /usr/local/lib/libmylib.a

修改CMAKE_INSTALL_PREFIX到./install

我们接着上面那个项目,进行构建 & 安装步骤

# 1. 创建构建目录
mkdir build && cd build

# 2. 配置
cmake .. -DCMAKE_INSTALL_PREFIX=./install

# 3. 编译
cmake --build .

# 4. 安装
cmake --install .

我们来看看下面这条语句啥意思?

cmake .. -DCMAKE_INSTALL_PREFIX=./install

cmake ..

  • cmake:运行 CMake,生成构建系统(比如 Makefile、Ninja 或者 Visual Studio 工程)。

  • ..:告诉 CMake 源代码的位置在上一级目录。

    • 一般你在 build/ 目录里运行它,所以 .. 指的就是你的工程根目录。


-D

  • -D 用来给 CMake 传递变量。

  • 格式:-D<变量名>=<值>。


CMAKE_INSTALL_PREFIX=./install

  • CMAKE_INSTALL_PREFIX:CMake 内置的变量,表示 cmake --install 命令安装时的目标根目录。

  • 改成 ./install 表示:安装到当前 build 目录 下的 install/ 文件夹,而不是系统目录。

这样你就不需要 sudo 就能测试安装结果。


📂 安装结果

……

这样你就有了一个最小可运行的 静态库 + 头文件安装例子。

3.1.2.不修改CMAKE_INSTALL_PREFIX

如果说我们把上面的这句

cmake .. -DCMAKE_INSTALL_PREFIX=./install

换成

cmake ..

那这些静态库和头文件会被安装到哪里去呢?

我们来试试看

# 1. 创建构建目录
mkdir build && cd build

# 2. 配置
cmake ..

# 3. 编译
cmake --build .

# 4. 安装
sudo cmake --install .

这里我们可是没有修改过CMAKE_INSTALL_PREFIX啊,那么按照我们的逻辑,

  • 静态库mylib会被安装到/usr/local/lib这个目录里
  • 头文件mylib.h会被安装到/usr/local/include这个目录里面

我们来看看是不是这么一回事?

还真是这么一回事啊!!!!现在这个静态库和头文件就被安装到系统目录里面去了

3.2.install(target)和install(export)

3.2.1.传统的安装使用机制

在之前,我们都是按照下面这样子的方式进行安装的

install(TARGETS hello
    DESTINATION lib
)

那么这个hello目标就会被安装到/usr/local/lib。

意义

  • 你确实得到了库文件(静态库 .a 或动态库 .so)。

  • 头文件也安装到了 include/,别人可以用 -I 去包含。

  • 但是 CMake 本身不知道这个库的“身份信息”,它不知道:

    • 这个库叫什么 target 名字?

    • 它的头文件在哪里?

    • 它的依赖库有哪些?

所以别的项目如果想要使用你这个库及其头文件的的时候,只能手动写上它们的具体路径:

include_directories(/usr/local/include)
target_link_libraries(app /usr/local/lib/libhello.a)

这就和“老式 Makefile 时代”差不多,要自己管理路径,很不方便。

3.2.2.现代CMake的安装机制

在现代 CMake 中,引入了 基于 target 的安装与导出机制。

通过生成一个 *Targets.cmake 文件,CMake 能够记录库的完整使用信息,包括:

  • 库的 target 名字(例如 MyLib::mylib),便于在项目中直接通过 target_link_libraries() 使用;

  • 头文件的安装路径,自动告诉使用者应该去哪里 #include;

  • 依赖库信息,保证在链接时会自动拉取所需的其他库;

  • 编译选项和宏定义(如果有),一并写入 target 的使用要求中。

有了这些导出的 target 文件,使用者不再需要手动写路径,例如:

target_link_libraries(myapp PRIVATE /usr/local/lib/libmylib.a)
include_directories(/usr/local/include)

而是可以直接写:

find_package(MyLib REQUIRED)
target_link_libraries(myapp PRIVATE MyLib::mylib)

这样不仅简化了使用方式,也让库的可移植性和可维护性大大增强。


不过在讲解这个具体的使用过程之前,我们先来好好讲讲install

install的第一个参数决定了这个install的主要任务是什么

比如说

  1. install(targets A……):它的主要任务是安装目标A
  2. install(export B……):它的主要任务是导出并安装目标B的相关配置文件

我们看个例子

install(TARGETS hello          
    EXPORT helloTargets        
    DESTINATION lib            
)

像上面的这个install,它的第一个参数是targets,这代表:

这个命令的核心操作是安装一个具体的构建目标(target),例如库文件(.a, .so, .lib, .dll)或可执行文件。EXPORT 关键字只是它的一个附加属性,用于声明“将这个目标的信息记录到某个导出集中”,但主要动作还是安装二进制文件本身。

再看一个例子

install(EXPORT helloTargets  
    FILE helloTargets.cmake   
    NAMESPACE Hello::        
    DESTINATION lib/cmake/hello
)

像上面的这个install,它的第一个参数是export,这代表:

这个命令的核心操作是生成并安装一个CMake配置文件(通常是.cmake文件)。它不处理任何实际的二进制文件,而是处理之前由TARGETS命令通过EXPORT关键字收集到的元信息清单(导出集),并将其写入文件。


现在我们看看在现代CMake的基于 target 的安装与导出机制里面,这个install(targets……)和install(export……)是怎么进行配合的

# 安装 "hello" 这个库/可执行文件
install(TARGETS hello          # 指定要安装的目标(这里是 hello,可能是 add_library/add_executable 定义的)
    EXPORT helloTargets        # 把这个目标记录到一个导出集(export set)里,名字叫 helloTargets
    DESTINATION lib            # 安装后的库文件放到 <prefix>/lib 目录下
)

# 安装导出文件(即生成 helloTargets.cmake)
install(EXPORT helloTargets    # 导出集的名字(上面 install(TARGETS ...) 已经定义过了)
    FILE helloTargets.cmake    # 输出的文件名,别人 find_package 时需要 include 它
    NAMESPACE Hello::          # 给目标加上命名空间,别人在用的时候就写 Hello::hello
    DESTINATION lib/cmake/hello # 安装路径,最终文件位置是 <prefix>/lib/cmake/hello/helloTargets.cmake
)

你可以把这两个命令的第一个参数(TARGETS 或 EXPORT)看作是定义了命令的“操作模式”:

  1. TARGETS模式:install(TARGETS ...)

    • 主要任务:安装二进制产物。

    • 附加功能:可以通过EXPORT选项顺便为这些产物创建一份“元信息清单”。

  2. EXPORT模式:install(EXPORT ...)

    • 唯一任务:将那份“元信息清单”转换并安装成一个正式的、可供find_package使用的CMake配置文件(如MyLibTargets.cmake)。

所以,在我的例子中:

  • 第一行是 TARGETS模式,所以它安装的是mylib这个库文件。

  • 第二行是 EXPORT模式,所以它安装的是MyLibTargets.cmake这个配置文件。

我们来仔细看看它们两个是怎么进行配合的

第1步:install(TARGETS hello ...) - “入库并做内部记录”

当CMake执行到这一行时,它做两件事:

  1. 主要任务(实物安装):

    • 找到名为 hello 的目标(无论是库还是可执行文件)。

    • 将其对应的实际二进制文件(例如在Linux上是 libhello.so 或 hello 可执行文件)复制到安装目录下的 lib/ 文件夹中。

    • 结果:在你的系统上,{CMAKE_INSTALL_PREFIX}/lib/ 目录下会出现 libhello.so 等文件。

  2. 附加任务(生成记录):

    • EXPORT helloTargets 关键字告诉CMake:“请把这个 hello 目标的所有相关信息(元数据)记录下来,归入一个名叫 helloTargets 的集合里。”

    • 这些“相关信息”包括:

      • 这个库安装后的完整路径(就是上一步的 {CMAKE_INSTALL_PREFIX}/lib/libhello.so)。

      • 它的头文件包含路径(如果通过 target_include_directories 设置了)。

      • 它的编译特性(如编译定义、编译选项等)。

      • 它依赖的其他库。

    • 关键点:此时,这个名叫 helloTargets 的集合还只是存在于CMake的内存中的一个逻辑概念,还没有生成任何实际的文件。

第2步:install(EXPORT helloTargets ...) - “绘制并发布地图”

紧接着,CMake执行到第二个命令。这个命令的唯一目的就是处理上一步创建的“逻辑集合”。

  1. 查找记录:EXPORT helloTargets 告诉CMake:“去找到那个名叫 helloTargets 的集合。”

  2. 生成地图文件:

    • FILE helloTargets.cmake 指定了CMake应该将这个集合里的所有信息写入到一个名为 helloTargets.cmake 的实际文件中。

    • 这个文件是一个CMake脚本,里面包含了创建导入目标(Imported Target) 的所有指令。

  3. 美化目标名:

    • NAMESPACE Hello:: 指示CMake,在生成这个文件时,不要创建原样的目标 hello,而是要创建一个带命名空间的高大上目标 Hello::hello。这是现代CMake的最佳实践,可以极大避免命名冲突。

  4. 安装地图:

    • DESTINATION lib/cmake/hello 指定将生成的 helloTargets.cmake 文件安装到 {CMAKE_INSTALL_PREFIX}/lib/cmake/hello/ 目录下。

    • 结果:你的安装目录中会出现 {CMAKE_INSTALL_PREFIX}/lib/cmake/hello/helloTargets.cmake 这个文件。

完整的安装结果可能就像下面这样子

/usr/local/
├── lib/
│   └── libhello.a
├── include/
│   └── hello.h
└── lib/cmake/hello/
    ├── helloTargets.cmake
    ├── helloTargets-noconfig.cmake
    └── helloConfig.cmake

意义

这里多出来了一组 CMake 配置文件,尤其是 helloTargets.cmake。

这个文件里保存了 hello 这个 target 的全部信息,例如:

add_library(Hello::hello STATIC IMPORTED)

set_target_properties(Hello::hello PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES "/path/to/install/include"
    IMPORTED_LOCATION "/path/to/install/lib/libhello.a"
)

这意味着:

  • CMake 知道 Hello::hello 这个 target 存在。

  • 知道它的头文件应该用 /path/to/install/include。

  • 知道它的库文件在哪。

所以别人只需要:

find_package(hello CONFIG REQUIRED)
target_link_libraries(app PRIVATE Hello::hello)

就能一键使用,不需要写死路径。

3.3.install综合示例一——安装到系统目录

我们这个示例和上面1.1举的例子是基本一样的。

我们还是用两个项目:

  • InstallExample → 库工程,编译并安装,导出 MyLibTargets.cmake。

  • UseMyLib → 使用安装好的库,通过 find_package(MyLib) 使用。


3.3.1.InstallExample

📂 目录结构

InstallAndUse/
└── InstallExample/       # 库项目
    ├── CMakeLists.txt
    ├── mylib.cpp
    └── mylib.h


📄 InstallExample/mylib.h

#pragma once
void hello();

📄 InstallExample/mylib.cpp

#include "mylib.h"
#include <iostream>

void hello() {
    std::cout << "Hello from MyLib (installed)!" << std::endl;
}

📄 InstallExample/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(InstallExample LANGUAGES CXX)

# 1. 创建静态库
add_library(mylib STATIC mylib.cpp)

# 2. 设置头文件搜索路径
target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
    $<INSTALL_INTERFACE:include>
)

# 3. 安装库
install(TARGETS mylib
    EXPORT MyLibTargets         # 导出信息
    DESTINATION lib             # # 实际会被安装到${CMAKE_INSTALL_PREFIX}/lib
)
# 4.安装头文件
install(FILES mylib.h DESTINATION include) # 实际会被安装到${CMAKE_INSTALL_PREFIX}/include

# 5. 安装导出文件(注意这一步会生成 MyLibTargets.cmake,并且把它安装到标准目录里面去)
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::           # 使用时的命名空间前缀
    DESTINATION lib/cmake/MyLib # 实际会被安装到${CMAKE_INSTALL_PREFIX}/lib/cmake/MyLib
)

我知道大家对里面的某些语句有点懵逼

# 3.安装库
install(TARGETS mylib
    EXPORT MyLibTargets
    DESTINATION lib
)

这句会做两件事:

  • 把编译好的 libmylib.a(静态库)安装到 ${CMAKE_INSTALL_PREFIX}/lib/ 目录下,默认安装前缀CMAKE_INSTALL_PREFIX的值是 /usr/local,所以结果会是:/usr/local/lib/libmylib.a
  • 把目标信息导出到 MyLibTargets 中,这个信息不会立即生成文件,而是供后面的 install(EXPORT ...) 使用。EXPORT MyLibTargets 的作用就是:记录 mylib 的构建方式、依赖、include 路径等信息,等后面第 5 步时统一写入 MyLibTargets.cmake。

接下来我们看看另外一句

# 5. 安装导出文件(注意这一步会生成 MyLibTargets.cmake,并且把它安装到标准目录里面去)
install(EXPORT MyLibTargets        # 1. 指定要处理的“导出集”名称
    FILE MyLibTargets.cmake        # 2. 指定输出的配置文件名
    NAMESPACE MyLib::              # 3. (可选)为所有目标添加命名空间前缀
    DESTINATION lib/cmake/MyLib    # 4. 指定配置文件的安装路径
)

这是一个至关重要的命令,它负责生成并安装一个让其他项目能够自动找到并使用你的库的配置文件。没有它,你的库安装后,其他CMake项目将无法通过 find_package() 轻松地定位和链接它。

它具体执行了以下几个步骤:

1. 查找导出集 (EXPORT MyLibTargets)

  • CMake 会在内存中寻找一个名为 MyLibTargets 的导出集(Export Set)。

  • 这个导出集是由之前一个或多个 install(TARGETS ... EXPORT MyLibTargets) 命令填充的。那些命令在安装实际库文件的同时,也将该目标的所有信息(如安装位置、头文件路径、编译定义、依赖项等)记录到了这个虚拟的 MyLibTargets 集合中。

2. 生成配置文件 (FILE MyLibTargets.cmake)

  • CMake 会读取 MyLibTargets 导出集中的所有信息。

  • 然后,它将根据这些信息,动态生成一个完整的CMake脚本文件,并命名为 MyLibTargets.cmake。

  • 这个文件不是你自己写的,而是CMake自动生成的。它的内容类似于:

    # ... 一些自动生成的代码和注释 ...
    add_library(MyLib::mylib SHARED IMPORTED) # 创建一个导入目标(IMPORTED)
    set_target_properties(MyLib::mylib PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include" # 自动设置头文件路径
      IMPORTED_LOCATION_RELEASE "/usr/local/lib/libmylib.so" # 自动设置库文件的实际位置
      # ... 以及其他所有在原始目标上设置的属性(如编译定义、依赖等)...
    )
    # ... 可能还有其他目标...
  • 这个文件的核心作用是:在下游项目的构建过程中,重新创建出你安装的库目标,并精确地指向它们被安装后的位置。

3. 添加命名空间 (NAMESPACE MyLib::)

  • 这是一个非常重要的最佳实践。它指示CMake在生成上述配置文件时,为里面所有的目标名加上一个命名空间前缀。

  • 如果没有命名空间:生成的文件会创建名为 mylib 的目标。这很容易与其他项目或系统库发生命名冲突。

  • 有了命名空间:生成的文件会创建名为 MyLib::mylib 的目标。这清晰地表明了该库的来源,极大地避免了冲突。

  • 下游项目使用时,会这样写:target_link_libraries(their_app PRIVATE MyLib::mylib),代码非常清晰和健壮。

4. 安装配置文件 (DESTINATION lib/cmake/MyLib)

  • 最后,CMake会将刚刚生成的 MyLibTargets.cmake 文件复制(安装) 到指定的目录。

  • 最终路径是:${CMAKE_INSTALL_PREFIX}/lib/cmake/MyLib/MyLibTargets.cmake。

  • 这个路径不是随意的。CMake的 find_package() 命令在模块模式(Module Mode) 下,有一个标准的搜索路径就是 ${CMAKE_INSTALL_PREFIX}/lib/cmake/。将配置文件安装在这个位置,就是为了让 find_package(MyLib) 能够自动发现它。


接下来我们可以使用下面这个命令来一键构建出整个InstallExample目录的结构及其文件

mkdir -p InstallAndUse/InstallExample && cd InstallAndUse/InstallExample && cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.10)
project(InstallExample LANGUAGES CXX)

add_library(mylib STATIC mylib.cpp)

target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
    $<INSTALL_INTERFACE:include>
)

install(TARGETS mylib
    EXPORT MyLibTargets
    DESTINATION lib
)
install(FILES mylib.h DESTINATION include)

install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)
EOF
cat > mylib.h <<'EOF'
#pragma once
void hello();
EOF
cat > mylib.cpp <<'EOF'
#include "mylib.h"
#include <iostream>
void hello() {
    std::cout << "Hello from MyLib (installed)!" << std::endl;
}
EOF
cd ../../

接下来我们就来构建我们的项目

cd InstallAndUse/InstallExample && mkdir build && cd build && cmake .. && make

这个时候我们看看${CMAKE_INSTALL_PREFIX}/lib/cmake/MyLib里面有没有这么一个文件。

ls -l /usr/local/lib/cmake/MyLib

我们发现查询不到!!这个是因为我们还没有执行安装动作,

进入 build/ 目录后执行:

cmake --install .

这样 CMake 才会把目标和导出的配置文件复制到 /usr/local/lib/cmake/MyLib。

安装完成后你再看:

ls -l /usr/local/lib/cmake/MyLib

就会看到:

我们可以打开这个文件看看

那这些箭头指向的都是啥意思呢?我们来看看


🔑 1. 创建导入库(库名 + 类型)

# Create imported target MyLib::mylib
add_library(MyLib::mylib STATIC IMPORTED)

👉 这里说明:

  • 生成了一个 IMPORTED target(不能编译,只能链接)。

  • 名字是 MyLib::mylib

  • 类型是 STATIC(静态库,如果是共享库则会是 SHARED)。


🔑 2. 头文件路径(包含目录)

set_target_properties(MyLib::mylib PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)

👉 这里告诉 CMake:

  • 以后 target_link_libraries(app MyLib::mylib) 时,
    编译器会自动加上 -I<安装路径>/include。

也就是说,使用者不用手动 include_directories()。


🔑 3. 库文件路径(在另一个文件里)

你会看到这段:

# Load information for each installed configuration.
get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
file(GLOB CONFIG_FILES "${_DIR}/MyLibTargets-*.cmake")
foreach(f ${CONFIG_FILES})
  include(${f})
endforeach()

👉 这会去加载同目录下的 MyLibTargets-noconfig.cmake 或 MyLibTargets-Release.cmake 等。
这些文件才真正包含 库文件的绝对路径:

set_target_properties(MyLib::mylib PROPERTIES
  IMPORTED_LOCATION "/usr/local/lib/libmylib.a"
)

也就是说:

  • MyLibTargets.cmake 定义了 目标名 + include 路径

  • MyLibTargets-<config>.cmake 定义了 库文件实际路径


我们可以去看看MyLibTargets-noconfig.cmake里面是啥?

我截取了一小部分

这个就是 MyLibTargets-noconfig.cmake,它正好补全了刚才我说的第 3 部分:库文件的路径信息。

我们拆开来看:


🔑 1. 导入配置

set_property(TARGET MyLib::mylib APPEND PROPERTY IMPORTED_CONFIGURATIONS NOCONFIG)

👉 说明 MyLib::mylib 这个库的导入配置是 NOCONFIG。
(如果是 Release / Debug 构建,会生成 MyLibTargets-Release.cmake / MyLibTargets-Debug.cmake,里面就是 IMPORTED_LOCATION_RELEASE 或 IMPORTED_LOCATION_DEBUG。)


🔑 2. 库文件信息

set_target_properties(MyLib::mylib PROPERTIES
  IMPORTED_LINK_INTERFACE_LANGUAGES_NOCONFIG "CXX"
  IMPORTED_LOCATION_NOCONFIG "${_IMPORT_PREFIX}/lib/libmylib.a"
)
  • IMPORTED_LINK_INTERFACE_LANGUAGES_NOCONFIG "CXX"
    👉 告诉 CMake,这个库是用 C++ 编译的。

  • IMPORTED_LOCATION_NOCONFIG "${_IMPORT_PREFIX}/lib/libmylib.a"
    👉 指定静态库的实际路径,${_IMPORT_PREFIX} 就是安装前缀(例如 /usr/local)。
    展开后就是:

    /usr/local/lib/libmylib.a
    

这就是 链接时真正用的库文件。


🔑 3. 文件存在性检查

list(APPEND _IMPORT_CHECK_TARGETS MyLib::mylib )
list(APPEND _IMPORT_CHECK_FILES_FOR_MyLib::mylib "${_IMPORT_PREFIX}/lib/libmylib.a" )

👉 CMake 在导入时会检查:/usr/local/lib/libmylib.a 是否存在,不存在就报错。


📌 总结:

  • MyLibTargets.cmake

    • 定义了 target 名(MyLib::mylib)

    • 设置了头文件路径(INTERFACE_INCLUDE_DIRECTORIES)

    • 再去加载 MyLibTargets-noconfig.cmake

  • MyLibTargets-noconfig.cmake

    • 定义了 target 对应的库文件路径(IMPORTED_LOCATION_NOCONFIG)

    • 指定语言接口(CXX)

    • 加入存在性检查

所以这两个文件 合起来,就把头文件和库文件的完整信息都告诉 CMake 了。

3.3.2. UseMyLib

我们接下来添加使用方,使得整个项目的目录结构变成下面这样子。

InstallAndUse/
├── InstallExample/       # 库项目
│   ├── CMakeLists.txt
│   ├── mylib.cpp
│   └── mylib.h
└── UseMyLib/             # 使用库的项目
    ├── CMakeLists.txt
    └── main.cpp

📄 UseMyLib/main.cpp

#include "mylib.h"

int main() {
    hello();
    return 0;
}

📄 UseMyLib/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(UseMyLib LANGUAGES CXX)

# 1. 查找 MyLib
# 在系统或指定路径里找到已安装的 MyLib,如果找不到就报错,不允许继续配置。
find_package(MyLib REQUIRED)

# 2. 创建可执行文件
add_executable(app main.cpp)

# 3. 链接 MyLib::mylib
target_link_libraries(app PRIVATE MyLib::mylib)

我们还是可以像上面那样子来复制下面的命令来一键搭建出这个目录的结构及其文件

mkdir -p InstallAndUse/UseMyLib && cd InstallAndUse/UseMyLib && cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.10)
project(UseMyLib LANGUAGES CXX)

find_package(MyLib REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE MyLib::mylib)
EOF
cat > main.cpp <<'EOF'
#include "mylib.h"
int main() {
    hello();
    return 0;
}
EOF
cd ../../

接下来我们就来构建我们的项目

cd InstallAndUse/UseMyLib && \
mkdir build && cd build && \
cmake ..

我们看看

我们遇到的这个报错是典型的 CMake 找不到已安装库的配置文件 的情况。

我们仔细分析一下:


1️⃣ 报错原因

Could not find a package configuration file provided by "MyLib" with any of
the following names:

  MyLibConfig.cmake
  mylib-config.cmake

意思是:

  • CMake 在默认路径下没有找到 MyLibConfig.cmake。

  • install(EXPORT …) 会生成 MyLibTargets.cmake,但 find_package 需要一个入口的 Config 文件,也就是 MyLibConfig.cmake。

  • 你的库只生成了 MyLibTargets.cmake,但是没有生成 MyLibConfig.cmake,所以 find_package(MyLib) 找不到。


2️⃣ 正确做法

要使用 find_package(MyLib),你需要:在库安装目录生成 Config 文件,指向 MyLibTargets.cmake。

那具体怎么做呢?我们接着往下看

第一步:在InstallExample/CMakeLists.txt 中添加下面这2句:

configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
    INSTALL_DESTINATION lib/cmake/MyLib
)

# 把MyLibConfig.cmake安装到系统目录里面去
install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
    DESTINATION lib/cmake/MyLib
)

第二步:在InstallExample/目录下新增一个文件 Config.cmake.in :

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")


上面这两个啥意思?

我们仔细想想:

还记得我们在1.1.2.的时候是怎么使用MyLibTargets.cmake来对库进行连接的吗?

我们看看

cmake_minimum_required(VERSION 3.10)
project(UseMyLib LANGUAGES CXX)

# 引入导出的 targets 文件
include(${CMAKE_SOURCE_DIR}/../ExportExample/build/MyLibTargets.cmake)

# 定义可执行文件
add_executable(app main.cpp)

# 链接导入的 mylib
target_link_libraries(app PRIVATE mylib)

其实最关键的就是

# 引入导出的 targets 文件
include(${CMAKE_SOURCE_DIR}/../ExportExample/build/MyLibTargets.cmake)

我们得include(Target.cmake文件),这样子我们才能链接成功这个库。

那么find_package也是这样子做的。

find_package(MyLib) 会根据 CMAKE_PREFIX_PATH 去找安装目录里的 Config文件,而Config文件里面就是include(Target.cmake文件)这一步,通过这一步就能寻找到 Targets.cmake文件。

那么有人就可能会好奇了

我们是先把MyLibConfig.cmake先生成到构建目录里面,那么MyLibConfig.cmake不是存的是下面这样子的吗

include("构建目录的路径/MyLibTargets.cmake")

然后再把这个MyLibConfig.cmake安装到系统默认目录里面去。那么岂不是还是指向了构建目录的

MyLibTargets.cmake吗?

也就是说

👉 MyLibConfig.cmake 里面的 include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake"),到底指向 构建目录 的 targets 还是 安装目录 的 targets?

答案就是:靠 @PACKAGE_INIT@ 来解决。


🔎 详细解释

生成阶段(构建目录里)

configure_package_config_file() 会在 构建目录 里生成 MyLibConfig.cmake。

这个时候它里面确实写了

include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
  • 但因为你还没 make install,所以这里的 CMAKE_CURRENT_LIST_DIR 就是构建目录下的路径。
  • 这保证了:你在 build tree 里也能直接用 find_package(MyLib)(开发者友好)。

安装阶段

当你执行 make install 时:

  • MyLibConfig.cmake 会被安装到 /usr/local/lib/cmake/MyLib/
  • MyLibTargets.cmake 也会被 install(EXPORT ...) 安装到同一个目录。

安装后的目录:

/usr/local/lib/cmake/MyLib/
    ├── MyLibConfig.cmake
    └── MyLibTargets.cmake

这时候再看 include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake"):

CMAKE_CURRENT_LIST_DIR 就是 /usr/local/lib/cmake/MyLib

所以它会 include 安装目录的 MyLibTargets.cmake ✅


为什么能自动区分?

这就是 @PACKAGE_INIT@ 的功劳。

它会展开成一段初始化代码,帮你正确设置 _IMPORT_PREFIX 等变量,从而寻找到库的安装目录,保证 无论你在构建目录用 还是 安装目录用,都能找到对的 MyLibTargets.cmake。

所以 MyLibConfig.cmake 实际上是 双模的:

  • 在 build tree 里:引用 build 目录下的 MyLibTargets.cmake
  • 在 install tree 里:引用 install 目录下的 MyLibTargets.cmake

现在就没有疑问了吧!!!

3.3.3.正确示例(使用find_package)

现在我们就来看看,如果说我们想要使用find_package(MyLib)来链接到这个库的话,需要怎么做?

首先我们要构建的项目如下


📂 根目录结构

InstallAndUse/
├── InstallExample/
│   ├── CMakeLists.txt
│   ├── Config.cmake.in
│   ├── mylib.h
│   └── mylib.cpp
└── UseMyLib/
    ├── CMakeLists.txt
    └── main.cpp

📄 InstallExample/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(InstallExample LANGUAGES CXX)

# 创建静态库
add_library(mylib STATIC mylib.cpp)

# 头文件路径
target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
    $<INSTALL_INTERFACE:include>
)

# 安装库和头文件
install(TARGETS mylib
    EXPORT MyLibTargets
    DESTINATION lib
)
install(FILES mylib.h DESTINATION include)

# 安装导出文件和 Config
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)

#以下是新增内容

# 生成 Config 文件(直接包含导出的 targets)
include(CMakePackageConfigHelpers)
configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
    INSTALL_DESTINATION lib/cmake/MyLib
)
# 把生成的 Config 文件安装到系统目录里面
install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
    DESTINATION lib/cmake/MyLib
)

📄 InstallExample/Config.cmake.in

@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")

📄 InstallExample/mylib.h

#pragma once
void hello();

📄 InstallExample/mylib.cpp

#include "mylib.h"
#include <iostream>
void hello() { std::cout << "Hello from MyLib (installed)!" << std::endl; }

📄 UseMyLib/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(UseMyLib LANGUAGES CXX)

# 查找 MyLib
find_package(MyLib REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE MyLib::mylib)

📄 UseMyLib/main.cpp

#include "mylib.h"
int main() { hello(); return 0; }

我们可以一键搭建出这个项目的目录结构和文件

# 创建根目录
mkdir -p InstallAndUse && cd InstallAndUse

# ================== InstallExample ==================
mkdir -p InstallExample
cd InstallExample

# CMakeLists.txt
cat > CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.10)
project(InstallExample LANGUAGES CXX)

# 创建静态库
add_library(mylib STATIC mylib.cpp)

# 头文件路径
target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
    $<INSTALL_INTERFACE:include>
)

# 安装库和头文件
install(TARGETS mylib
    EXPORT MyLibTargets
    DESTINATION lib
)
install(FILES mylib.h DESTINATION include)

# 生成 Config 文件(直接包含导出的 targets)
include(CMakePackageConfigHelpers)
configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
    INSTALL_DESTINATION lib/cmake/MyLib
)

# 安装导出文件和 Config
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)
install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake"
    DESTINATION lib/cmake/MyLib
)
EOF

# Config.cmake.in
cat > Config.cmake.in << 'EOF'
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")
EOF

# mylib.h
cat > mylib.h << 'EOF'
#pragma once
void hello();
EOF

# mylib.cpp
cat > mylib.cpp << 'EOF'
#include "mylib.h"
#include <iostream>
void hello() { std::cout << "Hello from MyLib (installed)!" << std::endl; }
EOF

cd ..

# ================== UseMyLib ==================
mkdir -p UseMyLib
cd UseMyLib

# main.cpp
cat > main.cpp << 'EOF'
#include "mylib.h"
int main() { hello(); return 0; }
EOF

# CMakeLists.txt
cat > CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.10)
project(UseMyLib LANGUAGES CXX)

# 查找 MyLib
find_package(MyLib REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE MyLib::mylib)
EOF

首先我们来构建并安装库

cd InstallExample && \
mkdir build && cd build && \
cmake .. && \
cmake --build . && \
make install

现在我们就来构建 UseMyLib

cd UseMyLib && \
mkdir build && cd build
cmake ..

接下来我们通过下面这个命令来详细看一下搭建过程

cmake --build . -v

发现它成功连接到了这个库。

我们去运行一下

很好!!!

3.4.install综合示例二——不安装到系统目录版

我们上面那个例子都是安装到系统目录里面的。因为CMAKE_INSTALL_PREFIX默认就是系统目录/usr/local吗,现在我们可以通过临时修改CMAKE_INSTALL_PREFIX来达到不安装到系统目录的效果!!

项目结构

simple/
├── CMakeLists.txt
├── hello.cpp
├── hello.h
└── helloConfig.cmake.in

我们执行下面这一条指令即可完成

mkdir -p simple && cd simple && touch CMakeLists.txt hello.cpp hello.h helloConfig.cmake.in

hello.h

#pragma once
#include <string>
std::string hello();

hello.cpp

#include "hello.h"
std::string hello() { return "Hello, world!"; }

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(SimpleHello)

# 生成一个库
add_library(hello hello.cpp)

# 头文件路径
target_include_directories(hello PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
    $<INSTALL_INTERFACE:include>
)

# 安装库和头文件
install(TARGETS hello
    EXPORT helloTargets           # 导出集合的名字
    DESTINATION lib
)
install(FILES hello.h DESTINATION include)

# 导出 Targets(核心!)
install(EXPORT helloTargets
    FILE helloTargets.cmake       # 导出的文件名
    NAMESPACE Hello::             # 使用时名字前缀 Hello::
    DESTINATION lib/cmake/hello   # 安装路径
)

# 一个简单的 Config 文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/helloConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/helloConfig.cmake
    INSTALL_DESTINATION lib/cmake/hello
)
# 安装我们上面生成的 Config 文件
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/helloConfig.cmake
    DESTINATION lib/cmake/hello
)

helloConfig.cmake.in

@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/helloTargets.cmake")

安装

在 build/ 目录里执行:

cmake .. -DCMAKE_INSTALL_PREFIX=../install
cmake --build .
cmake --install .

现在 simple/install/ 里面会有

……

我们可以进去helloTargets.cmake里面看看,我截取了下面这一部分

这两句啥意思我可以给你们讲讲

1. add_library(Hello::hello STATIC IMPORTED)

  • add_library: CMake命令,用于定义一个库目标。

  • Hello::hello: 这是目标的名称。使用双冒号 :: 是一种约定俗成的命名方式,通常表示这是一个“导入目标”(IMPORTED target),类似于命名空间,让使用者一眼就能看出这不是一个在本项目内构建的普通目标。

  • STATIC: 表明这个库是一个静态库(文件通常是 .a 或 .lib 后缀)。如果是动态库,则会写 SHARED。

  • IMPORTED: 关键字,表明这个库不是由当前CMake项目编译生成的,而是从外部导入的(即已经存在于此项目之外的预编译库文件)。

这一行的作用:告诉CMake:“我这里有一个已经编译好的静态库,它的逻辑名字叫 Hello::hello,你现在要认得它。”

2. set_target_properties(Hello::hello PROPERTIES ...)

  • set_target_properties: CMake命令,用于设置一个目标的属性。

  • Hello::hello: 指定要设置属性的目标名称。

  • PROPERTIES: 后面跟着要设置的属性键值对。

3. INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"

  • 这是在设置目标的一个属性。

  • INTERFACE_INCLUDE_DIRECTORIES: 这是一个非常重要的属性。它定义了任何要使用这个 Hello::hello 库的目标(例如你的可执行文件)所需要包含的头文件目录。关键字 INTERFACE 意味着这个属性是传递给“消费者”的,而不是自己使用的。

  • "${_IMPORT_PREFIX}/include": 这是头文件目录的具体路径。_IMPORT_PREFIX 是一个变量,它应该在这段代码之前被定义,其值通常是这个预编译库的安装根目录。

    假设 _IMPORT_PREFIX 的值是 /usr/local,那么这一行的意思就是:“要使用 Hello::hello 库,需要在编译时添加 -I/usr/local/include 选项。”


5. 使用这个库

我们在刚刚的

新建一个 app/:

app/
├── main.cpp
└── CMakeLists.txt

我们一句话创建好

mkdir app && touch app/main.cpp app/CMakeLists.txt

main.cpp

#include "hello.h"
#include <iostream>
int main() {
    std::cout << hello() << std::endl;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(App)

find_package(hello CONFIG REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Hello::hello)

接下来我们执行下面这个

mkdir build && cd build && /
cmake .. -DCMAKE_PREFIX_PATH=../install && /
cmake --build .

运行:

很完美啊!!

3.4.1.代码片段1讲解

接下来我们仔细讲解这个

install(TARGETS hello
    EXPORT helloTargets           # 导出集合的名字
    DESTINATION lib
)

每一部分含义

install(TARGETS hello …)

  • 这里的 hello 是你用 add_library(hello hello.cpp) 定义的目标(target)。

  • install(TARGETS …) 表示:把构建出来的目标安装到指定目录。

也就是说,当你运行:

cmake --install .

它会把 build/libhello.a 或 build/libhello.so 复制到安装目录(prefix)下。


EXPORT helloTargets

  • 这个参数不会影响文件复制,而是告诉 CMake:
    “除了安装库文件,还要把这个库加入一个导出集(export set)里面,这个集合叫 helloTargets。”

  • 这个集合之后会在:

    install(EXPORT helloTargets …)
    

    里被写进一个 helloTargets.cmake 文件,供别的项目使用。

👉 简单理解:

  • install(TARGETS …) → 安装物理文件(.a 或 .so)。

  • EXPORT helloTargets → 记录下“我安装了一个叫 hello 的库”,为后续生成 cmake 配置文件做准备。


DESTINATION lib

  • 指定安装位置。

  • 例如你配置了:

    cmake .. -DCMAKE_INSTALL_PREFIX=../install
    cmake --install .
    
  • 那么库会被安装到:

    ../install/lib/libhello.a
    

    或者

    ../install/lib/libhello.so
    

整体作用

这段代码干了两件事:

  1. 把 hello 库文件 安装到 <prefix>/lib/ 下。

  2. 把 hello 库的目标信息 放进一个导出集 helloTargets,后面 install(EXPORT …) 会基于它生成 helloTargets.cmake。


3.4.2.代码片段2讲解

我们仔细看下面这个片段

install(EXPORT helloTargets
    FILE helloTargets.cmake
    NAMESPACE Hello::
    DESTINATION lib/cmake/hello
)
  • install(EXPORT helloTargets …)

    • EXPORT helloTargets 指的是一个 导出集(export set)。

    • 这个导出集在前面通过

      install(TARGETS hello EXPORT helloTargets …)
      

      已经把 hello 库加进去了。

    • 所以这里的意思是:把 helloTargets 导出集里的所有库(目前就一个 hello)写进一个 CMake 文件。


  • FILE helloTargets.cmake

    • 导出生成的文件名。

    • 安装之后会变成

      install/lib/cmake/hello/helloTargets.cmake
      
    • 这个文件里面就是一堆 add_library(Hello::hello IMPORTED) 之类的代码,描述你的库安装在哪里、头文件在哪儿。


  • NAMESPACE Hello::

    • 给导出的目标加命名空间前缀。

    • 如果不用命名空间,那么别人 find_package 后拿到的目标名字就是 hello。

    • 加了之后,别人使用时写的是:

      target_link_libraries(app PRIVATE Hello::hello)
      
    • 这样就不会和别的项目里的 hello 冲突。


  • DESTINATION lib/cmake/hello

    • 指定安装路径。

    • 安装后实际路径:${CMAKE_PREFIX_PATH}/lib/cmake/hello/

    • find_package(hello CONFIG REQUIRED) 的时候,CMake 就会去 CMAKE_PREFIX_PATH/lib/cmake/hello/ 下找 helloConfig.cmake,而那个文件会再 include(helloTargets.cmake)。


总结

  1. install(TARGETS hello EXPORT helloTargets …) → 把库放进导出集。

  2. install(EXPORT helloTargets FILE helloTargets.cmake …) → 把导出集写进文件,描述如何找到库。

  3. helloConfig.cmake → 是一个“入口文件”,它会 include(helloTargets.cmake)。

  4. find_package(hello CONFIG REQUIRED) → 先找到 helloConfig.cmake,再读 helloTargets.cmake,最后生成一个可用的 IMPORTED target:Hello::hello。

3.5.GNUInstallDirs

GNUInstallDirs 是一个 CMake 内置模块。它的核心目的是提供一系列CMake变量,这些变量定义了符合GNU编码标准和文件系统层次标准(FHS)的安装目录。


1. 它会定义哪些变量?

常见的有:

变量默认值(Linux)含义
CMAKE_INSTALL_BINDIRbin可执行文件安装目录
CMAKE_INSTALL_SBINDIRsbin系统可执行文件目录
CMAKE_INSTALL_LIBDIRlib 或 lib64库文件目录(自动区分 32/64 位)
CMAKE_INSTALL_INCLUDEDIRinclude头文件目录
CMAKE_INSTALL_DATAROOTDIRshare共享数据目录
CMAKE_INSTALL_DATADIRshare程序数据目录
CMAKE_INSTALL_DOCDIRshare/doc文档目录
CMAKE_INSTALL_MANDIRshare/manman 帮助文档目录

这些变量的值会拼接到 ${CMAKE_INSTALL_PREFIX} 之后。

比如默认前缀是 /usr/local:

  • CMAKE_INSTALL_LIBDIR 的实际安装路径其实是${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR },也就是/usr/local/lib

  • CMAKE_INSTALL_INCLUDEDIR 的实际安装路径其实是${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR  },也就是 /usr/local/include

2. 使用场景

你就可以这样写安装规则:

include(GNUInstallDirs)

install(TARGETS mylib
    DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(FILES mylib.h
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

这样就不会写死路径 lib 或 include,而是根据系统架构和平台自动选择合适的目录(比如在某些 64 位 Linux 系统上,库可能会安装到 lib64)。

写死安装路径的情况

❌ 没有 GNUInstallDirs 的写法

install(TARGETS mylib
    DESTINATION lib
)

这里我们直接写死了 DESTINATION lib,意思就是把编译好的静态库/动态库(libmylib.a、libmylib.so 等)安装到${CMAKE_INSTALL_PREFIX}/lib,在Linux里面默认是:/usr/local/lib

如果你在 32 位 Linux 系统上,这个路径通常没问题。

但是在很多 64 位 Linux 系统(例如 Fedora、RHEL、OpenSUSE),系统规范要求 64 位库要安装在 /usr/local/lib64,而不是 /usr/local/lib。

这样就会导致库装错位置,系统找不到,用户还得手动调整 LD_LIBRARY_PATH 或 CMAKE_PREFIX_PATH,非常不方便。

换句话说,这种写法 移植性差,只适合临时测试,不适合写成通用的安装规则。


✅ 使用 GNUInstallDirs 的写法

include(GNUInstallDirs)

install(TARGETS mylib
    DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

这里我们引入了 GNUInstallDirs 模块,它会自动定义一系列标准化变量,其中 CMAKE_INSTALL_LIBDIR 就代表库文件目录。

这个mylib默认安装到${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}里面

这样写的好处是:

  • 在 32 位 Linux 上,CMAKE_INSTALL_LIBDIR=lib,库会被装到 /usr/local/lib。

  • 在 64 位 Linux 上,CMAKE_INSTALL_LIBDIR=lib64,库会被装到 /usr/local/lib64,完全符合系统规范。

  • 在 Windows 上,如果 CMAKE_INSTALL_PREFIX="C:/Program Files/InstallExample",库就会安装到 C:/Program Files/InstallExample/lib。

你完全不用关心具体平台的差异,CMake 会帮你自动选择合适的目录。

3.6.DIRECTORY和FILES_MATCHING  PATTERN

在 CMake 中,install 命令的 DIRECTORY 形式用于安装整个目录及其内容,而 FILES_MATCHING 与 PATTERN 选项结合使用可以精确控制要安装目录中的哪些文件。下面我将详细解释这些功能。

install(DIRECTORY ...) 基本用法

install(DIRECTORY ...) 命令用于安装整个目录结构到目标位置。基本语法如下:

install(DIRECTORY dir_name
    DESTINATION destination_path
    [OPTIONS...]
)

这会复制 dir_name 目录及其所有子目录和文件到 ${CMAKE_INSTALL_PREFIX}/destination_path。

关键特性:

  1. 保留目录结构:源目录的层次结构会被完整保留在目标位置

  2. 支持通配符:可以使用 */ 等模式匹配多个目录

  3. 安装空目录:默认会安装空目录(除非使用 PATTERN 排除所有内容)

FILES_MATCHING 和 PATTERN 选项

当您不想安装目录中的所有文件,而只想安装符合特定模式的文件时,就需要使用 FILES_MATCHING 和 PATTERN 选项。

  • FILES_MATCHING 选项告诉 CMake 您想要基于某种模式匹配来筛选要安装的文件。它必须与 PATTERN 或 REGEX 选项一起使用。
  • PATTERN 选项用于指定一个类似 shell 的通配符模式,以匹配要包含或排除的文件。它可以与 EXCLUDE 选项结合使用来排除匹配的文件。

组合使用示例

示例 1️⃣:只安装 .h 和 .hpp 头文件

install(DIRECTORY include/ DESTINATION include
    FILES_MATCHING
    PATTERN "*.h"
    PATTERN "*.hpp"
)

👉 效果:include/ 及其子目录里面的所有 .h 和 .hpp 文件都会被安装,其它扩展名(如 .txt、.cpp)都不会安装。


示例 2️⃣:排除某个子目录

install(DIRECTORY include/ DESTINATION include
    FILES_MATCHING
    PATTERN "*.h"
    PATTERN "internal/*" EXCLUDE
)

👉 效果:安装include/及其子目录下面的所有 .h 文件,但 include/internal/ 目录下的所有文件会被忽略。


示例 3️⃣:只安装特定子目录中的文件

install(DIRECTORY include/ DESTINATION include
    FILES_MATCHING
    PATTERN "subdir/*.h"
)

👉 效果:只安装 include/subdir/ 目录里的所有 .h 文件,其他目录下的头文件不会被安装。


示例 4️⃣:安装 .h 文件,但排除以 _test.h 结尾的

install(DIRECTORY include/ DESTINATION include
    FILES_MATCHING
    PATTERN "*.h"
    PATTERN "*_test.h" EXCLUDE
)

👉 效果:安装所有 .h 文件,但像 foo_test.h 这种测试用头文件不会被安装。


示例 5️⃣:只安装 .txt 文档文件

install(DIRECTORY docs/ DESTINATION share/docs
    FILES_MATCHING
    PATTERN "*.txt"
)

👉 效果:只安装 docs/ 目录里的 .txt 文件,不会安装 .md 或其他文件。


重要注意事项

  1. 目录行为:

    • 如果目录路径以斜杠结尾(如 include/),则安装该目录的内容

    • 如果目录路径不以斜杠结尾(如 include),则安装整个目录本身

  2. 模式匹配:

    • PATTERN 使用类似 shell 的通配符匹配(*, ?, [])

    • 对于更复杂的模式,可以使用 REGEX 选项替代 PATTERN

  3. 排除与包含:

    • 默认情况下,PATTERN 是包含模式

    • 添加 EXCLUDE 关键字可以将其变为排除模式

    • 模式的评估顺序很重要,后面的模式可以覆盖前面的模式

  4. 空目录处理:

    • 如果使用 FILES_MATCHING 后某个目录中没有匹配的文件,该目录不会被创建

    • 如果想保留空目录结构,需要使用 FILE_PERMISSIONS 和 DIRECTORY_PERMISSIONS 选项

四.write_basic_package_version_file

⽣成⼀个包含版本信息和兼容性检查的包版本⽂件,使得其他项⽬能够轻松地使⽤你的包,并确 保版本兼容性。

write_basic_package_version_file(
     <file>
     VERSION <version>
     [COMPATIBILITY <compatibility>]
     [ARCH_INDEPENDENT]
 )

我们用一个非常简单的例子来解释 write_basic_package_version_file。它就像是给你发布的软件包办一张 “身份证”。

这张“身份证”只做一件事:告诉别人(也就是 find_package 命令)我这个包是谁,是什么版本的,跟你想要的版本匹不匹配。


超级简单的比喻

想象一下你要举办一个派对,需要核对客人的邀请函。

  1. 你的派对(你的软件包)有一个版本号:比如 “v2.1.0”。

  2. 你发布了一个邀请说明(write_basic_package_version_file):这个说明上写着:“我的派对是 v2.1.0 版的。只欢迎想参加 v2.* 系列派对的人(v2.0.0, v2.1.1, v2.3.0 都行),v1.x 或 v3.x 的客人就别来了。”

  3. 客人(其他项目)拿着邀请函(调用 find_package)来敲门:

    • find_package(MyParty 2.0 REQUIRED) -> ✅ 匹配成功!(他要 2.0,我是 2.1,属于同一个大版本,欢迎!)

    • find_package(MyParty 1.5 REQUIRED) -> ❌ 匹配失败!(他要 1.5,我是 2.1,大版本号对不上,不让进!)

    • find_package(MyParty REQUIRED) -> ✅ 匹配成功!(他没说具体要哪个版本,只要有就行,欢迎!)

write_basic_package_version_file 就是帮你生成那个“邀请说明”的命令。

4.1.示例

我们举个例子就明白了。注意我们现在这个项目是对上面那个项目进行补充的

项目结构

simple/
├── CMakeLists.txt
├── hello.cpp
├── hello.h
└── helloConfig.cmake.in

我们执行下面这一条指令即可完成

mkdir -p simple && cd simple && touch CMakeLists.txt hello.cpp hello.h helloConfig.cmake.in

hello.h

#pragma once
#include <string>
std::string hello();

hello.cpp

#include "hello.h"
std::string hello() { return "Hello, world!"; }

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(SimpleHello)

# 生成一个库
add_library(hello hello.cpp)

# 头文件路径
target_include_directories(hello PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
    $<INSTALL_INTERFACE:include>
)

# 安装库和头文件
install(TARGETS hello
    EXPORT helloTargets           # 导出集合的名字
    DESTINATION lib
)
install(FILES hello.h DESTINATION include)

# 导出 Targets(核心!)
install(EXPORT helloTargets
    FILE helloTargets.cmake       # 导出的文件名
    NAMESPACE Hello::             # 使用时名字前缀 Hello::
    DESTINATION lib/cmake/hello   # 安装路径
)

# 一个简单的 Config 文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/helloConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/helloConfig.cmake
    INSTALL_DESTINATION lib/cmake/hello
)

# 1. 定义包的版本号 (身份证上的信息)
set(SimpleHello_VERSION 1.5.0)

# 2. 制作身份证 (Version 文件)
write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/helloConfigVersion.cmake  # 【输出】必须是 <包名>ConfigVersion.cmake 格式
    VERSION ${SimpleHello_VERSION}                        # 身份证上写的版本号
    COMPATIBILITY SameMajorVersion                        # 兼容规则:主版本号相同即可
)

# 安装 Config 和 Version 文件
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/helloConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/helloConfigVersion.cmake
    DESTINATION lib/cmake/hello
)

helloConfig.cmake.in

@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/helloTargets.cmake")

安装

我们执行下面这些命令

mkdir build && cd build && \
cmake .. -DCMAKE_INSTALL_PREFIX=../install && \
cmake --build . && \
cmake --install .

现在 simple/install/ 里面会有

……

我们可以进去helloConfigVersion.cmake里面看看,我截取了下面这一部分

这些都是进行版本比对的代码


5. 使用这个库

我们在刚刚的

新建一个 app/:

app/
├── main.cpp
└── CMakeLists.txt

我们一句话创建好

mkdir app && touch app/main.cpp app/CMakeLists.txt

main.cpp

#include "hello.h"
#include <iostream>
int main() {
    std::cout << hello() << std::endl;
}

4.1.1.使用方的CMakeLists.txt(不加版本号)

cmake_minimum_required(VERSION 3.15)
project(App)

find_package(hello CONFIG REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Hello::hello)

接下来我们执行下面这个

mkdir build && cd build && /
cmake .. -DCMAKE_PREFIX_PATH=../install && /
cmake --build .

运行:

很完美啊!!

4.1.2.使用方的CMakeLists.txt(版本号完全匹配)

cmake_minimum_required(VERSION 3.15)
project(App)

find_package(hello 1.5.0 CONFIG REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Hello::hello)

接下来我们执行下面这个

rm -rf build && /
mkdir build && cd build && /
cmake .. -DCMAKE_PREFIX_PATH=../install && /
cmake --build .

运行:

很完美啊!!

4.1.3.使用方的CMakeLists.txt(主版本号相同,但是请求的版本比库的版本大)

cmake_minimum_required(VERSION 3.15)
project(App)

find_package(hello 1.5.1 CONFIG REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Hello::hello)

接下来我们执行下面这个

rm -rf build && /
mkdir build && cd build && /
cmake .. -DCMAKE_PREFIX_PATH=../install && /
cmake --build .

运行:

我们发现直接报错了?难道不是主版本号>=1即可吗?这里其实还藏有一点细节!!!

当我们在使用方的 CMakeLists.txt 中写下:

find_package(hello 1.5.1 CONFIG REQUIRED)

CMake 会按照顺序执行以下两步检查,任何一步失败都会导致 REQUIRED 包找不到而报错:

  1. 数值比较 (Version Check):检查找到的 hello的实际版本是否 大于等于 使用方请求的版本 (我们这里是1.5.1)。如果否,直接失败。也就是说如果hello的实际版本小于1.5.1,那么就会直接报错

  2. 兼容性规则检查 (Compatibility Check):如果第1步通过,再根据 MyLib 提供的兼容性规则,检查其实际版本是否与请求的版本“兼容”。也就是根据 write_basic_package_version_file 的COMPATIBILITY 参数来决定是否接受更高版本。如果规则判定不兼容,也会失败。

我们来仔细学习一下COMPATIBILITY 参数的常见取值

1. ExactVersion

  • 规则:必须 完全相同。

  • 如果请求 1.0.0,实际版本必须就是 1.0.0。

  • 任何多一个 patch(1.0.1)、少一个(0.9.9)、换个次版本(1.1.0)都不行。

例子(库版本=1.0.0):

  • find_package(hello 1.0.0) → ✅ 成功

  • find_package(hello 1.0.1) → ❌ 失败

  • find_package(hello 1.1.0) → ❌ 失败

👉 适合那种 ABI 不兼容、非常敏感的库。


2. SameMajorVersion

  • 规则:只要 主版本号相同 就算兼容。

  • 但是别忘了:必须先通过 >= 检查。

    • 比如实际库是 1.0.0,请求 1.5.0 → ❌(因为 1.0.0 < 1.5.0,在第一步检查中直接挂了)。

    • 实际库是 1.5.0,请求 1.0.0 → ✅(因为 1.5.0 ≥ 1.0.0 且主版本号相同)。

例子(库版本=1.5.0):

  • find_package(hello 1.0.0) → ✅ (主版本 1 相同,1.5 ≥ 1.0)

  • find_package(hello 1.5.0) → ✅ (完全匹配)

  • find_package(hello 2.0.0) → ❌ (主版本不同)

👉 适合那种“同一个大版本号内 API 稳定、兼容”的库。


3. AnyNewerVersion

  • 规则:只要实际库版本 ≥ 请求版本,就算通过。

  • 不会管主版本号是不是一样。

例子(库版本=2.0.0):

  • find_package(MyLib 1.0.0) → ✅ (2.0 ≥ 1.0,不管主版本变了没)

  • find_package(MyLib 2.0.0) → ✅ (正好相等)

  • find_package(MyLib 3.0.0) → ❌ (2.0 < 3.0)

👉 适合那种“向前兼容”的库,比如一个 CLI 工具,用户只要用新版本就不会坏。

 4.1.4.使用方的CMakeLists.txt(主版本号相同,但是请求的版本比库的版本小)

cmake_minimum_required(VERSION 3.15)
project(App)

find_package(hello 1.4.9 CONFIG REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Hello::hello)

接下来我们执行下面这个

rm -rf build && /
mkdir build && cd build && /
cmake .. -DCMAKE_PREFIX_PATH=../install && /
cmake --build .

运行:

因为我们的1.4.9和hello库的版本1.5.0的主版本号相同,且1.5.0>1.4.9,所以没有问题。

 4.1.5.使用方的CMakeLists.txt(主版本号不同,但是请求的版本比库的版本小)

cmake_minimum_required(VERSION 3.15)
project(App)

find_package(hello 0.4.9 CONFIG REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Hello::hello)

接下来我们执行下面这个

rm -rf build && /
mkdir build && cd build && /
cmake .. -DCMAKE_PREFIX_PATH=../install && /
cmake --build .

运行:

这个是因为0.4.9这个版本的主版本号0和我们hello库的主版本号1对不上。所以报错

hello库是 1.x.x,请求的是 0.x.x → ❌ 主版本不一致 → 报错。

 4.1.6.使用方的CMakeLists.txt(主版本号不同,但是请求的版本比库的版本大)

cmake_minimum_required(VERSION 3.15)
project(App)

find_package(hello 2.4.9 CONFIG REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Hello::hello)

接下来我们执行下面这个

rm -rf build && /
mkdir build && cd build && /
cmake .. -DCMAKE_PREFIX_PATH=../install && /
cmake --build .

运行:

这个是因为2.4.9这个版本的主版本号2和我们hello库的主版本号1对不上。所以报错

更多推荐