ch14-02-publishing-to-crates-io.md
commit 7ddc28cfe0bfa6c531a6475c7fa41dfa66e8943c
我們曾經在項目中使用 crates.io 上的包作為依賴,不過你也可以通過發(fā)布自己的包來向他人分享代碼。crates.io 用來分發(fā)包的源代碼,所以它主要托管開源代碼。
Rust 和 Cargo 有一些幫助他人更方便找到和使用你發(fā)布的包的功能。我們將介紹一些這樣的功能,接著講到如何發(fā)布一個包。
準確的包文檔有助于其他用戶理解如何以及何時使用他們,所以花一些時間編寫文檔是值得的。第三章中我們討論了如何使用兩斜杠 //
注釋 Rust 代碼。Rust 也有特定的用于文檔的注釋類型,通常被稱為 文檔注釋(documentation comments),他們會生成 HTML 文檔。這些 HTML 展示公有 API 文檔注釋的內容,他們意在讓對庫感興趣的程序員理解如何 使用 這個 crate,而不是它是如何被 實現(xiàn) 的。
文檔注釋使用三斜杠 ///
而不是兩斜桿以支持 Markdown 注解來格式化文本。文檔注釋就位于需要文檔的項的之前。示例 14-1 展示了一個 my_crate
crate 中 add_one
函數(shù)的文檔注釋,
文件名: 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:一個函數(shù)的文檔注釋
這里,我們提供了一個 add_one
函數(shù)工作的描述,接著開始了一個標題為 Examples
的部分,和展示如何使用 add_one
函數(shù)的代碼??梢赃\行 cargo doc
來生成這個文檔注釋的 HTML 文檔。這個命令運行由 Rust 分發(fā)的工具 rustdoc
并將生成的 HTML 文檔放入 target/doc 目錄。
為了方便起見,運行 cargo doc --open
會構建當前 crate 文檔(同時還有所有 crate 依賴的文檔)的 HTML 并在瀏覽器中打開。導航到 add_one
函數(shù)將會發(fā)現(xiàn)文檔注釋的文本是如何渲染的,如圖 14-1 所示:
圖 14-1:add_one
函數(shù)的文檔注釋 HTML
示例 14-1 中使用了 # Examples
Markdown 標題在 HTML 中創(chuàng)建了一個以 “Examples” 為標題的部分。其他一些 crate 作者經常在文檔注釋中使用的部分有:
panic!
? 的場景。并不希望程序崩潰的函數(shù)調用者應該確保他們不會在這些情況下調用此函數(shù)。Result
?,此部分描述可能會出現(xiàn)何種錯誤以及什么情況會造成這些錯誤,這有助于調用者編寫代碼來采用不同的方式處理不同的錯誤。unsafe
? 代碼(這會在第十九章討論),這一部分應該會涉及到期望函數(shù)調用者支持的確保 ?unsafe
? 塊中代碼正常工作的不變條件(invariants)。大部分文檔注釋不需要所有這些部分,不過這是一個提醒你檢查調用你代碼的人有興趣了解的內容的列表。
在文檔注釋中增加示例代碼塊是一個清楚的表明如何使用庫的方法,這么做還有一個額外的好處:cargo test
也會像測試那樣運行文檔中的示例代碼!沒有什么比有例子的文檔更好的了,但最糟糕的莫過于寫完文檔后改動了代碼,而導致例子不能正常工作。嘗試 cargo test
運行像示例 14-1 中 add_one
函數(shù)的文檔;應該在測試結果中看到像這樣的部分:
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
現(xiàn)在嘗試改變函數(shù)或例子來使例子中的 assert_eq!
產生 panic。再次運行 cargo test
,你將會看到文檔測試捕獲到了例子與代碼不再同步!
還有另一種風格的文檔注釋,//!
,這為包含注釋的項,而不是位于注釋之后的項增加文檔。這通常用于 crate 根文件(通常是 src/lib.rs)或模塊的根文件為 crate 或模塊整體提供文檔。
作為一個例子,如果我們希望增加描述包含 add_one
函數(shù)的 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--
示例 14-2:my_crate
crate 整體的文檔
注意 //!
的最后一行之后沒有任何代碼。因為他們以 //!
開頭而不是 ///
,這是屬于包含此注釋的項而不是注釋之后項的文檔。在這個情況中,包含這個注釋的項是 src/lib.rs 文件,也就是 crate 根文件。這些注釋描述了整個 crate。
如果運行 cargo doc --open
,將會發(fā)現(xiàn)這些注釋顯示在 my_crate
文檔的首頁,位于 crate 中公有項列表之上,如圖 14-2 所示:
圖 14-2:包含 my_crate
整體描述的注釋所渲染的文檔
位于項之中的文檔注釋對于描述 crate 和模塊特別有用。使用他們描述其容器整體的目的來幫助 crate 用戶理解你的代碼組織。
第七章介紹了如何使用 mod
關鍵字來將代碼組織進模塊中,如何使用 pub
關鍵字將項變?yōu)楣?,和如何使?nbsp;use
關鍵字將項引入作用域。然而你開發(fā)時候使用的文件架構可能并不方便用戶。你的結構可能是一個包含多個層級的分層結構,不過這對于用戶來說并不方便。這是因為想要使用被定義在很深層級中的類型的人可能很難發(fā)現(xiàn)這些類型的存在。他們也可能會厭煩要使用 use my_crate::some_module::another_module::UsefulType;
而不是 use my_crate::UsefulType;
來使用類型。
公有 API 的結構是你發(fā)布 crate 時主要需要考慮的。crate 用戶沒有你那么熟悉其結構,并且如果模塊層級過大他們可能會難以找到所需的部分。
好消息是,即使文件結構對于用戶來說 不是 很方便,你也無需重新安排內部組織:你可以選擇使用 pub use
重導出(re-export)項來使公有結構不同于私有結構。重導出獲取位于一個位置的公有項并將其公開到另一個位置,好像它就定義在這個新位置一樣。
例如,假設我們創(chuàng)建了一個描述美術信息的庫 art
。這個庫中包含了一個有兩個枚舉 PrimaryColor
和 SecondaryColor
的模塊 kinds
,以及一個包含函數(shù) 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--
}
}
示例 14-3:一個庫 art
其組織包含 kinds
和 utils
模塊
cargo doc
所生成的 crate 文檔首頁如圖 14-3 所示:
圖 14-3:包含 kinds
和 utils
模塊的庫 art
的文檔首頁
注意 PrimaryColor
和 SecondaryColor
類型、以及 mix
函數(shù)都沒有在首頁中列出。我們必須點擊 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 中使用 art
crate 代碼的作者不得不搞清楚 PrimaryColor
位于 kinds
模塊而 mix
位于 utils
模塊。art
crate 的模塊結構相比使用它的開發(fā)者來說對編寫它的開發(fā)者更有意義。其內部的 kinds
模塊和 utils
模塊的組織結構并沒有對嘗試理解如何使用它的人提供任何有價值的信息。art
crate 的模塊結構因不得不搞清楚所需的內容在何處和必須在 use
語句中指定模塊名稱而顯得混亂和不便。
為了從公有 API 中去掉 crate 的內部組織,我們可以采用示例 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--
}
pub mod utils {
// --snip--
}
示例 14-5:增加 pub use
語句重導出項
現(xiàn)在此 crate 由 cargo doc
生成的 API 文檔會在首頁列出重導出的項以及其鏈接,如圖 14-4 所示,這使得 PrimaryColor
和 SecondaryColor
類型和 mix
函數(shù)更易于查找。
圖 14-10:art
文檔的首頁,這里列出了重導出的項
art
crate 的用戶仍然可以看見和選擇使用示例 14-4 中的內部結構,或者可以使用示例 14-5 中更為方便的結構,如示例 14-6 所示:
文件名: src/main.rs
use art::mix;
use art::PrimaryColor;
fn main() {
// --snip--
}
示例 14-6:一個使用 art
crate 中重導出項的程序
對于有很多嵌套模塊的情況,使用 pub use
將類型重導出到頂級結構對于使用 crate 的人來說將會是大為不同的體驗。
創(chuàng)建一個有用的公有 API 結構更像是一門藝術而非科學,你可以反復檢視他們來找出最適合用戶的 API。pub use
提供了解耦組織 crate 內部結構和與終端用戶體現(xiàn)的靈活性。觀察一些你所安裝的 crate 的代碼來看看其內部結構是否不同于公有 API。
在你可以發(fā)布任何 crate 之前,需要在 crates.io 上注冊賬號并獲取一個 API token。為此,訪問位于 crates.io 的首頁并使用 GitHub 賬號登錄。(目前 GitHub 賬號是必須的,不過將來該網站可能會支持其他創(chuàng)建賬號的方法)一旦登錄之后,查看位于 https://crates.io/me/ 的賬戶設置頁面并獲取 API token。接著使用該 API token 運行 cargo login
命令,像這樣:
$ cargo login abcdefghijklmnopqrstuvwxyz012345
這個命令會通知 Cargo 你的 API token 并將其儲存在本地的 ~/.cargo/credentials 文件中。注意這個 token 是一個 秘密(secret)且不應該與其他人共享。如果因為任何原因與他人共享了這個信息,應該立即到 crates.io 重新生成這個 token。
有了賬號之后,比如說你已經有一個希望發(fā)布的 crate。在發(fā)布之前,你需要在 crate 的 Cargo.toml 文件的 [package]
部分增加一些本 crate 的元信息(metadata)。
首先 crate 需要一個唯一的名稱。雖然在本地開發(fā) crate 時,可以使用任何你喜歡的名稱。不過 crates.io 上的 crate 名稱遵守先到先得的分配原則。一旦某個 crate 名稱被使用,其他人就不能再發(fā)布這個名稱的 crate 了。請在網站上搜索你希望使用的名稱來找出它是否已被使用。如果沒有,修改 Cargo.toml 中 [package]
里的名稱為你希望用于發(fā)布的名稱,像這樣:
文件名: Cargo.toml
[package]
name = "guessing_game"
即使你選擇了一個唯一的名稱,如果此時嘗試運行 cargo publish
發(fā)布該 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: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata
這是因為我們缺少一些關鍵信息:關于該 crate 用途的描述和用戶可能在何種條款下使用該 crate 的 license。為了修正這個錯誤,需要在 Cargo.toml 中引入這些信息。
描述通常是一兩句話,因為它會出現(xiàn)在 crate 的搜索結果中和 crate 頁面里。對于 license
字段,你需要一個 license 標識符值(license identifier value)。Linux 基金會的 Software Package Data Exchange (SPDX) 列出了可以使用的標識符。例如,為了指定 crate 使用 MIT License,增加 MIT
標識符:
文件名: Cargo.toml
[package]
name = "guessing_game"
license = "MIT"
如果你希望使用不存在于 SPDX 的 license,則需要將 license 文本放入一個文件,將該文件包含進項目中,接著使用 license-file
來指定文件名而不是使用 license
字段。
關于項目所適用的 license 指導超出了本書的范疇。很多 Rust 社區(qū)成員選擇與 Rust 自身相同的 license,這是一個雙許可的 MIT OR Apache-2.0
。這個實踐展示了也可以通過 OR
分隔為項目指定多個 license 標識符。
那么,有了唯一的名稱、版本號、由 cargo new
新建項目時增加的作者信息、描述和所選擇的 license,已經準備好發(fā)布的項目的 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 更容易被發(fā)現(xiàn)和使用!
現(xiàn)在我們創(chuàng)建了一個賬號,保存了 API token,為 crate 選擇了一個名字,并指定了所需的元數(shù)據(jù),你已經準備好發(fā)布了!發(fā)布 crate 會上傳特定版本的 crate 到 crates.io 以供他人使用。
發(fā)布 crate 時請多加小心,因為發(fā)布是 永久性的(permanent)。對應版本不可能被覆蓋,其代碼也不可能被刪除。crates.io 的一個主要目標是作為一個存儲代碼的永久文檔服務器,這樣所有依賴 crates.io 中的 crate 的項目都能一直正常工作。而允許刪除版本沒辦法達成這個目標。然而,可以被發(fā)布的版本號卻沒有限制。
再次運行 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 [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
恭喜!你現(xiàn)在向 Rust 社區(qū)分享了代碼,而且任何人都可以輕松的將你的 crate 加入他們項目的依賴。
當你修改了 crate 并準備好發(fā)布新版本時,改變 Cargo.toml 中 version
所指定的值。請使用 語義化版本規(guī)則 來根據(jù)修改的類型決定下一個版本號。接著運行 cargo publish
來上傳新版本。
雖然你不能刪除之前版本的 crate,但是可以阻止任何將來的項目將他們加入到依賴中。這在某個版本因為這樣或那樣的原因被破壞的情況很有用。對于這種情況,Cargo 支持 撤回(yanking)某個版本。
撤回某個版本會阻止新項目開始依賴此版本,不過所有現(xiàn)存此依賴的項目仍然能夠下載和依賴這個版本。從本質上說,撤回意味著所有帶有 Cargo.lock 的項目的依賴不會被破壞,同時任何新生成的 Cargo.lock 將不能使用被撤回的版本。
為了撤回一個 crate,運行 cargo yank
并指定希望撤回的版本:
$ cargo yank --vers 1.0.1
也可以撤銷撤回操作,并允許項目可以再次開始依賴某個版本,通過在命令上增加 --undo
:
$ cargo yank --vers 1.0.1 --undo
撤回 并沒有 刪除任何代碼。舉例來說,撤回功能并不意在刪除不小心上傳的秘密信息。如果出現(xiàn)了這種情況,請立即重新設置這些秘密信息。
更多建議: