本期我们就来介绍CMake在动态库的场景下的应用
相关的代码已经上传至我的gitee:CMake 学习: CMake工具开发介绍仓库,虽然标记的是C++喜欢请点个赞谢谢
目录
[Linux 动态链接器 (ld.so) 的搜索宝图](#Linux 动态链接器 (ld.so) 的搜索宝图)
[CMake 的幕后策划:构建时的设定与安装时的"手术"](#CMake 的幕后策划:构建时的设定与安装时的“手术”)
[构建树 (Build Tree):为"原地可运行"而生](#构建树 (Build Tree):为“原地可运行”而生)
[安装树 (Install Tree):通往可部署性的"外科手术"](#安装树 (Install Tree):通往可部署性的“外科手术”)
[如何设计你的 RPATH 策略](#如何设计你的 RPATH 策略)
[CMake 的 RPATH 工具箱:从全局到局部的精细控制](#CMake 的 RPATH 工具箱:从全局到局部的精细控制)
[方式 1:配置阶段指定(最常用)](#方式 1:配置阶段指定(最常用))
[方式 2:安装阶段临时覆盖](#方式 2:安装阶段临时覆盖)
[方式 3:DESTDIR(传统 Linux 打包)](#方式 3:DESTDIR(传统 Linux 打包))
[find_package 的两种模式](#find_package 的两种模式)
[Module 模式(Find .cmake)](#Module 模式(Find .cmake))
[Config 模式( Config.cmake 或 -config.cmake)](#Config 模式( Config.cmake 或 -config.cmake))
[find_package 在 Config 模式下需要准备哪些文件?](#find_package 在 Config 模式下需要准备哪些文件?)
导入目标文件:calculator-targets.cmake(或其他命名)
版本文件:calculatorConfigVersion.cmake(推荐)
[CMake 如何找到这些文件?(搜索顺序)](#CMake 如何找到这些文件?(搜索顺序))
[同时构建静态库和动态库的 CMakeLists.txt](#同时构建静态库和动态库的 CMakeLists.txt)
[包配置模板 calculator-config.cmake.in](#包配置模板 calculator-config.cmake.in)
示例代码
我们以一个简单的计算器为例,实现加减乘除的功能,来讲解一下

目录
CMakeLists.txt
bash
# 指定CMake最低版本要求,确保使用的CMake特性在该版本及以上可用
cmake_minimum_required(VERSION 3.10)
# 定义项目名称和使用的编程语言
# VERSION参数指定项目版本号,后续可通过PROJECT_VERSION变量访问
project(Calculator
VERSION 1.0.0
LANGUAGES CXX)
# 设置C++标准为C++11,确保使用现代C++特性
# CMAKE_CXX_STANDARD是CMake内置变量,控制C++标准版本
set(CMAKE_CXX_STANDARD 11)
# 设置C++标准为"必需",如果编译器不支持指定标准则报错
# CMAKE_CXX_STANDARD_REQUIRED为ON表示必须支持该标准
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 禁止C++标准的扩展,确保代码严格符合标准
# CMAKE_CXX_EXTENSIONS为OFF表示不使用编译器特定的扩展
set(CMAKE_CXX_EXTENSIONS OFF)
# 设置输出目录,将编译产物(库文件和可执行文件)统一输出到项目根目录下的build子目录
# CMAKE_ARCHIVE_OUTPUT_DIRECTORY: 静态库输出目录
# CMAKE_LIBRARY_OUTPUT_DIRECTORY: 动态库输出目录
# CMAKE_RUNTIME_OUTPUT_DIRECTORY: 可执行文件输出目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# 启用测试功能,允许使用add_test()等测试相关命令
enable_testing()
# 添加子目录,CMake会进入每个子目录并执行其中的CMakeLists.txt
# calculator: 动态库目录
# app: 应用程序目录
# test: 测试目录
add_subdirectory(calculator)
add_subdirectory(app)
add_subdirectory(test)
app
main.cpp
cpp
#include <iostream>
#include <limits>
#include "calculator.h"
int main()
{
Calculator calc;
double num1, num2;
char operation;
std::cout << "简单计算器" << std::endl;
std::cout << "支持运算: + - * /" << std::endl;
while (true)
{
std::cout << "\n请输入第一个数字 (或输入 q 退出): ";
if (!(std::cin >> num1))
{
std::string input;
std::cin.clear();
std::cin >> input;
if (input == "q")
{
break;
}
std::cout << "无效输入,请重试。" << std::endl;
continue;
}
std::cout << "请输入运算符 (+, -, *, /): ";
std::cin >> operation;
std::cout << "请输入第二个数字: ";
if (!(std::cin >> num2))
{
std::cout << "无效输入,请重试。" << std::endl;
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
continue;
}
try
{
double result;
switch (operation)
{
case '+':
result = calc.add(num1, num2);
break;
case '-':
result = calc.subtract(num1, num2);
break;
case '*':
result = calc.multiply(num1, num2);
break;
case '/':
result = calc.divide(num1, num2);
break;
default:
std::cout << "无效的运算符!" << std::endl;
continue;
}
std::cout << "结果: " << num1 << " " << operation << " " << num2 << " = " << result << std::endl;
}
catch (const std::exception& e)
{
std::cout << "错误: " << e.what() << std::endl;
}
}
std::cout << "感谢使用计算器!" << std::endl;
return 0;
}
CMakeLists.txt
bash
# 定义可执行文件的源文件
# APP_SOURCES变量存储应用程序的所有源文件
set(APP_SOURCES
main.cpp
)
# 创建可执行文件目标
# calculator_app: 目标名称,也是生成的可执行文件名
# ${APP_SOURCES}: 源文件列表
add_executable(calculator_app ${APP_SOURCES})
# 链接库文件
# PRIVATE: 私有链接,仅calculator_app需要calculator库
# calculator: 要链接的库名称,对应add_library()创建的目标
target_link_libraries(calculator_app PRIVATE calculator)
# 设置输出目录(可选,根目录已统一设置)
# set_target_properties(calculator_app PROPERTIES
# RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
# )
# 安装规则
# 将可执行文件安装到bin目录
install(TARGETS calculator_app
RUNTIME DESTINATION bin
)
calculator
include
calculator.h
cpp
#pragma once
// 跨平台符号导出宏
// CALCULATOR_EXPORTS在编译动态库时定义,在导入时未定义
#ifdef CALCULATOR_EXPORTS
#ifdef _WIN32
#define CALCULATOR_API __declspec(dllexport)
#else
#define CALCULATOR_API __attribute__((visibility("default")))
#endif
#else
#ifdef _WIN32
#define CALCULATOR_API __declspec(dllimport)
#else
#define CALCULATOR_API
#endif
#endif
class CALCULATOR_API Calculator
{
public:
Calculator() noexcept=default;
~Calculator() noexcept=default;
double add(double a, double b) noexcept;
double subtract(double a, double b) noexcept;
double multiply(double a, double b) noexcept;
double divide(double a, double b) noexcept;
};
src
calculator.cpp
cpp
#include "calculator.h"
#include <stdexcept>
double Calculator::add(double a, double b) noexcept
{
return a + b;
}
double Calculator::subtract(double a, double b) noexcept
{
return a - b;
}
double Calculator::multiply(double a, double b) noexcept
{
return a * b;
}
double Calculator::divide(double a, double b) noexcept
{
if (b == 0.0)
{
throw std::runtime_error("Division by zero");
}
return a / b;
}
CMakeLists.txt
cpp
# 定义动态库的源文件
# CALCULATOR_SOURCES变量存储所有源文件路径,便于后续引用
set(CALCULATOR_SOURCES
src/calculator.cpp
)
# 定义动态库的头文件
# CALCULATOR_HEADERS变量存储所有头文件路径,用于IDE支持和安装
set(CALCULATOR_HEADERS
include/calculator.h
)
# 创建动态库目标
# calculator: 目标名称,也是库文件名(生成libcalculator.so或calculator.dll)
# SHARED: 指定生成共享库(动态库),对应STATIC生成静态库
# ${CALCULATOR_SOURCES}: 源文件列表,展开为实际的源文件路径
add_library(calculator SHARED ${CALCULATOR_SOURCES} ${CALCULATOR_HEADERS})
# 设置目标属性
# TARGET_NAME: 目标名称,这里是calculator
# PROPERTY: 属性名称
# VERSION: 库的版本号,用于生成带版本号的库文件(如libcalculator.so.1.0.0)
# SOVERSION: 库的API版本号,用于生成符号链接(如libcalculator.so.1)
set_target_properties(calculator PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1
)
# 定义预处理器宏,用于Windows平台的动态库符号导出
# CALCULATOR_EXPORTS: 宏名称,在头文件中用于判断是导出还是导入
# PRIVATE: 可见性,仅在当前目标内部可见
target_compile_definitions(calculator PRIVATE CALCULATOR_EXPORTS)
# 设置头文件包含路径
# PUBLIC: 公开可见,意味着链接此库的目标也能访问这些路径
# $<BUILD_INTERFACE:...>: 生成器表达式,指定构建时的接口路径
# $<INSTALL_INTERFACE:...>: 生成器表达式,指定安装后的接口路径
target_include_directories(calculator PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 设置C++标准
# PUBLIC: 公开可见,确保使用此库的目标也使用相同的标准
target_compile_features(calculator PUBLIC cxx_std_11)
# 安装规则
# 将库文件安装到指定目录
# LIBRARY: 动态库文件(Unix-like系统)
# DESTINATION: 安装目标目录,${CMAKE_INSTALL_PREFIX}/lib
# ARCHIVE: 静态库文件
# RUNTIME: Windows平台的DLL文件
install(TARGETS calculator
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
# 安装头文件
# FILES: 要安装的文件列表
# DESTINATION: 安装目标目录,${CMAKE_INSTALL_PREFIX}/include
install(FILES ${CALCULATOR_HEADERS} DESTINATION include)
test
test.cpp
cpp
#include "calculator.h"
#include <iostream>
#include <cmath>
#include <string>
#include <vector>
// 简单的测试框架实现
// 测试统计信息
struct TestStats {
int total = 0;
int passed = 0;
int failed = 0;
};
static TestStats g_stats;
static std::vector<std::string> g_failed_tests;
// 断言宏
#define EXPECT_TRUE(condition) \
do { \
g_stats.total++; \
if (!(condition)) { \
g_stats.failed++; \
std::cout << " [FAILED] " << #condition << " is false" << std::endl; \
std::cout << " at " << __FILE__ << ":" << __LINE__ << std::endl; \
} else { \
g_stats.passed++; \
} \
} while(0)
#define EXPECT_FALSE(condition) EXPECT_TRUE(!(condition))
#define EXPECT_EQ(expected, actual) \
do { \
g_stats.total++; \
if ((expected) != (actual)) { \
g_stats.failed++; \
std::cout << " [FAILED] Expected: " << (expected) \
<< ", Actual: " << (actual) << std::endl; \
std::cout << " at " << __FILE__ << ":" << __LINE__ << std::endl; \
} else { \
g_stats.passed++; \
} \
} while(0)
#define EXPECT_NE(expected, actual) \
do { \
g_stats.total++; \
if ((expected) == (actual)) { \
g_stats.failed++; \
std::cout << " [FAILED] Expected: " << (expected) \
<< " != " << (actual) << std::endl; \
std::cout << " at " << __FILE__ << ":" << __LINE__ << std::endl; \
} else { \
g_stats.passed++; \
} \
} while(0)
#define EXPECT_NEAR(expected, actual, abs_error) \
do { \
g_stats.total++; \
double diff = std::abs((expected) - (actual)); \
if (diff > (abs_error)) { \
g_stats.failed++; \
std::cout << " [FAILED] Expected: " << (expected) \
<< ", Actual: " << (actual) \
<< ", Diff: " << diff << std::endl; \
std::cout << " at " << __FILE__ << ":" << __LINE__ << std::endl; \
} else { \
g_stats.passed++; \
} \
} while(0)
#define EXPECT_THROW(statement, exception_type) \
do { \
g_stats.total++; \
bool caught = false; \
try { \
statement; \
} catch (const exception_type&) { \
caught = true; \
} catch (...) { \
g_stats.failed++; \
std::cout << " [FAILED] Wrong exception type thrown" << std::endl; \
std::cout << " at " << __FILE__ << ":" << __LINE__ << std::endl; \
break; \
} \
if (!caught) { \
g_stats.failed++; \
std::cout << " [FAILED] No exception thrown" << std::endl; \
std::cout << " at " << __FILE__ << ":" << __LINE__ << std::endl; \
} else { \
g_stats.passed++; \
} \
} while(0)
// 测试用例宏
#define TEST(test_name) \
void test_##test_name(); \
struct TestRegistrar_##test_name { \
TestRegistrar_##test_name() { \
register_test(#test_name, test_##test_name); \
} \
}; \
static TestRegistrar_##test_name g_test_registrar_##test_name; \
void test_##test_name()
// 测试注册
using TestFunc = void(*)();
struct TestCase {
std::string name;
TestFunc func;
};
static std::vector<TestCase> g_test_cases;
void register_test(const std::string& name, TestFunc func) {
g_test_cases.push_back({name, func});
}
// 运行所有测试
bool run_all_tests() {
std::cout << "\n========================================" << std::endl;
std::cout << "Running " << g_test_cases.size() << " test cases..." << std::endl;
std::cout << "========================================\n" << std::endl;
for (const auto& test_case : g_test_cases) {
std::cout << "[TEST] " << test_case.name << std::endl;
test_case.func();
std::cout << std::endl;
}
std::cout << "========================================" << std::endl;
std::cout << "Test Results:" << std::endl;
std::cout << " Total: " << g_stats.total << std::endl;
std::cout << " Passed: " << g_stats.passed << std::endl;
std::cout << " Failed: " << g_stats.failed << std::endl;
std::cout << "========================================\n" << std::endl;
return g_stats.failed == 0;
}
// ==================== 测试用例 ====================
TEST(AddOperation) {
Calculator calc;
EXPECT_NEAR(calc.add(2.0, 3.0), 5.0, 0.0001);
EXPECT_NEAR(calc.add(-1.0, 1.0), 0.0, 0.0001);
EXPECT_NEAR(calc.add(0.0, 0.0), 0.0, 0.0001);
}
TEST(SubtractOperation) {
Calculator calc;
EXPECT_NEAR(calc.subtract(5.0, 3.0), 2.0, 0.0001);
EXPECT_NEAR(calc.subtract(1.0, 1.0), 0.0, 0.0001);
EXPECT_NEAR(calc.subtract(-1.0, -1.0), 0.0, 0.0001);
}
TEST(MultiplyOperation) {
Calculator calc;
EXPECT_NEAR(calc.multiply(2.0, 3.0), 6.0, 0.0001);
EXPECT_NEAR(calc.multiply(-1.0, 1.0), -1.0, 0.0001);
EXPECT_NEAR(calc.multiply(0.0, 5.0), 0.0, 0.0001);
}
TEST(DivideOperation) {
Calculator calc;
EXPECT_NEAR(calc.divide(6.0, 3.0), 2.0, 0.0001);
EXPECT_NEAR(calc.divide(1.0, 2.0), 0.5, 0.0001);
EXPECT_NEAR(calc.divide(-1.0, 1.0), -1.0, 0.0001);
}
TEST(DivideByZero) {
Calculator calc;
EXPECT_THROW(calc.divide(1.0, 0.0), std::runtime_error);
}
TEST(EdgeCases) {
Calculator calc;
EXPECT_NEAR(calc.add(1e10, 1e10), 2e10, 1e5);
EXPECT_NEAR(calc.multiply(0.1, 0.1), 0.01, 0.0001);
EXPECT_NEAR(calc.divide(1.0, 3.0), 0.333333, 0.000001);
}
// ==================== 主函数 ====================
int main()
{
bool all_passed = run_all_tests();
return all_passed ? 0 : 1;
}
CMakeLists.txt
bash
# 启用测试功能
enable_testing()
# 定义测试源文件
# TEST_SOURCES变量存储所有测试源文件
set(TEST_SOURCES
test.cpp
)
# 创建测试可执行文件
# calculator_test: 测试可执行文件名
# ${TEST_SOURCES}: 测试源文件列表
add_executable(calculator_test ${TEST_SOURCES})
# 链接被测试的calculator库
# PRIVATE: 私有链接,仅测试可执行文件需要这些库
# calculator: 被测试的动态库
target_link_libraries(calculator_test PRIVATE calculator)
# 添加测试到CTest
# calculator_test: 测试名称
# COMMAND: 要执行的命令,这里运行calculator_test可执行文件
add_test(NAME calculator_test COMMAND calculator_test)
# 设置测试属性
# TIMEOUT: 设置测试超时时间(秒)
# LABELS: 为测试添加标签,便于分类和筛选
set_tests_properties(calculator_test PROPERTIES
TIMEOUT 30
LABELS "unit"
)
CMake的rPath机制与Linux动态库加载
相关概念
RPATH(Runtime search PATH)或 RUNPATH 就像硬编码在 ELF 可执行文件或动态库(.so)里的"藏宝图",告诉 ld.so 在程序启动时应该优先去哪些非标准路径寻找它依赖的 .so 文件。
在 Linux 中,它对应 ELF 文件的 .dynamic 段里的 DT_RPATH 条目。而现代的 RUNPATH,对应 DT_RUNPATH 条目。
RPATH (DT_RPATH) 与 RUNPATH (DT_RUNPATH) 的核心差别:一个影响动态库搜索顺序的关键不同。
这两种机制的主要区别在于它们与 LD_LIBRARY_PATH 环境变量的优先级关系,这正是理解许多运行时行为的关键:
-
RPATH (旧) :优先级高于
LD_LIBRARY_PATH。这意味着一旦在编译时设定了 RPATH,用户将无法在运行时通过LD_LIBRARY_PATH来覆盖它,这可能导致部署或调试时的死板。 -
RUNPATH (新/推荐) :优先级低于
LD_LIBRARY_PATH。这赋予了用户更大的灵活性,他们可以在不重新编译程序的情况下,通过设置环境变量来临时覆盖或调整库的加载路径,更方便调试和问题排查。
关键规则 :当一个可执行文件或库中同时 设置了 RPATH 和 RUNPATH 时,RPATH 会被忽略,动态链接器将以 RUNPATH 为准。
自 binutils 2.17 起,使用 -Wl,-rpath,<path> 链接选项时,大多数现代 Linux 发行版默认生成的是 RUNPATH。如果需要强制生成 RPATH,可使用 -Wl,--disable-new-dtags
Linux 动态链接器 (ld.so) 的搜索宝图
理解 ld.so 的搜索顺序是解决问题的关键。当程序启动时,ld.so 会按照以下顺序查找其依赖的 .so 文件:
-
直接依赖库的 RPATH :
ld.so会先检查导致本次查找的那个库 自身的 RPATH。比如,libbar.so的 RPATH 会优先于foo的 RPATH 被搜索。 -
可执行文件的 RPATH:然后检查可执行文件本身的 RPATH。
-
LD_LIBRARY_PATH环境变量 :如果此时还未找到,则搜索此变量中的路径。注意 :如果可执行文件是setuid/setgid的,出于安全原因,LD_LIBRARY_PATH会被忽略。 -
可执行文件的 RUNPATH :
RPATH被LD_LIBRARY_PATH"打断"后,轮到 RUNPATH 登场。 -
系统缓存文件 (
/etc/ld.so.cache) :由ldconfig命令生成,包含/etc/ld.so.conf中配置的所有路径。 -
系统默认路径 :最后,搜索
/lib和/usr/lib等硬编码的默认路径
CMake 的幕后策划:构建时的设定与安装时的"手术"
CMake 在 RPATH 管理中扮演着总导演的角色,它根据项目所处的构建树(Build Tree) 和安装树(Install Tree) 两个阶段,智能、自动地处理 RPATH 问题。
构建树 (Build Tree):为"原地可运行"而生
当你在 build/ 下运行 make 后,CMake 会自动为你的可执行文件和库设置 BUILD_RPATH,使其指向构建树内其他依赖库的实际路径。这让你能在构建目录下直接运行程序进行测试,无需任何环境变量。
-
BUILD_RPATH(目标属性):用于指定额外的构建期搜索路径。 -
CMAKE_BUILD_RPATH(全局变量) :为所有目标设置默认的BUILD_RPATH。
安装树 (Install Tree):通往可部署性的"外科手术"
当你执行 make install 时,CMake 知道这些文件的最终位置会发生变化,因此它会进行关键的一步:剥离(Stripping)或替换 RPATH。
这个过程是"外科手术式"的:CMake 会利用 chrpath 或 patchelf 等工具,直接编辑二进制文件,清除 构建树的 BUILD_RPATH 路径,替换 为你预先设定的安装位置的路径。你可以通过 CMAKE_NO_BUILTIN_CHRPATH 变量来控制此编辑行为。这个步骤确保了最终部署版本的干净、灵活和可移植性。
如何设计你的 RPATH 策略
理论说再多,最终还是要落在 CMakeLists.txt 里。请根据你的部署场景选择策略。
场景一:打造可重定位的便携软件包
推荐做法 :利用 $ORIGIN 或 @loader_path 等动态变量,将所有依赖库放在程序可执行文件能通过相对路径找到的地方。这是分发软件的最佳方式。
bash
# 假设安装后目录结构:
# /opt/myapp/
# ├── bin/myapp
# └── lib/libmyutils.so
# 全局设置,让所有目标都能在运行时找到 ../lib 下的库
set(CMAKE_INSTALL_RPATH "\$ORIGIN/../lib")
# 在 CMake 中,$ORIGIN 必须加反斜杠转义,防止被当成 CMake 变量
# 可选:如果还需在构建树中直接运行,可保留构建期 RPATH
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
# 安装规则
install(TARGETS myapp RUNTIME DESTINATION bin)
install(TARGETS myutils LIBRARY DESTINATION lib)
场景二:服务器固定路径部署
推荐做法:使用绝对路径。这种方式不灵活但最直接,适合最终的安装位置固定不变的场景。
bash
# 为特定 target 设置绝对路径
set_target_properties(myapp PROPERTIES
INSTALL_RPATH "/opt/myapp/lib"
)
如果需要精确控制且路径不标准,可启用此选项让 CMake 自动添加链接库的原生路径:
bash
set_target_properties(myapp PROPERTIES
INSTALL_RPATH_USE_LINK_PATH TRUE
)
CMake 的 RPATH 工具箱:从全局到局部的精细控制
CMake 提供了从全局到局部的多层 RPATH 控制选项:
-
CMAKE_INSTALL_RPATH(推荐全局变量):设置所有目标的默认安装 RPATH。 -
INSTALL_RPATH(推荐目标属性):为特定目标精确设置安装 RPATH,可覆盖全局设定。路径之间用分号分隔。 -
INSTALL_RPATH_USE_LINK_PATH(实用属性) :这是一个非常方便的选项。当设置为TRUE时,CMake 会自动 将所有链接时用到的库的路径追加到INSTALL_RPATH中。这在依赖关系复杂时尤其有用,能省去手动罗列路径的麻烦
诊断与修复工具:你的尖刀和针线
遇到问题时,以下工具能帮你快速定位和修复 RPATH:
readelf -d <binary>:查看 ELF 文件的动态段信息,是诊断 RPATH/RUNPATH 问题的不二选择。
bash
readelf -d myapp | grep -E 'RPATH|RUNPATH'
ldd <binary>:打印程序所需的共享库,以及它们是否能被找到,是发现缺失库最快速的手段。
bash
ldd myapp # 如果看到某行结尾是 "not found",就说明该库缺失
chrpath与patchelf:用于在编译后修改或删除二进制文件中的 RPATH。patchelf功能更强大,可以修改一个原本没有 RPATH 的二进制文件。
bash
# 使用 patchelf 修改或添加 RPATH
patchelf --set-rpath '/opt/new/lib/path' ./myapp
CMake安装动态库
指令
动态库(.so)在 CMake 的安装语境下,属于 LIBRARY 类型。一个库目标可能需要安装多种产物:
| 产物类型 | CMake 关键字 | Linux 下对应 | Windows 下对应 |
|---|---|---|---|
| 动态库(共享库) | LIBRARY |
libxxx.so |
xxx.dll (注意:.dll 归 RUNTIME 管) |
| 静态库 | ARCHIVE |
libxxx.a |
xxx.lib |
| 可执行文件 | RUNTIME |
可执行文件 | .exe 和 .dll |
| 头文件 | PUBLIC_HEADER/PRIVATE_HEADER |
.h |
.h |
我们已经写好了对应的CMake脚本(这里是一部分)
bash
# 安装规则
# 将库文件安装到指定目录
# LIBRARY: 动态库文件(Unix-like系统)
# DESTINATION: 安装目标目录,${CMAKE_INSTALL_PREFIX}/lib
# ARCHIVE: 静态库文件
# RUNTIME: Windows平台的DLL文件
install(TARGETS calculator
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
# 安装头文件
# FILES: 要安装的文件列表
# DESTINATION: 安装目标目录,${CMAKE_INSTALL_PREFIX}/include
install(FILES ${CALCULATOR_HEADERS} DESTINATION include)
如何安装到指定目录路径
你有三种方式控制最终安装的根目录,取决于你的使用场景:
方式 1:配置阶段指定(最常用)
bash
cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/calculator
cmake --build build
cmake --install build
所有 DESTINATION 中写的相对路径都会以 /opt/calculator 为基础。
方式 2:安装阶段临时覆盖
bash
cmake --install build --prefix /home/me/my_install
这会忽略配置时设的 CMAKE_INSTALL_PREFIX,直接用 /home/me/my_install。
方式 3:DESTDIR(传统 Linux 打包)
bash
cmake --install build --prefix /usr/local
# 等价于
DESTDIR=/tmp/staging cmake --install build
最终文件会落到 /tmp/staging/usr/local/... 下,打包 .deb/.rpm 时非常方便。
推荐 :始终在 CMakeLists.txt 里使用相对路径,外部通过 CMAKE_INSTALL_PREFIX 决定安装位置
其他项目使用
其他项目想要使用它,只能:
bash
# 很不优雅的用法
include_directories(/opt/calculator/include)
target_link_libraries(myapp /opt/calculator/lib/libcalculator.so)
每次都得记住头文件和库路径,而且没有版本检查,容易出错。
专业做法:安装 CMake 包配置文件,让使用者只需:
bash
find_package(calculator REQUIRED)
target_link_libraries(myapp calculator::calculator)
但是这需要额外的讲解
find_package
find_package 的两种模式
find_package(calculator) 会按以下优先级尝试查找:
Module 模式(Find<Package>.cmake)
CMake 会在 CMAKE_MODULE_PATH 中搜索一个名为 Findcalculator.cmake 的文件。
如果找到,就由这个脚本全权负责定位库的所有部分(通常用 find_path、find_library 等命令)。
这种模式通常由库的使用者或 CMake 自身提供 (例如 FindZLIB.cmake),库作者一般不需要关心。
Config 模式(<Package>Config.cmake 或 <package>-config.cmake)
这是库作者最需要负责的模式。CMake 会搜索由库安装后提供的配置文件:
-
<Package>Config.cmake(首推) -
<lowercase-package>-config.cmake(也常被支持)
在我们的场景中,你作为 calculator 库的开发者,要准备的就是这套配置。
find_package(calculator REQUIRED) 找到 calculatorConfig.cmake 后,就会加载它,从而获得所有的导入目标(如 calculator::calculator)。
find_package 在 Config 模式下需要准备哪些文件?
为了让 find_package(calculator) 顺利工作,你需要在安装目录中包含以下文件集合(正好对应你之前脚本生成的):
包配置文件:calculatorConfig.cmake
这是 find_package 最直接寻找的文件入口 。
通常由模板 calculatorConfig.cmake.in 通过 configure_package_config_file() 生成,安装到:
bash
<prefix>/lib/cmake/calculator/calculatorConfig.cmake
其典型内容:
bash
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/calculator-targets.cmake")
check_required_components(calculator)
-
@PACKAGE_INIT@会被展开为包初始化代码,实现可重定位(无论安装包被移动到哪里,路径都能自动适配)。 -
它负责加载包含导入目标的文件(下一项)。
导入目标文件:calculator-targets.cmake(或其他命名)
这个文件由 install(EXPORT ...) 命令生成,记录了:
-
导入目标
calculator::calculator的库文件位置(IMPORTED_LOCATION) -
接口头文件路径(
INTERFACE_INCLUDE_DIRECTORIES) -
编译特性、依赖的其它库......
-
所有编译器/链接器要求(例如 C++11 标准)
生成它的关键指令:
bash
install(EXPORT calculator-targets
FILE calculator-targets.cmake
NAMESPACE calculator::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/calculator
)
calculatorConfig.cmake 正是通过 include("${CMAKE_CURRENT_LIST_DIR}/calculator-targets.cmake") 把它拉进来的。
版本文件:calculatorConfigVersion.cmake(推荐)
虽然严格来说不是必须的,但没有版本文件,任何版本要求都会失败 。
如果你的 find_package 调用了:
bash
find_package(calculator 1.2 REQUIRED)
CMake 就必须找到版本配置文件,否则会报错。
它由 write_basic_package_version_file() 生成:
bash
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/calculatorConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
文件名必须是 <Config>.cmake 同名的后缀 Version.cmake,即 calculatorConfigVersion.cmake。
头文件与库文件本身
当然,光有 CMake 文件不够,真正的资产(.so、.h)也必须安装到位。这就是你安装命令里的基础操作:
bash
install(TARGETS calculator ...)
install(FILES ${CALCULATOR_HEADERS} DESTINATION include/calculator)
导入目标 calculator::calculator 中的路径会指向这里。
CMake 如何找到这些文件?(搜索顺序)
Config 模式下,CMake 会在以下位置寻找 <package>Config.cmake(以 calculator 为例):
-
用户指定的路径
-
-Dcalculator_DIR=/some/path(精确指向包含配置文件的目录) -
-DCMAKE_PREFIX_PATH=/opt/calculator(会在其下的lib/cmake/calculator中寻找) -
-DCMAKE_FRAMEWORK_PATH、-DCMAKE_APPBUNDLE_PATH(macOS 框架相关)
-
-
环境变量
export calculator_DIR=/opt/calculator/lib/cmake/calculator
-
系统默认路径
如
/usr/local/lib/cmake/calculator/、/usr/lib/cmake/calculator/等(由CMAKE_SYSTEM_PREFIX_PATH决定)。
最佳实践 :
安装路径采用 <prefix>/lib/cmake/<package>/ 结构,这样使用者只需设置 CMAKE_PREFIX_PATH=<prefix>,就能一次性找到所有库。你的脚本已经做到了这一点:
bash
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/calculator
如何让其他项目也能用find_apckage找到动静态库
改造目标
我们想要最终用户能这样使用:
bash
find_package(calculator REQUIRED)
# 想用动态库
target_link_libraries(my_app PRIVATE calculator::calculator_shared)
# 或想用静态库
target_link_libraries(my_app PRIVATE calculator::calculator_static)
两个目标都会自动带上正确的头文件路径、编译定义和链接依赖,互不冲突。
同时构建静态库和动态库的 CMakeLists.txt
下面是我为你写的完整 CMakeLists.txt(基于你上一个版本优化,并补全了同时导出的所有细节):
bash
cmake_minimum_required(VERSION 3.14)
project(calculator VERSION 1.0.0 LANGUAGES CXX)
include(GNUInstallDirs)
# 源文件
set(CALCULATOR_SOURCES src/calculator.cpp)
set(CALCULATOR_HEADERS include/calculator.h)
# ========== 动态库目标 ==========
add_library(calculator_shared SHARED ${CALCULATOR_SOURCES} ${CALCULATOR_HEADERS})
add_library(calculator::calculator_shared ALIAS calculator_shared)
set_target_properties(calculator_shared PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1
)
# 动态库符号导出宏(Windows)
target_compile_definitions(calculator_shared PRIVATE CALCULATOR_EXPORTS)
# 头文件包含路径(构建树和安装树)
target_include_directories(calculator_shared PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_features(calculator_shared PUBLIC cxx_std_11)
# ========== 静态库目标 ==========
add_library(calculator_static STATIC ${CALCULATOR_SOURCES} ${CALCULATOR_HEADERS})
add_library(calculator::calculator_static ALIAS calculator_static)
# 静态库通常没有 VERSION 和 SOVERSION,可选设置输出名
set_target_properties(calculator_static PROPERTIES
OUTPUT_NAME "calculator"
)
# 静态库需要定义宏告诉使用者当前是静态链接(Windows 下重要)
target_compile_definitions(calculator_static PUBLIC CALCULATOR_STATIC)
target_include_directories(calculator_static PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_features(calculator_static PUBLIC cxx_std_11)
# ================= 安装规则 =================
# 安装动态库和静态库,同时导出到一个导出集
install(TARGETS calculator_shared calculator_static
EXPORT calculator-targets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # 动态库 .so
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} # 静态库 .a
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # Windows DLL
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
# 安装头文件
install(FILES ${CALCULATOR_HEADERS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
# 导出导入目标文件
install(EXPORT calculator-targets
FILE calculator-targets.cmake
NAMESPACE calculator::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/calculator
)
# 生成版本文件
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/calculator-config-version.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
# 生成包配置文件
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/calculator-config.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/calculator-config.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/calculator
)
# 安装 Config 和 Version 文件
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/calculator-config.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/calculator-config-version.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/calculator
)
包配置模板 calculator-config.cmake.in
和之前的几乎一样,因为它要做的只是加载那个包含了所有导入目标的文件:
bash
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/calculator-targets.cmake")
check_required_components(calculator)
不需要额外处理,因为两个目标都已经在 calculator-targets.cmake 里定义好了。
头文件中如何处理静态/动态符号导出?
为了让同一个头文件同时支持静态库和动态库的正确链接(尤其在 Windows 下必须区分 __declspec(dllimport)),你需要在 calculator.h 中这样写:
cpp
#ifndef CALCULATOR_H
#define CALCULATOR_H
#if defined(_WIN32) || defined(__CYGWIN__)
#ifdef CALCULATOR_STATIC
#define CALCULATOR_API
#elif defined(CALCULATOR_EXPORTS)
#define CALCULATOR_API __declspec(dllexport)
#else
#define CALCULATOR_API __declspec(dllimport)
#endif
#else
#define CALCULATOR_API
#endif
class CALCULATOR_API Calculator {
public:
double add(double a, double b);
double sub(double a, double b);
};
#endif // CALCULATOR_H
解释:
-
当构建 动态库 时,
CALCULATOR_EXPORTS被私有地定义在calculator_shared上,所以类被标记为dllexport。 -
当构建 静态库 时,
CALCULATOR_STATIC被公开地传给使用方的编译定义,因此CALCULATOR_API为空(不需要导入导出)。 -
当使用者链接动态库 时,没有定义
CALCULATOR_EXPORTS,也没有CALCULATOR_STATIC,因此会触发dllimport。
使用者如何选择静态库还是动态库?
安装好库后,使用方只需:
bash
cmake_minimum_required(VERSION 3.14)
project(my_app)
find_package(calculator REQUIRED)
add_executable(my_app main.cpp)
# 链接动态库
target_link_libraries(my_app PRIVATE calculator::calculator_shared)
# 或者链接静态库
# target_link_libraries(my_app PRIVATE calculator::calculator_static)
这两个目标可以在同一个项目中同时被使用,只要不混链同一个符号即可。
更优雅的方式:提供一个默认选择
你还可以在 calculator-config.cmake.in 中提供一个开关,让用户通过变量选择默认目标:
bash
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/calculator-targets.cmake")
# 如果用户没有显式选择,可设置默认目标别名
if(NOT TARGET calculator::calculator)
if(DEFINED calculator_USE_STATIC_LIBS AND calculator_USE_STATIC_LIBS)
add_library(calculator::calculator ALIAS calculator::calculator_static)
else()
add_library(calculator::calculator ALIAS calculator::calculator_shared)
endif()
endif()
check_required_components(calculator)
这样用户就可以用熟悉的 calculator::calculator,并通过
bash
-Dcalculator_USE_STATIC_LIBS=ON
控制链接方式。
本期内容就到这里了,请点个赞谢谢
封面图自取:
