您可能知道,Conan 官方支持与多个构建系统集成,例如 CMakeMSBuildMeson 等。但您可能不知道,如果您正在使用当前不支持的构建系统,Conan 提供了集成它的工具,并构建和使用使用它的包。

此帖子的代码现已在 Conan 示例仓库 中提供。请随时克隆它并尝试代码。

我从哪里开始?

假设您想使用特定构建系统创建一些包,并让其他人使用您的包并在其配置没有生成二进制文件的情况下构建它们。Conan 有三个功能可以帮助您做到这一点

  • Conan 生成器。它们以合适的格式为您的构建系统提供有关依赖项的所有信息。

  • Conan 安装程序。Conan 允许您为构建过程中所需的工具创建包,并使用 build_requires 在稍后安装它们,以便能够从 Conan 调用该工具。在我们的例子中,我们希望安装运行构建系统的工具。

  • Conan 构建助手。构建助手可帮助您将设置(例如 build_typecompiler.versionarch)转换为构建系统。它还可以调用构建系统工具来构建我们的源代码。要在 conanfile.py 中使用构建助手,我们将使用 Python 依赖项

Waf 构建系统的 Conan 生成器

为了测试这些工具,我们选择了 Waf 构建系统。Waf 是一种构建自动化工具,旨在帮助自动编译和安装计算机软件。它是用 Python 编写的开源软件,并在 BSD 许可证 的条款下发布。

Waf 是一个用于构建项目的通用实用程序,项目特定的详细信息存储在名为 wscript 的 Python 模块中。Waf 项目必须包含一个顶级 wscript,其中定义了执行构建的命令。此外,配置上下文将存储在构建过程中可能重复使用的数据。让我们看看一个最小的 wscript 实现对于 C++ 项目将是什么样子,在这个项目中我们想要构建一个依赖于 mylib 库的可执行文件。

#! /usr/bin/env python
# encoding: utf-8

top = '.'
out = 'build'

def options(opt):
    opt.load('compiler_cxx')

def configure(conf):
    conf.load('compiler_cxx')
    conf.env.INCLUDES_mylib = ['dir_to_mylib_includes']
    conf.env.LIBPATH_mylib = ['dir_to_mylib_libs']
    conf.env.LIB_mylib = 'mylib'

def build(bld):
    bld.program(source='main.cpp', target='app', use='mylib')

如您所见,这里定义了几个命令,分别是 configure()build(),它们是我们目前最关心的命令。

  • configure 命令负责设置一些设置并查找先决条件的位置。我们必须修改 配置上下文 (conf.env) 变量以告诉 Waf 它将在哪里找到 包含 文件。Conan 拥有所有这些信息,因此我们需要一个工具将这些信息转换为我们可以在 wscript 中加载的方式,这就是 Conan 生成器 的设计目的。

  • build 命令将源文件转换为构建文件。请注意,在对 bld.program 的调用中,我们可以使用 use 参数告诉 Waf 我们链接了哪些库。Conan 生成器 也必须为 Waf 提供此参数。

Waf 使我们能够使用 load 命令加载 python 模块。我们可以加载由 Conan 生成器 创建的 Python 代码来修改 Waf 配置上下文。这样,我们可以包含有关所有依赖项的信息。

自定义 Conan 生成器

Conan 中的 自定义生成器 是一个扩展 Generator 并实现两个属性的类

  • filename 应返回将生成的 文件的名称。在我们的例子中,我们将生成一个名为 waf_conan_libs_info.py 的文件

  • content 应以所需的格式返回 文件的内容。在这里,我们将从 Generator 类的 deps_build_info 属性中检索所有这些信息。该属性是一个字典,其中包含链接库所需的所有信息。

要在我们的使用者中使用 生成器,我们将不得不创建一个稍后可以作为 build_requires 加载的包。生成器的实现 将在 conanfile.py 中,并且可以像这样简单:

class Waf(Generator):
    def _remove_lib_extension(self, libs):
        return [lib[0:-4] if lib.endswith(".lib") else lib for lib in libs]

    @property
    def filename(self):
        return "waf_conan_libs_info.py"

    @property
    def content(self):
        sections = []
        sections.append("def configure(ctx):")
        conan_libs = []
        for dep_name, info in self.deps_build_info.dependencies:
            if dep_name not in self.conanfile.build_requires:
                dep_name = dep_name.replace("-", "_")
                sections.append("   ctx.env.INCLUDES_{} = {}".format(
                    dep_name, info.include_paths))
                sections.append("   ctx.env.LIBPATH_{} = {}".format(
                    dep_name, info.lib_paths))
                sections.append("   ctx.env.LIB_{} = {}".format(
                    dep_name, self._remove_lib_extension(info.libs)))
                conan_libs.append(dep_name)
        sections.append("   ctx.env.CONAN_LIBS = {}".format(conan_libs))
        sections.append("")
        return "\n".join(sections)

此生成器将创建包含所有依赖项信息的文件 waf_conan_libs_info.py。我们可以使用 wscript 中的 load 命令将此信息传递给 Waf

def configure(conf):
	conf.load('compiler_cxx')
	conf.load('waf_conan_libs_info', tooldir='.')

但这仅在我们路径中存在 Waf 构建工具的情况下有效。但是,我们不知道我们的使用者是否已安装它。我们可以通过创建 Conan 安装程序包 来解决此问题。

创建用于安装构建系统的包

正如我们所说,Waf 是用 Python 编写的构建系统,因此要使用它,我们需要从 Waf 仓库 下载 Python 脚本。我们可以创建一个 Conan 包来下载该工具并使其可用于执行我们的构建。这将是我们安装程序的 conanfile.py 的结构

class WAFInstallerConan(ConanFile):
    name = "waf"
    version = "2.0.18"
    settings = "os_build"
    homepage = "https://gitlab.com/ita1024/waf"
    license = "BSD"
    exports_sources = ["LICENSE"]

    def build(self):
        source_url = "https://waf.io/waf-%s" % (self.version)
        self.output.warn("Downloading Waf build system: %s" % (source_url))
        tools.download(source_url, "waf")
        if self.settings.os_build == "Windows":
            tools.download(
                "https://gitlab.com/ita1024/waf/raw/waf-{}/utils/waf.bat".format(
                    self.version), "waf.bat")
        elif self.settings.os_build == "Linux" or self.settings.os_build == "Macos":
            self.run("chmod 755 waf")

    def package(self):
        self.copy(pattern="LICENSE", src='.', dst="licenses")
        self.copy('waf', src='.', dst="bin", keep_path=False)
        self.copy('waf.bat', src='.', dst="bin", keep_path=False)

    def package_info(self):
        self.output.info("Using Waf %s version" % self.version)
        self.env_info.PATH.append(os.path.join(self.package_folder, "bin"))

请注意,从 conanfile.py 的设置中仅保留了 os_build 设置,因为创建不同的安装程序包(例如,根据 compilerarch)没有意义,因为该工具对于所有这些配置都是相同的。安装此包后,所有将其声明为 build_requires 的使用者都将可以在路径上使用此工具。

此时,我们能够告诉 Waf 库的位置,并且我们可以从 conanfile 中使用 self.run() 调用 Waf 并手动传递像 build_type 这样的设置。但是有一种更好的方法可以做到这一点,这将是我们拼图中缺失的部分:创建我们自己的 Conan 构建助手

Waf 的 Conan 构建助手

我们的构建助手将有两个任务

  • 使用 Conan 构建设置 将所有信息生成 Waf 可以理解的格式。我们将生成另一个 Python 模块,该模块设置 Conan 拥有的构建信息,例如 archbuild_typecompilercompiler.runtime 在 Waf 中。此文件的名称将为 waf_conan_toolchain.py

  • 协助在配方 的 build() 方法中编译库和应用程序。我们将创建一个方法来调用构建系统,从而抽象化对 conanfile 中的 self.run 的调用。

要创建我们自己的构建助手,我们将使用 Conan 的 python_requires() 功能。这样,我们就可以重用其他 conanfile.py 配方中现有的 python 代码。我们将创建一个包含构建助手代码的包,并在使用者中重用它,将其作为 Python 依赖项 导入。conanfile 中有一个 Python 依赖项 的最小实现,但所有重要的代码都将驻留在包含 WafBuildEnvironment 类的 waf_environment.py 文件中。要详细了解 Python 依赖项,请访问 Conan 文档

class PythonRequires(ConanFile):
    name = "waf-build-helper"
    version = "0.1"
    exports = "waf_environment.py"

正如我们所说,所有重要的代码都位于 waf_environment.py 中的 WafBuildEnvironment 类中。让我们看一个简化的构建助手实现示例,该示例仅考虑 Conan build_type。通过调用 WafBuildEnvironment 类的 configure 方法来配置环境。

class WafBuildEnvironment(object):
    def __init__(self, conanfile):
        self._conanfile = conanfile
        self._compiler = self._conanfile.settings.compiler
        self._build_type = self._conanfile.settings.build_type

    def _toolchain_content(self):
        sections = []
        sections.append("def configure(conf):")
        sections.append("    if not conf.env.CXXFLAGS:")
        sections.append("       conf.env.CXXFLAGS = []")
        sections.append("    if not conf.env.LINKFLAGS:")
        sections.append("       conf.env.LINKFLAGS = []")
        if "Visual Studio" in self._compiler:
            if self._build_type == "Debug":
                sections.append("    conf.env.CXXFLAGS.extend(['/Zi', '/FS'])")
                sections.append("    conf.env.LINKFLAGS.extend(['/DEBUG'])")
            elif self._build_type == "Release":
                sections.append("    conf.env.CXXFLAGS.extend(['/O2', '/DNDEBUG'])")
        else:
            if self._build_type == "Debug":
                sections.append("    conf.env.CXXFLAGS.extend(['-g'])")
            elif self._build_type == "Release":
                sections.append("    conf.env.CXXFLAGS.extend(['-O3'])")

        return "\n".join(sections)

    def _save_toolchain_file(self):
        filename = "waf_conan_toolchain.py"
        content = self._toolchain_content()
        output_path = self._conanfile.build_folder
        save(
            os.path.join(output_path, filename),
            content,
            only_if_modified=True)

    def configure(self, args=None):
        self._save_toolchain_file()
        args = args or []
        command = "waf configure " + " ".join(arg for arg in args)
        self._conanfile.run(command)

    def build(self, args=None):
        args = args or []
        command = "waf build " + " ".join(arg for arg in args)
        self._conanfile.run(command)

我们通过 conf.env 变量修改配置环境,根据我们是否使用 Visual Studio 或任何其他编译器设置所有相关的标志以进行 ReleaseDebug 配置。我们还定义了一个 build 方法,该方法运行 Waf 构建工具。

将所有内容整合在一起

构建库

此时,我们能够创建一个使用 Waf 构建系统构建库的配方。项目的结构示例如下所示

waf-mylib/
├── src/
│   └── mylib.cpp
├── include/
│   └── mylib.hpp
├── conanfile.py
└── wscript

使用一个 conanfile.py,该文件声明了构建项目所需的所有工具的依赖项。

waf_import = python_requires("waf-build-helper/0.1@user/channel")

class MyLibConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    name = "mylib-waf"
    version = "1.0"
    license = "MIT"
    author = "Conan Team"
    description = "Just a simple example of using Conan to package a Waf lib"
    exports = "LICENSE"
    exports_sources = "wscript", "src/mylib.cpp", "include/mylib.hpp"
    build_requires = "waf/2.0.18@user/channel"

    def build(self):
        waf = waf_import.WafBuildEnvironment(self)
        waf.configure()
        waf.build()

    def package(self):
        self.copy("*.hpp", dst="include", src="include", keep_path=False)
        self.copy("*.lib", dst="lib", src="build", keep_path=False)
        self.copy("*.dll", dst="bin", keep_path=False)
        self.copy("*.dylib*", dst="lib", src="build", keep_path=False)
        self.copy("*.so", dst="lib", src="build", keep_path=False)
        self.copy("*.a", dst="lib", src="build", keep_path=False)
        self.copy("LICENSE", dst="licenses", src=".", keep_path=False)

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

构建库的最简单的 wscript 可以这样写:

top = '.'
out = 'build'

def options(opt):
    opt.load('compiler_cxx')

def configure(conf):
    conf.load('compiler_cxx')
    conf.load('waf_conan_toolchain', tooldir='.')

def build(bld):
    bld.stlib(target='mylib', source='./src/mylib.cpp')

构建系统的信息通过加载由构建助手创建的文件 waf_conan_toolchain.py 传递。

使用库

现在我们可以使用库,即使我们没有安装 Waf,但为了完整起见,让我们也使用 Waf 来使用它。我们必须在 conanfile.py 中声明所需的 build_requirespython_requires

waf_import = python_requires("waf-build-helper/0.1@user/channel")

class TestWafConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    name = "waf-consumer"
    generators = "Waf"
    requires = "mylib-waf/1.0@user/channel"
    build_requires = "WafGen/0.1@user/channel", "waf/2.0.18@user/channel"
    exports_sources = "wscript", "main.cpp"

    def build(self):
        waf = waf_import.WafBuildEnvironment(self)
        waf.configure()
        waf.build()

并创建一个 wscript,该脚本将所有 Conan 信息加载到 Waf 环境中。

def options(opt):
    opt.load('compiler_cxx')

def configure(conf):
    conf.load('compiler_cxx')
    conf.load('waf_conan_libs_info', tooldir='.')
    conf.load('waf_conan_toolchain', tooldir='.')

def build(bld):
    bld.program(source='main.cpp', target='app', use=bld.env.CONAN_LIBS)

现在,我们可以使用 Conan 构建我们的应用程序

conan source . --source-folder=build
conan install . --install-folder=build
conan build . --build-folder=build

When the build is succesful...

此时,您应该对 Conan 生成器构建助手安装程序 是什么以及它们如何帮助您将几乎任何构建系统集成到 Conan 中有了大致的了解。现在您可以克隆 Conan 示例仓库 以更详细地查看实现,并开始将您最喜欢的构建系统集成到 Conan 包管理器中。