我们很高兴在 Conan 中推出新的 finalize() 方法,它可以让用户在本地定制包,同时保持包在 Conan 缓存中的不变性。此功能对于需要在本地机器上进行修改的情况至关重要,例如生成配置文件或管理执行生成的类似 Python 的 pycache 这样的文件。

为什么 finalize() 很重要?

包不变性是 Conan 中的一项关键原则,它确保了在不同环境下的始终如一和可靠性。 finalize() 方法尊重这一原则,同时允许进行本地调整,从而维护存储在 Conan 缓存中的原始包的完整性。

finalize() 的常见用例包括

  • 确保缓存完整性:以不改变原始包的方式处理包执行期间生成的文件(例如,pycache)。
  • 本地修改:创建或调整包在本地机器上正常运行所需的配置文件,这些操作无法在创建包时完成。

finalize() 的工作原理

finalize() 方法在包安装到本地缓存中但被使用者使用之前调用。这使您能够实现逻辑来在本地复制或生成文件,而不会影响原始包。例如

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

class Package(ConanFile):
    def package(self):
        copy(self, "*", src=self.source_folder, dst=os.path.join(self.package_folder, "bin"))

    def finalize(self):
        copy(self, "*", src=self.immutable_package_folder, dst=self.package_folder)

在这个例子中,我们引入了一个新的类属性 immutable_package_folder。此属性将始终指向在 package() 方法中使用的原始 self.package_folder

当使用声明了 finalize() 方法的包时,它的 package_folder 将不再指向以前的路径,而是指向缓存中遵循以下结构的新的路径 \<conan\_cache\>/p/b/\<build\_id\>/f。此文件夹不会被跟踪以确保完整性,因此对该路径的任何更改都将对 Conan 透明,并且始终保留为当前用户的本地更改。

这就是为什么在 finalize() 方法中,我们必须确保安装在 immutable_package_folder 中的所有必要文件也都被复制符号链接到新的 package_folder
这就是上面例子中最后一行代码所做的工作。

注意finalize() 方法每个 package_id 只运行一次。这意味着,如果包被多次使用, finalize() 方法只会在第一次运行,因此不同的使用者将使用相同的 最终文件夹

警告:包不能以任何方式改变其“二进制”兼容性或占用空间。否则,使用该包的其他包在上传和重复使用时将无法正常工作,因为它们依赖于未上传的二进制文件。此功能用于自定义运行时或构建工具以正确使用该包,针对该包进行构建并在运行时使用该包。

警告:在 Conan 配方中使用符号链接不是推荐的做法,因为它可移植性差,特别是对于 Windows 生态系统。尽管在特殊情况下和受控情况下使用符号链接可以避免臃肿的库重复,但用户应自行承担使用风险!

示例

Meson 缓存完整性

传统上,在上传包时维护缓存完整性,尤其是对于像 Meson 这样的工具,需要禁用 Python 字节码生成,以防止缓存损坏。但是,使用 finalize(),您可以保持 Python 的缓存效率,同时确保缓存完整性得以保留。让我们详细看看一个典型的流程

  1. 安装包
  2. 本地测试包以验证更改是否正确。在此步骤中,可能会在 Conan 本地缓存包文件夹中生成文件。在 Meson 的情况下,.pyc
  3. 将修改后的包上传到远程服务器。当使用 -–check 参数调用 Conan 时,它将在上传包之前对本地缓存执行完整性检查。以前,这会导致失败,因为缓存现在是“脏”的。这些 .pyc 文件是在 package_folder 中自动创建的,Conan 发现了不匹配。

这是当前 Meson 包方法的简化版本,其中 Python 字节码生成已禁用

def package(self):
    # Create wrapper functions
    save(self, os.path.join(self.package_folder, "bin", "meson"), textwrap.dedent("""\
        #!/usr/bin/env bash
        meson_dir=$(dirname "$0")
        export PYTHONDONTWRITEBYTECODE=1
        exec "$meson_dir/meson.py" "$@"
    """))

这是一个有效的解决方案,用于确保 Conan 缓存中的缓存完整性。请记住,这个问题不仅影响 Meson 维护者,而且影响所有使用 Meson 构建系统作为依赖项的用户。如果没有这种调整,执行 conan cache check-integrity "meson" 会失败。

但这仍然不是一个完美的解决方案,因为它抛弃了所有 Python 缓存效率。这是实现 finalize() 方法的主要原因之一。让我们看看如何修改 Meson 包以保持 Python 的效率和缓存完整性

 1. 首先,我们可以摆脱 PYTHONDONTWRITEBYTECODE 环境变量,因为我们希望 Python 生成 .pyc 文件

def package(self):
    # Create wrapper functions
    save(self, os.path.join(self.package_folder, "bin", "meson"), textwrap.dedent("""\
        #!/usr/bin/env bash
        meson_dir=$(dirname "$0")
        exec "$meson_dir/meson.py" "$@"
    """))

 2. 我们需要创建一个 finalize() 方法,它将复制 immutable_package_folder 的所有内容到最终的隔离的 package_folder

def finalize(self):
    copy(self, "*", src=self.immutable_package_folder, dst=self.package_folder)

如上所述,在 finalize() 方法的上下文中,使用者的 self.package_folder 现在将指向 最终文件夹。对于所有依赖项和它自己的 package_info() 方法,情况都是如此。

通过这种方式,利用这个新方法,我们可以完全隔离 Meson 应用程序,确保缓存完整性得到保留。

包范围内自定义配置文件

finalize() 方法对于需要在本地生成自定义配置文件的包也很有用。例如,一个包可以使用 finalize() 来创建一个包含当前用户名信息的 whoami.txt 文件,确保该文件存在,而不会改变原始包。

让我们使用一个非常简单的例子,该例子位于 examples2 仓库

 1. 克隆 example2 仓库

$ git clone [email protected]:conan-io/example2
$ cd examples2/examples/conanfile/finalize/finalize_method

 2. 在 src/main.cpp 中,我们有一个基本的程序,它读取一个名为 whoami.txt 的文件并将内容打印到标准输出

std::ifstream in("whoami.txt", std::ios_base::in);
std::cout << in.rdbuf() << '\n';

 3. 在 conanfile.py 中,我们重点介绍了 finalize() 方法

def finalize(self):
    copy(self, "*", src=self.immutable_package_folder, dst=self.package_folder)
    save(self, os.path.join(self.package_folder, "bin", "whoami.txt"), getpass.getuser())
  • 我们可以看到,第一行将从 immutable_package_folder(可执行文件本身)复制所有内容到 最终文件夹(请记住,缓存中具有以下模式的路径 \<conan_cache\>/p/b/\<build_id\>/f)。
  • 但我们也创建了一个名为 whoami.txt 的文件,其中包含调用 getpass.getuser() 的结果,这是获取当前用户的 Python 方法。请注意,在 package 方法中,我们无法做到这一点,因为我们想要的是运行机器的 whoami 结果,而不是打包机器的结果。

 4. 创建包并观察轨迹

$ conan create .
...
whoisconan/1.0: Calling package()
...
whoisconan/1.0: Package folder /Users/conan/.conan2/p/b/whoisf8485f8a03c9b/p
whoisconan/1.0: Calling finalize()
whoisconan/1.0: Finalized folder /Users/conan/.conan2/p/b/whoisf8485f8a03c9b/f

我们可以看到,在调用 package() 方法后,将运行 finalize() 方法,将 /p 文件夹的内容复制到 /f 文件夹。

 5. 转到最终文件夹路径

$ cd /Users/conan/.conan2/p/b/whoisf8485f8a03c9b/f/bin

 6. 运行应用程序并观察结果

$ ./whoisconan
conan

我们可以看到,可执行文件将对位于可执行文件旁边的 whoami.txt 文件的内容执行基本的 cat 操作。

此示例展示了 finalize() 方法如何允许包根据本地环境定制文件,或进行任何其他必要的修改,同时保持 Conan 缓存原始且不会改变 package_id

访问具有 finalize() 方法的包的文件夹

finalize() 方法的主要思想是以一种对使用者透明的方式重定向 package_folder

当使用者访问其 package_folder 依赖项时,它的工作方式将与往常一样。此文件夹将包含正常工作所需的内容,同时确保任何本地更改都不会影响存储在缓存中的不可变包。

让我们深入了解一个访问具有 finalize() 方法的包的文件夹的示例。

 1. 为了简化操作,让我们使用 example2 仓库中的 finalize_consume 示例

$ git clone [email protected]:conan-io/example2
$ cd examples2/examples/conanfile/finalize/finalize_consume

此文件夹包含两个包,一个是具有 finalize() 方法的依赖项,另一个是使用者,它只会打印其依赖项文件夹的内容。

 2. 创建依赖项包

$ conan create dependency
...
dependency/1.0: Calling package()
dependency/1.0: package(): Packaged 2 '.txt' files: file2.txt, file1.txt
...
dependency/1.0: Package folder /Users/conan/.conan2/p/b/depen856e3d9c06c1f/p
dependency/1.0: Calling finalize()
dependency/1.0: Running finalize method in /Users/conan/.conan2/p/b/depen856e3d9c06c1f/f
dependency/1.0: Finalized folder /Users/conan/.conan2/p/b/depen856e3d9c06c1f/f
dependency/1.0: Running package_info method in /Users/conan/.conan2/p/b/depen856e3d9c06c1f/f

原始 package_folder.conan2/p/b/depen856e3d9c06c1f/p)中打包了两个文件(file1.txtfile2.txt)。
我们还可以看到,在 package_info 方法的上下文中, package_folder 指向 最终文件夹.conan2/p/b/depen856e3d9c06c1f/f)。从现在开始,这将是在访问其 package_folder 时的真实路径。

 3. 创建使用者包

$ conan create consumer
...
consumer/1.0: Calling generate()
...
consumer/1.0: Running generate method
consumer/1.0: Dependency package_folder: /Users/conan/.conan2/p/b/depen856e3d9c06c1f/f
consumer/1.0: Content in dependency package_folder:
['file1.txt']
consumer/1.0: Dependency immutable_package_folder: /Users/conan/.conan2/p/b/depen856e3d9c06c1f/p
consumer/1.0: Content in dependency immutable_package_folder:
['file2.txt', 'file1.txt', 'conanmanifest.txt', 'conaninfo.txt']
...
consumer/1.0: Generating the package
consumer/1.0: Packaging in folder /Users/conan/.conan2/p/b/consuea78f76f2c500/p

如上所述,当使用者访问其依赖项 package_folder

self.dependencies["dependency"].package_folder

获得的路径将是 最终文件夹,而不是典型的 /p 路径(不可变路径)。
我们还可以看到, package_folder 的内容只包含我们在 dependency 包的 finalize 方法中明确复制的内容。

注意:在大多数情况下,用户无需访问 immutable_package_folder

注意:一旦配方定义了 finalize() 方法,生成的 package_folder 将指向最终文件夹(以 /f 结尾的文件夹)。这意味着即使方法为空,重定向也会执行。

为什么使用 immutable_package_folder

由于原始 package_folder最终文件夹覆盖,如果没有其他属性,将无法访问依赖项的原始包文件夹,甚至在 package_info() 方法中也无法访问。

此功能的创建理念是永远不需要访问原始 package_folder。用户永远不需要知道依赖项使用哪个文件夹(以 /p 结尾的文件夹或以 /f 结尾的文件夹)。这对用户来说始终是透明的。

但是,可能有一些使用新属性的原因。

请记住上一篇博文,我们讨论了新的“供应商功能”。我们解释说,想要供应商包的用户完全负责正确地将依赖项的所需组件封装到供应商包中。
在供应商时,可能有一些原因需要检查依赖项是否具有 finalize() 方法。可以通过比较 dependency.immutable_package_folder == dependency.package_folder 来轻松实现此检查。如果它们不同,则意味着 dependency.package_folder 是一个隔离的文件夹,而不是原始文件夹。
在这种情况下,用户可以决定从 package_folderimmutable_package_folder 或两者中供应商内容。

immutable_package_folder 也将在 conan graph info 的输出中序列化,这允许高级用法。

结论

finalize() 方法是 Conan 中的一项重大改进,它提供了一种灵活的方式来管理本地自定义,同时确保原始包的不可变性。无论您需要处理执行生成的

敬请期待更多更新,一如既往,祝您使用 Conan 打包愉快!