Rust Cargo Book笔记
1 - Rust Cargo Book之指南
https://doc.rust-lang.org/cargo/guide/index.html
2.1 为什么cargo会存在
前言
在Rust中,你可能知道,一个库或可执行程序被称为 crate。Crate 是用Rust编译器rustc来编译的。当开始使用Rust时,大多数人遇到的第一个源代码是古老的 “hello world” 程序,他们通过直接调用rustc来编译它。
$ rustc hello.rs
$ ./hello
Hello, world!
注意,上面的命令要求我们明确指定文件名。如果我们要直接使用 rustc 来编译一个不同的程序,就需要用不同的命令行来调用。如果我们需要指定任何特定的编译器标志或包括外部依赖,那么需要的命令将更加具体(和详细)。
此外,大多数非微不足道的程序可能会有对外部库的依赖性,因此也会对其依赖性进行转接。获得所有必要的依赖的正确版本,并保持它们的更新,如果用手工完成,将是费力和容易出错的。
我们可以通过引入更高级别的 “package/包” 抽象和使用包管理器来避免执行上述任务所涉及的手工繁琐工作,而不是仅仅使用 crates 和 rustc。
进入:Cargo
Cargo是Rust的包管理器。它是一个允许Rust包声明其各种依赖关系的工具,并确保你总是得到一个可重复的构建。
为了实现这个目标,Cargo做了四件事:
- 引入两个元数据文件,包含各种包的信息。
- 获取并构建软件包的依赖项。
- 调用 rustc 或其他具有正确参数的构建工具来构建你的软件包。
- 引入惯例,使Rust包的工作更容易。
在很大程度上,Cargo将构建一个特定程序或库所需的命令规范化;这是上述约定的一个方面。正如我们在后面所展示的,同一个命令可以用来构建不同的工件,不管它们的名字是什么。与其直接调用rustc,我们不如调用一些通用的东西,比如 cargo build,让cargo来考虑构建正确的rustc调用。此外,Cargo会自动从注册表中获取我们为工件定义的任何依赖关系,并根据需要安排将其纳入我们的构建中。
可以说,一旦你知道如何构建一个基于Cargo的项目,你就知道如何构建所有的项目,这只是一个小小的夸张。
2.2 创建一个新包
要用Cargo启动一个新的软件包,请使用cargo new:
$ cargo new hello_world --bin
我们传递 --bin
是因为我们在制作二进制程序:如果我们在制作库,我们会传递 --lib
。这也会默认初始化一个新的 git 仓库。如果你不希望它这么做,可以传递 --vcs none
。
让我们看看Cargo为我们生成了什么:
$ cd hello_world
$ tree .
.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
让我们仔细看看Cargo.toml。
[package]
name = "hello_world"
version = "0.1.0"
edition = "2018"
[dependencies]
这被称为 manifest / 清单,它包含了Cargo编译你的包时需要的所有元数据。这个文件是以TOML格式(发音为/tɑməl/)编写的。
下面是 src/main.rs 中的内容:
fn main() {
println! ("Hello, world!");
}
Cargo为我们生成了一个 “hello world " 程序,也就是所谓的二进制crate。让我们来编译它。
$ cargo build
Compiling hello_world v0.1.0 (file:///path/to/package/hello_world)
然后运行它:
$ ./target/debug/hello_world
Hello, world!
我们也可以用cargo run来编译,然后运行,一步到位(如果你在上次编译后没有做任何改动,你就不会看到编译这一行):
$ cargo run
Compiling hello_world v0.1.0 (file:///path/to/package/hello_world)
Running `target/debug/hello_world`
Hello, world!
现在你会注意到一个新文件,Cargo.lock。它包含了关于我们的依赖关系的信息。因为我们还没有任何依赖,所以它不是很有趣。
一旦你准备好发布,你可以使用cargo build –release来编译你的文件,并打开优化功能。
$ cargo build --release
Compiling hello_world v0.1.0 (file:///path/to/package/hello_world)
cargo build --release
将生成的二进制文件放在 target/release
而不是 target/debug
中。
在调试模式下编译是默认的开发模式。由于编译器不做优化,所以编译时间更短,但代码的运行速度会更慢。发布模式的编译时间更长,但代码的运行速度会更快。
2.3 在现有的Cargo包上工作
如果你下载了一个现有的使用Cargo的包,那就很容易开始工作了。
首先,从某处获取该包。在这个例子中,我们将使用从GitHub上的存储库中克隆的rand:
$ git clone https://github.com/rust-lang-nursery/rand.git
$ cd rand
要构建,请使用 cargo build。
$ cargo build
Compiling rand v0.1.0 (file:///path/to/package/rand)
这将获取所有的依赖项,然后与软件包一起构建它们。
2.4 依赖
crates.io 是Rust社区的中央软件包注册中心,作为发现和下载软件包的地点。Cargo的配置是默认使用它来寻找所需的软件包。
要依赖crates.io上的库,请将其添加到你的Cargo.toml中。
添加依赖项
如果你的 Cargo.toml
还没有 [dependencies]
部分,请添加它,然后列出你想要使用的 crate 名称和版本。这个例子添加了对 time
crate 的依赖:
[dependencies]
time = "0.1.12"
版本字符串要求是 semver (语义化)版本。在指定依赖关系的文档中,有更多关于你在这里的选项的信息。
如果我们还想添加对 regex
crate 的依赖,我们就不需要为列出的每个 crate 添加 [dependencies]
。下面是你的整个 Cargo.toml
文件中对 time 和 regex crate 的依赖情况:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2018"
[dependencies]
time = "0.1.12"
regex = "0.1.41"
重新运行 cargo build
,Cargo会获取新的依赖项和所有的依赖项,将它们全部编译,并更新 Cargo.lock
:
$ cargo build
Updating crates.io index
Downloading memchr v0.1.5
Downloading libc v0.1.10
Downloading regex-syntax v0.2.1
Downloading memchr v0.1.5
Downloading aho-corasick v0.3.0
Downloading regex v0.1.41
Compiling memchr v0.1.5
Compiling libc v0.1.10
Compiling regex-syntax v0.2.1
Compiling memchr v0.1.5
Compiling aho-corasick v0.3.0
Compiling regex v0.1.41
Compiling hello_world v0.1.0 (file:///path/to/package/hello_world)
我们的 Cargo.lock 包含了所有这些依赖的确切信息,即我们使用了哪个版本。
现在,如果 regex
被更新,我们仍然会用相同的版本进行构建,直到我们选择 cargo update
。
现在你可以在 main.rs 中使用 regex 库了。
使用 regex::Regex。
use regex::Regex;
fn main() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
println!("Did our date match? {}", re.is_match("2014-01-01"));
}
运行它将显示:
$ cargo run
Running `target/hello_world`
Did our date match? true
2.5 包的布局
Cargo使用约定俗成的文件放置方式,使其更容易深入到一个新的Cargo包中:
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
│ └── bin/
│ ├── named-executable.rs
│ ├── another-executable.rs
│ └── multi-file-executable/
│ ├── main.rs
│ └── some_module.rs
├── benches/
│ ├── large-input.rs
│ └── multi-file-bench/
│ ├── main.rs
│ └── bench_module.rs
├── examples/
│ ├── simple.rs
│ └── multi-file-example/
│ ├── main.rs
│ └── ex_module.rs
└── tests/
├── some-integration-tests.rs
└── multi-file-test/
├── main.rs
└── test_module.rs
Cargo.toml
和Cargo.lock
存放在包的根目录下(package root)。- 源代码放在
src
目录中。 - 默认的库文件是
src/lib.rs
。 - 默认的可执行文件是
src/main.rs
。- 其他可执行文件可以放在
src/bin/
目录中。
- 其他可执行文件可以放在
- 基准文件放在
benches
目录下。 - 示例放在
examples
目录中。 - 集成测试放在
tests
目录中。
如果binary、example、bench或集成测试由多个源文件组成,请将 main.rs
文件与额外的模块一起放在 src/bin
、examples
、benches
或 tests
目录的子目录中。可执行文件的名称将是该目录的名称。
你可以在书中了解更多关于Rust的模块系统。
参见配置目标以了解手动配置目标的更多细节。关于控制Cargo如何自动推断目标名称的更多信息,请参见目标自动发现。
2.6 Cargo.toml vs Cargo.lock
Cargo.toml 和 Cargo.lock 有两种不同的用途。在我们谈论它们之前,这里有一个总结:
- Cargo.toml 是广义的描述你的依赖关系,由你来写。
- Cargo.lock 包含了关于你的依赖关系的确切信息。它是由 Cargo 维护的,不应该被手动编辑。
如果你正在构建一个非最终产品,比如一个其他 rust 包会依赖的 rust 库,把 Cargo.lock 放在你的 .gitignore 中。如果你正在构建一个终端产品,如可执行的命令行工具或应用程序,或一个系统库,其 crate-type 为 staticlib 或 dylib,请将 Cargo.lock 放入git中。如果你想知道为什么会这样,请看FAQ中的 “为什么二进制文件有Cargo.lock的版本控制,而库没有?"。
让我们再深入了解一下。
Cargo.toml
是一个清单文件,我们可以在其中指定关于我们包的一系列不同的元数据。例如,我们可以说我们依赖另一个包:
[package]
name = "hello_world"
version = "0.1.0"
[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand.git" }
这个包只有一个依赖关系,就是对rand库的依赖。在这种情况下,我们已经说明了我们依靠的是住在GitHub上的一个特定的Git仓库。由于我们没有指定任何其他信息,Cargo认为我们打算使用主分支上的最新提交来构建我们的包。
听起来不错吧?有一个问题:如果你今天构建了这个包,然后你把副本发给我,我明天再构建这个包,就会发生一些不好的事情。在这期间,可能会有更多的提交给rand,而我的构建会包括新的提交,而你的则不会。因此,我们会得到不同的版本。这就不好了,因为我们需要可重复的构建。
我们可以在Cargo.toml中加入rev行来解决这个问题:
[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand.git", rev = "9f35b8e" }
现在我们的构建将是一样的。但是有一个很大的缺点:现在我们每次要更新我们的库的时候,都要手动考虑SHA-1的问题。这既乏味又容易出错。
进入Cargo.lock。因为它的存在,我们不需要手动跟踪确切的修订。Cargo会帮我们做到这一点。当我们有一个像这样的清单时:
[package]
name = "hello_world"
version = "0.1.0"
[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand.git" }
当我们第一次构建时,Cargo会将最新的提交信息写入我们的Cargo.lock中。该文件看起来像这样:
[[package]]
name = "hello_world"
version = "0.1.0"
dependencies = [
"rand 0.1.0 (git+https://github.com/rust-lang-nursery/rand.git#9f35b8e439eeedd60b9414c58f389bdc6a3284f9)",
]
[[package]]
name = "rand"
version = "0.1.0"
source = "git+https://github.com/rust-lang-nursery/rand.git#9f35b8e439eeedd60b9414c58f389bdc6a3284f9"
你可以看到这里有更多的信息,包括我们用来构建的确切版本。现在当你把你的包给别人时,他们会使用完全相同的SHA,尽管我们没有在Cargo.toml中指定它。
当我们准备选择使用新版本的库时,Cargo可以重新计算依赖关系并为我们更新:
$ cargo update # 更新所有的依赖项
$ cargo update -p rand # 只更新 "rand"。
这将写出一个新的 Cargo.lock
,其中包含新的版本信息。请注意,cargo update
的参数实际上是一个软件包ID规范,而 rand
只是一个简短的规范。
2.7 测试
Cargo 可以通过 cargo test
命令来运行测试。Cargo会在两个地方寻找要运行的测试:每个 src
文件中的测试和 tests/
中的任何测试。src
文件中的测试应该是单元测试,而 tests/
中的测试应该是集成测试。因此,你需要将你的 crate 导入到测试的文件中。
下面是一个在我们的软件包中运行cargo test的例子,它目前没有测试:
$ cargo test
Compiling rand v0.1.0 (https://github.com/rust-lang-nursery/rand.git#9f35b8e)
Compiling hello_world v0.1.0 (file:///path/to/package/hello_world)
Running target/test/hello_world-9c2b65bbb79eabce
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
如果我们的包有测试,我们会看到更多的输出,有正确的测试数量。
你也可以通过传递一个过滤器来运行一个特定的测试:
$ cargo test foo
这将运行任何名称中含有 foo 的测试。
cargo test
还可以运行额外的检查。它将编译你所包含的任何例子,也将测试你文档中的例子。请参阅Rust文档中的测试指南以了解更多细节。
2.9 Cargo Home
“Cargo home” 的功能是下载和源码缓存。当构建一个 crate 时,Cargo会将下载的构建依赖项存储在 Cargo home 中。你可以通过设置 CARGO_HOME 环境变量来改变 Cargo 主页的位置。如果你在你的 Rust crate 中需要这个信息,home crate 提供了一个API来获取这个位置。默认情况下,Cargo home 位于 $HOME/.cargo/
。
请注意,Cargo home的内部结构并不稳定,可能随时会有变化。
Cargo home 由以下部分组成:
文件:
-
config.toml
create 的全局配置文件,见参考文献中的config条目。 -
credentials.toml
来自cargo login的私人登录凭证,以便登录到注册表。 -
.crates.toml
这个隐藏文件包含了通过cargo install安装的crates的软件包信息。请不要用手去编辑!
备注: 新版本下目录不是这个样子的……
目录:
-
bin
目录包含了通过cargo install
或rustup
安装的 crate 的可执行文件。为了能够访问这些二进制文件,请将该目录的路径添加到你的$PATH
环境变量中。 -
git
Git的源代码存放在这里:-
git/db
当 crate 依赖 git 仓库时,Cargo 会将该仓库作为裸仓库克隆到这个目录,并在必要时进行更新。 -
git/checkouts
如果使用的是 git source,所需的 repo 提交会从 git/db 中的裸repo中签出到这个目录。这为编译器提供了包含在该依赖提交的 repo 中的实际文件。同一个 repo 的不同提交可以进行多次签出。
-
-
registry
crate注册表(如crates.io)的包和元数据都在这里。-
registry/index
索引是一个裸露的 git 仓库,包含了注册中心所有可用板条的元数据(版本、依赖关系等)。 -
registry/cache
下载的依赖关系被存储在缓存中。crates是压缩的gzip文件,以.crate为扩展名。 -
registry/src
如果下载的.crate归档文件是软件包所需要的,它将被解压到registry/src文件夹中,rustc将在那里找到.rs文件。
-
缓存CI中的Cargo home
为了避免在持续集成过程中重新下载所有的 crate 依赖项,你可以缓存 $CARGO_HOME
目录。然而,缓存整个目录往往是低效的,因为它将包含两次下载的源代码。如果我们依赖像 serde 1.0.92
这样的 crate 并缓存整个 $CARGO_HOME
,我们实际上会缓存两次源代码,在 registry/cache
中的 serde-1.0.92.crate
和 registry/src
中的 serde 的提取.rs文件。这可能会不必要地减慢构建速度,因为下载、提取、重新压缩和重新上传缓存到CI服务器可能需要一些时间。
在整个构建过程中,只缓存以下目录就足够了:
bin/
registry/index/
registry/cache/
git/db/
清除缓存
理论上说,你可以随时删除缓存的任何部分,如果一个crate需要源代码,Cargo会尽力恢复这些源代码,方法是重新解压归档或检查裸露的repo,或者直接从网上重新下载源代码。
另外,cargo-cache
crate提供了一个简单的CLI工具,可以只清除缓存中选定的部分,或者在命令行中显示其组件的大小。
2.10 构建缓存
Cargo 将构建的输出存储在 “target” 目录下。默认情况下,这是位于工作区根部的名为target
的目录。要改变这个位置,你可以设置 CARGO_TARGET_DIR
环境变量、build.target-dir
配置值,或者 --target-dir
命令行标志。
目录布局取决于你是否使用 --target
标志为特定的平台进行构建。如果没有指定 --target
,Cargo 会在为主机架构构建的模式下运行。输出会进入目标目录的根部,并根据是否是发布版构建而分开。
目录 | 描述 |
---|---|
target/debug/ |
包含调试构建输出。 |
target/release/ |
包含发行版的构建输出(有 --release 标志)。 |
当用 --target
为另一个目标构建时,输出会被放在一个带有目标名称的目录中:
目录 | 范例 |
---|---|
target/<triple>/debug/ |
target/thumbv7em-none-eabihf/debug/ |
target/<triple>/release/ |
target/thumbv7em-none-eabihf/release/ |
注意: 如果不使用
--target
,这将导致 Cargo 与构建脚本和 proc 宏共享你的依赖项。RUSTFLAGS 将与每个 rustc 调用共享。如果使用--target
标志,构建脚本和进程宏会单独构建(针对主机架构),并且不共享 RUSTFLAGS。
在配置文件目录(debug
或release
)中,工件被放置在以下目录中。
目录 | 描述 |
---|---|
target/debug/ |
包含正在构建的软件包的输出([[bin]]可执行文件和 [lib]‘库目标)。 |
target/debug/examples/ |
包含实例 ([[example]] targets). |
Some commands place their output in dedicated directories in the top level of the target
directory: 有些命令将它们的输出放在 “target” 目录顶层的专用目录中:
目录 | 描述 |
---|---|
target/doc/ |
包含rustdoc文档 (cargo doc ). |
target/package/ |
包含cargo package 和cargo publish 命令的输出。 |
Cargo还创建了其他一些构建过程中需要的目录和文件。它们的布局被认为是Cargo内部的,并且会被改变。这些目录中的一些是。
Directory | Description |
---|---|
target/debug/deps/ |
Dependencies and other artifacts. |
target/debug/incremental/ |
rustc incremental output, a cache used to speed up subsequent builds. |
target/debug/build/ |
Output from build scripts. |
Dep-info文件
在每个编译的工件旁边都有一个后缀为 .d
的文件,叫做 “dep info” 文件。这个文件是一个类似于 Makefile 的语法,表明重建工件所需的所有文件依赖关系。这些文件旨在与外部构建系统一起使用,以便它们能够检测到 Cargo 是否需要被重新执行。文件中的路径默认是绝对的。参见 build.dep-info-basedir 配置选项来使用相对路径。
共享的缓存
第三方工具 sccache
可以用来在不同的工作空间中共享构建的依赖关系。
要设置 sccache
,请用 cargo install sccache
安装它,并在调用Cargo前将 RUSTC_WRAPPER
环境变量设为 ccache
。如果你使用bash,在 .bashrc
中添加 export RUSTC_WRAPPER=sccache
是很有意义的。另外,你也可以在Cargo配置中设置 build.rustc-wrapper
。更多细节请参考sccache文档。
2 - Cargo Reference笔记: 依赖
https://doc.rust-lang.org/cargo/reference/index.html
3.1 指定依赖关系
crates 可以依赖 crates.io 或其他注册表、git 存储库或本地文件系统中的子目录中的其他库。你也可以暂时覆盖一个依赖的位置–例如,能够测试出你正在本地工作的依赖中的错误修复。你可以为不同的平台设置不同的依赖关系,以及只在开发期间使用的依赖关系。让我们来看看如何做到这些。
从crates.io中指定依赖项
Cargo的配置是默认在 crates.io 上寻找依赖项。在这种情况下,只需要名称和一个版本字符串。在 crate 指南中,我们指定了对 time create 的依赖:
[dependencies]
time = "0.1.12"
字符串 “0.1.12” 是 semver (语义化)版本要求。由于这个字符串中没有任何运算符,它被解释为与我们指定 “^0.1.12” 的方式相同,这被称为关照需求(caret requirement)。
Caret Requirement
关照需求(caret requirement)允许 SemVer 兼容更新到一个指定的版本。如果新的版本号没有修改主要、次要、补丁分组中最左边的非零数字,则允许更新。在这种情况下,如果我们运行 cargo update -p time
,如果是最新的 0.1.z
版本,cargo应该将我们更新到 0.1.13
版本,但不会将我们更新到 0.2.0
。如果我们将版本字符串指定为 ^1.0
,如果是最新的 1.y
版本,cargo应该更新到 1.1
,但不是 2.0
。0.0.x
版本不被认为与其他任何版本兼容。
下面是一些更多的关照需求的例子,以及允许使用这些要求的版本:
^1.2.3 := >=1.2.3, <2.0.0
^1.2 := >=1.2.0, <2.0.0
^1 := >=1.0.0, <2.0.0
^0.2.3 := >=0.2.3, <0.3.0
^0.2 := >=0.2.0, <0.3.0
^0.0.3 := >=0.0.3, <0.0.4
^0.0 := >=0.0.0, <0.1.0
^0 := >=0.0.0, <1.0.0
这个兼容性约定与 SemVer 在对待1.0.0之前的版本的方式不同。SemVer 说 1.0.0 之前没有兼容性,而 Cargo 认为 0.x.y
与 0.x.z
兼容,其中 y≥z
且 x>0
。
Tilde Requirement
Tilde Requirement指定了最小版本并具有一定的更新能力。如果你指定一个主要的、次要的和补丁的版本,或者只指定一个主要的和次要的版本,那么只允许补丁级别的改变。如果你只指定一个主要的版本,那么次要的和补丁级的改变是允许的。
~1.2.3
是Tilde Requirement的例子:
~1.2.3 := >=1.2.3, <1.3.0
~1.2 := >=1.2.0, <1.3.0
~1 := >=1.0.0, <2.0.0
通配符要求
通配符要求允许通配符定位的任何版本。
*
、1.*
和1.2.*
是通配符要求的例子。
* := >=0.0.0
1.* := >=1.0.0, <2.0.0
1.2.* := >=1.2.0, <1.3.0
注意
crates.io不允许有裸*版本。比较要求
比较要求允许手动指定一个版本范围或一个确切的版本来依赖。
下面是一些比较要求的例子:
>= 1.2.0
> 1
< 2
= 1.2.3
多重要求
如上面的例子所示,多个版本的要求可以用逗号分开,例如,>=1.2
,<1.5
。
指定来自其他注册表的依赖关系
要从 crates.io 以外的注册中心指定一个依赖关系,首先必须在 .cargo/config.toml
文件中配置该注册中心。更多信息请参见注册表文档。在依赖关系中,将 registry
的键值设置为要使用的注册表的名称。
[dependencies]
some-crate = { version = "1.0", registry = "my-registry" }
注意
crates.io不允许发布依赖其他注册机构的软件包。指定来自 git 仓库的依赖关系
要依赖位于git仓库中的库,你需要指定的最小信息是带有git钥匙的仓库的位置:
[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand" }
Cargo会在这个位置获取git仓库,然后在git仓库的任何地方寻找所要求的 crate 的 Cargo.toml(不一定在根部–例如,指定一个工作区的成员crate名称,并设置git为包含该工作区的仓库)。
由于我们没有指定任何其他信息,Cargo假定我们打算使用主分支上的最新提交来构建我们的包。你可以将 git
key 与 rev
、tag
或 branch
键结合起来,指定其他内容。下面是一个指定要使用 next
分支上的最新提交的例子:
[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand", branch = "next" }
一旦添加了git依赖,Cargo会将该依赖锁定在当时的最新提交。一旦锁定,新的提交将不会被自动拉下。不过,它们可以通过 cargo update
手动拉下。
请参阅Git认证,以获得关于私有仓库的Git认证的帮助。
注意
crates.io 不允许发布带有git依赖的软件包(git dev-dependencies 被忽略)。请参阅 “多个位置"一节,以获得后备选择。指定路径依赖
随着时间的推移,我们在指南中的 ·hello_world· 包的体积已经大大增加了。我们可能想把它拆成一个单独的 crate 供别人使用。为了做到这一点,Cargo支持路径依赖,通常是在一个仓库中的子程序。让我们先在 hello_world
包中建立一个新的crate:
# 在hello_world/的内部
$ cargo new hello_utils
这将创建一个新的文件夹 hello_utils
,其中的 Cargo.toml
和 src
文件夹已经准备好被配置。为了告诉 Cargo 这一点,打开 hello_world/Cargo.toml
,将 hello_utils
添加到你的依赖项中。
[dependencies]
hello_utils = { path = "hello_utils" }
这就告诉Cargo,我们依赖一个叫 hello_utils
的crate,这个crate在 hello_utils
文件夹中(相对于它所写的 Cargo.toml
)。
就这样了! 下一次 crate 构建将自动构建 hello_utils
和它自己的所有依赖项,其他人也可以开始使用这个crate。然而,crates.io 上不允许使用只指定路径的依赖项。如果我们想发布我们的 hello_world
crate,我们需要将 hello_utils
的一个版本发布到crates.io,并在依赖项中指定其版本。
[dependencies]
hello_utils = { path = "hello_utils", version = "0.1.0" }
注意
crates.io 不允许发布带有路径依赖的包(路径 dev-dependencies 被忽略)。请参阅 “多个位置” 一节,以获得后备选择。多个位置
可以同时指定注册表版本和 git
或 path
位置。git
或 path
依赖将在本地使用(在这种情况下,version
会与本地副本进行核对),而当发布到 crates.io 这样的注册表时,它将使用注册表的版本。其他组合是不允许的。例子:
[dependencies]
# 在本地使用时使用`my-bitflags`,发布时使用 crates.io 的 1.0 版本。
bitflags = { path = "my-bitflags", version = "1.0" }
# 在本地使用时使用给定的git repo,发布时使用crates.io的1.0版本。
smallvec = { git = "https://github.com/servo/rust-smallvec", version = "1.0" }
# 注意:如果版本不匹配,Cargo将无法编译!
一个有用的例子是,当你把一个库拆成多个包放在同一个工作区时。你可以使用路径依赖来指向工作区中的本地包,以便在开发过程中使用本地版本,然后在发布后使用 crates.io 版本。这类似于指定一个覆盖,但只适用于这一个依赖声明。
特定平台的依赖性
特定平台的依赖关系采用相同的格式,但被列在 target
部分。通常情况下,类似Rust的 #[cfg]语法
将被用来定义这些部分:
[target.'cfg(windows)'.dependencies]
winhttp = "0.4.0"
[target.'cfg(unix)'.dependencies]
openssl = "1.0.1"
[target.'cfg(target_arch = "x86")'.dependencies]
native = { path = "native/i686" }
[target.'cfg(target_arch = "x86_64")'.dependencies]
native = { path = "native/x86_64" }
和Rust一样,这里的语法支持 not
、any
和 all
操作符,以组合各种 cfg name/value 对。
如果你想知道哪些 cfg 目标在你的平台上可用,在命令行中运行 rustc --print=cfg
。如果你想知道哪些cfg目标在其他平台上可用,比如64位Windows,可以运行 rustc --print=cfg --target=x86_64-pc-windows-msvc
。
linux平台的执行结果:
$ rustc --print=cfg
debug_assertions
target_arch="x86_64"
target_endian="little"
target_env="gnu"
target_family="unix"
target_feature="fxsr"
target_feature="sse"
target_feature="sse2"
target_os="linux"
target_pointer_width="64"
target_vendor="unknown"
unix
与你的 Rust 源代码不同,你不能使用 [target.'cfg(feature = "fancy-feature")'.dependencies]
来添加基于可选特性的依赖关系。请使用 [features]
部分来代替。
[dependencies]
foo = { version = "1.0", optional = true }
bar = { version = "1.0", optional = true }
[features]
fancy-feature = ["foo", "bar"]
这同样适用于 cfg(debug_assertions)
、cfg(test)
和 cfg(proc_macro)
。这些值不会像预期的那样工作,而总是有 rustc --print=cfg
所返回的默认值。目前还没有办法根据这些配置值来添加依赖关系。
除了 #[cfg]
语法外,Cargo还支持列出依赖关系所适用的完整目标:
[target.x86_64-pc-windows-gnu.dependencies]
winhttp = "0.4.0"
[target.i686-unknown-linux-gnu.dependencies]
openssl = "1.0.1"
开发依赖
你可以在你的 Cargo.toml 中添加一个 [dev-dependencies]
部分,其格式相当于[dependencies]
。
[dev-dependencies]
tempdir = "0.3"
Dev-dependencies 在编译软件包时不使用,但用于编译 tests、 examples 和 benchmarks。
这些依赖关系不会传播到其他依赖该软件包的软件包上。
你也可以通过在目标部分的标题中使用 dev-dependencies
而不是 dependencies
来获得特定目标的开发依赖。比如说:
[target.'cfg(unix)'.dev-dependencies]
mio = "0.0.1"
注意
注意:当一个软件包被发布时,只有指定version
的 dev-dependencies
才会被包含在发布的crate中。对于大多数使用情况,发布时不需要 dev-dependencies
,尽管有些用户(如操作系统打包者)可能想在crate中运行测试,所以如果可能的话,提供 version
仍然是有益的。
构建依赖性
您可以在您的构建脚本中依赖其他基于Cargo的 crate 来使用。依赖关系通过清单中的 build-dependencies
部分来声明。
[build-dependencies]
cc = "1.0.3"
你也可以通过在目标部分标题中使用 build-dependencies
而不是 dependencies
来获得特定目标的构建依赖。比如说:
[target.'cfg(unix)'.build-dependencies]
cc = "1.0.3"
在这种情况下,只有当主机平台与指定的目标相匹配时,才会构建该依赖关系。
构建脚本不能访问在 dependencies
或 dev-dependencies
部分列出的依赖项。除非在依赖关系部分列出,否则构建依赖关系对软件包本身也同样不可用。软件包本身和它的构建脚本是分开构建的,所以它们的依赖关系不需要重合。通过为独立的目的使用独立的依赖关系,Cargo可以保持更简单、更干净。
选择特性
如果你依赖的软件包提供了有条件的特性,你可以指定使用哪些特性:
[dependencies.awesome]
version = "1.3.5"
default-features = false # 不包括默认功能,并可选择挑选个别功能
features = ["secure-password", "civet"]
重命名Cargo.toml中的依赖项
在 Cargo.toml 中编写 [dependencies]
部分时,你为依赖关系编写的键通常与你在代码中导入的crate的名称一致。但对于某些项目,你可能希望在代码中用不同的名字来引用crate,而不管它在 crates.io 上是如何发布的。例如,你可能希望:
- 避免在Rust源代码中需要
use foo as bar
。 - 依赖于一个 crate 的多个版本。
- 依赖于不同注册中心的同名crate。
为了支持这一点,Cargo在 [dependencies]
部分支持包的 key,即应该依赖哪个包:
[package]
name = "mypackage"
version = "0.0.1"
[dependencies]
foo = "0.1"
bar = { git = "https://github.com/example/project", package = "foo" }
baz = { version = "0.1", registry = "custom", package = "foo" }
在这个例子中,你的Rust代码中现在有三个crate可用:
extern crate foo; // crates.io
extern crate bar; // git repository
extern crate baz; // registry `custom`
这三个crate在它们自己的 Cargo.toml 中都有 foo
的包名,所以我们明确地使用 package key 来通知Cargo我们想要foo的包,尽管我们在本地调用了其他东西。如果没有指定 package 的 key,则默认为被请求的依赖关系的名称。
注意,如果你有一个可选的依赖关系,比如:
[dependencies]
bar = { version = "0.1", package = 'foo', optional = true }
你依赖于 crates.io 的 crate foo,但你的 crate 有一个 bar 特性而不是 foo 特性。也就是说,当重命名时,特性的名称会跟随依赖关系的名称,而不是软件包的名称。
启用反式依赖的工作原理与此类似,例如我们可以在上述清单中添加以下内容:
[features]
log-debug = ['bar/log-debug'] # using 'foo/log-debug' would be an error!
3.1.1 覆盖依赖
覆盖依赖关系
覆盖依赖关系的愿望可以通过多种情况产生。然而,大多数情况下,都可以归结为在 crates.io 上发布之前就能使用一个crate。比如说:
- 你正在开发的一个 crate 也被用于你正在开发的一个更大的应用程序中,你想在这个更大的应用程序中测试一个库的错误修复。
- 一个你不负责的上游代码库在其 git 仓库的主分支上有一个新的功能或错误修复,你想测试一下。
- 你即将发布一个新的主要版本,但你想在整个软件包中进行集成测试,以确保新的主要版本能够正常运行。
- 你已经为你发现的一个bug向上游 crate 提交了一个修复,但你想立即让你的应用程序开始依赖于 crate 的固定版本,以避免bug修复被合并时的阻塞。
这些情况可以通过 [patch] 清单部分
来解决。
本章将介绍几种不同的使用情况,并详细介绍覆盖依赖关系的不同方法。
测试错误修复
假设你正在使用 uuid crate,但当你在工作中发现了一个错误。不过,你很有进取心,所以你决定也尝试修复这个错误。最初,你的清单是这样的:
[package]
name = "my-library"
version = "0.1.0"
[dependencies]
uuid = "1.0"
我们要做的第一件事是通过以下方式在本地克隆 uuid 仓库:
$ git clone https://github.com/uuid-rs/uuid
接下来我们将编辑my-library的清单,使其包含:
[patch.crates-io]
uuid = { path = "../path/to/uuid" }
这里我们声明,我们正在给源代码 crates-io
打上一个新的依赖关系。这将有效地把本地签出的 uuid
版本添加到我们本地软件包的 crates.io 注册表中。
接下来我们需要确保我们的锁文件被更新以使用这个新版本的 uuid
,这样我们的包就会使用本地签出的副本而不是来自 crates.io
的。[patch]
的工作方式是,它将在 ../path/to/uuid
加载依赖关系,然后当 crates.io
被查询 uuid
的版本时,它也会返回本地版本。
这意味着本地检出的版本号很重要,会影响到补丁是否被使用。我们的清单声明 uuid="1.0"
,这意味着我们只解析 >=1.0.0,<2.0.0
,而且Cargo的贪婪解析算法也意味着我们会解析到该范围内的最大版本。通常情况下,这并不重要,因为 git 仓库的版本已经大于或符合 crates.io 上发布的最大版本,但记住这一点很重要!
在任何情况下,通常你现在需要做的就是:
$ cargo build
Compiling uuid v1.0.0 (.../uuid)
Compiling my-library v0.1.0 (.../my-library)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
就这样了,您现在正在使用本地版本的 uuid 进行构建(注意构建输出中括号中的路径)。如果你没有看到本地路径的版本被构建,那么你可能需要运行 cargo update -p uuid --precise $version
,其中 $version
是本地检查出的 uuid 副本的版本。
一旦你修复了你最初发现的错误,你要做的下一件事可能是把它作为一个拉动请求提交给 uuid crate 本身。一旦你完成了这项工作,你也可以更新 [patch]
部分。[patch]
中的列表就像 [dependencies]
部分一样,所以一旦你的拉动请求被合并,你可以将你的路径依赖改为。
[patch.crates-io]
uuid = { git = 'https://github.com/uuid-rs/uuid' }
使用未发布的次要版本
现在让我们从修复bug到添加功能的过程中转换一下齿轮。当你在 my-library 上工作时,你发现 uuid crate 中需要一个全新的功能。你已经实现了这个功能,用 [patch]
在本地进行了测试,并提交了一个拉动请求。让我们来看看在实际发布之前,你如何继续使用和测试它。
我们还假设 crates.io 上 uuid 的当前版本是 1.0.0,但此后 git 仓库的 master 分支已经更新到 1.0.1。这个分支包括你之前提交的新特性。为了使用这个仓库,我们将编辑我们的 Cargo.toml,使其看起来像:
[package]
name = "my-library"
version = "0.1.0"
[dependencies]
uuid = "1.0.1"
[patch.crates-io]
uuid = { git = 'https://github.com/uuid-rs/uuid' }
请注意,我们对 uuid 的本地依赖已经更新到了 1.0.1,因为一旦发布 crate,我们就需要这个版本。不过这个版本在 crates.io 上并不存在,所以我们在清单的 [patch] 部分提供了它。
现在,当我们的库被构建时,它会从 git 仓库中获取 uuid 并解析到仓库中的 1.0.1,而不是试图从 crates.io 上下载一个版本。一旦1.0.1在crates.io上发布,[patch]
部分就可以被删除。
值得注意的是,[patch]
也是过渡性应用。假设你在一个更大的软件包中使用 my-library,例如:
[package]
name = "my-binary"
version = "0.1.0"
[dependencies]
my-library = { git = 'https://example.com/git/my-library' }
uuid = "1.0"
[patch.crates-io]
uuid = { git = 'https://github.com/uuid-rs/uuid' }
请记住,[patch]
是过渡性的,但只能在顶层定义,所以我们 my-library 的消费者必须在必要时重复 [patch]
部分。不过在这里,新的 uuid crate 同时适用于我们对 uuid 的依赖和 my-library -> uuid 的依赖。uuid crate 将被解析为整个 crate 图的一个版本,即 1.0.1,并将从 git 仓库中提取。
3 - Cargo Reference笔记: 清单
https://doc.rust-lang.org/cargo/reference/manifest.html
3.2 清单格式
每个软件包的Cargo.toml文件被称为其清单。它是以TOML格式编写的。每个清单文件由以下部分组成。
[package] 部分
Cargo.toml
的第一个部分是 [package]
.
[package]
name = "hello_world" # the name of the package
version = "0.1.0" # the current version, obeying semver
authors = ["Alice <a@example.com>", "Bob <b@example.com>"]
Cargo要求的唯一字段是 name 和 version。如果发布到注册表,注册表可能需要额外的字段。关于发布到crates.io的要求,见下面的注释和发布章节。
name字段
包的 name 是一个用来指代包的标识符。它在被列为另一个包的依赖关系时使用,并作为推断的lib和bin目标的默认名称。
name 必须只使用字母数字字符或 -
或 _
,而且不能为空。请注意,cargo new
和 cargo init
对包名施加了一些额外的限制,比如强制要求它是一个有效的Rust标识符,而不是一个关键字。crates.io 甚至施加了更多的限制,比如强制要求只能使用ASCII字符,不能使用保留名,不能使用 “nul” 这样的特殊Windows名称,不能太长,等等。
version字段
Cargo采用了语义版本控制(Semantic Versioning)的概念,因此请确保遵循一些基本规则。
- 在你达到1.0.0之前,什么都可以做,但如果你做了破坏性的修改,就要递增次要版本。在Rust中,破坏性改动包括向结构体添加字段或向枚举添加变体。
- 在1.0.0之后,只有在增加主版本的时候才可以进行破坏性修改。不要破坏构建。
- 1.0.0之后,不要在补丁级版本中添加任何新的公共API(没有新的pub东西)。如果你添加任何新的pub结构、特征、字段、类型、函数、方法或其他东西,一定要增加次要版本。
- 使用有三个数字部分的版本号,如1.0.0而不是1.0。
authors字段
可选的 authors 字段列出了被认为是包的 “作者” 的人或组织。确切的含义是可以解释的–它可以列出包的原始或主要作者、当前的维护者或所有者。一个可选的电子邮件地址可以包含在每个作者条目末尾的斜方括号内。
这个字段只在软件包元数据和 build.rs 中的 CARGO_PKG_AUTHORS
环境变量中出现。它不显示在crates.io的用户界面上。
edition字段
edition
是一个可选的键,它影响你的包是用哪个Rust版本编译的。在 [package]
中设置 edition key 会影响到包中的所有 target/crates,包括测试套件、基准、二进制文件、例子等等。
[package]
# ...
edition = '2021'
大多数清单的版本字段都由 cargo new 自动填入最新的稳定版本。默认情况下,cargo new 创建的清单目前使用的是 2021 版。
如果 Cargo.toml
中没有 edition 字段,那么为了向后兼容,会假定为2015版。请注意,用 cargo new
创建的所有清单都不会使用这种历史回退,因为它们会明确指定版本为较新的值。
rust-version字段
rust-version字段是一个可选的键,它告诉cargo你的包可以用哪个版本的Rust语言和编译器进行编译。如果当前选择的Rust编译器的版本比声明的版本早,cargo会退出,并告诉用户需要什么版本。
第一个支持这个字段的Cargo版本是随着Rust 1.56.0发布的。在旧版本中,这个字段将被忽略,Cargo会显示警告
[package]
# ...
rust-version = "1.56"
Rust版本必须是由两到三个部分组成的裸版本号;它不能包括 semver 操作符或预发布标识符。编译器的预发布标识符,如 -nightly
,在检查Rust版本时将被忽略。rust-version
必须等于或高于首次引入配置版本的版本。
rust-version
可以使用 --ignore-rust-version
选项来忽略。
在 [package]
中设置 rust-version
键将影响到包中的所有 target/crate,包括测试套件、基准、二进制文件、示例等。
description字段
description 是关于软件包的简短介绍。crates.io
会在你的软件包中显示这个描述。这应该是纯文本(不是Markdown)。
[package]
# ...
description = "A short description of my package"
documentation字段
documentation 字段指定了存放 crate 文档的网站的一个 URL。如果清单文件中没有指定 URL,crates.io 将自动将您的 crate 链接到相应的 docs.rs 页面。
[package]
# ...
documentation = "https://docs.rs/bitflags"
readme字段
readme字段应该是包根部的一个文件的路径(相对于这个Cargo.toml
),其中包含关于包的一般信息。crates.io 会将其解释为 Markdown 并在crate的页面上呈现。
[package]
# ...
readme = "README.md"
如果没有指定这个字段的值,并且在软件包根中存在一个名为 README.md、README.txt 或 README 的文件,那么将使用该文件的名称。你可以通过把这个字段设置为false来抑制这种行为。如果该字段被设置为 “true”,将假定默认值为README.md。
homepage字段
homepage字段应该是一个网站的URL,这个网站是你的包的主页:
[package]
# ...
homepage = "https://serde.rs/"
repository字段
repository字段应该是软件包的源存储库的URL:
[package]
# ...
repository = "https://github.com/rust-lang/cargo/"
license和license-file字段
license字段包含软件包发布时使用的软件许可名称。license-file字段包含了一个包含许可证文本的文件的路径(相对于这个Cargo.toml)。
crates.io 将许可证字段解释为 SPDX 2.1
许可证表达式。该名称必须是 SPDX许可证列表3.11
中的一个已知许可证。目前不支持括号。更多信息请参见SPDX网站。
SPDX许可证表达式支持AND和OR运算符来组合多个许可证。
[package]
# ...
license = "MIT OR Apache-2.0"
使用 OR 表示用户可以选择任何一个许可证。使用 AND 表示用户必须同时遵守两个许可证。使用 WITH 运算符表示一个有特殊例外的许可证。一些例子:
- MIT OR Apache-2.0
- LGPL-2.1-only AND MIT AND BSD-2-Clause
- GPL-2.0-or-later WITH Bison-exception-2.2
如果一个软件包使用的是非标准的许可证,那么可以指定 license-file 字段来代替 license 字段。
[package]
# ...
license-file = "LICENSE.txt"
注意
crates.io 需要设置 license 或 license-file。keywords字段
keywords 字段是一个描述此包的字符串数组。当在注册表上搜索该包时,这可以提供帮助,你可以选择任何可以帮助别人找到这个crate的词语。
[package]
# ...
keywords = ["gamedev", "graphics"]
注意
crates.io 最多有5个关键词。每个关键词必须是ASCII文本,以字母开头,并且只包含字母、数字、_
或-
,并且最多拥有20个字符。
categories字段
categories字段是一个字符串数组,表示该包属于的类别:
categories = ["command-line-utilities", "development-tools::cargo-plugins"]
注意
crates.io 最多有5个类别。每个类别都应该与 https://crates.io/category_slugs 中的一个匹配,并且必须完全匹配。workspace字段
workspace字段可以用来配置这个包的工作空间。如果没有指定,这将被推断为文件系统中第一个带有 [workspace]
的 Cargo.toml
向上。如果成员不在工作区根目录下,设置这个字段很有用。
[package]
# ...
workspace = "path/to/workspace/root"
如果清单中已经定义了 [workspace]
表,则不能指定此字段。也就是说,一个 crate 不能既是一个 workspace 的根板块(包含 [workspace]
),又是另一个 workspace 的成员 crate(包含 package.workspace
)。
欲了解更多信息,请参见工作空间一章。
build字段
build字段在包根中指定了一个文件,该文件是用于构建本地代码的构建脚本。更多信息可以在构建脚本指南中找到。
[package]
# ...
build = "build.rs"
默认是 “build.rs”,它从软件包根目录下一个名为 build.rs
的文件中加载该脚本。使用 build = "custom_build_name.rs"
来指定一个不同文件的路径,或者 build = false
来禁止自动检测构建脚本。
links字段
links字段指定了被链接的本地库的名称。更多信息可以在构建脚本指南的链接部分找到。
[package]
# ...
links = "foo"
exclude和include字段
exclude 和 include 字段可以用来明确指定在打包发布项目时包括哪些文件,以及某些类型的变更跟踪(如下所述)。在 exclude 字段中指定的模式确定了一组不包括的文件,而 include 中的模式则指定了明确包括的文件。你可以运行 cargo package --list
来验证哪些文件将被包含在软件包中。
[package]
# ...
exclude = ["/ci", "images/", ".*"]
[package]
# ...
include = ["/src", "COPYRIGHT", "/examples", "!/examples/big_example"]
如果这两个字段都没有被指定,默认情况是包括软件包根部的所有文件,除了下面列出的排除项。
如果没有指定 include
,那么下面的文件将被排除。
- 如果软件包不在git仓库中,所有以点开始的 “隐藏 “文件将被跳过。
- 如果软件包在git仓库中,任何被仓库和全局git配置的gitignore规则所忽略的文件都将被跳过。
不管是指定exclude还是include,以下文件总是被排除在外:
- 任何子包将被跳过(任何包含Cargo.toml文件的子目录)。
- 在包的根目录下名为target的目录将被跳过。
以下文件总是被包括在内:
- 包本身的Cargo.toml文件总是被包含,它不需要被列在include中。
- 如果软件包包含二进制或示例目标,则会自动包含一个最小化的Cargo.lock,更多信息请参见cargo package。
- 如果指定了 license-file,它总是被包括在内。
这些选项是相互排斥的;设置 include 会覆盖 exclude。如果你需要对一组 include 文件进行排除,请使用下面描述的 !
操作符。
这些模式应该是 gitignore 风格的模式。简而言之:
TODO: 这里内容太细了,跳过
publish字段
publish字段可以用来防止一个包被错误地发布到包注册中心(如crates.io),例如,在一个公司中保持一个包的私有性:
[package]
# ...
publish = false
该值也可以是一个字符串数组,这些字符串是允许被发布到的注册表名称:
[package]
# ...
publish = ["some-registry-name"]
如果publish数组包含单个注册表,当没有指定 --registry
标志时,crate 发布命令将使用它。
元数据表
默认情况下,Cargo 会对 Cargo.toml 中未使用的键进行警告,以帮助检测错别字之类的。而 package.metadata
表则完全被Cargo忽略,不会被警告。这一部分可以用于那些想在Cargo.toml
中存储软件包配置的工具。比如说:
[package]
name = "..."
# ...
# Metadata used when generating an Android APK, for example.
[package.metadata.android]
package-name = "my-awesome-android-app"
assets = "path/to/static"
在工作区层面也有一个类似的表,名为 workspace.metadata
。虽然 crate 没有为这两个表的内容指定格式,但建议外部工具可能希望以一致的方式使用它们,例如,如果 package.metadata
中缺少数据,就引用 workspace.metadata
中的数据,如果这对有关工具来说是合理的。
default-run字段
清单 [package]
部分中的 default-run
字段可用于指定 crate 运行所选的默认二进制文件。例如,当同时存在 src/bin/a.rs
和 src/bin/b.rs 时
:
[package]
default-run = "a"
3.2.1 Cargo Target
Cargo包由 target 组成,这些 target 对应于可以被编译成 crate 的源文件。包可以有库、二进制、example、测试和 benchmark target。target 列表可以在 Cargo.toml 清单中配置,通常由源文件的目录布局自动推断出来。
有关配置目标设置的详情,请参见下面的配置目标。
Library/库
库目标定义了 “library/库”,可以被其他库和可执行文件使用和链接。文件名默认为 src/lib.rs
,库的名称默认为软件包的名称。包只能有一个库。库的设置可以在 Cargo.toml 的 [lib]
表中自定义。
# Example of customizing the library in Cargo.toml.
[lib]
crate-type = ["cdylib"]
bench = false
Binaries/二进制文件
二进制目标是经过编译后可以运行的可执行程序。默认的二进制文件名是 src/main.rs
,它默认为软件包的名称。其他二进制文件存储在 src/bin/
目录中。每个二进制文件的设置可以在 Cargo.toml 中的 [[bin]]
表中进行自定义。
二进制文件可以使用包的库的公共API。它们也会与 Cargo.toml 中定义的 [dependencies]
链接。
你可以用带有 --bin <bin-name>
选项的 cargo run
命令来运行单个二进制文件。cargo install
可以用来将可执行文件复制到一个公共位置。
# Example of customizing binaries in Cargo.toml.
[[bin]]
name = "cool-tool"
test = false
bench = false
[[bin]]
name = "frobnicator"
required-features = ["frobnicate"]
示例
位于 examples
目录下的文件是该库所提供的功能的使用示例。当编译时,它们被放置在 target/debug/examples
目录下。
示例可以使用包的库的公共API。它们也与 Cargo.toml 中定义的 [dependencies]
和 [dev-dependencies]
连接。
默认情况下,示例是可执行的二进制文件(有一个 main()
函数)。你可以指定 crate-type
字段来使示例被编译为库。
[[example]]
name = "foo"
crate-type = ["staticlib"]
可以使用 cargo run
命令,并使用 --example <example-name>
选项来运行单个可执行的示例。库中的示例可以用带 --example <example-name>
选项的 cargo build
来构建。带 --example <example-name>
选项的 cargo install
可以用来将可执行的二进制文件复制到一个共同的位置。默认情况下,示例是由 cargo test
编译的,以保护它们不被咬坏。如果你的例子中有 #[test]
函数,而你想用 cargo test
来运行的话,请把 test
字段设置为true。
测试
在Cargo项目中,有两种形式的测试:
- 单元测试是位于库或二进制文件(或任何有
test
字段的 target)中标有#[test]
属性的函数。这些测试可以访问位于它们所定义的目标中的私有API。 - 集成测试是一个单独的可执行二进制文件,也包含
#[test]
函数,它与项目的库相连,可以访问其公共API。
测试是通过 cargo test
命令运行的。默认情况下,Cargo 和 rustc 使用 libtest harness,它负责收集带有 #[test]
属性注释的函数并并行执行,报告每个测试的成功和失败。
集成测试
位于 tests
目录下的文件是集成测试。当你运行 cargo test
时,Cargo会把这些文件中的每一个都编译成一个单独的crate,并执行它们。
集成测试可以使用包的库的公共API。它们也会与 Cargo.toml
中定义的 [dependencies]
和 [dev-dependencies]
相连接。
如果你想在多个集成测试中共享代码,你可以把它放在一个单独的模块中,比如 test/common/mod.rs
,然后把 mod common;
放在每个测试中来导入它。
每个集成测试会生成一个单独的可执行二进制文件,crate 测试将连续运行它们。在某些情况下,这可能是低效的,因为它可能需要更长的时间来编译,而且在运行测试时可能无法充分利用多个CPU。如果你有很多集成测试,你可能要考虑创建一个单一的集成测试,并将测试分成多个模块。libtest 线束会自动找到所有 #[test]
注释的函数,并并行运行它们。你可以向 crate 测试传递模块名称,以便只运行该模块内的测试。
如果有集成测试,二进制目标会自动构建。这允许集成测试执行二进制文件以锻炼和测试其行为。CARGO_BIN_EXE_<name>
环境变量在集成测试建立时被设置,以便它可以使用 env 宏来定位可执行文件。
基准
基准提供了一种使用 cargo bench
命令来测试代码性能的方法。它们遵循与测试相同的结构,每个基准函数都用 #[bench]
属性来注释。与测试类似:
- 基准被放置在
benches
目录下。 - 在库和二进制文件中定义的基准函数可以访问它们所定义的目标中的私有API。benches 目录中的基准可以使用公共API。
- bench 字段可以用来定义哪些目标是默认的基准。
- harness 字段可以用来禁用内置的harness。
注意
目前#[bench]
属性还不稳定,只在 nightly 频道中可用。crates.io
上有一些软件包,可能有助于在稳定频道上运行基准,比如Criterion。
配置目标
Cargo.toml中所有的 [lib]
、[[bin]]
、[[example]]
、[[test]]
和 [[bench]]
部分都支持类似的配置,以指定目标应该如何构建。像 [[bin]]
这样的双括号部分是TOML的数组表,这意味着你可以写一个以上的 [[bin]]
部分来在你的 crate
中制作多个可执行文件。你只能指定一个库,所以 [lib]
是一个普通的TOML表。
下面是每个目标的TOML设置的概述,每个字段在下面有详细描述:
[lib]
name = "foo" # The name of the target.
path = "src/lib.rs" # The source file of the target.
test = true # Is tested by default.
doctest = true # Documentation examples are tested by default.
bench = true # Is benchmarked by default.
doc = true # Is documented by default.
plugin = false # Used as a compiler plugin (deprecated).
proc-macro = false # Set to `true` for a proc-macro library.
harness = true # Use libtest harness.
edition = "2015" # The edition of the target.
crate-type = ["lib"] # The crate types to generate.
required-features = [] # Features required to build this target (N/A for lib).
name字段
name 字段指定了目标的名称,它对应于将被生成的工件的文件名。对于一个库来说,这是依赖项用来引用它的板条箱名称。
对于 [lib]
和默认的二进制文件 (src/main.rs
),这默认为包的名称,任何破折号都用下划线代替。对于其他自动发现的目标,它默认为目录或文件名。
除了 [lib]之外
,所有目标都需要这样做。
path字段
路径字段指定了 crate 的源文件的位置,相对于 Cargo.toml 文件。
如果没有指定,将使用基于目标名称的推断路径。
test字段
test字段表示目标是否默认由 crate 测试来测试。对于lib、bins和测试,默认为true。
doctest字段
doctest字段表示文档实例是否默认由cargo test来测试。这只与库有关,对其他部分没有影响。对于库来说,默认为真。
bench字段
bench字段表示目标是否默认由 cargo bench 进行基准测试。对于lib、bins和benchmark,默认为true。
doc字段
doc字段表示目标程序是否默认包含在由 cargo doc 生成的文档中。默认情况下,库和二进制文件为真。
plugin字段
这个字段用于rustc插件,这些插件正在被废弃。
proc-macro字段
proc-macro 字段表示该库是一个过程性的宏(引用)。这只对 [lib] 目标有效。
harness字段
harness 字段表示 --test
标志将被传递给 rustc,它将自动包含 libtest 库,它是收集和运行标有 #[test]
属性的测试或标有 #[bench]
属性的基准的驱动。默认情况下,所有目标都是true。
如果设置为false,那么你要负责定义一个main()函数来运行测试和基准。
无论 harness 是否被启用,测试都有 cfg(test)
条件表达式被启用。
edition字段
edition字段定义了目标将使用的Rust版本。如果没有指定,它默认为 [package]
的版本字段。这个字段通常不应该被设置,它只适用于高级场景,例如将一个大的包逐步过渡到一个新的版本。
Crate-type字段
crate-type 字段定义了将由目标生成的 crate 类型。它是一个字符串数组,允许你为一个目标指定多个 crate 类型。这只能被指定给库和例子。二进制文件、测试和基准总是 “bin” crate类型。默认值是:
Target | Crate Type |
---|---|
Normal library | "lib" |
Proc-macro library | "proc-macro" |
Example | "bin" |
可用的选项有 bin、lib、rlib、dylib、cdylib、staticlib 和 proc-macro。你可以在《Rust参考手册》中阅读更多关于不同crate类型的信息。
required-features字段
required-features字段指定了目标需要哪些特征才能被构建。如果任何所需的功能没有被启用,目标将被跳过。这只与 [[bin]]
、[[bench]]
、[[test]]
和 [[example]]
部分有关,对 [lib]
没有影响。
[features]
# ...
postgres = []
sqlite = []
tools = []
[[bin]]
name = "my-pg-tool"
required-features = ["postgres", "tools"]
目标自动发现
默认情况下,Cargo 会根据文件系统中的文件布局自动确定要构建的目标。目标配置表,如 [lib]
、[[bin]]
、[[test]]
、[[bench]]
或 [[example]]
,可以用来添加不遵循标准目录布局的额外目标。
自动发现目标的功能可以被禁用,这样就只有手动配置的目标会被建立。将 [package]
部分的key autobins
、autoexamples
、autotests
或 autobenches
设置为false将禁用相应目标类型的自动发现。
[package]
# ...
autobins = false
autoexamples = false
autotests = false
autobenches = false
只有在特殊情况下才需要禁用自动发现功能。例如,如果你有一个库,你想要一个名为 bin
的模块,这将会带来一个问题,因为Cargo通常会试图将bin目录下的任何东西编译为可执行文件。下面是这种情况下的一个布局样本:
├── Cargo.toml
└── src
├── lib.rs
└── bin
└── mod.rs
为了防止 Cargo 将 src/bin/mod.rs
推断为可执行文件,请在 Cargo.toml
中设置 autobins = false
来禁止自动发现。
[package]
# …
autobins = false
注意
对于2015版的软件包,如果在Cargo.toml中至少有一个目标是手动定义的,那么自动发现的默认值为false。从2018版开始,默认值始终为 “true”。4 - Cargo Reference笔记: 工作区
https://doc.rust-lang.org/cargo/reference/workspaces.html
3.3 工作区
工作空间是一个或多个软件包的集合,它们共享共同的依赖性解析(有一个共享的Cargo.lock)、输出目录和各种设置,如配置文件。作为工作区一部分的包被称为工作区成员。有两种类型的工作空间:根包(Root package)或虚拟清单(Virtual manifest)。
根包(Root Package)
通过在 Cargo.toml
中添加 [workspace]
部分可以创建一个工作区。这可以添加到已经定义了 [package]
的 Cargo.toml
中,在这种情况下,该包是工作区的根包(Root package)。工作区根(workspace root)是工作区的 Cargo.toml
所在的目录。
虚拟清单(Virtual Manifest)
另外,在创建 Cargo.toml 文件时,也可以在其中加入 [workspace]
部分,但不加入 [package]
部分。这被称为虚拟清单。这通常适用于没有 “主要 " 软件包的情况,或者您希望将所有软件包放在不同的目录中。
关键特征
工作区的关键点是:
-
所有软件包共享一个共同的
Cargo.lock
文件,该文件驻留在工作区根部。 -
所有软件包共享一个共同的输出目录,该目录默认为工作区根目录下的target。
-
Cargo.toml
中的[patch]
、[replace]
和[profile.*]
部分只在根清单中被识别,而在 crate 的清单中被忽略。
[workspace]部分
Cargo.toml
中的 [workspace]
表定义了哪些软件包是工作区的成员。
[workspace]
members = ["member1", "path/to/member2", "crates/*"]
exclude = ["crates/foo", "path/to/other"]
驻留在工作区目录中的所有路径依赖自动成为成员。其他成员可以用 members 键列出,members 键应该是一个包含 Cargo.toml
文件的目录的字符串数组。
成员列表还支持使用典型的文件名glob模式,如 *
和 ?
exclude key 可以用来防止路径被包含在一个工作区中。如果某些路径的依赖关系根本不希望出现在工作区中,或者使用 glob 模式而你想删除一个目录,这就很有用。
空的 [workspace]
表可以和 [package]
一起使用,以方便地创建一个包含该包和其所有路径依赖的工作区。
工作区选择
当在工作区的子目录内时,Cargo
会自动在父目录中搜索带有 [workspace]
定义的 Cargo.toml
文件,以确定使用哪个工作区。package.workspace
清单键可以在 crate 中用来指向工作空间的根,以覆盖这种自动搜索。如果成员不在工作区根的子目录内,手动设置会很有用。
包的选择
在工作区中,与包相关的 cargo
命令,如 cargo build
,可以使用 -p / --package
或 --workspace
命令行标志来决定对哪些包进行操作。如果没有指定这两个标志,Cargo将使用当前工作目录下的软件包。如果当前目录是一个虚拟工作区,它将适用于所有成员(就像在命令行中指定 --workspace
一样)。
可以指定可选的 default-members
键,以设置在工作区根部和不使用包选择标志时要操作的成员。
[workspace]
members = ["path/to/member1", "path/to/member2", "path/to/member3/*"]
default-members = ["path/to/member2", "path/to/member3/foo"]
当指定时,default-members
必须扩展到一个成员的子集。
Workspace.metadata表
Workspace.metadata
表会被 Cargo
忽略,不会被警告。这一部分可以用于那些想在 Cargo.toml
中存储工作空间配置的工具。比如说。
[workspace]
members = ["member1", "member2"]
[workspace.metadata.webcontents]
root = "path/to/webproject"
tool = ["npm", "run", "build"]
# ...
在 package.metadata
中也有一组类似的表格。虽然 cargo 没有为这两个表的内容指定格式,但建议外部工具可能希望以一致的方式使用它们,例如,如果 package.metadata
中缺少数据,可以参考 workspace.metadata
中的数据,如果这对相关工具来说是有意义的。
5 - Cargo Reference笔记: 特性
https://doc.rust-lang.org/cargo/reference/features.html
Cargo的 “feature” 提供了一种用来表达条件编译和可选依赖关系的机制。包在 Cargo.toml
的 [features]
表中定义了一组命名的特性,每个特性都可以被启用或禁用。正在构建的包的特性可以在命令行中用 --features
等标志启用。依赖项的特性可以在Cargo.toml
的依赖项声明中启用。
关于如何使用特性的一些例子,也请参见特性示例一章。
[features]部分
在 Cargo.toml
的 [features]
表中定义了特性。每个特性都指定了它所启用的其他特性或可选依赖关系的数组。下面的例子说明了如何将特性用于二维图像处理库,其中对不同图像格式的支持可以选择性地包括在内。
[features]
# 定义了一个名为 "webp"的特性,该特性不启用任何其他特性。
webp = []
定义了这个特性后,cfg表达式可以用来在编译时有条件地包含代码以支持所要求的特性。例如,在包的lib.rs里面可以包括这个:
// 这有条件地包括一个实现WEBP支持的模块。
#[cfg(feature = "webp")]
pub mod webp;
Cargo 使用 rustc --cfg
标志在包中设置特性,代码可以用 cfg
属性或 cfg
宏来测试它们的存在。
特性可以列出要启用的其他特性。例如,ICO图像格式可以包含BMP和PNG图像,所以当它被启用时,应该确保这些其他功能也被启用。
[features]
BMP = []
png = []
ico = ["bmp", "png"]
webp = []
特性名称可以包括 Unicode XID标准
中的字符(包括大多数字母),另外还允许以 _
或数字0到9开头,在第一个字符之后还可以包含-
、+
或.
注意
crates.io 对特性名称的语法施加了额外的限制,即它们必须是 ASCII 字母数字字符或_
、-
或+
。
默认特性
默认情况下,所有的特性都是禁用的,除非明确启用。这可以通过指定默认功能来改变。
[features]
default = ["ico", "webp"]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
当软件包被构建时,default
特性被启用,这反过来又会启用所列出的特性。这种行为可以通过以下方式改变:
-
--no-default-features
命令行标志禁用软件包的默认特性。 -
可以在依赖关系声明中指定
default-features = false
选项。
注意
在选择默认特性集时要小心。默认特性是一种便利,使用户更容易使用软件包,而不需要强迫用户仔细选择启用哪些常用的特性,但也有一些缺点。除非指定 default-features = false
,否则依赖关系会自动启用默认功能。这就很难保证默认特性不被启用,特别是对于一个在依赖关系图中出现多次的依赖关系。每个包都必须确保指定 default-features = false
来避免启用它们。
另一个问题是,从默认集中删除一个特性可能是SemVer不兼容的改变,所以你应该确信你会保留这些特性。
可选依赖关系
依赖关系可以被标记为"optional",这意味着它们将不会被默认编译。例如,假设我们的2D图像处理库使用一个外部包来处理GIF图像。这可以这样表达:
[dependencies]
gif = { version = "0.11.1", optional = true }
可选依赖关系隐含地定义了一个与依赖关系同名的特性。这意味着可以在代码中使用相同的 cfg(feature = "gif")
语法,并且可以像启用 --features gif
这样的特性一样启用该依赖关系(见下面的命令行特性选项)。
注意
[feature]
表中的特性不能与依赖关系使用相同的名称。实验性地支持启用此功能和其他扩展功能,可通过命名功能在 nightly 频道中使用。
明确定义的特性也可以启用可选的依赖关系。只要在特性列表中包括可选依赖的名称即可。例如,假设为了支持AVIF图像格式,我们的库需要启用另外两个依赖项:
[dependencies]
ravif = { version = "0.6.3", optional = true }
rgb = { version = "0.8.25", optional = true }
[features]
avif = ["ravif", "rgb"]
在这个例子中,avif特性将启用列出的两个依赖项。
注意
另一种包含依赖关系的方法是使用特定平台的依赖关系。与使用特性不同,这些是基于目标平台的条件。依赖关系的特征
依赖关系的特性可以在依赖关系声明中启用。features
key 表示要启用哪些特征:
[dependencies]
# 启用serde的 `derive` 特性。
serde = { version = "1.0.118", features = ["derive"] }
可以使用 default-features = false
来禁用默认的特性:
[dependencies]
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
注意
这可能无法确保默认功能被禁用。如果另一个依赖关系包括 flate2 而没有指定default-features = false
,那么默认特性将被启用。更多细节请参见下面的特性统一。
依赖关系的特性也可以在 [features] 表中启用。语法是 “package-name/feature-name”。例如。
[dependencies]
jpeg-decoder = { version = "0.1.20", default-features = false }
[features]
# 通过启用 jpeg-decoder 的 "rayon" 特性来启用并行处理支持。
parallel = ["jpeg-decoder/rayon"]。
注意
“package-name/feature-name” 语法也将启用 package-name,如果它是一个可选的依赖关系。禁用该行为的实验性支持在 nightly 频道中通过弱依赖性特性提供。命令行特性选项
以下命令行标志可用于控制哪些特性被启用:
-
--features
FEATURES: 启用列出的特性。多个特性可以用逗号或空格分开。如果使用空格,如果从shell中运行Cargo,请确保在所有特性周围使用引号(如--features "foo bar"
)。如果在一个工作区构建多个包,可以使用package-name/feature-name
语法来指定特定工作区成员的特性。 -
--all-features
: 激活在命令行上选择的所有软件包的所有特性。 -
--no-default-features
: 不激活所选软件包的默认特性。
特性统一
特性对于定义它们的包来说是唯一的。在一个包上启用一个特性并不会在其他包上启用同名的特性。
当一个依赖关系被多个包使用时,Cargo 会在构建依赖关系时使用该依赖关系上启用的所有特性的联合。这有助于确保只使用该依赖关系的一个副本。更多细节请参见解析器文档的特性部分。
例如,让我们看一下使用大量特性的 winapi
包。如果你的包依赖于包 foo
,它启用了winapi的 “fileapi” 和 “handleapi” 特性,而另一个依赖关系 bar
启用了 winapi 的 “std” 和 “winnt” 特性,那么 winapi 将在启用所有这四个特性后被构建:
这样做的结果是,特性应该是累加的。也就是说,启用特性不应该使功能失效,而且通常启用任何特性的组合都是安全的。特性不应该引入 SemVer 不兼容的变化。
例如,如果你想选择性地支持 no_std
环境,不要使用 no_std
特性。相反,使用启用 std
的特性。例如:
#![no_std]
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "std")]
pub fn function_that_requires_std() {
// ...
}
相互排斥的特征
在极少数情况下,功能可能会相互不兼容。如果可能的话,应该避免这种情况,因为这需要协调依赖关系图中包的所有用途,以合作避免一起启用它们。如果不可能,可以考虑增加一个编译错误来检测这种情况。例如。
#[cfg(all(feature = "foo", feature = "bar"))]
compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");
与其使用相互排斥的特性,不如考虑一些其他的选择:
- 将功能分成独立的包。
- 当有冲突时,选择一个特性而不是另一个。
cfg-if
包可以帮助编写更复杂的cfg
表达式。 - 构建代码以允许这些特性同时启用,并使用运行时选项来控制使用哪一个。例如,使用配置文件、命令行参数或环境变量来选择启用哪种行为。
检查已解决的特性
在复杂的依赖关系图中,有时很难理解不同的特性是如何在不同的软件包上被启用的。cargo tree
命令提供了几个选项来帮助检查和显示哪些特性被启用。一些选项可以试试:
-
cargo tree -e features
: 这将显示依赖关系图中的特性。每个特性会显示哪个软件包启用了它。 -
cargo tree -f "{p} {f}"
: 这是一个更紧凑的视图,显示每个软件包上启用的特性的逗号分隔列表。 -
cargo tree -e features -i foo
: 这将反转树形图,显示特性如何流入给定的软件包 “foo”。这可能很有用,因为查看整个图表可能相当大,而且让人不知所措。当你试图弄清楚哪些特性在一个特定的包上被启用以及为什么被启用时,可以使用这个方法。
特征解析器版本2
可以用 Cargo.toml 中的 resolver
字段指定不同的特性解析器,像这样:
[package]
name = "my-package"
version = "1.0.0"
resolver = "2"
关于指定解析器版本的更多细节,请参见 解析器版本 部分。
版本 “2” 的解析器在少数情况下避免了统一特性,因为这种统一可能是不需要的。确切的情况在解析器章节中有描述,但简而言之,它避免在这些情况下进行统一:
-
对于当前没有被构建的目标,在特定平台依赖项上启用的特性被忽略。
-
构建依赖和 proc-macros 不与普通依赖共享特性。
-
Dev-dependencies 不激活功能,除非构建一个需要这些功能的目标(如测试或示例)。
在某些情况下,避免统一的做法是必要的。例如,如果 build-dependency
启用了 std
特性,而同一依赖关系被用作 no_std
环境的正常依赖关系,启用 std
会破坏构建。
然而,一个缺点是,这可能会增加构建时间,因为依赖关系会被构建多次(每次都有不同的特性)。当使用版本 “2” 解析器时,建议检查那些被多次构建的依赖关系,以减少总体构建时间。如果不需要用单独的特性来构建那些重复的包,可以考虑在依赖关系声明中的特性列表中添加特性,这样重复的包最终就会有相同的特性(因此Cargo只会构建一次)。你可以用 cargo tree --duplicates
命令来检测这些重复的依赖关系。它将显示哪些软件包被多次构建;寻找任何列出相同版本的条目。关于获取已解决特征信息的更多信息,请参见检查已解决特征。对于构建依赖,如果你使用 --target
标志进行交叉编译,则没有必要这样做,因为在这种情况下,构建依赖总是与普通依赖分开构建。
TBD: 后面的内容似乎意思不大,以后再细看。