介绍 finalize() 方法:在本地自定义包
我们很高兴推出 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 的缓存效率完整,同时确保缓存完整性得到保留。让我们深入了解一个典型的流程
- 安装包
- 在本地测试包以验证更改是否正确。在此步骤中,文件可能会在 Conan 本地缓存包文件夹中生成。在 Meson 的情况下,
.pyc
- 将修改后的包上传到远程。当使用
-–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 git@github.com: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
)。 - 但我们还在使用
getpass.getuser()
调用结果创建了一个名为whoami.txt
的文件,这是获取当前用户的 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 git@github.com: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
两个文件(file1.txt
和 file2.txt
)打包在原始的 package_folder
(.conan2/p/b/depen856e3d9c06c1f/p
)中
我们还可以看到,在
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_folder
而是从immutable_package_folder
或两者中进行供应商化内容…
immutable_package_folder
也将序列化到conan graph info
的输出中,这允许高级用法。
结论
finalize()
方法是Conan中的一项重大改进,它提供了一种灵活的方法来管理本地自定义,同时确保原始包的不可变性。无论您需要处理执行生成的文 件还是创建自定义配置,finalize()
都提供了一个强大的解决方案,可以同时保持性能和缓存完整性。此方法是Conan工具集中的一个强大补充,使开发人员能够根据其本地环境定制包,而不会影响可靠的包管理所需的 一致性。
敬请期待更多更新,并一如既往地享受使用Conan打包的乐趣!