一些开发者认为,C/C++ 的包管理器应该默认情况下将调试和发布构件打包到同一个包中,以便开发者在工作时轻松地更改配置并使用它们。

但其他开发者可能认为这不是最佳实践,并且发布和调试包应该不同,并默认情况下分别安装。Linux 的“-dbg”符号包就是一个例子。

事实上,两者都各有优缺点,如果我们从开发包管理器的经验中吸取了教训,那就是没有绝对的真理,C/C++ 包管理器应该为开发者提供支持他们想要实现的打包范式的方法。我们一直在倾听用户的反馈,最新的 Conan 0.20 版本包含了一些有助于支持多种包管理范式的实用程序。

首先,回顾和理解 Conan 如何处理包很有意思

上图中的每个块都是给定包的文件夹。包配方存储在“export”文件夹中,它被复制到“source”文件夹中,以便配方source()方法可以获取包源代码。然后,对于每个不同的配置(不同的设置,例如不同的编译器版本或架构),都会使用一个新的、干净的构建文件夹,配方build()方法被触发,最后,构件(通常是头文件和库文件)由package()方法提取到最终的包文件夹中。每个包都由配置值的 SHA-1 哈希标识。

单一配置包

这是 Conan 中最常用的方法,在文档和 conan.io 中的大多数包中都有广泛使用。使用这种方法,每个包只包含一个配置的构件。因此,如果有一个构建“hello”库的包配方,则将有一个包包含hello.lib库的发布版本,另一个包包含该库的hello_d.lib调试版本。名称后缀是可选的,库可以命名相同,没有任何问题,但这里使用它是为了使其更清晰。

其典型配方如下(不是完整的配方)

class HelloConan(ConanFile):

    settings = "os", "compiler", "build_type", "arch"

    def build(self):
        cmake = CMake(self.settings)
        cmake.configure(self) # calls "cmake . -G ... "
        cmake.build(self) # calls "cmake --build ."

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

非常重要的是要注意,它将build_type声明为一个设置。这意味着将为该设置的每个不同值生成一个不同的包。

安装这些包时,为构建系统生成的,如conanbuildinfo.cmake文件,由cmake生成器生成,将包含根据安装设置而不同的信息

set(CONAN_LIBS_HELLO hello)
...
set(CONAN_LIBS hello ${CONAN_LIBS})

如果开发者想要切换依赖项的配置,他通常会使用

$ conan install -s build_type=Release ... 
// when need to debug
$ conan install -s build_type=Debug ... 

这些切换将很快,因为所有依赖项都已缓存在本地。

此过程具有一些优点:它非常易于实现和维护。包的大小最小,因此磁盘空间和传输速度更快,并且从源代码构建也保持在必要的最低限度。配置的解耦可能有助于隔离与混合不同类型的构件相关的问题,并防止部署和分发错误造成宝贵信息的丢失。例如,调试构件可能包含符号或源代码,这可能有助于或直接提供反向工程的方法。因此,按构件分发调试构件可能是一个非常危险的问题。主要的缺点是必须记住在从调试切换到发布,反之亦然时安装依赖项的特定配置。对于像 Visual Studio 这样的大型 IDE 用户来说,这会有些不方便。

使用多个调试/发布单一配置包

即使包是单一配置的,如果最终用户开发者想要在多配置环境(如 Visual Studio)中轻松使用它们,他们可以通过 CMakecmake_multi生成器来做到这一点。使用该生成器,只需安装依赖项的调试和发布配置即可

$ conan install -g cmake_multi -s build_type=Release -s compiler.runtime=MD ... 
$ conan install -g cmake_multi -s build_type=Debug -s compiler.runtime=MDd ...

这些命令将生成 3 个文件:conanbuildinfo_multi.cmakeconanbuildinfo_debug.cmakeconanbuildinfo_release.cmake_debug_release文件将包含各自的 cmake 变量。

然后,在消费者 CMakeLists.txt 中使用

project(MyHello)
cmake_minimum_required(VERSION 2.8.12)

include(${CMAKE_BINARY_DIR}/conanbuildinfo_multi.cmake)
conan_basic_setup()

add_executable(say_hello main.cpp)
conan_target_link_libraries(say_hello)

多配置包

在多配置包中,同一个包将包含不同配置的构件。在我们的示例中,同一个包可以同时包含库“hello”的发布和调试版本。

这并不意味着你将只有一个包或严格地一个构建文件夹每个配方,因为你仍然可以为不同的架构(例如)或不同的编译器版本拥有不同的包。包创建者可以定义他们独特的打包逻辑。

要实现这种方法,包配方可以执行以下操作

def build(self):
    cmake = CMake(self.settings)
    if cmake.is_multi_configuration:
        cmd = 'cmake "%s" %s' % (self.conanfile_directory, cmake.command_line)
        self.run(cmd)
        self.run("cmake --build . --config Debug")
        self.run("cmake --build . --config Release")
    else:
        for config in ("Debug", "Release"):
            self.output.info("Building %s" % config)
            self.run('cmake "%s" %s -DCMAKE_BUILD_TYPE=%s'
                        % (self.conanfile_directory, cmake.command_line, config))
            self.run("cmake --build .")
            shutil.rmtree("CMakeFiles")
            os.remove("CMakeCache.txt")

假设正在使用_d后缀名称(其他方法也是有效的,如具有不同的文件夹),则package_info()方法可以是

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

这些包不需要在安装时指定构建类型,如果提供,它将被忽略,例如,对于使用 cmake 生成器的使用者

  $ conan install -g cmake  # no -s build_type=Release/Debug

这将在同一个conanbuildinfo.cmake中为使用者构建系统生成不同的变量,例如

set(CONAN_LIBS_HELLO_DEBUG hello_d)
set(CONAN_LIBS_HELLO_RELEASE hello)
...
set(CONAN_LIBS_DEBUG hello_d ${CONAN_LIBS_DEBUG})
set(CONAN_LIBS_RELEASE hello ${CONAN_LIBS_RELEASE})

这种方法将存在分发调试构件的风险,如上所述,这是一个重要问题,可能有助于反向工程。此外,包将始终更大,需要更多时间来构建、传输和安装,即使你没有使用所有构件(如在生产中)。主要优点是开发者可以在 IDE 中更容易地在调试和发布配置之间切换,而无需执行任何其他操作。

构建一次,打包多次

一个现有的构建脚本可能一次构建不同配置的二进制文件,例如调试/发布,或不同的架构(32/64 位),或库类型(共享/静态)。如果在之前的“单一配置包”方法中使用此构建脚本,它肯定会毫无问题地工作,但我们会浪费宝贵的构建时间,因为我们会为每个包重新构建整个项目,然后提取给定配置的相关构件,留下其他构件。

使用 Conan 0.20,可以指定逻辑,以便可以重用相同的构建来创建不同的包,这将更有效率

这可以通过在包配方中定义一个build_id()方法来完成,该方法将指定逻辑。

settings = "os", "compiler", "arch", "build_type"

def build_id(self):
    self.info_build.settings.build_type = "Any"

def package(self):
    if self.settings.build_type == "Debug":
        #package debug artifacts
    else: 
        # package release

请注意,build_id()方法使用self.info_build对象来更改构建哈希。如果该方法不更改它,则哈希将与包文件夹的哈希匹配。通过设置build_type="Any",我们强制对于build_typeDebugRelease值,哈希都相同(特定的字符串大多无关紧要,只要两个配置的字符串相同即可)。请注意,构建哈希sha3将不同于sha1sha2包标识符。

这并不意味着将严格地只有一个构建文件夹。将为每个配置(架构、编译器版本等)创建一个构建文件夹。因此,如果我们只有调试/发布构建类型,并且我们为 N 个不同的配置生成 N 个包,我们将拥有 N/2 个构建文件夹,节省一半的构建时间。

结论

这篇博文说明了 Conan 如何允许非常不同的打包范式。这是一项持续不断的努力,由我们的维护者、贡献者和用户组成的社区推动,他们试图提供工具来支持我们在 C 和 C++ 社区中遇到的各种不同的用例和需求。非常感谢他们所有人!