将Crate发布到Crates.io

我们已经使用了来自crates.io的包作为我们项目的依赖,但你也可以通过发布自己的包来与他人共享代码。crates.io的包注册表分发你的包的源代码,因此它主要托管开源代码。

Rust 和 Cargo 拥有使您的发布包更容易被人发现和使用的功能。我们接下来将讨论其中的一些功能,然后解释如何发布一个包。

编写有用的文档注释

准确地记录你的包将帮助其他用户知道如何以及何时使用它们,因此值得花时间编写文档。在第 3 章中,我们讨论了如何使用两个斜杠 // 来注释 Rust 代码。Rust 还有一种特别的注释用于文档,方便地称为 文档注释,它将生成 HTML 文档。HTML 显示了文档注释中公共 API 项的内容,这些内容是为那些对如何 使用 你的 crate 感兴趣的程序员准备的,而不是对你的 crate 是如何 实现 的感兴趣。

文档注释使用三个斜杠,///,而不是两个,并支持使用 Markdown 标记来格式化文本。将文档注释放在它们所描述的项之前。列表 14-1 显示了在名为 my_crate 的 crate 中的 add_one 函数的文档注释。

Filename: 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
}
Listing 14-1: A documentation comment for a function

这里,我们描述了 add_one 函数的作用,开始一个标题为 Examples 的部分,然后提供演示如何使用 add_one 函数的代码。我们可以通过运行 cargo doc 从这个文档注释生成 HTML 文档。此命令运行随 Rust 分发的 rustdoc 工具,并将生成的 HTML 文档放在 target/doc 目录中。

为了方便起见,运行 cargo doc --open 将为您的当前 crate 的文档(以及您的所有 crate 依赖项的文档)构建 HTML,并在网页浏览器中打开结果。导航到 add_one 函数,您将看到文档注释中的文本是如何呈现的,如图 14-1 所示:

Rendered HTML documentation for the `add_one` function of `my_crate`

图14-1:HTML 文档中的 add_one 函数

Commonly Used Sections

我们在清单 14-1 中使用了 # Examples Markdown 标题来创建一个标题为“Examples”的 HTML 部分。这里有一些其他部分,crate 作者通常会在他们的文档中使用:

  • 恐慌: 该函数可能引发恐慌的情况。不希望程序恐慌的调用者应确保不在这些情况下调用该函数。
  • 错误: 如果函数返回一个 Result,描述可能会发生哪些类型的错误以及哪些条件可能导致这些错误的发生,对于调用者来说是有帮助的,这样他们可以编写代码以不同方式处理不同类型的错误。
  • 安全性: 如果函数调用是不安全的(我们在第20章中讨论不安全性),应该有一个部分解释为什么该函数是不安全的,并涵盖函数期望调用者维护的不变性。

大多数文档注释并不需要所有这些部分,但这是一个很好的检查列表,可以提醒你用户会对你的代码的哪些方面感兴趣。

Documentation Comments as Tests

在文档注释中添加示例代码块可以帮助演示如何使用您的库,这样做还有一个额外的好处:运行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,我们会看到文档测试捕获了示例和代码不同步的问题!

Commenting Contained Items

文档注释风格 //! 会将文档添加到包含注释的项中,而不是添加到注释之后的项中。我们通常在 crate 根文件(按惯例为 src/lib.rs)或模块内部使用这些文档注释,以记录整个 crate 或模块。

例如,要添加描述包含 add_one 函数的 my_crate crate 目的的文档,我们在 src/lib.rs 文件的开头添加以 //! 开头的文档注释,如清单 14-2 所示:

Filename: 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
}
Listing 14-2: Documentation for the my_crate crate as a whole

注意,在以 //! 开头的最后一行之后没有任何代码。因为 我们用 //! 而不是 /// 开始了注释,所以我们正在为包含此注释的项编写文档,而不是为此注释之后的项编写文档。在 这种情况下,该项是 src/lib.rs 文件,它是 crate 根。这些 注释描述了整个 crate。

当我们运行cargo doc --open时,这些注释将显示在my_crate文档的首页上,在crate的公共项目列表上方,如图14-2所示:

Rendered HTML documentation with a comment for the crate as a whole

图14-2: my_crate 的渲染文档,包括描述整个 crate 的注释

文档注释在项内非常有用,特别是用于描述 crate 和模块。使用它们来解释容器的总体目的,以帮助用户理解 crate 的组织结构。

使用 pub use 导出方便的公共 API

发布 crate 时,您的公共 API 的结构是一个重要的考虑因素。使用您的 crate 的人对结构的熟悉程度不如您,如果您的 crate 拥有大型模块层次结构,他们可能会难以找到他们想要使用的部分。

在第 7 章中,我们介绍了如何使用 pub 关键字使项目公开,以及如何使用 use 关键字将项目引入作用域。然而,你在开发一个 crate 时觉得合理的结构可能对你的用户来说并不方便。你可能希望将你的结构组织成包含多个层次的层次结构,但这样可能会使想要使用你定义的深层类型的人难以找到该类型。他们可能也会因为必须输入 use my_crate::some_module::another_module::UsefulType; 而不是 use my_crate::UsefulType; 而感到烦恼。

好消息是,如果该结构便于其他库使用,您不必重新安排内部组织:相反,您可以使用pub use重新导出项目,以创建一个与您的私有结构不同的公共结构。重新导出会将一个位置的公共项目在另一个位置也设为公共,就像它是在另一个位置定义的一样。

例如,假设我们创建了一个名为 art 的库,用于建模艺术概念。 在这个库中有两个模块:一个 kinds 模块包含两个枚举类型 名为 PrimaryColorSecondaryColor,以及一个 utils 模块包含一个 名为 mix 的函数,如清单 14-3 所示:

Filename: 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!();
    }
}
Listing 14-3: An art library with items organized into kinds and utils modules

图14-3显示了由cargo doc生成的此crate的文档首页的样子:

Rendered documentation for the `art` crate that lists the `kinds` and `utils` modules

图14-3: art 文档的首页,列出了 kindsutils 模块

请注意,PrimaryColorSecondaryColor 类型没有列在首页上,mix 函数也没有。我们必须点击 kindsutils 才能看到它们。

另一个依赖于这个库的crate需要use语句,将art中的项引入作用域,并指定当前定义的模块结构。清单14-4展示了使用art crate中的PrimaryColormix项的crate示例:

Filename: src/main.rs
use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}
Listing 14-4: A crate using the art crate’s items with its internal structure exported

列表 14-4 中使用了 art crate 的代码作者不得不弄清楚 PrimaryColor 位于 kinds 模块中,而 mix 位于 utils 模块中。art crate 的模块结构对于开发 art crate 的开发者来说更为重要,而对于使用它的开发者来说则不然。内部结构对于试图了解如何使用 art crate 的人来说并不包含任何有用的信息,反而会造成混淆,因为使用它的开发者必须弄清楚去哪里查找,并且必须在 use 语句中指定模块名称。

为了从公共 API 中移除内部组织,我们可以修改 art crate 代码(如清单 14-3 所示),添加 pub use 语句以在顶级重新导出这些项,如清单 14-5 所示:

Filename: 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
    }
}
Listing 14-5: Adding pub use statements to re-export items

cargo doc 为这个 crate 生成的 API 文档现在将在首页列出并链接重新导出的内容,如图 14-4 所示,使 PrimaryColorSecondaryColor 类型以及 mix 函数更容易找到。

Rendered documentation for the `art` crate with the re-exports on the front page

图14-4: art 文档的首页,列出了重新导出的内容

art crate 的用户仍然可以查看和使用来自列表 14-3 的内部结构,如列表 14-4 所示,或者他们可以使用列表 14-5 中更方便的结构,如列表 14-6 所示:

Filename: src/main.rs
use art::mix;
use art::PrimaryColor;

fn main() {
    // --snip--
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}
Listing 14-6: A program using the re-exported items from the art crate

在有许多嵌套模块的情况下,使用 pub use 在顶层重新导出类型可以显著改善使用该 crate 的人的体验。另一种常见的 pub use 用法是重新导出依赖项的定义,使这些定义成为你 crate 的公共 API 的一部分。

创建一个有用的公共 API 结构更像是一门艺术而不是科学,你可以通过迭代来找到最适合你用户的 API。选择 pub use 为你提供了在 crate 内部结构上的灵活性,并将这种内部结构与你呈现给用户的部分解耦。查看一些你已安装的 crate 的代码,看看它们的内部结构是否与其公共 API 不同。

设置 Crates.io 账户

在发布任何 crate 之前,您需要在 crates.io 上创建一个帐户并获取一个 API 令牌。为此,请访问 crates.io 的首页并通过 GitHub 帐户登录。(目前 GitHub 帐户是必需的,但该网站将来可能会支持其他创建帐户的方式。)登录后,请访问您的帐户设置页面 https://crates.io/me/ 并获取您的 API 密钥。然后运行 cargo login 命令,并在提示时粘贴您的 API 密钥,如下所示:

$ cargo login
abcdefghijklmnopqrstuvwxyz012345

此命令将告知 Cargo 您的 API 令牌,并将其存储在 ~/.cargo/credentials 中。请注意,此令牌是 机密:不要与任何人共享。如果您因任何原因与任何人共享了此令牌,您应该撤销它并在 crates.io 生成一个新的令牌。

为新 crate 添加元数据

假设你有一个想要发布的 crate。在发布之前,你需要在 crate 的 Cargo.toml 文件的 [package] 部分添加一些元数据。

您的crate需要一个独特的名称。当您在本地开发crate时, 您可以随意命名crate。然而,crates.io 上的crate名称是按先到先得的原则分配的。一旦某个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 field

这会出错,因为你缺少一些关键信息:需要提供描述和许可证,这样人们才能知道你的crate是做什么的,以及他们可以在什么条件下使用它。在Cargo.toml中,添加一个简短的描述,因为这将出现在搜索结果中与你的crate一起。对于license字段,你需要提供一个许可证标识符值Linux基金会的软件包数据交换(SPDX)列出了你可以用于此值的标识符。例如,要指定你使用MIT许可证授权你的crate,添加MIT标识符:

文件名: Cargo.toml

[package]
name = "guessing_game"
license = "MIT"

如果您想使用未出现在 SPDX 中的许可证,您需要将该许可证的文本放在一个文件中,将该文件包含在您的项目中,然后使用 license-file 来指定该文件的名称,而不是使用 license 键。

关于选择适合您项目的许可证的指导超出了本书的范围。Rust 社区中的许多人通过使用 MIT OR Apache-2.0 双许可证来授权他们的项目,这种做法与 Rust 本身相同。这表明您还可以通过用 OR 分隔多个许可证标识符来为您的项目指定多个许可证。

有了一个独特的名称、版本号、描述和许可证后,准备发布的项目的 Cargo.toml 文件可能如下所示:

文件名: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
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令牌,选择了包的名字,并指定了所需的元数据,你已经准备好发布了!发布一个包会将特定版本上传到crates.io供其他人使用。

小心,因为发布是永久的。版本不能被覆盖,代码也不能被删除。crates.io的一个主要目标是作为代码的永久存档,以确保依赖于来自crates.io的crate的所有项目的构建都能继续工作。允许删除版本将使实现这一目标变得不可能。但是,你可以发布的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,但可以防止任何未来的项目将它们添加为新的依赖项。这在某个 crate 版本因某种原因损坏时非常有用。在这种情况下,Cargo 支持 撤销 一个 crate 版本。

撤销一个版本可以防止新项目依赖于该版本,同时允许所有现有的依赖于它的项目继续使用。本质上,撤销意味着所有具有 Cargo.lock 的项目都不会中断,而任何未来生成的 Cargo.lock 文件都不会使用已撤销的版本。

要撤销某个版本的 crate,在你之前发布的 crate 的目录中运行 cargo yank 并指定要撤销的版本。例如,如果我们发布了一个名为 guessing_game 版本 1.0.1 的 crate 并且想要撤销它,在 guessing_game 的项目目录中我们会运行:

$ cargo yank --vers 1.0.1
    Updating crates.io index
        Yank guessing_game@1.0.1

通过在命令中添加 --undo,您还可以撤销一次 yank 操作,并允许项目再次依赖于某个版本:

$ cargo yank --vers 1.0.1 --undo
    Updating crates.io index
      Unyank guessing_game@1.0.1

A yank 不会 删除任何代码。例如,它不能删除意外上传的秘密。如果发生这种情况,您必须立即重置这些秘密。