构建包:build() 方法

我们已经使用了一个包含 build() 方法 的 Conan 配方,并学习了如何使用它来调用构建系统并构建我们的包。在本教程中,我们将修改该方法并解释如何使用它来完成以下事情:

  • 构建并运行测试

  • 条件性地修补源代码

  • 条件性地选择您想要使用的构建系统

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

$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/creating_packages/build_method

构建并运行项目的测试

您会注意到上一个配方的 conanfile.py 文件中有些变化。让我们检查相关部分

配方中引入的变更

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

    ...

    def source(self):
        git = Git(self)
        git.clone(url="https://github.com/conan-io/libhello.git", target=".")
        # Please, be aware that using the head of the branch instead of an immutable tag
        # or commit is not a good practice in general
        git.checkout("with_tests")

    ...

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

    ...

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

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()
        if not self.conf.get("tools.build:skip_test", default=False):
            test_folder = os.path.join("tests")
            if self.settings.os == "Windows":
                test_folder = os.path.join("tests", str(self.settings.build_type))
            self.run(os.path.join(test_folder, "test_hello"))

    ...
  • 我们将 gtest/1.11.0 需求作为 test_requires() 添加到了配方中。这是一种用于测试库(如 Catch2gtest)的需求类型。

  • 我们使用 tools.build:skip_test 配置(默认为 False)来告诉 CMake 是否构建并运行测试。有几点需要注意:

    • 如果我们将 tools.build:skip_test 配置设置为 True,Conan 将自动将 BUILD_TESTING 变量注入到 CMake 并设置为 OFF。您将在下一节中看到,我们正在 CMakeLists.txt 中使用此变量来决定是否构建测试。

    • 我们在 build() 方法中使用 tools.build:skip_test 配置,在构建包和测试之后,以决定是否要运行测试。

    • 在这种情况下,我们正在使用 gtest 进行测试,并且我们必须检查构建方法是否要运行测试。如果您使用 CTest,此配置还会影响 CMake.test() 的执行;如果您使用 Meson,则会影响 Meson.test() 的执行。

库源中引入的变更

首先,请注意我们正在使用 另一个分支libhello 库。该分支在库方面有两个新颖之处:

  • 我们向 库源 添加了一个名为 compose_message() 的新函数,以便我们可以对该函数添加一些单元测试。此函数仅根据传入的参数创建一个输出消息。

  • 如前一节所述,库的 CMakeLists.txt 使用 BUILD_TESTING CMake 变量,该变量条件性地添加 tests 目录。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(hello CXX)

...

if (NOT BUILD_TESTING STREQUAL OFF)
    add_subdirectory(tests)
endif()

...

只要 tools.build:skip_test 配置设置为 True,Conan 就会声明 BUILD_TESTING CMake 变量 并将其设置为 OFF(如果尚未定义)。此变量通常在使用 CTest 时由 CMake 声明,但即使您使用其他测试框架,通过 tools.build:skip_test 配置也可以在 CMakeLists.txt 中使用它。

我们还在 tests 文件夹中有一个 CMakeLists.txt,使用 googletest 进行测试

tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(PackageTest CXX)

find_package(GTest REQUIRED CONFIG)

add_executable(test_hello test.cpp)
target_link_libraries(test_hello GTest::gtest GTest::gtest_main hello)

包含对 compose_message() 函数功能的 G基本测试

tests/test.cpp
#include "../include/hello.h"
#include "gtest/gtest.h"

namespace {
    TEST(HelloTest, ComposeMessages) {
      EXPECT_EQ(std::string("hello/1.0: Hello World Release! (with color!)\n"), compose_message("Release", "with color!"));
      ...
    }
}

现在我们已经检查了代码中的所有更改,让我们来试一试。

$ conan create . --build=missing -tf=""
...
[ 25%] Building CXX object CMakeFiles/hello.dir/src/hello.cpp.o
[ 50%] Linking CXX static library libhello.a
[ 50%] Built target hello
[ 75%] Building CXX object tests/CMakeFiles/test_hello.dir/test.cpp.o
[100%] Linking CXX executable test_hello
[100%] Built target test_hello
hello/1.0: RUN: ./tests/test_hello
Capturing current environment in /Users/user/.conan2/p/tmp/c51d80ef47661865/b/build/generators/deactivate_conanbuildenv-release-x86_64.sh
Configuring environment variables
Running main() from /Users/user/.conan2/p/tmp/3ad4c6873a47059c/b/googletest/src/gtest_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from HelloTest
[ RUN      ] HelloTest.ComposeMessages
[       OK ] HelloTest.ComposeMessages (0 ms)
[----------] 1 test from HelloTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.
hello/1.0: Package '82b6c0c858e739929f74f59c25c187b927d514f3' built
...

如您所见,测试已构建并运行。现在让我们在命令行中使用 tools.build:skip_test 配置来跳过测试的构建和运行。

$ conan create . -c tools.build:skip_test=True -tf=""
...
[ 50%] Building CXX object CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX static library libhello.a
[100%] Built target hello
hello/1.0: Package '82b6c0c858e739929f74f59c25c187b927d514f3' built
...

您现在可以看到只有库目标被构建,没有测试被构建或运行。

条件性地修补源代码

如果您需要修补源代码,推荐的方法是在 source() 方法中进行。有时,如果修补依赖于设置或选项,您必须在启动构建之前使用 build() 方法来应用源代码补丁。Conan 中有 几种方法可以做到这一点。其中一种是使用 replace_in_file 工具。

import os
from conan import ConanFile
from conan.tools.files import replace_in_file


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

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

    def build(self):
        replace_in_file(self, os.path.join(self.source_folder, "src", "hello.cpp"),
                        "Hello World",
                        "Hello {} Friends".format("Shared" if self.options.shared else "Static"))

请注意,如果可能,应避免在 build() 中进行修补,并且仅在非常特殊的情况下才这样做,因为它会使您在本地开发包变得更加困难(我们将在稍后的 本地开发流程部分 详细解释这一点)。

条件性地选择构建系统

有些包根据我们所构建的平台需要不同的构建系统,这并不少见。例如,hello 库可以在 Windows 上使用 CMake 构建,而在 Linux 和 macOS 上使用 Autotools 构建。这可以在 build() 方法中轻松处理,如下所示:

...

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

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

    ...

    def generate(self):
        if self.settings.os == "Windows":
            tc = CMakeToolchain(self)
            tc.generate()
            deps = CMakeDeps(self)
            deps.generate()
        else:
            tc = AutotoolsToolchain(self)
            tc.generate()
            deps = PkgConfigDeps(self)
            deps.generate()

    ...

    def build(self):
        if self.settings.os == "Windows":
            cmake = CMake(self)
            cmake.configure()
            cmake.build()
        else:
            autotools = Autotools(self)
            autotools.autoreconf()
            autotools.configure()
            autotools.make()

    ...