将代码箱发布到 Crates.io

Publishing a Crate to Crates.io

咱们已将 crates.io 上的一些包,用作了咱们项目的依赖,而通过发布自己的包,咱们还可以与其他人分享咱们自己的代码。位于 crates.io 网站的代码箱登记,会分发咱们包的源码,因此其主要保存开放源码的代码。

Rust 与 Cargo,均有着令到咱们所发布的包,易于为他人找到并使用的一些特性。咱们将讲到其中一些特性,并讲解怎样发布包,how to publish a package。

制作有用的文档注释

Making Useful Documentation Comments

准确地为咱们的包编写文档,将帮助到其他使用者获悉怎样及何时来使用他们,因此投入时间来编写文档是值得的。第 3 章中,咱们曾讨论过如何使用双斜杠 //来注释 Rust 代码。Rust 还有用于文档的一种将生成 HTML 文档的特殊注释,而被方便地称作 文档注释,documentation comment。这些 HTML 会显示出公开 API 项目的文档注释内容,这些内容是为对了解怎样 使用,use 咱们的代码箱,而非咱们代码箱如何实现感兴趣的程序员所准备的。

文档注释用的是三斜杠 /// 而非双斜杠,并支持用于格式化文本的 Markdown 写法。要把文档注释恰好放在他们要注释的项目前,而紧接着注释项目。下面清单 14-1 给出了名为 cargo_features_demo 代码箱中,add_one 函数的文档注释。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
/// 将一加到所给数字。
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = cargo_features_demo::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 将构建出当前代码箱文档(以及咱们代码箱全部依赖的文档)的 HTML,并随后在 web 浏览器中打开得到的结果。导航到那个 add_one 函数,咱们将看到文档注释中的文本如何渲染出来,如下图片 14-01 中所示:

add_one 函数的 HTML 文档

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

经常用到的小节

Commonly Used Sections

咱们曾使用清单 14-1 中的 # Examples Markdown 标题,来创建出 HTML 中带有标题 “Examples” 的小节。下面是代码箱作者们,经常在他们文档中用到的一些其他小节:

  • Panics:被文档注释的函数可能终止运行的情形。那些不愿其程序终止运行的调用者,就应确保在这些情形下他们不会调用该函数;
  • Errors:若函数返回了 Result,那么描述出可能发生的各种错误,及何种条件下会造成那些错误的返回,就能有效帮助到调用者,从而他们可以编写出以不同方式,处理不同类别错误的代码;
  • Safety:若函数调用起来是 unsafe 的(在第 19 章咱们就会讨论到不安全),那么就应有一个解释为何该函数不安全,并说明该函数期望调用者要遵守哪些不变因素的小节,if the funciton is unsafe to call(we discuss unsafety in Chapter 19), there should be a section explaining why the function is unsafe and covering the invariants that the function expects callers to uphold。

多数的文档注释并不需要全部这些小节,但这仍不失为一个提醒咱们,关于咱们代码使用者将有兴趣了解的各方面的一个良好检查单。

作为测试的文档注释

Documentation Comments as Tests

在文档注释中添加一些示例代码块,可以帮助演示怎样使用咱们的库,且这样做有着附带的好处,an additional bonus:运行 cargo test 将把文档中示例代码作为测试运行!带有示例的文档属实很好。而在文档编写好后,由于代码已被修改而造成示例不工作,也是极为糟糕的。当咱们以清单 14-1 中 add_one 函数的文档,运行 cargo test,就将在测试结果中看到这样一个小节:

   Doc-tests cargo_features_demo

running 1 test
test src/lib.rs - add_one (line 7) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s

现在当咱们修改那个函数或者那个示例,从而让示例中的 assert_eq! 终止运行,并再次运行 cargo tset 时,咱们将看到文档测试,the doc tests,捕获到示例与代码不再相互同步!

注:此状况下的输出为:

   Doc-tests cargo_features_demo

running 1 test
test src/lib.rs - add_one (line 7) ... FAILED

failures:

---- src/lib.rs - add_one (line 7) stdout ----
Test executable failed (exit status: 101).

stderr:
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `6`,
 right: `7`', src/lib.rs:7:1
stack backtrace:
   0:     0x5620cf499480 - std::backtrace_rs::backtrace::libunwind::trace::h32eb3e08e874dd27
                               at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/../../backtrace/src/back                             trace/libunwind.rs:93:5
   // ...
  36:                0x0 - <unknown>



failures:
    src/lib.rs - add_one (line 7)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s

error: doctest failed, to rerun pass `--doc`

注:执行 cargo test --doc,将只运行文档注释中的示例代码。

注释被包含所在项目

Commenting Contained Items,代码箱、模组整体的注释

//! 样式的文档注释,会把文档添加到包含注释的条目,而非注释之后的条目。咱们通常在代码箱根文件里(依惯例即 src/lib.rs),或模组里,添加这些文档注释,来将代码箱或模组作为整体,而为其编写文档,the style of doc comment //! adds documentation to the item contains the comments rather than to the items following the comments. We typically use these doc comments inside the crate root file(src/lib.rs by convention) or inside a module to document the crate or the module as a whole。

比如,要添加描述包含了 add_one 函数的 cargo_features_demo 代码箱目的的文档,咱们就要添加以 //! 开始的文档注释,到 src/lib.rs 文件的开头,如下清单 14-2 中所示:

文件:src/lib.rs

#![allow(unused)]
fn main() {
//! # Cargo 特性示例代码箱
//!
//! `cargo_features_demo` 是令到执行某些确切计算更便利
//! 的一些工具的集合。
//!

/// 将一加到所给数字。
// --跳过代码--
}

清单 14-2:作为一个整体的 cargo_features_demo 代码箱的文档

请注意由于咱们是以 //! 而非 /// 开始的这些注释,因此在以 //! 开始的最后一行后,并无任何代码的,咱们是在给包含此注释的程序项目,而非紧接着此注释的程序项目编写文档。在此示例中,那个程序项目就是 src/lib.rs 文件,为代码箱根。这些注释描述了整个代码箱。

当咱们运行 cargo doc --open 时,这些注释将显示在 cargo_features_demo 代码箱文档的首页,he front page,位处代码箱公开项目的清单之上,如下图 14-02 中所示:

渲染出的 cargo_features_demo 代码箱文档

图 14-02:渲染出的 cargo_features_demo 代码箱文档, 包括着将该代码箱作为整体描述的注释

程序项目里的文档注释,用于对描述代码箱及模组尤其有用。使用他们来解释容器,the container,的整体目标,有助于咱们的用户们理解代码箱的组织结构。

使用 pub use 导出便利的公开 API

Exporting a Convinient Public API with pub use

在咱们发布代码箱时,公开 API 的结构是主要的考量。相比与咱们,使用咱们代码箱的人们对代码箱结构的没有那么熟悉,并在咱们的代码箱有着大型模组层次结构时,难于找到他们打算使用的部分。

在第 7 章中,咱们曾讲到过怎样使用 pub 关键字把一些程序项目构造为公开,与怎样使用 use 关键字,把程序项目带入到作用域。但是,咱们在开发某个代码箱时,对咱们有意义的组织结构(模组树),对于咱们的用户则可能不那么便利。咱们会打算把代码箱结构组织为包含多个级别的层次,但随后某个想要使用定义在层次结构深处类型的人,就可能在找出那个类型是否存在上遇到麻烦。他们可能还会对必须敲入 use cargo_features_demo::some_module::another_module::UsefulType;,而非敲入 use cargo_features_demo::UsefulType; 而感到恼火。

可喜的是,若代码箱组织结构 便于其他人在另一库中使用,咱们不必重新调整代码箱的内部组织:相反,咱们可通过使用 pub use,重新导出程序项目,而构造出一种不同于咱们私有组织结构的公开组织结构。重新导出,re-export,会取一处的公开程序项目,而在另一处将其构造为公开,就跟这个项目是在那另一处被定义过一样。

比如说,咱们构造了用于建模美术概念的一个名为 art 的库。这个库里有两个模组:包含了两个名为 PrimaryColorSeccondaryColor 枚举的 kinds 模组,与包含了名为 mix 函数的 utils 模组,如下清单 14-3 中所示:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
//! # art
//!
//! 建模诸多美术概念的一个库。

pub mod kinds {
    /// RYB 颜色模型下的主要颜色。
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// RYB 颜色模型下的次要颜色。
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    use crate::kinds::*;

    /// 结合两种等量的主要颜色,创建出
    /// 某种次要颜色。
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        // --跳过代码--
        SecondaryColor::Purple
    }
}
}

清单 14-3:有着组织到 kindsutils 两个模组中的一些程序项目的 art

下图 14-03 展示了由 cargo doc 产生出的该代码箱文档首页,看起来的样子:

列出 kinds 与 utils 两个模组的 art 代码箱文档首页

图 14-3:列出 kindsutils 两个模组的 art 代码箱文档首页

请注意 PrimaryColorSecondaryColor 两个类型,及 mix 函数都未在首页上列出。要看到他们,咱们必须点击 kindsutils

依赖于这个库的另一代码箱,将需要把程序项目从 art 带入到作用域的 use 语句,与指明当前定义的模组结构。下面清单 14-4 给出了用到 art 代码箱中 PrimaryColormix 两个程序项目的代码箱示例:

文件名: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 代码箱以内部组织结构导出程序项目的代码箱

:使用本地未发布代码箱的方法,是在 Cargo.toml[dependencies] 小节中,列出要使用的本地未发布代码箱。参见 How to use a local unpublished crate?

文件:Cargo.toml

// --跳过代码--

[dependencies]
art = { path = "../art" }

清单 14-4 中用到 art 代码箱代码的作者,不得不搞清楚 PrimaryColor 是在 kinds 模组中,及 mix 函数是在 utils 模组中。art 代码箱的模组结构(即模组树),相比于用到该代码箱的开发者,与在 art 代码箱上编写代码的开发者要更为密切。对于试图搞清楚怎样使用 art 代码箱的人来说,其内部组织结构并未包含任何有用信息,而因为要用到他的开发者,不得不搞明白要在那里去查看,且必须在 use 语句中指明那些模组名字,这反而会造成混乱。

要从公开 API 中移除内部组织结构,咱们可把清单 14-3 中 art 代码箱的代码,修改为添加一些 pub use 语句,来在顶层处重导出程序项目,to re-export the items at the top level,如下清单 14-5 中所示:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
//! # art
//!
//! 建模诸多美术概念的一个库。

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds;
pub mod utils;
}

清单 14-5:添加 pub use 语句来重导出程序项目

如下图 14-04 中所示,cargo doc 为此代码箱所产生出的 API 文档,现在将在首页上列出并链接到重导出项,从而令到 PrimaryColorSecondaryColor 两个类型及 mix 函数更易于找到。

列出了重导出项目的 art 代码箱文档首页

图 14-4:列出重导出项的 art 代码箱文档首页

art 代码箱的用户,依然可以像清单 14-4 中所演示的那样,发现及使用清单 14-3 的内部结构,或者他们可使用清单 14-5 中更为便利的结构,如下清单 14-6 中所示:

文件名:src/main.rs

use art::mix;
use art::PrimaryColor;

fn main() {
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}

清单 14-6:使用着 art 代码箱重导出项的程序

其中有许多嵌套模组的情形下,以 pub use 在顶层重导出类型,可在用到该代码箱的人的体验方面,造成显著不同。pub use 的另一常见用途则是,为将依赖代码箱的定义构造为咱们自己代码箱公开 API 的一部分,而重导出当前代码箱中某个依赖的定义。

创建出有用的公开 API 结构,与其说是一门科学,不如说是一门艺术,而咱们可不断迭代,来找到对用户运作最佳的 API。选择 pub use 会给到咱们在内部组织代码箱方式上的灵活性,并解除了内部结构与呈现给代码箱用户的组织结构的耦合。请查看咱们曾安装的代码箱代码,来发现他们的内部结构,是否不同于其公开 API。

建立 Crates.io 帐号

在咱们能发布代码箱之前,咱们需要在 crates.io 上创建帐号,并得到 API 令牌,an API token。而要这样做,就要访问 crates.io 处的主页,并通过 GitHub 帐号登录。(目前 GitHub 帐号是必须的,但该站点今后可能会支持其他创建帐号途径。)在登录后,咱们就要访问 https://crates.io/me/ 处的帐号设置,而获取自己的 API 密钥,API key。然后使用咱们的 API 密钥,运行 cargo login 命令,如下:

$ cargo login abcdefghijklmnopqrstuvwxyz012345

此命令将告知 Cargo 咱们的 API 令牌,并在 ~/.cargo/credentials 文件中本地存储起来。请注意此令牌是个 秘密,secret:不要与任何人分享。不论因何种缘故,与任何人分享了,咱们都应吊销他,并在 crates.io 上生成新的令牌。

添加元数据到新代码箱

Adding Metadata to a New Crate**

假设咱们有了个打算发布的代码箱。在发布前,咱们将需要在代码箱的 Cargo.toml 文件的 [package] 小节中,添加一些元数据。

咱们的代码箱将需要一个独特的名字。当咱们在本地于代码箱上工作时,咱们可以给代码箱取任意喜欢的名字。但是,crates.io 上代码箱的名字,则是以先到先得的原则分配的,allocated on a first-come, first-served basis。一旦某个代码箱名字已被占用,其他人就不能发布有着那个名字的代码箱。在尝试发布某个代码箱之前,咱们要检索一下打算使用的名字。若这个名字已被使用,咱们将需要找到另一名字,并编辑 Cargo.toml 文件中 [package] 小节下的 name 字段,来使用这个用作发布的新名字,像下面这样:

文件名:Cargo.toml

[package]
name = "guessing_game"

即使咱们已选了个独特的名字,当咱们此时运行 cargo publish 来发布这个代码箱时,仍将得到一条告警及随后的报错:

cargo publish                                                                                          lennyp@vm-manjaro
    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.
   Packaging guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game)
   Verifying guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game)
   Compiling libc v0.2.132
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.16
   Compiling getrandom v0.2.7
   Compiling rand_core v0.6.3
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game/target/package/guessing_game-0.1.0)
    Finished dev [unoptimized + debuginfo] target(s) in 3.55s
   Uploading guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game)
error: failed to publish to registry at https://crates.io

Caused by:
  the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata

此报错是由于咱们缺失了一些重要信息:描述及许可证是必须的,由此人们就会明白咱们的代码箱完成的什么,及在何种条件下他们可以使用他。在 Cargo.toml 中,由于代码箱的描述,会与咱们的代码箱一起呈现在搜索结果中,因此请添加仅仅一两句话的描述。而对于 license 字段,则需要提供 某个许可证标识符值,a licence identifier valueLinux 基金会的软件包数据交换站,Linux Foundation's Software Package Data Exchange, SPDX,spdx.org 列出了可供这个值使用的标识符。比如,为指明咱们已使用 MIT 许可证,授权咱们的软件包,就要添加 MIT 的许可证标识符:

文件名:Cargo.toml

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

若咱们打算使用某个未出现于 SPDX 中的许可证,咱们就需要把那种许可证的文本,放置于某个文件里,把这个文件包含在咱们的项目中,并于随后使用 license-file 来指出那个文件的名字,而不再使用 license 键,the license key。

至于哪种许可证适合于咱们的项目方面的指南,是超出这本书的范围的。Rust 社区的许多人,都以 Rust 项目同样的方式,即采用 MIT OR Apache-2.0 双重许可证,授权他们的项目。这种实践表明,咱们也可以通过 OR 来指定出多个许可证标识符,从而让咱们的项目有着多种许可证。

在添加了独特名字、版本号、代码箱描述及许可证后,已准备好发布项目的 Cargo.toml文件,就会看起来像下面这样:

文件名:Cargo.toml

[package]
name = "guessing_game"
license = "MIT"
version = "0.1.0"
description = "一个在其中猜出计算机所选数字的有趣游戏。"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.3"

Cargo 文档 介绍了为确保其他人能更容易发现并使用咱们代码箱,而可指明的别的一些元数据。

发布到 Crates.io

既然咱们已经创建了账号,保存了 API 令牌,选择了代码箱名字,并指定了必需的元数据,那么咱们就准备好发布了!发布代码箱,会上传特定版本到 crates.io,供其他人使用。

因为发布是 永久性的,permanent,因此要当心。版本绝无可能被覆盖,且代码无法被删除。crates.io 的一个主要目标,是要充当代码的永久存档,以便依赖于 crates.io 中代码箱的所有项目构建都将持续工作。而允许版本的删除,就会令到实现那个目标几无可能。不过,在咱们可发布的代码箱版本数目上没有限制。

再度运行 cargo publish 命令。现在他就应成功了:

$ cargo publish                                                                        lennyp@vm-manjaro
    Updating crates.io index
warning: manifest has no documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
   Packaging guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game)
   Verifying guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game)
   Compiling libc v0.2.132
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.16
   Compiling getrandom v0.2.7
   Compiling rand_core v0.6.3
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game/target/package/guessing_game-xfossdotcom-0.1.0)
    Finished dev [unoptimized + debuginfo] target(s) in 2.73s
   Uploading guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game)

恭喜!现在咱们就已与 Rust 社区分享了咱们的代码,且任何人都可将咱们的代码箱,添加为他们项目的依赖。

注:在 Crates.io 上的账号电子邮箱未验证时,将报出如下错误:

Caused by:
  the remote server responded with an error: A verified email address is required to publish crates to crates.io. Visit https://crates.io/me to set and verify your email address.

发布既有代码箱的新版本

咱们完成咱们代码箱的修改,而准备好发布新版本时,咱们要修改 Cargo.toml 中所指定的 version 值并重新发布。请运用 语义版本控制规则,Semantic Versioning rules,根据咱们已做出修改的类别,来确定出恰当的下一版本编号为何。然后运行 cargo publish 来上传新版本。

使用 cargo yank 命令弃用 Crates.io 上的版本

Depracating Versions from Crates.io with cargo yank

尽管咱们无法移除代码箱的先前版本,但咱们可以阻止任何今后的项目,将其添加为新的依赖项。这在某个代码箱版本由于某种原因,或别的问题而损坏时是有用的。在诸如此类的情形下,Cargo 支持把某个代码箱版本 抽出来,in such situations, Cargo supports yanking a crate version。

抽出某个版本,在允许所有依赖该版本的既有项目继续工作的同时,会阻止新项目依赖那个版本。本质上,一次版本抽出,表示带有 Cargo.lock 的全部项目不会破坏,而任何今后生成的 Cargo.lock 文件,都将不使用被抽出的版本。

要抽出代码箱的某个版本,就要在咱们先前已发布的代码箱目录中,运行 cargo yank 并指定出要抽出的版本。比如,咱们曾发布了名为 guessing_game 代码箱的 0.1.0 版本,而打算抽出他,咱们就要在 guessing_game 的项目目录下,运行下面的命令:

$ cargo yank --vers 0.1.0                                                           4s lennyp@vm-manjaro
    Updating crates.io index
        Yank guessing_game-xfossdotcom@0.1.0

通过把 --undo 添加到这个命令,咱们还可以撤销某次抽出,而允许项目开始再度依赖于某个版本:

$ cargo yank --vers 0.1.0 --undo                                                    lennyp@vm-manjaro
    Updating crates.io index
      Unyank guessing_game-xfossdotcom@0.1.0

抽出版本,不会 删除任何代码。比如,其无法删除那些不小心上传的机密信息。若发生了机密信息被上传的情况,咱们必须立即重置这些机密信息。

Last change: 2023-12-01, commit: 6fc28b5