# Chapter 11 - 关于Cargo和Crates.io

目前为止我们只使用到cargo创建、运行、测试、构建等基础功能。本章会介绍它的其他功能。包括:

  • 使用配置文件定制构建
  • crates.io发布库
  • 使用工作空间管理大型项目
  • 安装crates.io上的可执行文件
  • 定制化命令扩展cargo

可以在cargo官方文档查看全部功能说明。

# Section 1 - 使用配置文件定制构建

Rust中,*配置文件(release profiles)*是预定义的、可自定义的文件,不同的配置选项可以对代码编译进行控制。每个文件的配置都是独立的。

cargo有两个主要的配置文件

  • dev配置文件。cargo build命令会使用这个配置文件。包含针对开发环境的默认配置
  • release配置文件。cargo build --release命令会使用这个配置文件。包含针对发布环境的默认配置。

Cargo.toml文件中没有[profile.*]声明时,cargo会使用默认配置。对想要修改的配置文件加[profile.*]声明,就可以对默认配置进行覆盖和定制化。例如:

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3
1
2
3
4
5

分别对devrelease配置文件,针对opt-level配置进行了定制化。

# Section 2 - 发布Crate到Crates.io

Rust和Cargo有很多功能,辅助包发布和查找。首先介绍三个功能然后再介绍如何发布自己的包。

# 有用的文档注释

准确描述你的包有助于让其他人更容易地了解如何使用、何时使用你的包,所以有用的文档很有必要。代码注释时使用双斜线//,Rust也有专门针对文档的注释,称之为文档注释(documentation comment),文档注释会用来生成html页面。html展示了你对公开API的介绍,文档内容应当是介绍如何使用你的API,而不是描述你如何实现的这些API。

文档注释使用三斜线///,并且支持md语法。文档注释放置于被描述对象之前。例如我们创建一个my_crate库,里面包含一个add_one函数。

/// 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13

我们描述了add_one函数的功能是什么,然后Examples下面是一个示例代码块。cargo doc命令可以基于这些描述生成html文档。这个命令运行了Rust提供的rustdoc工具,并且将生成的html文件放置于target/doc目录下。

使用--open参数会在文档创建完成之后打开浏览器,效果如下: rust doc

# 常用模块

我们已经使用# Examplesmd语法,创建了一个示例代码模块,下面还有一些常用的模块:

  • Panics:会导致代码出错的场景。
  • Errors:如果函数返回的是一个Result,描述一下返回的错误类型,以及如何处理这些错误。
  • Safety:如果这个函数调用是unsafe的,应当描述函数为何是unsale,以及涵盖希望由调用者维护的不变性(invariants)。

# 文档注释测试用例

cargo test命令会将文档注释中examples模块下的示例代码作为测试用例运行。这会保证你的代码和示例代码是同步的,因为当你修改其中任意部分时,如果出错文档测试会捕获到。

# 目录性描述

对crate包含内容的一个总体描述。

//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/// Adds one to the number given.
1
2
3
4
5
6

rust doc

# 使用pub use导出公有API

在写代码时,你的代码结构可能自己比较清楚,但对于使用者来说可能不是特别方便。在组织代码时,可能进行了很深的模块嵌套,但是当使用者想要使用一个定义的很深的API时,找到它就比较费劲了。例如:use my_crate::some_module::another_module::UsefulType;。显然use my_crate::UsefulType对调用者更加的友好。

因此当发布包时,API的结构是首先要考虑的问题。因为使用者不会像你一样熟悉你的代码结构。

当一个API对调用者不太友好时,你不需要修改你的代码组织,你可以使用pub use二次导出。pub use导入一个共有API,并且将其再对外公开暴露。

创建一个art包,它的内容如下所示:

//! # 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--
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

查看它的文档: rust doc

PrimaryColorSecondaryColormix都没有在首页展示出来,需要我们手动点入这些模块。并且其他模块调用我们的包时,引用的链路特别长:

use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}
1
2
3
4
5
6
7
8

为了移除这个调用者冗长的调用链路,我们可以在自己的包中,将这些API进行二次导出:

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
1
2
3

PrimaryColorSecondaryColormix都更容易找到了,文档如下: rust doc

并且调用者在引用代码,代码量也更少:

use art::PrimaryColor;
use art::mix;
1
2

# 创建Crates.io账号

首先去crates.io注册一个账号,验证邮箱之后,获取API token。然后在终端使用token登陆。

cargo login abcdefghijklmnopqrstuvwxyz012345

token会被存储在*~/.cargo/credentials.*文件下。注意不要将token分享出去。

# 为新包添加元数据

发布之前需要在Cargo.toml文件的[package]模块下追加一些元数据。

注意你的包名必须是唯一的。当你在本地开发时,跟目录名称可以随意,但是在发布时,元数据当中的包名称必须是唯一的,不能跟别人的包名称冲突。因此在发布之前可以去网站上先搜索一下你的名字有没有被使用。

Cargo.toml文件下的[package]模块中定义包名称:

[package]
name = "ksleo_public_test"
1
2

当你选好唯一的名称之后,使用cargo publish发布,会出现一个错误

error: api errors (status 200 OK): missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata

因为你的项目还缺少一些关键信息:描述和证书。描述你的包是做什么用,并且你的包是基于什么证书给别人使用的。

[package]
license = "MIT"
description = "just for pub test"
1
2
3

在[package]模块下加入这些内容后,就可以成功发布了。

# 发布新版本

修改Cargo.toml文件下的版本号,重新发布即可。

# 使用cargo yank禁用版本

cargo不支持删除某个历史版本,但可以将历史版本禁用,防止其他人将该版本加入依赖。

yanking只能防止新项目将该版本作为依赖,已存在的项目依旧可以下载该版本。

下面的命令就将1.0.1版本禁用了,任何新项目都不能再依赖该版本

$ cargo yank --vers 1.0.1

如果要撤销某个版本的禁用,可以使用--undo参数

$ cargo yank --vers 1.0.1 --undo

# Section 3 - Cargo工作空间

在开发过程中,你的包会变得越来越大,此时应该对它们进一步切分。在这种场景下,Cargo提供了*工作空间(work space)*的功能让你来管理一些相关的包。

# 创建工作空间

工作空间是一组共享Cargo.toml文件和输出目录的包集合。创建工作空间有很多方式,下面展示最常用的一种。我们将创建包含1个可执行crate和1个库crate的工作空间。可执行crate提供入口函数main,其余库crate提供方法。

首先创建一个add目录。然后进入目录创建Cargo.toml文件。这个toml文件不会包含之前出现的[package]块,或者其他元数据。它以[workspace]块开始,下面会定义工作空间所包含的成员。

[workspace]

members = [
    "adder",
]
1
2
3
4
5

然后在add目录下新建一个可执行包adder。最后使用cargo build创建工作空间,此时项目结构应该如下:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

工作空间根目录有一个放置编译结果的target目录,adder包内部没有target目录了。就算我们进入adder项目运行Cargo build,编译结果还是会被存放到根目录下的target目录中,而不是add/adder/target目录。Cargo这样组织target目录是因为,工作空间下的包是相互依赖的,如果每个包都有自己的target目录来存放编译结果,则所有的包都需要互相为其他包构建一份。

# 创建第二个包

接下来创建工作空间下的第二个包add_one。修改根目录下的Cargo.toml文件。

[workspace]

members = [
    "adder",
    "add-one",
]
1
2
3
4
5
6

然后创建一个库crate。cargo new add_one --lib。现在目录应该包含以下内容

├── Cargo.lock
├── Cargo.toml
├── add-one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

add-one/src/lib.rs文件中添加一个add_one函数。

pub fn add_one(x:i32) -> i32 {
    x + 1
}
1
2
3

接下来可执行crate可以对库crate进行依赖,首先在需要引入依赖的crate的toml文件中,定义[dependencies]模块。指定库crate的路径。

[dependencies]

add-one = { path = "../add-one" }
1
2
3

Cargo不会对依赖进行解析,所以需要我们明确指定crate间的依赖关系。

接下来可以在addercrate中通过use引入add-one模块了。

use add_one;

fn main() {
    let num = 10;
    println!(
        "Hello, world! {} plus one is {}!",
        num,
        add_one::add_one(num)
    );
}
1
2
3
4
5
6
7
8
9
10

最后在根目录下运行cargo build构建。使用cargo run命令运行代码,使用-p参数指定运行的包:cargo run -p adder

# 依赖外部包

整个工作空间只有根目录下有Cargo.lock文件,这样保证所有包使用的外部依赖都是相同的版本。如果你在每个包目录下的Cargo.toml文件中都指定了外部依赖,Cargo会将他们处理成统一的版本并记录在根目录下的Cargo.lock文件中。这样保证所有包使用的依赖都是兼容的。下面我们先在add-one/Cargo.toml文件中添加一个rand外部依赖。然后在文件中引用这个依赖并执行编译。

[dependencies]
rand = "0.5.5"
1
2
  Updating crates.io index
Downloaded libc v0.2.76
 Compiling rand v0.5.6
 Compiling add-one v0.1.0 (/Users/ksleo/private/add/add-one)
 Compiling adder v0.1.0 (/Users/ksleo/private/add/adder)

现在根目录下的Cargo.lock已经有了add-one依赖rand包的信息。尽管rand包以及在工作空间内部使用了,但是在add-one以外的包中,引入rand包还是会报错,除非我们在他们的Cargo.toml文件中也指定rand依赖。此时Cargo.lock文件中也会记录adder引用rand的信息,但是不会重新下载多余的代码。这种方式既保证所有包都使用同一个版本的外部依赖,又不会因为代码拷贝从而对空间造成浪费。

# 为工作空间添加测试

add-one包的公有方法添加一个测试用例

pub fn add_one(x: i32) -> i32 {
    x + 1
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

运行cargo test

    Running target/debug/deps/add_one-f0253159197f7841
running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

    Running target/debug/deps/adder-d6b6ef1ba6873bae

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

  Doc-tests add-one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

第一块内容表示add-one包有一个测试用例被运行;第二块表示adder包中没有测试用例可以执行;第三块表示add-one包中没有文档测试可以执行。在工作空间中执行测试,会将工作空间下所有包的测试用例都执行。

也可以使用-p参数指定测试的包,与cargo run使用方式一样。

如果你要发布你的包,那么你需要对每个包分别执行发布操作,cargo publish没有--all或者-p参数。

# Section 4 - 使用cargo install从crates.io安装可执行文件

cargo install命令可以让你在本地下载和使用可执行crate。只能安装带有可执行编译目标的包。可执行编译目标是由具有src/main.rs文件,或者其他被指定为可执行文件的包,编译而来的可执行程序。相对应的库crate则不能独立运行,只能被其他crate引用。通常crate都会又README文件说明,crate是可执行crate还是库crate。

可执行文件被下载到安装目录的bin目录下,如果你没有进行过任何自定义配置,这个目录是$HOME/.cargo/bin。确保该路径被添加进$PATH环境变量了,否则可能无法运行可执行文件。

# Section 5 - 子命令扩展Cargo

Cargo被设计为你不需要修改Cargo本身就可以扩展很多子命令,如果你的$PATH中有个可执行程序叫cargo-something,你可以用类似子命令的方式运行它cargo something。可以通过cargo --list命令列出所有cargo扩展命令。使用cargo install来安装这些可执行扩展,就像使用内置工具一样方便。