Conan 2.x 通用 conanfile.py 模板详解

发布时间: 更新时间: 总字数:3052 阅读时间:7m 作者:IP:上海 网址

本文提供一份可直接落地的 Conan 2.x 通用 conanfile.py 模板,按元数据、二进制配置(settings/options)、目录布局、依赖声明、生成器、构建、打包与 package_info 九个阶段逐段解析其职责与最佳实践;并深入讲解依赖版本通配符 zlib/1.3.Z 的语义,以及 embed/non_embed 等 package_id 模式如何决定二进制兼容性与复用策略,帮助 C++ 工程师快速构建规范化、可复现的包管理配方。

完整模板

python
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 流程中的调用顺序大致为:

text
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)

  1. configure() 中移除 fPIC 的时机:rm_safe("fPIC") 必须放在 configure() 而非 config_options() 中——前者在 options 解析后执行,可以看到 shared 的取值;后者执行时选项尚未被命令行/Profile 覆盖。
  2. exports_sources 与 conan create 的配合:exports_sources 中的文件会被快照进本地缓存,conan build 使用的是缓存副本而非工作目录中的源码——修改源码后忘记重新 conan install 是最常见的"改了没生效"问题。
  3. cpp_info.libs 只写库名:不要带 lib 前缀或 .a/.so 后缀,Conan 会按平台惯例自动补全;写错会导致下游 find_package 成功但链接失败。
  4. transitive_headers/transitive_libs 不是常态:仅在"下游确实需要看到依赖的头文件/库"(如 header-only 包装库)时手动声明;普通场景下依赖 Conan 从 package_type 自动推断即可,过度声明会放大依赖可见性、拖慢下游解析。

参考

  1. https://docs.conan.io/2/reference/conanfile/attributes.html
  2. https://docs.conan.io/2/reference/binary_model/dependencies.html
  3. https://docs.conan.io/2/reference/conanfile/methods/requirements.html
  4. https://docs.conan.io/2/reference/tools/cmake/cmaketoolchain.html