C/C++ Linux 开发教程(GCC + CMake) / CMake 包管理(find_package/FetchContent/Conan)
CMake 包管理(find_package/FetchContent/Conan)
现代 C++ 项目通常依赖大量第三方库。CMake 提供了多种方式管理这些依赖。本文讲解 find_package、FetchContent、Conan 和 vcpkg 等包管理方案。
find_package 工作原理
两种查找模式
CMake 的 find_package() 有两种工作模式:
| 模式 | 说明 | 配置文件 |
|---|---|---|
| Config 模式 | 库自己提供的配置文件 | <Lib>Config.cmake, <lib>-config.cmake |
| Module 模式 | CMake 内置或自定义的查找模块 | Find<Lib>.cmake |
# 查找流程:
# 1. Module 模式: 在 CMAKE_MODULE_PATH 中查找 FindXxx.cmake
# 2. Config 模式: 在标准路径中查找 XxxConfig.cmake
# 3. 两种模式都失败则报错
基本使用
# Config 模式(推荐)
find_package(OpenSSL REQUIRED)
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
# Module 模式
find_package(Threads REQUIRED)
# 可选依赖
find_package(CUDA QUIET)
if(CUDA_FOUND)
message(STATUS "CUDA support enabled")
add_definitions(-DHAVE_CUDA)
endif()
# 查找后使用
target_link_libraries(app PRIVATE
OpenSSL::SSL
OpenSSL::Crypto
Boost::filesystem
Boost::system
Threads::Threads
)
查找路径配置
# 指定查找路径
cmake -B build \
-DCMAKE_PREFIX_PATH="/opt/boost;/opt/openssl" \
-DOpenSSL_ROOT_DIR=/opt/openssl \
-DBoost_ROOT=/opt/boost
# 多个路径用分号分隔
cmake -B build -DCMAKE_PREFIX_PATH="/opt/lib1;/opt/lib2;/opt/lib3"
# CMakeLists.txt 中设置
list(APPEND CMAKE_PREFIX_PATH /opt/mylib)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
# 环境变量
# export CMAKE_PREFIX_PATH="/opt/lib1;/opt/lib2"
FindXxx.cmake 编写
完整的 Find 模块
# cmake/FindSQLite3.cmake
#
# 查找 SQLite3 库
#
# 定义以下变量:
# SQLite3_FOUND - 是否找到
# SQLite3_INCLUDE_DIR - 头文件路径
# SQLite3_LIBRARY - 库文件路径
# SQLite3_VERSION - 版本号
#
# 定义以下目标:
# SQLite3::SQLite3 - 导入目标
include(FindPackageHandleStandardArgs)
include(CheckIncludeFile)
# 查找头文件
find_path(SQLite3_INCLUDE_DIR
NAMES sqlite3.h
PATHS
/usr/local/include
/usr/include
${SQLite3_ROOT}/include
$ENV{SQLite3_ROOT}/include
)
# 查找库文件
find_library(SQLite3_LIBRARY
NAMES sqlite3
PATHS
/usr/local/lib
/usr/lib
${SQLite3_ROOT}/lib
$ENV{SQLite3_ROOT}/lib
)
# 获取版本号
if(SQLite3_INCLUDE_DIR AND EXISTS "${SQLite3_INCLUDE_DIR}/sqlite3.h")
file(STRINGS "${SQLite3_INCLUDE_DIR}/sqlite3.h"
SQLite3_VERSION_LINE
REGEX "^#define SQLITE_VERSION[ \t]+\"[0-9]+\\.[0-9]+\\.[0-9]+\"")
if(SQLite3_VERSION_LINE)
string(REGEX REPLACE "^#define SQLITE_VERSION[ \t]+\"([0-9]+\\.[0-9]+\\.[0-9]+)\".*"
"\\1" SQLite3_VERSION "${SQLite3_VERSION_LINE}")
endif()
endif()
# 检查是否找到所有必需的组件
find_package_handle_standard_args(SQLite3
REQUIRED_VARS SQLite3_LIBRARY SQLite3_INCLUDE_DIR
VERSION_VAR SQLite3_VERSION
)
# 创建导入目标
if(SQLite3_FOUND AND NOT TARGET SQLite3::SQLite3)
add_library(SQLite3::SQLite3 UNKNOWN IMPORTED)
set_target_properties(SQLite3::SQLite3 PROPERTIES
IMPORTED_LOCATION "${SQLite3_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${SQLite3_INCLUDE_DIR}"
)
# 平台相关依赖
if(UNIX AND NOT APPLE)
set_property(TARGET SQLite3::SQLite3 APPEND PROPERTY
INTERFACE_LINK_LIBRARIES dl pthread
)
endif()
endif()
mark_as_advanced(SQLite3_LIBRARY SQLite3_INCLUDE_DIR)
# 使用自定义的 Find 模块
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
find_package(SQLite3 3.35 REQUIRED)
target_link_libraries(app PRIVATE SQLite3::SQLite3)
查找系统库的 Find 模块
# cmake/FindEpoll.cmake
include(CheckIncludeFile)
include(CheckSymbolExists)
check_include_file("sys/epoll.h" HAVE_EPOLL_H)
if(HAVE_EPOLL_H)
check_symbol_exists(epoll_create "sys/epoll.h" HAVE_EPOLL_CREATE)
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Epoll
DEFAULT_MSG
HAVE_EPOLL_H
HAVE_EPOLL_CREATE
)
FetchContent 外部依赖管理
FetchContent 基础
include(FetchContent)
# 声明外部依赖
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
GIT_SHALLOW TRUE # 浅克隆,节省时间
)
# 下载并添加到项目
FetchContent_MakeAvailable(googletest)
# 使用
add_executable(my_test test.cpp)
target_link_libraries(my_test PRIVATE GTest::gtest_main)
FetchContent 参数详解
FetchContent_Declare(
mylib
# Git 方式
GIT_REPOSITORY https://github.com/user/mylib.git
GIT_TAG v2.0.0 # tag 或 commit hash
GIT_SHALLOW TRUE # 浅克隆
GIT_PROGRESS TRUE # 显示进度
# 或 URL 方式(推荐,更稳定)
# URL https://github.com/user/mylib/archive/v2.0.0.tar.gz
# URL_HASH SHA256=abc123...
# 可选配置
PATCH_COMMAND git apply ${CMAKE_SOURCE_DIR}/patches/mylib.patch
UPDATE_DISCONNECTED TRUE # 配置后不再更新
)
FetchContent 高级用法
include(FetchContent)
# 预先检查是否可用(不下载)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
FetchContent_GetProperties(fmt)
if(NOT fmt_POPULATED)
FetchContent_Populate(fmt)
add_subdirectory(${fmt_SOURCE_DIR} ${fmt_BINARY_DIR})
endif()
# 覆盖已有库的选项
set(FMT_DOC OFF CACHE BOOL "" FORCE)
set(FMT_TEST OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(fmt)
FetchContent 常用第三方库模板
include(FetchContent)
# Google Test
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
# spdlog
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.13.0
)
# nlohmann/json
FetchContent_Declare(nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
)
# fmt
FetchContent_Declare(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
# 一次性下载所有依赖
FetchContent_MakeAvailable(googletest spdlog nlohmann_json fmt)
Conan2 包管理器集成
Conan2 基础
# 安装 Conan
pip install conan
# 检测配置
conan profile detect
# 查看默认配置
conan profile show default
conanfile.txt
# conanfile.txt — 最简单的依赖声明
[requires]
openssl/3.2.0
zlib/1.3.1
fmt/10.2.1
spdlog/1.13.0
[generators]
CMakeDeps # 生成 xxx-config.cmake
CMakeToolchain # 生成工具链文件
[options]
openssl/*:shared=True
conanfile.py(推荐)
# conanfile.py — 更灵活的依赖声明
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
from conan.tools.files import copy
import os
class MyAppConan(ConanFile):
name = "myapp"
version = "1.0.0"
# 元数据
license = "MIT"
description = "My application"
url = "https://github.com/user/myapp"
# 设置
settings = "os", "compiler", "build_type", "arch"
# 选项
options = {
"shared": [True, False],
"fPIC": [True, False],
"with_openssl": [True, False],
}
default_options = {
"shared": False,
"fPIC": True,
"with_openssl": True,
}
# 依赖
requires = [
"fmt/10.2.1",
"spdlog/1.13.0",
]
# 构建依赖
tool_requires = ["cmake/3.28.1"]
# 导出的源文件
exports_sources = "CMakeLists.txt", "src/*", "include/*"
def config_options(self):
if self.settings.os == "Windows":
self.options.rm_safe("fPIC")
def configure(self):
if self.options.shared:
self.options.rm_safe("fPIC")
def requirements(self):
if self.options.with_openssl:
self.requires("openssl/3.2.0")
def layout(self):
cmake_layout(self)
def generate(self):
tc = CMakeToolchain(self)
tc.generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
copy(self, "LICENSE", src=self.source_folder,
dst=os.path.join(self.package_folder, "licenses"))
cmake = CMake(self)
cmake.install()
def package_info(self):
self.cpp_info.libs = ["myapp"]
Conan + CMake 集成
# 安装依赖
conan install . --output-folder=build --build=missing
# 配置项目(使用 Conan 生成的工具链文件)
cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release
# 构建
cmake --build build --parallel
# 一步完成(Conan 2 推荐方式)
conan build . --build-folder=build
# CMakeLists.txt — 自动找到 Conan 安装的库
cmake_minimum_required(VERSION 3.15)
project(MyApp)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
if(WITH_OPENSSL)
find_package(OpenSSL REQUIRED)
endif()
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE
fmt::fmt
spdlog::spdlog
$<TARGET_NAME_IF_EXISTS:OpenSSL::SSL>
)
Conan Profile 管理
# 创建交叉编译 profile
cat > profiles/arm64-linux << 'EOF'
[settings]
os=Linux
os_build=Linux
arch=armv8
arch_build=x86_64
compiler=gcc
compiler.version=12
compiler.libcxx=libstdc++11
build_type=Release
[options]
[env]
CC=aarch64-linux-gnu-gcc
CXX=aarch64-linux-gnu-g++
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
EOF
# 使用 profile 安装依赖
conan install . -pr profiles/arm64-linux --output-folder=build-arm64
vcpkg 集成
vcpkg 基础
# 安装 vcpkg
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg && ./bootstrap-vcpkg.sh
# 全局安装库
./vcpkg install fmt spdlog nlohmann-json
# 查看已安装
./vcpkg list
vcpkg + CMake 集成
# 使用工具链文件
cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake \
-DVCPKG_TARGET_TRIPLET=x64-linux
cmake --build build
# CMakeLists.txt — 正常使用 find_package
cmake_minimum_required(VERSION 3.15)
project(MyApp)
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE
fmt::fmt
spdlog::spdlog
nlohmann_json::nlohmann_json
)
vcpkg.json 清单模式
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "myapp",
"version-string": "1.0.0",
"dependencies": [
{
"name": "fmt",
"version>=": "10.0.0"
},
"spdlog",
"nlohmann-json"
],
"overrides": [
{
"name": "fmt",
"version": "10.2.1"
}
],
"features": {
"openssl": {
"description": "Enable TLS support",
"dependencies": ["openssl"]
}
}
}
系统库 vs 第三方库策略
依赖策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 系统库 | 轻量、安全更新 | 版本不可控 | 稳定的生产环境 |
| FetchContent | 无额外工具 | 编译时间长 | 简单项目 |
| Conan/vcpkg | 版本锁定、二进制缓存 | 额外工具依赖 | 复杂项目 |
| 源码子模块 | 完全可控 | 维护成本高 | 核心依赖 |
推荐策略
# 策略 1: 优先系统库,备选 FetchContent
find_package(ZLIB QUIET)
if(NOT ZLIB_FOUND)
message(STATUS "ZLIB not found, fetching...")
include(FetchContent)
FetchContent_Declare(zlib
URL https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz
)
FetchContent_MakeAvailable(zlib)
endif()
# 策略 2: 选项控制
option(USE_SYSTEM_ZLIB "Use system zlib" OFF)
if(USE_SYSTEM_ZLIB)
find_package(ZLIB REQUIRED)
else()
include(FetchContent)
FetchContent_Declare(zlib ...)
FetchContent_MakeAvailable(zlib)
endif()
依赖版本锁定
FetchContent 版本锁定
# 使用 commit hash 锁定精确版本
FetchContent_Declare(
mylib
GIT_REPOSITORY https://github.com/user/mylib.git
GIT_TAG a1b2c3d4e5f6... # 精确到 commit
)
# 使用 URL + hash 锁定
FetchContent_Declare(
mylib
URL https://github.com/user/mylib/archive/v2.0.0.tar.gz
URL_HASH SHA256=abc123def456...
)
CMake 预设锁定
// CMakePresets.json — 锁定依赖版本
{
"version": 6,
"configurePresets": [
{
"name": "default",
"cacheVariables": {
"FETCHCONTENT_FULLY_DISCONNECTED": "ON",
"FETCHCONTENT_UPDATES_DISCONNECTED": "ON"
}
}
]
}
Conan 版本锁定
# 生成 lockfile
conan lock create conanfile.py
# 使用 lockfile 安装
conan install . --lockfile=conan.lock
# 更新 lockfile
conan lock update conanfile.py
私有仓库管理
Git 私有仓库
# 使用 SSH 协议访问私有仓库
FetchContent_Declare(
mylib
GIT_REPOSITORY [email protected]:myorg/mylib-private.git
GIT_TAG v1.0.0
)
# 使用 token 访问
FetchContent_Declare(
mylib
GIT_REPOSITORY https://${GITHUB_TOKEN}@github.com/myorg/mylib-private.git
GIT_TAG v1.0.0
)
Conan 私有仓库
# 添加 Artifactory 私有仓库
conan remote add private https://conan.example.com/api/conan/conan
# 登录
conan remote login private myuser
# 上传包到私有仓库
conan create . --user=mycompany --channel=stable
conan upload mylib/1.0.0@mycompany/stable --remote=private
# 从私有仓库安装
conan install . -r=private
内部镜像
# 使用内部 Git 镜像
cmake -B build -DFETCHCONTENT_BASE_DIR=/opt/cmake-cache
# 使用内部 Conan 镜像
conan remote add internal https://conan.internal.company.com
包管理最佳实践
1. 最小化依赖
# ❌ 避免:依赖整个 Boost
find_package(Boost REQUIRED)
# ✅ 推荐:只依赖需要的组件
find_package(Boost REQUIRED COMPONENTS filesystem system)
2. 使用导入目标
# ❌ 避免:手动管理路径
include_directories(${ZLIB_INCLUDE_DIRS})
link_libraries(${ZLIB_LIBRARIES})
# ✅ 推荐:使用导入目标
target_link_libraries(app PRIVATE ZLIB::ZLIB)
3. 条件化可选依赖
# 可选的 TLS 支持
option(ENABLE_TLS "Enable TLS support" ON)
if(ENABLE_TLS)
find_package(OpenSSL REQUIRED)
target_compile_definitions(app PRIVATE ENABLE_TLS)
target_link_libraries(app PRIVATE OpenSSL::SSL)
endif()
4. 版本兼容性检查
find_package(Python 3.8 REQUIRED COMPONENTS Interpreter)
if(Python_VERSION VERSION_LESS "3.8")
message(FATAL_ERROR "Python 3.8+ is required")
endif()
5. 依赖传递性控制
# PRIVATE: 仅当前目标使用(不传递)
target_link_libraries(app PRIVATE fmt::fmt)
# PUBLIC: 当前目标和依赖者都使用
target_link_libraries(mylib PUBLIC OpenSSL::SSL)
# INTERFACE: 仅传递给依赖者(如 header-only 库)
target_link_libraries(header_lib INTERFACE nlohmann_json::nlohmann_json)
⚠️ 注意点
- FetchContent 重复下载:使用
FETCHCONTENT_UPDATES_DISCONNECTED ON避免每次配置都更新 - Conan 工具链文件:必须使用 Conan 生成的
conan_toolchain.cmake - vcpkg 三元组:确保 triplet 与目标平台匹配
- find_package 版本:系统库版本可能不满足最低要求
- 依赖冲突:多个依赖库可能要求不同版本的同一个库
💡 提示
- 加速 FetchContent:使用
GIT_SHALLOW TRUE浅克隆 - Conan 二进制缓存:使用
conan install --build=missing只编译缺失的包 - vcpkg 清单模式:推荐使用
vcpkg.json而非命令行安装 - 混合管理:核心依赖用 FetchContent,常用库用 Conan/vcpkg
- CI 依赖缓存:缓存
~/.conan2或 vcpkg 的installed/目录
工程场景
场景 1:新项目依赖配置
# 推荐: Conan + CMake 预设
# conanfile.py 定义依赖
# CMakePresets.json 定义构建配置
# CI 只需:
# conan install . -of=build --build=missing
# cmake --preset default
# cmake --build --preset default
场景 2:迁移遗留项目到 CMake 包管理
# 步骤 1: 替换手动 -I/-L 路径为 find_package
# 步骤 2: 使用导入目标替代全局设置
# 步骤 3: 添加 Conan/vcpkg 管理第三方库
# 步骤 4: 更新 CI 流水线
场景 3:容器化构建环境
# Dockerfile — 带依赖管理的构建环境
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
build-essential cmake ninja-build \
python3-pip
RUN pip3 install conan
WORKDIR /app
COPY conanfile.py .
RUN conan install . -of=build --build=missing \
-s compiler=gcc -s compiler.version=11 \
-s build_type=Release
COPY . .
RUN cmake -B build \
-G Ninja \
-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
&& cmake --build build