第14章 更多关于 Cargo 和 Crates.io 的内容
到目前为止,我们只用过 Cargo 最基础的功能来构建、运行和测试代码,但它还能做得更多。本章会介绍 Cargo 其他一些更高级的功能,并展示如何:
- 使用发布配置(release profiles)来自定义构建
- 将库发布到 crates.io
- 使用工作空间(workspaces)来组织更大的项目
- 从 crates.io 安装二进制文件
- 使用自定义的命令来扩展 Cargo
Cargo 能做的事远不止本章涵盖的这些;如果想全面了解它的所有功能,请查看官方文档。
采用发布配置自定义构建
采用发布配置自定义构建
在 Rust 中,发布配置(release profiles)是预定义且可定制的配置文件集,它们包含不同的配置,允许程序员更灵活地控制代码编译的多种选项。每一种配置都独立于其他配置。
Cargo 有两个主要的配置:运行 cargo build 时采用的 dev 配置和运行 cargo build --release 的 release 配置。dev 配置为开发定义了良好的默认配置,release 配置则为发布构建定义了良好的默认配置。
这些配置名称可能很眼熟,因为它们出现在构建的输出中:
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32s
构建输出中的 dev 和 release 表明编译器在使用不同的配置。
当项目的 Cargo.toml 文件中没有显式增加任何 [profile.*] 部分的时候,Cargo 会对每一个配置都采用默认设置。通过增加任何希望定制的配置对应的 [profile.*] 部分,我们可以选择覆盖任意默认设置的子集。例如,如下是 dev 和 release 配置的 opt-level 设置的默认值:
文件名:Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level 设置控制 Rust 会对代码进行何种程度的优化。这个配置的值从 0 到 3。越高的优化级别需要更多的时间编译,所以如果你在进行开发并经常编译,可能会希望在牺牲一些代码性能的情况下减少优化以便编译得快一些。因此 dev 的 opt-level 默认为 0。当你准备发布时,花费更多时间在编译上则更好。只需要在发布模式编译一次,而编译出来的程序则会运行很多次,所以发布模式用更长的编译时间换取运行更快的代码。这正是为什么 release 配置的 opt-level 默认为 3。
我们可以选择通过在 Cargo.toml 增加不同的值来覆盖任何默认设置。比如,如果我们想要在开发配置中使用级别 1 的优化,则可以在 Cargo.toml 中增加这两行:
文件名:Cargo.toml
[profile.dev]
opt-level = 1
这会覆盖默认的设置 0。现在运行 cargo build 时,Cargo 将会使用 dev 的默认配置加上定制的 opt-level。因为 opt-level 设置为 1,Cargo 会比默认进行更多的优化,但是没有发布构建那么多。
对于每个配置的设置和其默认值的完整列表,请参阅Cargo 的文档。
将 crate 发布到 Crates.io
将 crate 发布到 Crates.io
我们曾经在项目中使用 crates.io 上的包作为依赖,不过你也可以通过发布自己的包来向他人分享代码。crates.io 上的 crate 注册表会分发你包的源代码,因此它主要托管开源代码。
Rust 和 Cargo 提供了一些功能,让你发布的包更容易被他人找到和使用。接下来我们会介绍其中一些功能,然后说明如何发布包。
编写有用的文档注释
准确的包文档有助于其他用户理解如何以及何时使用它们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用双斜杠 // 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为文档注释(documentation comments),它们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,它们意在让对库感兴趣的程序员理解如何使用这个 crate,而不是它是如何被实现的。
文档注释使用三条斜杠 ///,而不是两条斜杠,并且支持使用 Markdown 标记来格式化文本。将文档注释放在它所说明的项之前。示例 14-1 展示了名为 my_crate 的 crate 中一个 add_one 函数的文档注释。
文件名:src/lib.rs
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
示例 14-1:一个函数的文档注释
这里,我们描述了 add_one 函数的功能,接着以 Examples 为标题开始了一个小节,并给出了展示如何使用 add_one 函数的代码。可以运行 cargo doc 来根据这些文档注释生成 HTML 文档。这个命令会运行 Rust 自带的 rustdoc 工具,并将生成的 HTML 文档放到 target/doc 目录中。
为了方便起见,运行 cargo doc --open 会为当前 crate 的文档构建 HTML(以及它所有依赖的文档),并在浏览器中打开结果。定位到 add_one 函数时,你会看到文档注释中的文本是如何被渲染的,如图 14-1 所示:
图 14-1:add_one 函数的文档注释 HTML
常用章节
示例 14-1 中使用了 # Examples Markdown 标题在 HTML 中创建了一个以 “Examples” 为标题的部分。其他一些 crate 作者经常在文档注释中使用的部分有:
- Panics:函数在什么情况下可能会
panic!。不希望程序 panic 的调用者应确保不会在这些情况下调用该函数。 - Errors:如果函数返回
Result,说明可能出现哪些错误,以及什么条件会导致返回这些错误,会有助于调用者编写代码,以不同方式处理不同种类的错误。 - Safety:如果调用该函数是
unsafe的(我们会在第二十章讨论不安全代码),这里应解释为什么它是不安全的,并说明函数要求调用者维持哪些不变式。
大多数文档注释不需要包含所有这些章节,但这是一份很好的检查清单,可以提醒你关注用户会想了解的内容。
文档注释作为测试
在文档注释中添加示例代码块,有助于展示如何使用你的库,而且还有一个额外的好处:运行 cargo test 时,文档中的示例代码也会作为测试运行!没有什么比带示例的文档更好了,但也没有什么比示例失效的文档更糟糕了。如果我们对示例 14-1 中 add_one 函数的文档运行 cargo test,会在测试结果中看到如下内容:
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
现在,如果我们修改函数或示例中的任意一方,使示例里的 assert_eq! 触发 panic,然后再次运行 cargo test,就会看到文档测试捕获到了示例与代码不同步的问题!
注释包含项的结构
//! 这种文档注释风格为“包含这些注释的项”添加文档,而不是为“位于这些注释之后的项”添加文档。我们通常在 crate 根文件(按惯例是 src/lib.rs)或模块内部使用这种文档注释,为整个 crate 或整个模块编写说明。
例如,为了添加描述包含 add_one 函数的 my_crate crate 的用途的文档,我们可以在 src/lib.rs 文件开头加入以 //! 开头的文档注释,如示例 14-2 所示:
文件名:src/lib.rs
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
示例 14-2:my_crate crate 整体的文档
注意,最后一行以 //! 开头的注释后面没有任何代码。因为我们使用的是 //! 而不是 ///,所以这里记录的是“包含这条注释的项”的文档,而不是“紧随这条注释之后的项”的文档。在这里,这个项就是 src/lib.rs 文件,也就是 crate 根。这些注释描述的是整个 crate。
运行 cargo doc --open 后,这些注释会显示在 my_crate 文档首页的 crate 公有项列表上方,如图 14-2 所示:
图 14-2:包含 my_crate 整体描述的注释所渲染的文档
项内部的文档注释特别适合用来描述 crate 和模块。使用它们来解释这个容器整体的目的,可以帮助用户理解 crate 的组织方式。
导出实用的公有 API
公有 API 的结构是你发布 crate 时主要需要考虑的。crate 用户没有你那么熟悉其结构,并且如果模块层级过大他们可能会难以找到所需的部分。
第七章介绍了如何使用 pub 关键字使项公开,以及如何使用 use 关键字将项引入作用域。不过,在你开发 crate 时对你来说合理的结构,对用户而言可能并不方便。你可能想把结构体组织成一个包含多层的层级结构,但想使用你定义在深层级中的某个类型的人,可能很难发现它的存在。他们也可能会厌烦不得不写 use my_crate::some_module::another_module::UsefulType;,而不是更简单的 use my_crate::UsefulType;。
好消息是,如果这种结构对外部用户来说并不方便,你也不必重新安排内部组织。你可以使用 pub use 来重导出项,从而建立一个与私有结构不同的公有结构。重导出(re-export) 会把某个位置的公有项在另一个位置再次公开,就好像它原本就定义在那里一样。
例如,假设我们创建了一个名为 art 的库,用来建模艺术概念。在这个库里,有两个模块:kinds 模块包含两个枚举 PrimaryColor 和 SecondaryColor,utils 模块包含一个名为 mix 的函数,如示例 14-3 所示:
文件名:src/lib.rs
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
unimplemented!();
}
}
示例 14-3:一个库 art 其组织包含 kinds 和 utils 模块
图 14-3 展示了 cargo doc 为这个 crate 生成的文档首页。
图 14-3:包含 kinds 和 utils 模块的库 art 的文档首页
注意 PrimaryColor 和 SecondaryColor 类型、以及 mix 函数都没有在首页中列出。我们必须点击 kinds 或 utils 才能看到它们。
依赖这个库的另一个 crate 需要使用 use 语句,把 art 中的项引入作用域,同时必须指定当前定义的模块结构。示例 14-4 展示了一个使用 art crate 中 PrimaryColor 和 mix 的 crate:
文件名:src/main.rs
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
示例 14-4:一个通过导出内部结构使用 art crate 中项的 crate
示例 14-4 中这段代码的作者,必须先弄清楚 PrimaryColor 在 kinds 模块中,而 mix 在 utils 模块中。art crate 的模块结构,对开发 art crate 的人来说比对使用它的人更有意义。这种内部结构并没有给想理解如何使用 art crate 的人提供有价值的信息,反而会带来困惑,因为用户必须先搞清楚该去哪里找需要的内容,还要在 use 语句中写出模块名。
为了从公有 API 中去掉内部组织细节,我们可以修改示例 14-3 中的 art crate,加入 pub use 语句,在顶层重导出这些项,如示例 14-5 所示:
文件名:src/lib.rs
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
// --snip--
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
// --snip--
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
SecondaryColor::Orange
}
}
示例 14-5:增加 pub use 语句重导出项
现在,cargo doc 为这个 crate 生成的 API 文档会在首页列出这些重导出项及其链接,如图 14-4 所示,这使 PrimaryColor、SecondaryColor 和 mix 更容易被找到。
图 14-4:列出重导出项的 art 文档首页
art crate 的用户仍然可以像示例 14-4 那样看到并使用示例 14-3 中的内部结构,也可以使用示例 14-5 中更方便的结构,如示例 14-6 所示:
文件名:src/main.rs
use art::PrimaryColor;
use art::mix;
fn main() {
// --snip--
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
示例 14-6:一个使用 art crate 中重导出项的程序
在存在很多嵌套模块的情况下,使用 pub use 将类型重导出到顶层,会显著改善使用这个 crate 的体验。pub use 的另一个常见用法,是把当前 crate 的某个依赖中的定义重新导出,让那个 crate 的定义成为你这个 crate 公有 API 的一部分。
创建有用的公有 API 结构更像是一门艺术,而不是科学;你可以不断迭代,找到最适合用户的 API。选择 pub use 能让你在 crate 内部结构的组织方式上保持灵活,并将其与你呈现给用户的结构解耦。可以看看你安装过的一些 crate 的源码,观察它们的内部结构是否和公有 API 不同。
创建 Crates.io 账号
在发布任何 crate 之前,你需要在 crates.io 上创建账号并获取一个 API token。为此,请访问 crates.io 首页,并通过 GitHub 账号登录。(目前 GitHub 账号仍然是必需的,不过未来这个网站可能会支持其他注册方式。)登录之后,前往 https://crates.io/me/ 的账户设置页面获取 API key。然后运行 cargo login 命令,并在提示时粘贴你的 API key,如下所示:
$ cargo login
abcdefghijklmnopqrstuvwxyz012345
这个命令会把你的 API token 告诉 Cargo,并将其保存在本地的 ~/.cargo/credentials 文件中。注意,这个 token 是一个秘密,不应该与任何人共享。如果你因为任何原因泄露了它,应立即到 crates.io 撤销并重新生成一个 token。
向新 crate 添加元数据
比如说你已经有一个希望发布的 crate。在发布之前,你需要在 crate 的 Cargo.toml 文件的 [package] 部分增加一些本 crate 的元数据(metadata)。
首先,crate 需要一个唯一的名称。虽然在本地开发 crate 时,你可以随意命名,但 crates.io 上的 crate 名称遵循先到先得的原则。一旦某个 crate 名称已经被占用,就没有其他人能再用这个名称发布 crate。请搜索你想使用的名称,确认它是否已被占用。如果没有,就把 Cargo.toml 中 [package] 里的 name 字段改成你想发布时使用的名称,如下所示:
文件名:Cargo.toml
[package]
name = "guessing_game"
即使你选择了一个唯一的名称,如果此时尝试运行 cargo publish 发布该 crate 的话,会得到一个警告接着是一个错误:
$ cargo publish
Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error (status 400 Bad Request): missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring these fields
这个错误是因为我们缺少一些关键信息:关于该 crate 用途的描述,以及用户可以在什么许可条款下使用它。在 Cargo.toml 中添加一两句简短描述即可,因为它会在搜索结果中和你的 crate 一起显示。对于 license 字段,你需要填写一个许可证标识符值(license identifier value)。Linux 基金会的 Software Package Data Exchange (SPDX) 列出了可用的标识符。例如,如果要指定 crate 使用 MIT License,就添加 MIT 标识符:
文件名:Cargo.toml
[package]
name = "guessing_game"
license = "MIT"
如果你想使用 SPDX 中不存在的许可证,就需要把许可证文本放入一个文件中,将该文件包含到项目里,然后使用 license-file 指定该文件名,而不是使用 license 字段。
关于项目应采用何种许可证的建议超出了本书的范围。很多 Rust 社区成员选择与 Rust 本身相同的许可证,也就是双许可证 MIT OR Apache-2.0。这个例子也说明了,你可以用 OR 分隔多个许可证标识符,来为项目指定多个许可证。
那么,有了唯一的名称、版本号、由 cargo new 新建项目时增加的作者信息、描述和所选择的 license,已经准备好发布的项目的 Cargo.toml 文件可能看起来像这样:
文件名:Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
Cargo 文档 还描述了其他可指定的元数据,它们可以帮助你的 crate 更容易被发现和使用!
发布到 Crates.io
现在,我们已经创建了账号、保存了 API token、为 crate 选好了名字,并填入了所需的元数据,你就可以发布了!发布 crate 会将该 crate 的某个特定版本上传到 crates.io 供他人使用。
发布 crate 时务必小心,因为发布是永久性的。对应版本无法被覆盖,其代码也无法被删除。crates.io 的一个主要目标,是充当代码的永久归档服务器,这样所有依赖 crates.io 上 crate 的项目都能一直正常工作。而如果允许删除版本,就无法实现这一目标。不过,可发布的版本号数量并没有限制。
再次运行 cargo publish 命令。这次它应该会成功:
$ cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
至此,你已经把代码分享给 Rust 社区了,任何人都可以轻松地把你的 crate 加入自己的项目依赖中。
发布现有 crate 的新版本
当你修改了 crate 并准备发布新版本时,修改 Cargo.toml 中 version 的值。请使用语义化版本控制规则,根据修改的类型决定下一个版本号。然后再次运行 cargo publish 来上传新版本。
使用 cargo yank 从 Crates.io 撤回版本
虽然你不能删除 crate 的历史版本,但可以阻止未来的新项目把它加入依赖。这在某个版本因为某种原因损坏时会很有用。为此,Cargo 支持对某个版本执行撤回(yank)。
撤回某个版本会阻止新项目依赖这个版本,不过所有已经依赖它的项目仍然可以下载并继续依赖它。从本质上说,撤回意味着:所有已有 Cargo.lock 的项目都不会因此损坏,而任何新生成的 Cargo.lock 都不会再使用被撤回的版本。
要撤回 crate 的某个版本,请在之前发布该 crate 的目录中运行 cargo yank,并指定要撤回的版本。例如,如果我们发布了名为 guessing_game 的 crate 的 1.0.1 版本,并想撤回它,就在 guessing_game 项目目录中运行:
$ cargo yank --vers 1.0.1
Updating crates.io index
Yank guessing_game@1.0.1
你也可以撤销这次撤回,让项目重新可以依赖该版本,只需在命令中加上 --undo:
$ cargo yank --vers 1.0.1 --undo
Updating crates.io index
Unyank guessing_game@1.0.1
撤回不会删除任何代码。例如,撤回功能并不能删除你不小心上传的秘密信息。如果发生了这种情况,请立刻轮换这些秘密信息。
Cargo 工作空间
Cargo 工作空间
第十二章中,我们构建了一个同时包含二进制 crate 和库 crate 的包。随着项目不断发展,你可能会发现库 crate 变得越来越大,并希望进一步将这个包拆分为多个库 crate。Cargo 提供了一项叫作工作空间(workspace)的功能,可以帮助管理多个彼此相关、并行开发的包。
创建工作空间
工作空间是一组共享同一个 Cargo.lock 和输出目录的包。让我们用工作空间创建一个项目,这里会使用简单代码,以便把注意力集中在工作空间的结构上。组织工作空间的方式有很多种,因此我们只展示一种常见方式。这个工作空间会包含一个二进制 crate 和两个库。二进制 crate 提供主要功能,并依赖这两个库。一个库提供 add_one 函数,另一个库提供 add_two 函数。这三个 crate 都属于同一个工作空间。我们先为工作空间创建一个新目录:
$ mkdir add
$ cd add
接下来,在 add 目录中创建 Cargo.toml 文件,用来配置整个工作空间。这个文件不会有 [package] 部分,而是会以 [workspace] 部分开头,这样我们就能向工作空间添加成员。我们还会把 resolver 的值设为 "3",以便在工作空间中使用 Cargo 最新的依赖解析算法:
文件名:Cargo.toml
[workspace]
resolver = "3"
接下来,在 add 目录运行 cargo new 新建 adder 二进制 crate:
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
在工作空间中运行 cargo new 时,新创建的包也会被自动加入工作空间 Cargo.toml 中 [workspace] 定义的 members 键,像这样:
[workspace]
resolver = "3"
members = ["adder"]
现在,我们可以运行 cargo build 来构建工作空间。你的 add 目录中的文件应如下所示:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
工作空间在顶层只有一个 target 目录,用来存放编译产物;adder 包不会有自己的 target 目录。即使我们在 adder 目录中运行 cargo build,编译产物也仍会放到 add/target,而不是 add/adder/target。Cargo 之所以这样组织工作空间中的 target 目录,是因为工作空间中的 crate 本来就是要彼此依赖的。如果每个 crate 都有各自的 target 目录,那么每个 crate 都不得不重新编译工作空间中的其他 crate,才能把产物放进自己的 target 目录。共享一个 target 目录可以避免不必要的重复构建。
在工作空间中创建第二个包
接下来,让我们在工作空间中创建另一个成员包,并将其命名为 add_one。生成一个名为 add_one 的库 crate:
$ cargo new add_one --lib
Created library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
现在顶层的 Cargo.toml 的 members 列表将会包含 add_one 路径:
文件名:Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]
现在 add 目录应该有如下目录和文件:
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
在 add_one/src/lib.rs 文件中,增加一个 add_one 函数:
文件名:add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
现在,我们可以让二进制包 adder 依赖包含库的 add_one 包了。首先,需要在 adder/Cargo.toml 中把 add_one 添加为一个路径依赖:
文件名:adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Cargo 并不会假定工作空间中的 crate 会彼此依赖,因此我们需要显式声明这些依赖关系。
接下来,让我们在 adder crate 中使用 add_one crate 里的 add_one 函数。打开 adder/src/main.rs 文件,并将 main 函数改为调用 add_one,如示例 14-7 所示。
文件名:adder/src/main.rs
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
示例 14-7:在 adder crate 中使用 add_one 库 crate
在顶层 add 目录中运行 cargo build 来构建工作空间!
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
要从 add 目录运行这个二进制 crate,可以在 cargo run 时通过 -p 参数加上包名,指定要运行工作空间中的哪个包:
$ cargo run -p adder
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
这会运行 adder/src/main.rs 中的代码,其依赖 add_one crate。
依赖外部包
注意,工作空间只在顶层有一个 Cargo.lock 文件,而不是让每个 crate 目录里都各自有一个 Cargo.lock。这能确保所有 crate 使用的都是同一个版本的依赖。如果我们把 rand 包同时加到 adder/Cargo.toml 和 add_one/Cargo.toml 中,Cargo 会把它们都解析为同一个 rand 版本,并把结果记录到唯一的 Cargo.lock 中。让工作空间中的所有 crate 使用相同依赖,意味着这些 crate 会始终彼此兼容。现在我们先把 rand crate 加到 add_one/Cargo.toml 的 [dependencies] 部分,以便能在 add_one crate 中使用它:
文件名:add_one/Cargo.toml
[dependencies]
rand = "0.9.3"
现在我们可以在 add_one/src/lib.rs 中加入 use rand;,然后在 add 目录中运行 cargo build 来构建整个工作空间,这会引入并编译 rand crate。我们会得到一条警告,因为我们并没有实际使用引入到作用域中的 rand:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
顶层的 Cargo.lock 现在已经包含了 add_one 依赖 rand 的信息。不过,即使 rand 在工作空间的某处被使用,我们也不能直接在工作空间里的其他 crate 中使用它,除非也把 rand 加到它们各自的 Cargo.toml 中。例如,如果我们在 adder 包的 adder/src/main.rs 中加入 use rand;,就会得到一个错误:
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
要修复这个错误,就编辑 adder 包的 Cargo.toml 文件,声明 rand 也是它的依赖。构建 adder 包时,会把 rand 加到 Cargo.lock 中 adder 的依赖列表里,但不会额外下载一份新的 rand。Cargo 会确保工作空间中每个使用 rand 的 crate 都使用同一个版本,只要它们声明的是彼此兼容的 rand 版本,这样既节省空间,也确保工作空间中的 crate 彼此兼容。
如果工作空间中的 crate 为同一个依赖指定了彼此不兼容的版本,Cargo 仍然会分别解析它们,但会尽量把版本数量控制得尽可能少。
为工作空间增加测试
作为另一个改进,让我们为 add_one crate 中的 add_one::add_one 函数增加一个测试:
文件名:add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
现在,在顶层 add 目录中运行 cargo test。在这种结构的工作空间里运行 cargo test,会执行工作空间中所有 crate 的测试:
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
输出的第一部分表明 add_one crate 中的 it_works 测试通过了。下一部分表明在 adder crate 中没有找到测试,最后一部分表明 add_one crate 中也没有文档测试。
你也可以选择只运行工作空间中某个特定 crate 的测试,只需在根目录中使用 -p 参数并指定想要测试的 crate 名称:
$ cargo test -p add_one
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
输出表明,cargo test 只运行了 add_one crate 的测试,而没有运行 adder crate 的测试。
如果你打算把工作空间中的 crate 发布到 crates.io 上,那么工作空间中的每个 crate 都需要单独发布。和 cargo test 一样,你可以通过 -p 参数并指定要发布的 crate 名称,来发布工作空间中的某个特定 crate。
现在,试着仿照 add_one crate 的方式,把 add_two crate 也加入工作空间,作为额外练习吧!
随着项目规模增长,可以考虑使用工作空间:每个较小的组件都比一大块代码更容易理解。如果这些组件经常需要一起修改,那么把它们保留在同一个工作空间中,会更容易协调彼此的变更。
使用 cargo install 安装二进制文件
使用 cargo install 安装二进制文件
cargo install 命令允许你在本地安装和使用二进制 crate。它并不是为了替代系统包管理器,而是为 Rust 开发者提供一种方便的方式,用来安装他人在 crates.io 上分享的工具。注意,只有带有二进制目标的包才能被安装。二进制目标是指当 crate 包含 src/main.rs 文件,或将其他文件指定为二进制目标时所生成的可运行程序;这与库目标不同,库目标本身不能单独运行,但适合被其他程序引入。通常,crate 的 README 文件会说明它是库、带有二进制目标,还是两者兼有。
所有通过 cargo install 安装的二进制文件,都会放在安装根目录下的 bin 文件夹中。如果你使用 rustup.rs 安装 Rust,并且没有做任何自定义配置,那么这个目录就是 $HOME/.cargo/bin。请确保这个目录已经加入你的 $PATH,这样你才能运行通过 cargo install 安装的程序。
例如,在第十二章中我们提到过,有一个名为 ripgrep 的 grep 工具 Rust 实现,可用于搜索文件。要安装 ripgrep,可以运行以下命令:
$ cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v14.1.1
Downloaded 1 crate (213.6 KB) in 0.40s
Installing ripgrep v14.1.1
--snip--
Compiling grep v0.3.2
Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v14.1.1` (executable `rg`)
输出的倒数第二行展示了已安装二进制文件的位置和名称;对于 ripgrep 来说,这个可执行文件名是 rg。只要安装目录已经像前面说的那样加入了 $PATH,你就可以运行 rg --help,开始使用这个更快、也更“Rust 风格”的文件搜索工具了!
Cargo 自定义扩展命令
使用自定义命令扩展 Cargo
Cargo 的设计允许你用新的子命令来扩展它,而不必修改 Cargo 本身。如果你的 $PATH 中有一个名为 cargo-something 的二进制文件,那么你就可以像运行 Cargo 子命令一样,通过 cargo something 来运行它。这类自定义命令也会在你运行 cargo --list 时显示出来。Cargo 这种设计带来了一个非常方便的好处:你可以用 cargo install 安装扩展,然后像使用 Cargo 内建工具一样运行它们。
总结
通过 Cargo 和 crates.io 分享代码,是 Rust 生态系统之所以能适用于众多不同任务的重要原因之一。Rust 的标准库小而稳定,但 crate 很容易被分享、使用和改进,而且它们的演进节奏也可以不同于语言本身。不要犹豫,把那些对你有用的代码分享到 crates.io 上;它很可能也会对别人有用!