使用 Conan 支持不同的 C/C++ 包管理范式
一些开发者认为,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.cmake
、conanbuildinfo_debug.cmake
和conanbuildinfo_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_type
的Debug
和Release
值,哈希都相同(特定的字符串大多无关紧要,只要两个配置的字符串相同即可)。请注意,构建哈希sha3
将不同于sha1
和sha2
包标识符。
这并不意味着将严格地只有一个构建文件夹。将为每个配置(架构、编译器版本等)创建一个构建文件夹。因此,如果我们只有调试/发布构建类型,并且我们为 N 个不同的配置生成 N 个包,我们将拥有 N/2 个构建文件夹,节省一半的构建时间。
结论
这篇博文说明了 Conan 如何允许非常不同的打包范式。这是一项持续不断的努力,由我们的维护者、贡献者和用户组成的社区推动,他们试图提供工具来支持我们在 C 和 C++ 社区中遇到的各种不同的用例和需求。非常感谢他们所有人!