向 Crates.io 发布 crate

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

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

编写有用的文档注释

准确地记录你的包将帮助其他用户了解如何以及何时使用它们,因此投入时间编写文档是值得的。在第 3 章中,我们讨论了如何使用双斜杠 // 对 Rust 代码进行注释。Rust 还有一种专门用于文档的注释,俗称 文档注释,它可以生成 HTML 文档。HTML 会显示公共 API 项的文档注释内容,这些内容面向的是那些对如何 使用 你的 crate 感兴趣的程序员,而不是对 crate 如何 实现 感兴趣的程序员。

文档注释使用三个斜杠 /// 代替两个斜杠,并支持 Markdown 标记进行文本格式化。将文档注释放在要记录的项之前。清单 14-1 显示了 crate my_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 的文档(以及你的 crate 的所有依赖项的文档)构建 HTML,并在 Web 浏览器中打开结果。导航到 add_one 函数,你将看到文档注释中的文本是如何渲染的,如图 14-1 所示

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

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

常用章节

我们在清单 14-1 中使用了 # Examples Markdown 标题在 HTML 中创建了一个标题为“Examples.”的章节。以下是 crate 作者在其文档中常用的一些其他章节

  • Panic:被记录的函数可能发生 panic 的场景。不希望程序发生 panic 的函数调用者应该确保他们不会在这些情况下调用该函数。
  • 错误:如果函数返回 Result,描述可能发生的错误类型以及哪些条件可能导致返回这些错误,这对调用者非常有用,他们可以编写代码以不同的方式处理不同类型的错误。
  • 安全性:如果函数是 unsafe 的(我们在第 20 章讨论不安全性),应该有一个章节解释为什么函数是 unsafe 的,并涵盖函数期望调用者维护的不变式(invariants)。

大多数文档注释不需要包含所有这些章节,但这不失为一个很好的检查清单,提醒你用户会关注你的代码的哪些方面。

将文档注释作为测试

在文档注释中添加示例代码块可以帮助演示如何使用你的库,这样做还有一个额外的好处:运行 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 所示。

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 来重新导出(re-export)项,以创建一个与你的私有结构不同的公共结构。重新导出 是将一个位于某处的公共项在另一个位置也公开,就好像该项是在后一个位置定义的一样。

例如,假设我们创建了一个名为 art 的库,用于建模艺术概念。在这个库中,有两个模块:一个名为 kinds 的模块,包含两个名为 PrimaryColorSecondaryColor 的枚举;以及一个名为 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:一个将项组织到 kindsutils 模块中的 art

图 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 示例

文件名: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 中使用 art crate 的代码作者必须弄清楚 PrimaryColorkinds 模块中,而 mixutils 模块中。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 所示,这使得 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 所示

文件名: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 公共 API 的一部分。

创建有用的公共 API 结构更像是一门艺术而不是科学,你可以迭代地尝试,找到最适合你的用户的 API。选择使用 pub use 使你在内部组织 crate 时更灵活,并将内部结构与你向用户展示的公共接口解耦。查看一些你已经安装的 crate 的代码,看看它们的内部结构是否与其公共 API 不同。

设置 Crates.io 账户

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

$ cargo login
abcdefghijklmnopqrstuvwxyz012345

这个命令会告诉 Cargo 你的 API token,并将其本地存储在 ~/.cargo/credentials 文件中。请注意,这个 token 是一个 秘密:不要与任何人分享。如果出于任何原因与任何人分享了它,你应该立即撤销它,并在 crates.io 上生成一个新的 token。.

为新 crate 添加元数据

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

你的 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.net.cn/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.net.cn/cargo/reference/manifest.html for more information on configuring these fields

这会导致错误,因为你缺少一些关键信息:需要提供描述和许可,以便人们知道你的 crate 的功能以及使用条款。在 Cargo.toml 中,添加一个一两句话的描述,因为它会显示在 crate 的搜索结果中。对于 license 字段,你需要提供一个 许可标识符值Linux 基金会的软件包数据交换 (SPDX) 列出了你可以用于此值的标识符。例如,要指定你使用 MIT 许可证授权你的 crate,添加 MIT 标识符

文件名:Cargo.toml

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

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

关于哪种许可证适合你的项目的指导超出了本书的范围。Rust 社区中的许多人像 Rust 项目本身一样,通过使用 MIT OR Apache-2.0 的双许可证来授权他们的项目。这种做法表明,你也可以指定由 OR 分隔的多个许可证标识符,为你的项目拥有多个许可证。

添加了唯一的名称、版本、描述和许可证后,准备发布的项目的 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 会将特定版本上传到 crates.io供他人使用。

请注意,发布是 永久性的版本永远不能被覆盖,代码也不能被删除。crates.io 的一个主要目标是作为代码的永久归档,以便所有依赖来自 crates.io 的 crate 的所有项目的构建能够

继续工作。允许删除版本将使实现该目标变得不可能。然而,你可以发布的 crate 版本数量没有限制。

$ 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)

再次运行 cargo publish 命令。现在应该会成功了

恭喜!你现在已经与 Rust 社区分享了你的代码,任何人都可以轻松地将你的 crate 添加为他们项目的依赖项。

发布现有 crate 的新版本

当你对 crate 进行了更改并准备发布新版本时,你需要更改 Cargo.toml 文件中指定的 version 值并重新发布。使用 语义化版本规范 来根据你进行的更改类型决定合适的下一个版本号。然后运行 cargo publish 上传新版本。

使用 cargo yank 从 Crates.io 废弃版本

虽然你不能删除 crate 的旧版本,但可以阻止任何未来的项目将其作为新的依赖项添加。当某个 crate 版本由于某种原因损坏时,这非常有用。在这种情况下,Cargo 支持废弃(yank)crate 版本。

废弃(Yanking) 一个版本可以阻止新项目依赖该版本,同时允许所有已依赖该版本的现有项目继续使用。本质上,废弃意味着所有带有 Cargo.lock 文件的项目不会因此中断,并且将来生成的任何 Cargo.lock 文件将不会使用被废弃的版本。

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

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

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

通过给命令加上 --undo,你也可以撤销废弃,允许项目再次依赖该版本