理解 Conan 包布局

在前一节中,我们介绍了可编辑包的概念,并提到当它们处于可编辑模式时能够开箱即用的原因是由于 layout() 方法中信息的当前定义。让我们更详细地检查这个功能。

在本教程中,我们将继续使用 say/1.0 包和 可编辑包 教程中使用的 hello/1.0 消费者。

请首先克隆源代码以重新创建此项目。你可以在 GitHub 的 examples2 仓库中找到它们

$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/developing_packages/package_layout

注意

我们在本示例中使用 CMake 预设。这需要 CMake >= 3.23,因为从 CMakeUserPresets.jsonCMakePresets.json 的 “include” 仅自该版本起受支持。如果你不想使用预设,你可以使用类似如下的方法

cmake <path> -G <CMake generator> -DCMAKE_TOOLCHAIN_FILE=<path to
conan_toolchain.cmake> -DCMAKE_BUILD_TYPE=Release

如果你无法使用预设功能,Conan 将在你每次运行 conan install 时显示确切的 CMake 命令。

正如你所看到的,主要的文件夹结构是相同的

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

这里的主要区别在于我们没有在 say/1.0 ConanFile 中使用预定义的 cmake_layout(),而是声明我们自己的自定义布局。让我们看看我们如何在 layout() 方法中描述信息,以便它在我们在 Conan 本地缓存中创建包时以及包处于可编辑模式时都能工作。

say/conanfile.py
import os
from conan import ConanFile
from conan.tools.cmake import CMake


class SayConan(ConanFile):
    name = "say"
    version = "1.0"

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

    ...

    def layout(self):

        ## define project folder structure

        self.folders.source = "."
        self.folders.build = os.path.join("build", str(self.settings.build_type))
        self.folders.generators = os.path.join(self.folders.build, "generators")

        ## cpp.package information is for consumers to find the package contents in the Conan cache

        self.cpp.package.libs = ["say"]
        self.cpp.package.includedirs = ["include"] # includedirs is already set to 'include' by
                                                   # default, but declared for completion
        self.cpp.package.libdirs = ["lib"]         # libdirs is already set to 'lib' by
                                                   # default, but declared for completion

        ## cpp.source and cpp.build information is specifically designed for editable packages:

        # this information is relative to the source folder that is '.'
        self.cpp.source.includedirs = ["include"] # maps to ./include

        # this information is relative to the build folder that is './build/<build_type>', so it will
        self.cpp.build.libdirs = ["."]  # map to ./build/<build_type> for libdirs

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

让我们回顾一下 layout() 方法。你可以看到我们正在为 self.foldersself.cpp 设置值。让我们解释一下这些值的用途。

self.folders

定义 say 项目的结构,用于源代码以及 Conan 生成的文件和构建工件所在的文件夹。此结构独立于包是否处于可编辑模式或导出并在 Conan 本地缓存中构建。让我们为 say 包定义文件夹结构

say
 ├── CMakeLists.txt
 ├── conanfile.py
 ├── include
 │   └── say.h
 ├── src
 │   └── say.cpp
 └── build
     ├── Debug            --> Built artifacts for Debug
     │   └── generators   --> Conan generated files for Debug config
     └── Release          --> Built artifacts for Release
         └── generators   --> Conan generated files for Release config
  • 由于我们的 CMakeLists.txt. 文件夹中,因此 self.folders.source 设置为 .

  • 我们将 self.folders.build 设置为 ./build/Release./build/Debug,具体取决于 build_type 设置。这些是我们希望构建的二进制文件所在的文件夹。

  • self.folders.generators 文件夹是我们为 Conan 生成器创建的所有文件设置的位置。在本例中,CMakeToolchain 生成器生成的所有文件都将存储在那里。

注意

请注意,以上值适用于单配置 CMake 生成器。为了支持多配置生成器,例如 Visual Studio,你应该对此布局进行一些更改。有关同时支持单配置和多配置的完整布局,请查看 Conan 文档中的 cmake_layout()

self.cpp

此属性用于定义消费者将在哪里找到包内容(头文件、库等),具体取决于包是否处于可编辑模式。

cpp.package

首先,我们设置 cpp.package 的信息。这定义了包的内容及其相对于包在本地缓存中存储位置的位置。请注意,定义此信息等同于在 package_info() 方法中定义 self.cpp_info。这是我们定义的信息

  • self.cpp.package.libs: 我们添加了 say 库,以便消费者知道他们应该与之链接。这等同于在 package_info() 方法中声明 self.cpp_info.libs

  • self.cpp.package.libdirs: 我们添加了 lib 文件夹,以便消费者知道他们应该在那里搜索库。这等同于在 package_info() 方法中声明 self.cpp_info.libdirs。请注意,cpp_infocpp.packagelibdirs 的默认值均为 ["lib"],因此我们可以省略该声明。

  • self.cpp.package.includedirs: 我们添加了 include 文件夹,以便消费者知道他们应该在那里搜索库头文件。这等同于在 package_info() 方法中声明 self.cpp_info.includedirs。请注意,cpp_infocpp.packageincludedirs 的默认值均为 ["include"],因此我们可以省略该声明。

为了检查此信息如何影响消费者,我们将首先在 say 包上执行 conan create

$ cd say
$ conan create . -s build_type=Release

当我们调用 conan create 时,Conan 会将配方和配方中声明的源文件移动到本地缓存以导出到配方文件夹,然后它将创建一个单独的包文件夹来构建二进制文件并存储实际的包内容。如果你检查 [YOUR_CONAN_HOME]/p 文件夹,你将找到两个类似于以下的新文件夹

提示

你可以使用 conan cache 命令或检查 conan create 命令的输出,来获取这些文件夹的确切位置。

<YOUR_CONAN_HOME>/p
├── sayb3ea744527a91      --> folder for sources
│   └── ...
│
└── say830097e941e10      --> folder for building and storing the package binaries
    ├── b
    │   ├── build
    │   │   └── Release
    │   ├── include
    │   │   └── say.h
    │   └── src
    │       ├── hello.cpp
    │       └── say.cpp
    └── p
        ├── include       --> defined in cpp.package.includedirs
        │   └── say.h
        └── lib           --> defined in cpp.package.libdirs
            └── libsay.a  --> defined in self.cpp.package.libs

你可以在那里识别出我们在 layout() 方法中定义的结构。如果你现在构建 hello 消费者项目,它将在本地缓存中该文件夹内的 cpp.package 定义的位置搜索 say 的所有头文件和库

$ cd ../hello
$ conan install . -s build_type=Release

# Linux, MacOS
$ cmake --preset conan-release --log-level=VERBOSE
# Windows
$ cmake --preset conan-default --log-level=VERBOSE

...
-- Conan: Target declared 'say::say'
-- Conan: Library say found <YOUR_CONAN_HOME>p/say8938ceae216fc/p/lib/libsay.a
-- Created target CONAN_LIB::say_say_RELEASE STATIC IMPORTED
-- Conan: Found: <YOUR_CONAN_HOME>p/p/say8938ceae216fc/p/lib/libsay.a
-- Configuring done
...

$ cmake --build --preset conan-release
[ 50%] Building CXX object CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello

cpp.source 和 cpp.build

我们还在配方中定义了 cpp.sourcecpp.build 属性。这些属性仅在包处于可编辑模式时使用,并指向消费者将用于查找头文件和二进制文件的位置。我们定义了

  • self.cpp.source.includedirs 设置为 ["include"]。此位置相对于我们定义为 .self.folders.source。在可编辑包的情况下,此位置将是我们项目的本地文件夹。

  • self.cpp.build.libdirs 设置为 ["."]。此位置相对于我们定义为 ./build/<build_type>self.folders.build。在可编辑包的情况下,此位置将指向 <本地文件夹>/build/<build_type>

请注意,其他的 cpp.sourcecpp.build 定义也是可能的,它们具有不同的含义和目的,例如

  • 如果我们有预编译的库在源代码仓库中(例如提交到 git),则可以使用 self.cpp.source.libdirsself.cpp.source.libs。它们不是构建的产物,而是源代码的一部分。

  • self.cpp.build.includedirs 可以用于包含在构建时生成的头文件的文件夹,这通常发生在某些代码生成器在开始编译项目之前被触发时。

为了检查此信息如何影响消费者,我们将首先将 say 包置于可编辑模式并在本地构建它。

$ cd ../say
$ conan editable add . --name=say --version=1.0
$ conan install . -s build_type=Release
$ cmake --preset conan-release
$ cmake --build --preset conan-release

你现在可以检查 say 项目文件夹的内容,你可以看到输出文件夹与我们用 self.folders 定义的文件夹匹配

.
├── CMakeLists.txt
├── CMakeUserPresets.json
├── build
│   └── Release       --> defined in cpp.build.libdirs
│       ├── ...
│       ├── generators
│       │   ├── CMakePresets.json
│       │   ├── ...
│       │   └── deactivate_conanrun.sh
│       └── libsay.a  --> no need to define
├── conanfile.py
├── include           --> defined in cpp.source.includedirs
│   └── say.h
└── src
    ├── hello.cpp
    └── say.cpp

现在我们已经将 say 包置于可编辑模式,如果我们构建 hello 消费者项目,它将在 cpp.sourcecpp.build 定义的文件夹中搜索 say 的所有头文件和库

$ cd ../hello
$ conan install . -s build_type=Release

# Linux, MacOS
$ cmake --preset conan-release --log-level=VERBOSE
# Windows
$ cmake --preset conan-default --log-level=VERBOSE

...
-- Conan: Target declared 'say::say'
-- Conan: Library say found <local_folder>/examples2/tutorial/developing_packages/package_layout/say/build/Release/libsay.a
-- Conan: Found: <local_folder>/examples2/tutorial/developing_packages/package_layout/say/build/Release/libsay.a
-- Configuring done
...

$ cmake --build --preset conan-release
[ 50%] Building CXX object CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello

$ conan editable remove --refs=say/1.0

注意

请注意,我们没有定义 self.cpp.build.libs = ["say"]。这是因为在 self.cpp.sourceself.cpp.build 中设置的信息将与在 self.cpp.package 中设置的信息合并,因此你只需定义可编辑包的更改。出于同样的原因,你也可以省略设置 self.cpp.source.includedirs = ["include"],但我们将其保留在那里以展示 cpp.source 的用法。