原文: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 做了什么:
- Export(导出):把
conanfile.py和源代码复制到 Conan 缓存(~/.conan2/) - Install(安装):解析依赖(本项目无外部依赖)
- Build(构建):从源码构建,生成二进制包
- 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_sources 为 source() 方法:
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=True 和 shared=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 exist | Windows 上不存在 fPIC 选项 | 在 config_options() 中删除 fPIC |
ERROR: hello/1.0 not found | 包未创建或不在缓存中 | 先运行 conan create . |
Conan: Target declared 'hello::hello' | 缺少 set_property | 用 self.cpp_info.set_property(...) 指定 |
Undefined symbols for architecture | 链接了错误的库 | 检查 self.cpp_info.libs |
fatal error: 'fmt/color.h' file not found | 头文件未传递 | 用 transitive_headers=True 或直接依赖 fmt |

324

被折叠的 条评论
为什么被折叠?



