创建你的第一个 Conan 包

在前几节中,我们消费了 Conan 包(例如 Zlib),首先使用 conanfile.txt,然后使用 conanfile.py。但是 conanfile.py 配方文件不仅用于消费其他包,它也可以用来创建你自己的包。在本节中,我们将解释如何使用 conanfile.py 配方创建一个简单的 Conan 包,以及如何使用 Conan 命令从源代码构建这些包。

重要提示

这是一个教程部分。 建议你执行这些命令。 对于这个具体的例子,你需要安装 CMake 在你的路径中。 Conan 并非严格要求创建包,你可以使用其他构建系统(例如 VS、Meson、Autotools 甚至你自己的系统)来做到这一点,而无需依赖 CMake。

使用 conan new 命令来创建一个 “Hello World” C++ 库示例项目

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

这将创建一个具有以下结构的 Conan 包项目。

.
├── CMakeLists.txt
├── conanfile.py
├── include
│   └── hello.h
├── src
│   └── hello.cpp
└── test_package
    ├── CMakeLists.txt
    ├── conanfile.py
    └── src
        └── example.cpp

生成的文件是

  • conanfile.py:在根文件夹中,有一个 conanfile.py,它是主要的配方文件,负责定义如何构建和消费包。

  • CMakeLists.txt:一个简单的通用 CMakeLists.txt,其中没有任何关于 Conan 的特定内容。

  • srcinclude 文件夹:包含简单的 C++ “hello” 库的文件夹。

  • test_package 文件夹:包含一个 example 应用程序,它将需要并链接到创建的包。 这不是强制性的,但它有助于检查我们的包是否已正确创建。

让我们看看包配方 conanfile.py

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


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

    # Optional metadata
    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>")

    # Binary configuration
    settings = "os", "compiler", "build_type", "arch"
    options = {"shared": [True, False], "fPIC": [True, False]}
    default_options = {"shared": False, "fPIC": True}

    # Sources are located in the same place as this recipe, copy them to the recipe
    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"]

让我们简要解释一下配方的不同部分

首先,你可以看到定义的 Conan 包的名称和版本

  • name:一个字符串,最小为 2 个,最大为 100 个小写字符,用于定义包名称。 它应该以字母数字字符或下划线开头,并且可以包含字母数字、下划线、+、.、- 字符。

  • version:它是一个字符串,可以采用任何值,匹配与 name 属性相同的约束。 如果版本遵循语义版本控制,格式为 X.Y.Z-pre1+build2,则该值可能用于通过版本范围而不是精确版本来要求此包。

然后,你可以看到一些定义元数据的属性。 这些是可选的但建议使用,并定义诸如包的简短 description、打包库的 authorlicense、包存储库的 url 以及包相关的 topics 等内容。

之后,有一个与二进制配置相关的部分。 本节定义了包的有效设置和选项。 正如我们在 消费包部分 中解释的那样

  • settings 是项目范围内的配置,不能在配方中默认设置。 诸如操作系统、编译器或构建配置之类的东西,对于多个 Conan 包来说是常见的

  • options 是特定于包的配置,可以在配方中默认设置。 在这种情况下,我们可以选择将包创建为共享库或静态库,默认情况下是静态库。

之后,设置 exports_sources 属性以定义哪些源文件是 Conan 包的一部分。 这些是你想要打包的库的源文件。 在这种情况下,是我们 “hello” 库的源文件。

然后,声明了几个方法

  • config_options() 方法(与 configure() 方法一起)允许微调二进制配置模型。 例如,在 Windows 上,没有 fPIC 选项,因此可以将其删除。

  • layout() 方法声明了我们希望找到源文件和构建过程中生成的文件的目标位置。 示例目标文件夹是生成的二进制文件的文件夹以及 Conan 生成器在 generate() 方法中创建的所有文件。 在这种情况下,由于我们的项目使用 CMake 作为构建系统,我们调用 cmake_layout()。 调用此函数将设置 CMake 项目的预期位置。

  • generate() 方法准备从源代码构建包。 在这种情况下,它可以简化为属性 generators = "CMakeToolchain",但保留它以显示这个重要的方法。 在这种情况下,执行 CMakeToolchain generate() 方法将创建一个 conan_toolchain.cmake 文件,该文件将 Conan settingsoptions 转换为 CMake 语法。 添加 CMakeDeps 生成器是为了完整起见,但在将 requires 添加到配方之前,它不是严格必需的。

  • build() 方法使用 CMake 包装器来调用 CMake 命令。 这是一个薄层,在这种情况下,它将传递 -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake 参数。 它将配置项目并从源代码构建它。

  • package() 方法将工件(标头、库)从构建文件夹复制到最终包文件夹。 可以使用裸 “copy” 命令来完成,但在这种情况下,它利用了已经存在的 CMake 安装功能。 如果 CMakeLists.txt 没有实现它,那么使用 copy() 工具package() 方法中编写一个等效项会很容易。

  • 最后,package_info() 方法定义了消费者在使用此包时必须链接到 “hello” 库。 还可以定义诸如 include 或 lib 路径之类的其他信息。 此信息用于生成器创建的文件(如 CMakeDeps),以供消费者使用。 这是关于当前包的通用信息,并且无论他们使用的构建系统是什么,也无论我们在 build() 方法中使用了什么构建系统,都可供消费者使用。

test_package 文件夹对于现在理解如何创建包并不重要。 重要的位是

  • test_package 文件夹不同于单元测试或集成测试。 这些测试是 “package” 测试,并验证包是否已正确创建,以及包使用者将能够链接到它并重用它。

  • 它本身就是一个小的 Conan 项目。 它包含自己的 conanfile.py 及其源代码,包括构建脚本,该脚本依赖于正在创建的包,并构建和执行需要包中库的小型应用程序。

  • 它不属于包。 它只存在于源代码存储库中,而不是在包中。

让我们使用当前的默认配置从源代码构建包,然后让 test_package 文件夹测试该包

$ 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 ========
...
[ 50%] Building CXX object CMakeFiles/example.dir/src/example.cpp.o
[100%] Linking CXX executable example
[100%] Built target example

======== Testing the package: Executing test ========
hello/1.0 (test package): Running test()
hello/1.0 (test package): RUN: ./example
hello/1.0: Hello World Release!
  hello/1.0: __aarch64__ defined
  hello/1.0: __cplusplus201703
  hello/1.0: __GNUC__4
  hello/1.0: __GNUC_MINOR__2
  hello/1.0: __clang_major__15
  hello/1.0: __apple_build_version__15000309
...

如果显示 “Hello world Release!”,则它起作用了。 这就是发生的事情

  • conanfile.py 连同 src 文件夹的内容已被复制(在 Conan 术语中为导出)到本地 Conan 缓存。

  • 开始了 hello/1.0 包的新建源构建,调用 generate()build()package() 方法。 这将在 Conan 缓存中创建二进制包。

  • 然后,Conan 移动到 test_package 文件夹并执行 conan install + conan build + test() 方法,以检查包是否已正确创建。

我们现在可以验证配方和包二进制文件是否在缓存中

$ conan list hello
Local Cache
  hello
    hello/1.0

conan create 命令接收与 conan install 相同的参数,因此你可以将相同的设置和选项传递给它。 如果我们执行以下几行,我们将为 Debug 配置创建新的包二进制文件,并将 hello 库构建为共享库

$ 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 all the binaries built for the hello/1.0 package in the cache
$ conan list "hello/1.0:*"
Local Cache
  hello
    hello/1.0
      revisions
        dcbfe21e5250264b26595d151796be70 (2024-05-10 09:40:15 UTC)
          packages
            2505f7ebb5a4cca156b2d6b8534f415a4a48b5c9
              info
                settings
                  arch: armv8
                  build_type: Release
                  compiler: apple-clang
                  compiler.cppstd: gnu17
                  compiler.libcxx: libc++
                  compiler.version: 15
                  os: Macos
                options
                  shared: True
            39f48664f195e0847f59889d8a4cdfc6bca84bf1
              info
                settings
                  arch: armv8
                  build_type: Release
                  compiler: apple-clang
                  compiler.cppstd: gnu17
                  compiler.libcxx: libc++
                  compiler.version: 15
                  os: Macos
                options
                  fPIC: True
                  shared: False
            814ddaac84bc84f3595aa076660133b88e49fb11
              info
                settings
                  arch: armv8
                  build_type: Debug
                  compiler: apple-clang
                  compiler.cppstd: gnu17
                  compiler.libcxx: libc++
                  compiler.version: 15
                  os: Macos
                options
                  fPIC: True
                  shared: False

现在我们已经创建了一个简单的 Conan 包,我们将更详细地解释 Conanfile 的每个方法。 你将学习如何修改这些方法以实现从外部存储库检索源文件、向我们的包添加依赖项、自定义我们的工具链等等。

关于 Conan 缓存的说明

当你执行 conan create 命令时,包的构建没有在你本地文件夹中进行,而是在 Conan 缓存中的另一个文件夹中进行。 此缓存位于用户主文件夹下的 .conan2 文件夹中。 Conan 将使用 ~/.conan2 文件夹来存储构建的包和不同的配置文件。 你已经使用了 conan list 命令来列出存储在本地缓存中的配方和二进制文件。

一个重要提示:Conan缓存对于Conan客户端是私有的 - 修改、添加、删除或更改Conan缓存中的文件都是未定义的行为,可能导致故障。