Conan 实战:创建并发布自己的包

原文:Conan Documentation — Release 2.29.0


第一步:创建你的第一个 Conan 包

使用 conan new 创建项目

$ conan new cmake_lib -d name=hello -d version=1.0

输出:

File created: CMakeLists.txt
File created: conanfile.py
File created: include/hello.h
File created: src/hello.cpp
File created: test_package/CMakeLists.txt
File created: test_package/conanfile.py
File created: test_package/src/example.cpp

项目结构:

.
├── CMakeLists.txt          # 普通 CMake 构建文件,不含 Conan 相关内容
├── conanfile.py            # Conan 配方文件——核心文件
├── include/
│   └── hello.h             # 库的头文件
├── src/
│   └── hello.cpp           # 库的实现
└── test_package/
    ├── CMakeLists.txt      # 测试包的 CMake 文件
    ├── conanfile.py        # 测试包的配方
    └── src/
        └── example.cpp     # 测试代码

解析 conanfile.py

from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps

class helloRecipe(ConanFile):
    name = "hello"
    version = "1.0"

    # 元数据(可选但推荐)
    license = "<Put the package license here>"
    author = "<Put your name here> <And your email here>"
    url = "<Package recipe repository url here, for issues about the package>"
    description = "<Description of hello package here>"
    topics = ("<Put some tag here>", "<here>", "<and here>")

    # 二进制配置
    settings = "os", "compiler", "build_type", "arch"
    options = {"shared": [True, False], "fPIC": [True, False]}
    default_options = {"shared": False, "fPIC": True}

    exports_sources = "CMakeLists.txt", "src/*", "include/*"

    def config_options(self):
        if self.settings.os == "Windows":
            del self.options.fPIC

    def layout(self):
        cmake_layout(self)

    def generate(self):
        deps = CMakeDeps(self)
        deps.generate()
        tc = CMakeToolchain(self)
        tc.generate()

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def package(self):
        cmake = CMake(self)
        cmake.install()

    def package_info(self):
        self.cpp_info.libs = ["hello"]

各部分作用:

部分作用
name + version包的唯一标识,如 hello/1.0
settings项目级配置(OS、编译器、架构、构建类型)
options包级选项(shared/fPIC 等),可在 recipe 中设置默认值
exports_sources要将哪些文件复制到 Conan 缓存中作为源码
config_options()根据平台调整可用选项(如 Windows 无 fPIC)
layout()定义构建目录结构
generate()生成构建所需的辅助文件(工具链、依赖信息)
build()执行构建
package()将构建产物(头文件、库文件)打包到 Conan 包中
package_info()告诉消费者如何链接这个包

构建包:conan create

$ conan create .

完整输出(节选):

======== Exporting recipe to the cache ========
hello/1.0: Exporting package recipe
...
hello/1.0: Exported: hello/1.0#dcbfe21e5250264b26595d151796be70 (2024-03-04 17:52:39 UTC)

======== Installing packages ========
-------- Installing package hello/1.0 (1 of 1) --------
hello/1.0: Building from source
hello/1.0: Calling build()
...
hello/1.0: Package '9bdee485ef71c14ac5f8a657202632bdb8b4482b' built

======== Testing the package: Building ========
...
[100%] Built target example
======== Testing the package: Executing test ========
hello/1.0 (test package): RUN: ./example
hello/1.0: Hello World Release!

conan create 做了什么:

  1. Export(导出):把 conanfile.py 和源代码复制到 Conan 缓存(~/.conan2/
  2. Install(安装):解析依赖(本项目无外部依赖)
  3. Build(构建):从源码构建,生成二进制包
  4. Test(测试):进入 test_package/,验证包可以被正常消费

构建不同配置

# Debug 版本
$ conan create . -s build_type=Debug
...
hello/1.0: Hello World Debug!

# 共享库版本
$ conan create . -o hello/1.0:shared=True
...
hello/1.0: Hello World Release!

查看缓存的包

$ conan list "hello/1.0:*"
Local Cache
hello
hello/1.0
  revisions
    dcbfe21e5250264b26595d151796be70
    packages
      2505f7ebb5a4cca156b2d6b8534f415a4a48b5c9   # shared=True 的包
      39f48664f195e0847f59889d8a4cdfc6bca84bf1   # Release 静态库
      814ddaac84bc84f3595aa076660133b88e49fb11   # Debug 静态库

每个不同的配置 → 不同的 package_id(哈希值)→ 缓存中不同的二进制包。


第二步:处理包源码(source() 方法)

方式 A:从 ZIP 文件获取源码

替换 exports_sourcessource() 方法:

from conan.tools.files import get

class helloRecipe(ConanFile):
    ...
    def source(self):
        get(self, "https://github.com/conan-io/libhello/archive/refs/heads/main.zip",
            strip_root=True)

构建输出(注意下载和解压过程):

-------- Installing packages ----------
hello/1.0: Calling source() in /Users/user/.conan2/p/0fcb5ffd11025446/s/.
Downloading update_source.zip
hello/1.0: Unzipping 3.7KB
Unzipping 100 %
hello/1.0: Copying sources to build folder
...
hello/1.0: Hello World Release!

方式 B:从 Git 仓库获取源码

from conan.tools.scm import Git

class helloRecipe(ConanFile):
    ...
    def source(self):
        git = Git(self)
        git.clone(url="https://github.com/conan-io/libhello.git", target=".")
        git.checkout("v1.0")  # 必须指定 tag 或 commit!

⚠️ 重要: source() 方法必须使用不可变的引用(tag 或 commit hash)。使用 HEAD 分支是不允许的,会导致缓存不一致和难以排查的构建问题。

使用 conandata.yml(推荐方式)

创建 conandata.yml

sources:
  "1.0":
    url: "https://github.com/conan-io/libhello/archive/refs/heads/main.zip"
    sha256: "7bc71c682895758a996ccf33b70b91611f51252832b01ef3b4675371510ee466"
    strip_root: true
  "1.1":
    url: ...
    sha256: ...

然后在 conanfile.py 中引用:

def source(self):
    get(self, **self.conan_data["sources"][self.version])

这样更新版本时只需修改 conandata.yml,无需改动 conanfile.py


第三步:给包添加依赖(requirements() 方法)

以给 hello 库添加 fmt(彩色输出)为例:

from conan.tools.build import check_max_cppstd, check_min_cppstd

class helloRecipe(ConanFile):
    ...
    generators = "CMakeDeps"

    def validate(self):
        check_min_cppstd(self, "11")
        check_max_cppstd(self, "20")

    def requirements(self):
        self.requires("fmt/8.1.1")

    def source(self):
        git = Git(self)
        git.clone(url="https://github.com/conan-io/libhello.git", target=".")
        git.checkout("require_fmt")

构建:

$ conan create . --build=missing
...
hello/1.0 (test package): RUN: ./example
hello/1.0: Hello World Release!

现在输出是彩色的。

头文件传递性(transitive_headers)

默认情况下,hello 依赖 fmt 对消费者是隐藏的。如果 hello 的公共头文件中包含了 fmt 的头文件,需要声明传递性:

def requirements(self):
    self.requires("fmt/8.1.1", transitive_headers=True)

最佳实践: 消费者如果直接使用 fmt,应在自己的 recipe 中显式声明 self.requires("fmt/8.1.1"),而不应该依赖传递性。


第四步:准备构建(generate() 方法)

示例:条件编译(WITH_FMT)

class helloRecipe(ConanFile):
    options = {"shared": [True, False],
               "fPIC": [True, False],
               "with_fmt": [True, False]}
    default_options = {"shared": False,
                       "fPIC": True,
                       "with_fmt": True}

    def requirements(self):
        if self.options.with_fmt:
            self.requires("fmt/8.1.1")

    def generate(self):
        tc = CMakeToolchain(self)
        if self.options.with_fmt:
            tc.variables["WITH_FMT"] = True
        tc.generate()

CMakeLists.txt 对应部分:

if (WITH_FMT)
    find_package(fmt)
    target_link_libraries(hello fmt::fmt)
    target_compile_definitions(hello PRIVATE USING_FMT=1)
endif()

构建两种版本对比:

# 启用 fmt(彩色输出)
$ conan create . --build=missing -o with_fmt=True
...
hello/1.0: Hello World Release! (with color!)

# 禁用 fmt(普通输出)
$ conan create . --build=missing -o with_fmt=False
...
hello/1.0: Hello World Release! (without color)

第五步:配置设置和选项(configure() / config_options())

class helloRecipe(ConanFile):
    options = {"shared": [True, False],
               "fPIC": [True, False],
               "with_fmt": [True, False]}
    default_options = {"shared": False,
                       "fPIC": True,
                       "with_fmt": True}

    def config_options(self):
        if self.settings.os == "Windows":
            del self.options.fPIC

    def configure(self):
        if self.options.shared:
            self.options.rm_safe("fPIC")

config_options() vs configure() 的区别:

方法调用时机效果
config_options()在选项被赋值之前删除选项就像从未声明过,传值会报错
configure()在选项被赋值之后传值不会报错,但不影响计算

Windows 上误传 fPIC 会报错:

$ conan create . --build=missing -o fPIC=True
...
ERROR: option 'fPIC' doesn't exist
Possible options are ['shared', 'with_fmt']

Package ID 与二进制兼容性

package_id 是根据 settings、options 和依赖信息计算出的哈希值:

# Release 构建
$ conan create . --build=missing -s build_type=Release -tf=""
...
Package '738feca714b7251063cc51448da0cf4811424e7c' built

# Debug 构建
$ conan create . --build=missing -s build_type=Debug -tf=""
...
Package '3d27635e4dd04a258d180fe03cfa07ae1186a828' built

为什么删除选项会影响 package_id?

如果你删除了 fPIC 选项,它就不参与 package_id 的计算。所以 shared=True, fPIC=Trueshared=True, fPIC=False 会得到相同的 package_id:

# 第一次:shared=True, fPIC=True
$ conan create . --build=missing -o shared=True -o fPIC=True -tf=""
...
Package '2a899fd0da3125064bf9328b8db681cd82899d56' created

# 第二次:shared=True, fPIC=False
$ conan create . --build=missing -o shared=True -o fPIC=False -tf=""
...
hello/1.0: Already installed!   # ← 因为 package_id 相同!

C 库的特殊处理

def configure(self):
    self.settings.rm_safe("compiler.cppstd")
    self.settings.rm_safe("compiler.libcxx")

从 Conan 2.4 开始,如果 recipe 中定义了 languages = "C",上述代码不再是必须的。


第六步:构建包(build() 方法)

构建时运行测试

def build(self):
    cmake = CMake(self)
    cmake.configure()
    cmake.build()
    if not self.conf.get("tools.build:skip_test", default=False):
        self.run(f"ctest --output-on-failure -j{self.conf.get('tools.build:jobs', default='4')}")

外部跳过测试:

$ conan create . -c tools.build:skip_test=True

条件性打补丁

from conan.tools.files import apply_conandata_patches

def build(self):
    apply_conandata_patches(self)
    cmake = CMake(self)
    cmake.configure()
    cmake.build()

第七步:打包文件(package() 方法)

使用 CMake install(最简单)

def package(self):
    cmake = CMake(self)
    cmake.install()

要求 CMakeLists.txt 中定义了 install() 目标。如需手动复制:

from conan.tools.files import copy

def package(self):
    copy(self, "LICENSE", src=self.source_folder,
         dst=os.path.join(self.package_folder, "licenses"))
    copy(self, "*.h",
         src=os.path.join(self.source_folder, "include"),
         dst=os.path.join(self.package_folder, "include"))
    copy(self, "*.a", src=self.build_folder,
         dst=os.path.join(self.package_folder, "lib"), keep_path=False)

处理符号链接

from conan.tools.files.symlinks import absolute_to_relative_symlinks

def package(self):
    copy(self, "LICENSE", ...)
    copy(self, "*.h", ...)
    copy(self, "*.a", ...)
    absolute_to_relative_symlinks(self, self.package_folder)

第八步:定义消费者信息(package_info() 方法)

最基本的用法:

def package_info(self):
    self.cpp_info.libs = ["hello"]

cpp_info 的默认值:

self.cpp_info.libdirs = ["lib"]
self.cpp_info.includedirs = ["include"]

除非你打包时放到其他位置,否则不需要显式设置。

根据配置动态设置

def package_info(self):
    if self.options.shared:
        self.cpp_info.libs = ["hello-shared"]
    else:
        self.cpp_info.libs = ["hello-static"]

验证输出:

$ conan create . --build=missing
...
-- Installing: .../libhello-static.a
hello/1.0 package(): Packaged 1'.a' file: libhello-static.a
hello/1.0: Package 'fd7c4113dad406f7d8211b3470c16627b54ff3af' created

自定义 CMake target 名称

默认 hello::hello,需要改为 hello::myhello

def package_info(self):
    self.cpp_info.libs = ["hello"]
    self.cpp_info.set_property("cmake_target_name", "hello::myhello")

然后消费者改为:

find_package(hello CONFIG REQUIRED)
target_link_libraries(example hello::myhello)

验证输出:

-- Conan: Target declared 'hello::myhello'

传递环境信息

def package_info(self):
    self.cpp_info.libs = ["hello"]
    self.runenv_info.define("MY_LIBRARY_PATH", self.package_folder)
    self.buildenv_info.append("PATH", os.path.join(self.package_folder, "bin"))

多库组件(components)

def package_info(self):
    self.cpp_info.libs = []

    self.cpp_info.components["ssl"].libs = ["ssl"]
    self.cpp_info.components["ssl"].requires = ["crypto"]

    self.cpp_info.components["crypto"].libs = ["crypto"]

第九步:测试包(test_package)

test_package 结构

test_package/
├── CMakeLists.txt
├── conanfile.py
└── src/
    └── example.cpp

test_package/conanfile.py

import os
from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout
from conan.tools.build import can_run

class helloTestConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    generators = "CMakeDeps", "CMakeToolchain"

    def requirements(self):
        self.requires(self.tested_reference_str)

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def layout(self):
        cmake_layout(self)

    def test(self):
        if can_run(self):
            cmd = os.path.join(self.cpp.build.bindir, "example")
            self.run(cmd, env="conanrun")

单独运行 test_package

$ conan test test_package hello/1.0
...
-------- Testing the package: Running test() --------
hello/1.0 (test package): RUN: ./example
hello/1.0: Hello World Release! (with color!)

第十步:特殊类型的包

10.1 仅头文件库(Header-only)

from conan import ConanFile
from conan.tools.files import copy

class SumConan(ConanFile):
    name = "sum"
    version = "0.1"
    exports_sources = "include/*"
    no_copy_source = True
    package_type = "header-library"

    def package(self):
        copy(self, "*.h", self.source_folder, self.package_folder)

    def package_info(self):
        self.cpp_info.bindirs = []
        self.cpp_info.libdirs = []

构建:

$ conan create .
...
sum/0.1 (test package): RUN: ./example
1+3=4

不同 build_type 不影响 package_id:

$ conan list "sum/0.1#:*"
Local Cache
sum
sum/0.1
  packages
    da39a3ee5e6b4b0d3255bfef95601890afd80709

$ conan create . -s build_type=Debug
$ conan list "sum/0.1#:*"
# 还是同一个 package_id!因为没有 settings

10.2 预构建二进制包

$ conan export-pkg . -s build_type=Release

10.3 工具包(Tool requires)

class ToolConan(ConanFile):
    name = "mytool"
    version = "1.0"
    package_type = "application"

    def package_info(self):
        self.cpp_info.bindirs = ["bin"]

常见错误排查

错误信息原因解决
ERROR: option 'fPIC' doesn't existWindows 上不存在 fPIC 选项config_options() 中删除 fPIC
ERROR: hello/1.0 not found包未创建或不在缓存中先运行 conan create .
Conan: Target declared 'hello::hello'缺少 set_propertyself.cpp_info.set_property(...) 指定
Undefined symbols for architecture链接了错误的库检查 self.cpp_info.libs
fatal error: 'fmt/color.h' file not found头文件未传递transitive_headers=True 或直接依赖 fmt
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值