2.4 补丁依赖项
你可能会时不时遇到一个问题,需要修补一个上游的 crate(即你项目外部依赖的 crate)。我遇到过很多次需要更新我所依赖的另一个 crate 的情况,通常是因为一些小问题。为了修复一个或两个小 bug 而替换上游 crate 的功能通常是不值得的。在某些情况下,你可能可以简单地切换到 crate 的预发布版本,或者你必须自己修补它。
修补上游 crate 的过程大致如下:
1 在 GitHub 上创建一个 fork。
2 在你的 fork 中修补 crate。
3 向上游项目提交一个 pull request。
4 在等待 pull request 合并并发布时,更改你的 Cargo.toml 指向你的 fork。
这个过程并非没有问题。一个障碍是跟踪上游 crate 的变更并按需整合它们。另一个问题是你的补丁可能永远不会被上游接受——在这种情况下,你可能会卡在 fork 上。在处理上游 crate 时,你应该尽可能避免 fork。
Cargo 提供了一种方法,让你可以使用前面的 fork 方法修补 crate,而不会太麻烦;然而,有一些注意事项。为了说明,让我们通过一个典型的修补 crate 的过程来走一遍。在这个例子中,我会创建一个源代码的本地副本,而不是在 GitHub 上创建一个 forked 项目。
让我们修改 num_cpus crate,用我们自己的修补版本替换它。我选择这个 crate 是因为它很简单;它返回逻辑 CPU 核心的数量。首先,创建一个空项目:
接下来,在Cargo.toml中添加num_cpus依赖项:
更新src/main.rs以打印CPU数量:
最后,运行新的crates。
在这个阶段,我们还没有打补丁或进行任何修改。让我们在同一工作目录内创建一个新的库,在这里我们将重新实现相同的API:
$ cargo new num_cpus --lib
接下来,我们将修补默认的src/lib.rs以实现num_cpus::get()。将src/lib.rs更新为以下内容,从num_cpus目录中获取:
现在,我们有了自己的实现版本的num_cpus,它返回一个相当无意义的硬编码值(在这种情况下是100)。回到原始的patch-num-cpus项目目录,修改Cargo.toml以使用替代的crate。
[dependencies]
num_cpus = { path = "num_cpus" }
使用修补过的包运行相同的代码:
这个例子虽然没什么实际意义,但它有效地说明了这个过程。例如,如果你想使用GitHub上的一个分支来修复一个依赖项,你可以直接将你的依赖项指向你的GitHub仓库,像这样(在Cargo.toml文件中):
如果你现在执行 cargo run,你应该再次看到报告的正确CPU数量(因为我创建了上述的分支,但没有进行任何更改)。rev在前面的例子中指的是写作时最新提交的Git哈希值。当你编译项目时,Cargo会从GitHub仓库中获取源代码,检出指定的特定修订版本(可能是提交、分支或标签),并编译那个版本作为依赖项。
2.4.1 间接依赖
有时,你需要修补依赖项的依赖项。也就是说,你可能依赖于一个需要修补的另一个依赖项的crate。以num_cpus为例,该crate目前依赖于libc = "0.2.26"(但仅限于非Windows平台)。为了这个例子,我们可以通过更新Cargo.toml来修补那个依赖项到一个更新的版本,如下所示:
[patch.crates-io]
libc = { git = "https://github.com/rust-lang/libc", tag = "0.2.88" }
在这个例子中,我们将指向libc的Git仓库,并明确指定0.2.88标签。Cargo.toml中的补丁部分是用来修补crates.io注册表本身,而不是直接修补一个包。实际上,你是用你自己的版本替换了libc的所有上游依赖。
请谨慎使用这个功能,只在特殊情况下使用。它不会影响下游依赖,意味着任何依赖于你的包的crate都不会继承这个补丁。这是Cargo目前的一个限制,目前还没有合理的解决办法。在你需要更多控制二级和三级依赖的情况下,你需要要么fork所有相关的项目,要么使用工作区将它们直接包含在你自己的项目中作为子项目(稍后在本章中讨论)。
2.4.2 依赖补丁的最佳实践
在进行依赖补丁时,我们应该尽量遵循以下几条规则,如下所示:
依赖补丁应该是最后的手段,因为随着时间的推移,维护补丁会变得困难。
当需要打补丁时,对于开源项目,特别是当许可证要求时(例如,GPL许可证的代码),提交包含所需更改的补丁到上游。
避免分叉上游的包,如果不可避免,请尽快回到主分支。长期存在的分叉会偏离,并且最终可能成为维护的噩梦。
2.5 发布crate
对于你希望发布到crates.io的项目,过程很简单。一旦你的crate准备就绪,你可以运行cargo publish,Cargo会处理细节。发布crate有几个要求,比如指定许可证,提供某些项目细节如文档和仓库URL,并确保所有依赖项也对crates.io可用。
可以发布到私有注册表;然而,在撰写本文时,Cargo对私有注册表的支持相当有限。因此,建议使用私有Git仓库和标签,而不是依赖crates.io来发布私有crate。
2.5.1 CI/CD集成
对于大多数crate,你可能想要设置一个系统,以便自动发布到crates.io。持续集成/持续部署(CI/CD)系统是现代开发周期的常见组成部分。它们通常由两个不同的步骤组成:
? 持续集成(CI)—一个系统,用于编译、检查和验证对VCS仓库的每次提交
? 持续部署(CD)—一个系统,如果提交或发布通过了CI的所有必要检查,则自动部署
为了演示这一点,我将通过dryoc项目来介绍,该项目使用GitHub Actions(
https://github.com/features/actions),对开源项目免费提供。
在查看代码之前,让我们用典型的Git工作流程描述发布过程,一旦你决定是时候发布一个版本:
1 如果需要,更新Cargo.toml中的版本属性为你想要发布的版本。
2 CI系统将运行,验证所有测试和检查是否通过。
3 你将创建并推送一个发布标签(使用版本前缀,如git tag -s vX.Y.Z)。
4 CD系统将运行,构建标记的发布,并使用cargo publish发布到crates.io。
5 在新的提交中更新Cargo.toml中的版本属性,为下一个发布周期做准备。
注意:已发布的crates是不可变的,因此任何更改都将需要向前滚动。一旦crates被发布到crates.io,就无法回滚或对crates进行更改。
让我们来检查一下dryoc crate,它使用GitHub Actions实现了这个模式。
有两个Actions需要查看:
-
.github/workflows/build-and-test.yml —— 为一系列功能、平台和工具包的组合构建和运行测试(http://mng.bz/VRmP)
-
.github/workflows/publish.yml —— 为符合v*模式的标记发布构建和运行测试,将crate发布到crates.io(http://mng.bz/xjAW)
清单2.4显示了构建作业参数,包括功能、通道和平台矩阵。这些作业使用brndnmtthws/rust-action GitHub Action(http://mng.bz/A87z)来设置Rust环境。
以下列表展示了构建、测试、格式化和运行Clippy(在第三章讨论过)的各个步骤。
以下列表显示了发布我们的crates所需的步骤。
注意:GitHub的Actions目前不支持在使用独立阶段时(例如,在进行部署阶段之前等待构建阶段成功)设置发布门控。为了实现这一点,你必须在推送任何标签之前验证构建阶段是否成功。
在最终的发布步骤中,你需要为https://crates.io提供一个令牌。这可以通过创建一个crates.io账户,从crates.io账户设置中生成一个令牌,然后将其添加到GitHub仓库设置中的GitHub秘密存储来完成。
2.6 链接到C语言库
有时你可能会发现自己需要使用非Rust代码中的外部库。这通常是通过外部函数接口(FFI)来实现的。FFI是一种相当标准的方式来实现跨语言的互操作性。我们将在第4章中更详细地再次讨论FFI。
让我们通过一个简单的例子来演示如何从一个非常流行的C语言库:zlib中调用函数。选择zlib是因为它几乎无处不在,这个例子应该可以在任何安装了zlib的平台上轻松地直接运行。我们将在Rust中实现两个函数:compress() 和 uncompress()。以下是zlib库中的定义(为了本示例的目的已经简化)。
首先,我们将使用extern在Rust中定义C接口。
我们已经将libc作为依赖项包含在内,它在Rust中提供了与C兼容的类型。
当你链接到C库时,你将希望使用来自libc的类型以保持兼容性。如果不这样做可能会导致未定义的行为。我们定义了来自zlib的三个实用函数:compress、compressBound和uncompress。
链接属性告诉rustc我们需要将这些函数链接到zlib。这相当于在链接时添加-lz标志。在macOS上,你可以使用otool -L来验证这一点,如下所示的代码(在Linux上,使用ldd,在Windows上,使用dumpbin):
接下来,我们需要编写Rust函数来封装C函数,以便从Rust代码中调用。在Rust中直接调用C函数被认为是不安全的,因此你必须将调用封装在一个unsafe {}块中。
前面函数的zlib_uncompress版本几乎相同,除了我们需要为目的地缓冲区提供自己的长度。最后,我们可以展示如下列表所示的使用方法。
处理FFI(外部函数接口)时最大的挑战是一些C API的复杂性以及映射各种类型和函数。为了解决这个问题,你可以使用rust bindgen工具,这在第4章中有更详细的讨论。
2.7 二进制分发
Rust的二进制文件由给定平台的所有Rust依赖项组成,作为单一二进制文件——不包括C运行时——以及可能已经动态链接的任何非Rust库。你可以构建与C运行时静态链接的二进制文件,但默认情况下,这是可选的。因此,在分发Rust二进制文件时,你需要考虑是否希望静态链接C运行时或依赖系统的运行时。
这些二进制文件本身是平台依赖的。它们可以为不同的平台进行交叉编译,但你不能将不同的架构或平台与同一个Rust二进制文件混合。为基于Intel的x64-64 CPU编译的二进制文件不会在基于ARM的平台(如AArch64,也称为ARMv8)上运行,除非使用某种类型的仿真。为macOS编译的二进制文件不会在Linux上运行。
一些操作系统供应商,特别是苹果的macOS,为其他CPU平台提供仿真。可以使用苹果的Rosetta工具自动在ARM上运行x86-64二进制文件,这应该会自动发生。有关macOS二进制分发的更多详细信息,请查阅苹果的开发者文档,网址为http://mng.bz/ZRvP。在大多数情况下,你会想坚持使用你正在使用的平台的默认设置,但也有例外。
如果你来自像Go这样的语言,你可能已经习惯了分发预编译的二进制文件而不必担心C运行时。与Go不同,Rust需要C运行时,并且默认使用动态链接。
2.7.1 跨平台编译
你可以使用Cargo来为不同的目标平台交叉编译二进制文件,但前提是该目标平台有编译器支持。例如,你可以在Windows上轻松编译Linux二进制文件,但在Linux上编译Windows二进制文件就不那么容易了(但并非不可能)。
你可以使用rustup来列出你的宿主平台上可用的目标平台。
$ rustup target list
rustup target list
aarch64-apple-darwin
aarch64-apple-ios
aarch64-fuchsia
aarch64-linux-android
aarch64-pc-windows-msvc
..
你可以使用rustup target add <目标>来安装不同的目标,然后使用cargo build --target <目标>来为特定目标构建。例如,在我的基于Intel的macOS机器上,我可以运行以下命令来编译AArch64(M1芯片使用的)的二进制文件:
然而,如果我尝试运行这个二进制文件,它会失败:
$ ./target/aarch64-apple-darwin/debug/simple-project-bash: ./target/aarch64-apple-darwin/debug/simple-project: Bad CPU type in
executable
如果我能够访问一台AArch64架构的macOS设备,我可以将这个二进制文件复制到那台机器上,并且在那里成功运行它。
2.7.2 构建静态链接的二进制文件
普通的Rust二进制文件包含了所有编译后的依赖项,除了C运行时库。在Windows和macOS上,分发预编译的二进制文件并链接到操作系统的C运行时库是正常的。然而,在Linux上,大多数包是由发行版的维护者从源代码编译的,发行版负责管理C运行时。
当在Linux上分发Rust二进制文件时,你可以根据你的偏好选择使用glibc或musl。Glibc是大多数Linux发行版上的默认C库运行时。然而,当我想分发时,我推荐静态链接到musl。
Linux二进制文件为了最大的可移植性。实际上,在尝试在某些目标上静态链接时,Rust 假设你想要使用 musl。
注意:在某些情况下,musl 与 glibc 的行为略有不同。这些差异在 musl wiki 上有文档记录,网址为 http://mng.bz/Rm7K。
你可以通过这样的方式使用 target-feature 标志指示 rustc 使用静态 C 运行时:
在这段代码中,我们通过RUSTFLAGS环境变量将-C target-feature=+crt-static传递给rustc,这将被Cargo解释并传递给rustc。
我们使用以下代码在x86-64 Linux上静态链接musl:
要明确禁用静态链接,请使用 RUSTFLAGS="-C target-feature=-crt-static" 替代(通过将加号[+]改为减号[-])。这可能对于默认静态链接的目标是可取的——如果不确定,请使用默认参数。
或者,你可以通过 ~/.cargo/config 指定 Cargo 的 rustc 标志:
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
当添加到 ~/.cargo/config 中时,上述代码将指示 rustc 在使用 x86_64-pc-windows-msvc 目标时静态链接。
2.8 文档化 Rust 项目
Rust 默认随 Rust 一起提供的代码文档工具称为 rustdoc。
如果你使用过其他项目中的代码文档工具(例如,Javadoc、docstring 或 RDoc),那么使用 rustdoc 将会很自然。
使用 rustdoc 就像在代码中添加注释并生成文档一样简单。
让我们快速过一个示例。首先,创建一个库:
现在,让我们编辑 src/lib.rs 文件来添加一个名为 mult 的函数,它接受两个整数(a 和 b)并将它们相乘。我们还将添加一个测试:
我们还没有添加任何文档。在我们添加之前,让我们使用Cargo生成一些空文档。
现在,你应该能在目标目录下看到生成的HTML文档。如果你想在浏览器中打开这些文档,可以打开
target/doc/src/rustdoc_example/lib.rs.html来查看它们。结果应该看起来像图2.1。默认的文档是空的,但你可以看到文档中列出了公共函数mult。
接下来,让我们给我们的项目添加一个编译器属性和一些文档。更新src/lib.rs,使其看起来像这样:
TIP Rust文档使用CommonMark格式编写,这是Markdown的一个子集。CommonMark的参考可以在
https://commonmark.org/help找到。
如果你重新运行cargo doc并用新创建的代码文档打开它在浏览器中,你将看到图2.2所示的输出。对于发布到crates.io的crates,有一个配套的rustdoc网站,它会自动为crates生成并托管文档,网址为https://docs.rs。例如,dryoc crate的文档可以在https://docs.rs/dryoc找到。
在已记录的库中,你应该更新Cargo.toml以包含文档属性,该属性链接到项目的文档。这对于那些人在crates.io上寻找资源的人来说是有帮助的。例如,dryoc crate 在 Cargo.toml 中有以下内容:
[package]
name = "dryoc"
documentation = "https://docs.rs/dryoc"
你不需要做任何其他事情就可以使用docs.rs。当新版本发布到crates.io时,网站会自动生成更新的文档。