Conan 2.x 通用 conanfile.py 模板详解
本文提供一份可直接落地的 Conan 2.x 通用
conanfile.py模板,按元数据、二进制配置(settings/options)、目录布局、依赖声明、生成器、构建、打包与package_info九个阶段逐段解析其职责与最佳实践;并深入讲解依赖版本通配符zlib/1.3.Z的语义,以及 embed/non_embed 等package_id模式如何决定二进制兼容性与复用策略,帮助 C++ 工程师快速构建规范化、可复现的包管理配方。
完整模板
import os
from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps
from conan.tools.files import copy
class MyGenericProjectConan(ConanFile):
# ==========================================
# 1. 项目基础元数据 (Metadata)
# ==========================================
name = "my_project" # 包的名称
version = "1.0.0" # 包的版本号
license = "MIT" # 开源协议或授权声明
author = "Your Name <your.email@example.com>" # 作者信息
url = "https://github.com/your-repo/my_project" # 源码或 Conan 菜谱的托管地址
description = "这是一个通用的 Conan C++ 项目模板" # 项目简短描述
topics = ("cpp", "template", "cmake") # 标签,方便在包管理器中搜索
# ==========================================
# 2. 构建与二进制配置 (Configuration)
# ==========================================
# settings 影响构建的二进制哈希值(即不同的操作系统、编译器会导致生成不同的包)
settings = "os", "compiler", "build_type", "arch"
# options 允许在编译时以传参的方式开关某些特性
options = {
"shared": [True, False], # 是否编译为动态库
"fPIC": [True, False], # 是否生成位置无关代码 (Linux/macOS 下动态库通常需要)
}
# default_options 指定上述选项的默认值
default_options = {
"shared": False,
"fPIC": True,
}
# 指定哪些源文件需要与此 recipe 一起被打包(用于本地构建包)
# 通常会将 CMakeLists.txt 和源码目录包含进去
exports_sources = "CMakeLists.txt", "src/*", "include/*"
# ==========================================
# 3. 选项配置 (Config Options & Configure)
# ==========================================
def config_options(self):
"""
在解析选项之前调用。通常用于根据不同的操作系统动态移除不适用的选项。
"""
# Windows 系统原生不支持也不需要 fPIC,因此将其移除
if self.settings.os == "Windows":
del self.options.fPIC
def configure(self):
"""
在 settings 和 options 被解析后调用。
常用于处理设置或选项之间的冲突、依赖关系。
"""
# 如果当前构建的是动态库 (shared=True),则 fPIC 必然生效,
# 移除这个单独的选项,避免它参与 package_id 计算
if self.options.shared:
self.options.rm_safe("fPIC")
# 如果这是一个纯 C 项目,可以移除 C++ 运行时的设定:
# self.settings.rm_safe("compiler.libcxx")
# self.settings.rm_safe("compiler.cppstd")
# ==========================================
# 4. 目录结构布局 (Layout)
# ==========================================
def layout(self):
"""
定义项目的目录结构(源码在哪,构建目录在哪)。
使用 CMake 的项目直接调用 cmake_layout(self) 即可获得最佳实践的标准布局。
"""
cmake_layout(self)
# ==========================================
# 5. 依赖管理 (Requirements)
# ==========================================
def requirements(self):
"""
声明项目所需要的依赖包。
"""
# 示例:添加 zlib 和 fmt 作为依赖库
# self.requires("zlib/1.3.1")
# self.requires("fmt/10.2.1")
#
# transitive_headers=True: 将依赖的头文件包含路径(Include Directories)
# 传递给任何依赖"当前包"的下游包
# transitive_libs=True: 将依赖的二进制库文件(.lib, .a, .so 等)
# 传递给下游,使其参与最终的链接过程
# self.requires("zlib/1.3.1", transitive_headers=True, transitive_libs=True)
#
# 提示:若在类属性中声明了 package_type = "header-library",
# Conan 会自动为所有依赖加上这两个 trait,无需手动指定
pass
def build_requirements(self):
"""
声明仅在构建时需要的工具(如 CMake, Ninja, 单元测试框架等),
这些依赖不会包含在最终产物中。
"""
# 示例:强制使用指定版本的 CMake 和 GTest
# self.tool_requires("cmake/[>=3.24]")
# self.test_requires("gtest/1.14.0")
pass
# ==========================================
# 6. 生成器配置 (Generate)
# ==========================================
def generate(self):
"""
在实际调用构建系统(如 CMake)之前运行。
用于生成构建系统能读懂的工具链文件(Toolchain)和依赖查找文件(Dependencies)。
"""
# 生成第三方库的依赖配置文件 (例如为 zlib 生成 zlib-config.cmake)
deps = CMakeDeps(self)
deps.generate()
# 生成 CMake 工具链文件 (conan_toolchain.cmake),将 settings 传递给 CMake
tc = CMakeToolchain(self)
# 如果需要向 CMake 传递额外的预定义变量,可以在这里添加:
# tc.variables["MY_CUSTOM_CMAKE_VAR"] = True
tc.generate()
# ==========================================
# 7. 构建过程 (Build)
# ==========================================
def build(self):
"""
执行实际的编译指令。相当于在命令行手动执行 cmake .. && cmake --build .
"""
cmake = CMake(self)
cmake.configure() # 对应 `cmake -S . -B build`
cmake.build() # 对应 `cmake --build build`
# ==========================================
# 8. 打包产物 (Package)
# ==========================================
def package(self):
"""
定义哪些文件(如头文件、静态库、动态库、可执行文件)应该被提取到最终的 Conan 包中。
"""
# 通常直接调用 CMake 的 install 目标即可(前提是 CMakeLists.txt 中写好了 install 指令)
cmake = CMake(self)
cmake.install()
# 也可以手动复制所需的 License 文件到包目录中
# copy(self, "LICENSE", src=self.source_folder,
# dst=os.path.join(self.package_folder, "licenses"))
# ==========================================
# 9. 包信息配置 (Package Info)
# ==========================================
def package_info(self):
"""
告诉依赖此包的其他项目,在使用本包时需要链接哪些库文件,以及包含哪些目录。
"""
# 指定要链接的库名称(不需要写 .lib 或 .a 等后缀,假设生成的库名字叫 my_project)
self.cpp_info.libs = ["my_project"]
# 如果你的库包含额外的系统依赖项(如 Linux 下的 pthread 或 math 库),需要在此处追加:
# if self.settings.os in ["Linux", "FreeBSD"]:
# self.cpp_info.system_libs = ["m", "pthread"]
# cpp_info 默认会将 package_folder 里的 "include" 作为头文件目录,"lib" 作为库目录。
# 一般不需要特别修改,但可以根据需要添加宏定义等:
# self.cpp_info.defines.append("MY_PROJECT_ENABLED=1")各阶段执行顺序
上述方法在 conan install / conan create 流程中的调用顺序大致为:
config_options → configure → layout → requirements/build_requirements
→ generate → build → package → package_info理解这一顺序有助于定位"选项为什么被移除"、“依赖为什么没被传递"这类问题。
exports 与 exports_sources 的区别
conanfile.py 中与文件导出相关的两个属性极易混淆,核心差异在于文件何时被客户端下载:
| 属性 | 方法形式 | 文件去向 | 何时被下载 |
|---|---|---|---|
exports |
export() |
随配方一同存储,是配方的必需组成部分 | 消费者获取配方时始终下载——无论安装二进制包还是从源码构建(如 conandata.yml、LICENSE、配方自身要读取的辅助脚本) |
exports_sources |
export_sources() |
打包为 conan_sources.tgz,与配方一同上传 |
仅当没有匹配的二进制包、必须从源码编译(触发 conan build 环节)时才被客户端下载 |
选择依据很简单:配方"跑起来"必需的文件放 exports;只用于编译的源码放 exports_sources,让纯二进制消费场景免于下载源码,节省网络带宽与存储。
版本通配符与 package_id 模式
zlib/1.3.Z 的含义
在 Conan 2 的依赖影响 package_id 的二进制模型中,大写字母 Z 是一个版本占位符(Wildcard / Placeholder),表示"任意 patch 版本”:
- 所有
zlib的1.3.x补丁版本(1.3.1、1.3.2……)在消费者的package_id中都会被折叠为zlib/1.3.Z,即二进制被认为与任意1.3.x兼容——patch 变化无需重新编译消费者; - 一旦出现新的 minor 版本(如
zlib/1.4.0),占位符变为zlib/1.4.Z,产生新的package_id,触发消费者重建。
这背后是 Conan 2 的 package_id 模式机制,不同模式对应不同的折叠策略:
| 模式 | 适用场景(默认) | 折叠策略 |
|---|---|---|
embed_mode |
静态库被链入(app→static、shared→static、header-only),默认 full_mode |
任何变化(含版本、revision、package_id)都触发消费者重建 |
non_embed_mode |
非嵌入链接(shared→shared、static→static、app→shared),默认 minor_mode |
patch 折叠为 Z;minor/major 变化才触发重建 |
unknown_mode |
无法推断链接关系时,默认 semver_mode |
≥1.0 按 major 折叠,<1.0 按 patch 折叠 |
python_mode |
python_requires 消费者,默认 minor_mode |
同 minor_mode |
直白地说:non_embed 场景下消费者只是链接依赖而没有把依赖"复制进自己身体",所以 patch 级别的变化被安全忽略;而 embed 场景下依赖代码被编译进了消费者二进制,任何变化都必须重建。可在配方中通过 package_id_embed_mode、package_id_non_embed_mode 等类属性自定义这些模式。
[!TIP] 该机制是 Conan 2 实现"二进制复用"的核心:CI 上游只需为每个 minor 版本编译一次 zlib,下游所有项目即可直接复用这些二进制,不必跟随每个 patch 重新构建。
常见陷阱(Gotchas)
configure()中移除fPIC的时机:rm_safe("fPIC")必须放在configure()而非config_options()中——前者在 options 解析后执行,可以看到shared的取值;后者执行时选项尚未被命令行/Profile 覆盖。exports_sources与conan create的配合:exports_sources中的文件会被快照进本地缓存,conan build使用的是缓存副本而非工作目录中的源码——修改源码后忘记重新conan install是最常见的"改了没生效"问题。cpp_info.libs只写库名:不要带lib前缀或.a/.so后缀,Conan 会按平台惯例自动补全;写错会导致下游find_package成功但链接失败。transitive_headers/transitive_libs不是常态:仅在"下游确实需要看到依赖的头文件/库"(如 header-only 包装库)时手动声明;普通场景下依赖 Conan 从package_type自动推断即可,过度声明会放大依赖可见性、拖慢下游解析。
参考
- https://docs.conan.io/2/reference/conanfile/attributes.html
- https://docs.conan.io/2/reference/binary_model/dependencies.html
- https://docs.conan.io/2/reference/conanfile/methods/requirements.html
- https://docs.conan.io/2/reference/tools/cmake/cmaketoolchain.html