键盘快捷键

按 ← 或 → 在章节之间导航

按 S 或 / 在本书中搜索

按 ? 显示此帮助

按 Esc 隐藏此帮助

将 Crate 发布到 Crates.io

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

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

编写有用的文档注释

准确地为你的包编写文档将帮助其他用户了解如何以及何时使用它们,因此值得花时间编写文档。在第 3 章中,我们讨论了如何使用两个斜杠 // 来注释 Rust 代码。Rust 还有一种特殊的文档注释,方便地称为文档注释(documentation comment),它会生成 HTML 文档。该 HTML 显示公共 API 项的文档注释内容,面向有兴趣了解如何使用你的 crate 而非你的 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
}
Listing 14-1: 一个函数的文档注释

在这里,我们给出了 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:add_one 函数的 HTML 文档

常用章节

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

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

大多数文档注释并不需要所有这些部分,但这是一个很好的检查清单,提醒你代码中用户会有兴趣了解的方面。

将文档注释作为测试

在文档注释中添加示例代码块可以帮助演示如何使用你的库,并且还有一个额外的好处:运行 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,我们将看到 doc 测试会发现示例和代码彼此不同步!

包含项注释

文档注释样式 //! 为包含该注释的项添加文档,而不是为注释之后的项添加文档。我们通常在 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
}
Listing 14-2: 整个 my_crate crate 的文档

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

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

项内部的文档注释对于描述 crate 和模块特别有用。使用它们来解释容器的整体用途,以帮助你的用户理解 crate 的组织方式。

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

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

导出方便的公共 API

发布 crate 时,公共 API 的结构是一个主要考虑因素。使用你 crate 的人不如你熟悉其结构,如果你的 crate 有庞大的模块层次结构,他们可能很难找到想要使用的部分。

在第 7 章中,我们介绍了如何使用 pub 关键字使项成为公共项,以及如何使用 use 关键字将项引入作用域。然而,在你开发 crate 时对你有意义的结构对你的用户来说可能不太方便。你可能希望将结构体组织在包含多个层级的层次结构中,但随后想要使用你在层次结构深处定义的类型的人可能很难发现该类型的存在。他们也可能对必须输入 use my_crate::some_module::another_module::UsefulType; 而不是 use my_crate::UsefulType; 感到烦恼。

好消息是,如果该结构不便于他人从另一个库中使用,你不必重新安排内部组织:相反,你可以通过使用 pub use 重新导出项,使公共结构与私有结构不同。重新导出(Re-exporting)将一个位置的公共项变为另一个位置的公共项,就好像它是在另一个位置定义的一样。

例如,假设我们创建了一个名为 art 的库,用于对艺术概念进行建模。该库中有两个模块:一个包含名为 PrimaryColor 和 SecondaryColor 的两个枚举的 kinds 模块,以及一个包含名为 mix 的函数的 utils 模块,如清单 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!();
    }
}
Listing 14-3: 一个将项组织到 kinds 和 utils 模块中的 art 库

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

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

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

请注意,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);
}
Listing 14-4: 一个使用 art crate 项并导出其内部结构的 crate

清单 14-4 中使用 art crate 的代码作者必须弄清楚 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
    }
}
Listing 14-5: 添加 pub use 语句以重新导出项

cargo doc 为此 crate 生成的 API 文档现在会在首页列出并链接重新导出的项,如图 14-4 所示,使 PrimaryColor 和 SecondaryColor 类型以及 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);
}
Listing 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.toml 中。请注意,此 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.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 字段,你需要给出一个许可证标识符值。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 命令。它现在应该成功了:

$ cargo publish
    Updating crates.io index
   Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
    Packaged 6 files, 1.2KiB (895.0B compressed)
   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)
    Uploaded guessing_game v0.1.0 to registry `crates-io`
note: waiting for `guessing_game v0.1.0` to be available at registry
`crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
   Published guessing_game v0.1.0 at registry `crates-io`

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

发布现有 Crate 的新版本

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

弃用 Crates.io 上的版本

虽然你不能删除 crate 的先前版本,但你可以阻止任何未来的项目将它们作为新依赖添加。当某个 crate 版本因某种原因损坏时,这很有用。在这种情况下,Cargo 支持 yank(撤回)一个 crate 版本。

Yank 一个版本可以防止新项目依赖该版本,同时允许所有已经依赖它的现有项目继续。本质上,yank 意味着所有带有 Cargo.lock 的项目不会被破坏,而未来生成的任何 Cargo.lock 文件都不会使用被 yank 的版本。

要 yank 一个 crate 的版本,在你之前发布的 crate 目录中运行 cargo yank 并指定你想要 yank 的版本。例如,如果我们发布了一个名为 guessing_game 的 crate 版本 1.0.1,并且我们想要 yank 它,那么我们会在 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

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