Appearance
32.2 项目配置
在Cargo.toml
文件中,我们可以指定一个 crate 依赖哪些项目。这些依赖既可以是来自官方的 crates.io,也可以是某个 git 仓库地址,还可以是本地文件路径。示例如下:
toml
[dependencies]
lazy_static = "1.0.0"
bar = { version = "0.1", package = 'foo', optional = true }
regex = { workspace = true, features = ["unicode"] }
rand = { git = https://github.com/rust-lang-nursery/rand, branch = "master" }
my_local = { path = "/my/local/path", version = "0.1.0" }
[target.i686-unknown-linux-gnu.dependencies]
openssl = "1.0.1"
[build-dependencies]
cc = "1.0.3"
[dev-dependencies]
tempfile = "3.0.8"
env_logger = "0.7.1"
[dependencies.nom]
version = "4.2.3"
features = ["verbose-errors"]
dependencies
部分的依赖,可以直接通过 cargo add xxx
来安装官方 xxx 依赖。 卸载时,可通过 cargo remove xxx
从 Cargo.toml
移除 xxx 依赖。
关于 dev-dependencies
,可以通过 cargo add --dev xxx
来安装,它们是仅为示例、测试和基准测试而包含的依赖项。最重要的是,当您进行构建时,它们将不会被包含在内。更多查看文档 development-dependencies部分
dependencies.nom
是其中一个依赖项的名称。这里解释一下 nom 的其他部分:
version = "4.2.3"
:指定所需的 nom 版本为 4.2.3。在构建项目时,Cargo 会下载并使用 nom 的指定版本。如果未指定版本,则默认使用最新版。features = ["verbose-errors"]
:用于启用 nom 的 "verbose-errors" 特性。这个特性会在解析过程中显示更详细的错误信息,以帮助调试和诊断解析错误。在某些情况下,默认情况下可能没有启用特定的特性,需要显式地通过在 Cargo.toml 文件中定义特性来启用它们。更多查看文档
build-dependencies
,查看文档
crate 版本号
Rust 里面的 crate 都是自带版本号的。版本号采用的是语义版本的思想。基本意思如下:
1.0.0
以前的版本是不稳定版本,如果出现了不兼容的改动,升级次版本号,比如从0.2.15
升级到0.3.0
;在
1.0.0
版本之后,如果出现了不兼容的改动,需要升级主版本号,比如从1.2.3
升级到2.0.0
;在
1.0.0
版本之后,如果是兼容性的增加 API,虽然不会导致下游用户编译失败,但是增加公开的 API 情况,应该升级次版本号。
下面详细讲一下在[dependencies]
里面的几种依赖项的格式:
(1)来自 crates.io 的依赖
绝大部分优质开源库,作者都会发布到官方仓库中,所以我们大部分的依赖都是来自于这个地方。在 crates.io 中,每个库都有一个独一无二的名字,我们要依赖某个库的时候,只需指定它的名字及版本号即可;
toml
[dependencies]
lazy_static = "1.0"
指定版本号的时候,可以使用模糊匹配的方式。
^
符号,如^1.2.3
代表1.2.3<=version<2.0.0
;~
符号,如~1.2.3
代表1.2.3<=version<1.3.0
;*
符号,如1.*
代表1.0.0<=version<2.0.0
;比较符号,比如
>=1.2.3
、>1.2.3
、<2.0.0
、=1.2.3
含义基本上一目了然,还可以把多个限制条件合起来用逗号分开,比如version=">1.2,<1.9"
。
直接写一个数字的话,等同于^
符号的意思。所以lazy_static="1.0"
等同于lazy_static="^1.0"
,含义是1.0.0<=version<2.0.0
。cargo 会到网上找到当前符合这个约束条件的最新的版本下载下来。
(2)来自 git 仓库的依赖
除了最简单的git = "…"
指定 repository 之外,我们还可以指定对应的分支:
toml
rand = { git = https://github.com/rust-lang-nursery/rand, branch = "next" }
或者指定当前的 commit 号:
toml
rand = { git = https://github.com/rust-lang-nursery/rand, branch = "master", rev = "31f2663" }
还可以指定对应的 tag 名字:
toml
rand = { git = https://github.com/rust-lang-nursery/rand, tag = "0.3.15" }
(3)来自本地文件路径的依赖
指定本地文件路径,既可以使用绝对路径也可以使用相对路径。
当我们使用cargo build
编译完项目后,项目文件夹内会产生一个新文件,名字叫Cargo.lock
。 它实际上是一个纯文本文件,同样也是 toml 格式。它里面记录了当前项目所有依赖项目的具体版本。每次编译项目的时候,如果该文件存在,cargo 就会使用这个文件中记录的版本号编译项目;如果该文件不存在,cargo 就会使用 Cargo.toml 文件中记录的依赖项目信息,自动选择最合适的版本。
一般来说:如果我们的项目是库,那么最好不要把Cargo.lock
文件纳入到版本管理系统中,避免依赖库的版本号被锁死; 如果我们的项目是可执行程序,那么最好要把Cargo.lock
文件纳入到版本管理系统中,这样可以保证,在不同机器上编译使用的是同样的版本,生成的是同样的可执行程序。
对于依赖项,我们不仅要在Cargo.toml
文件中写出来,还要在源代码中写出来。 在 Rust 2018 之前的版本中,必须在 crate 的入口处(对库项目就是lib.rs
文件,对可执行程序项目就是main.rs
文件)写上:
rust
extern crate hello; // 声明外部依赖
extern crate hello as hi; // 可以重命名
Profiles 配置文件
Cargo 有两个主要的配置文件:在运行 cargo build
命令时使用的 dev 配置文件,在运行 cargo build --release
时使用的 release 配置文件。它们都有各自的默认值。所以在运行构建时,我们会看到如下输出:
sh
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$ cargo build --release
Finished release [optimized] target(s) in 0.0s
我们可以通过在项目 Cargo.toml
文件中设置 [profile.*]
配置,来自定义构建配置:
toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level
设置控制对你代码的优化数量,范围为0
到3
。dev 的默认 opt-level
是 0
,因为我们再开发过程中,经常会进行构建输出,所以我们减少对代码的优化,而加快开发速度。
更多信息查看 Cargo-Profiles
32.2.1 cargo 配置文件
cargo 也支持配置文件。配置文件可以定制 cargo 的许多行为,就像我们给 git 设置配置文件一样。 类似的,cargo 的配置文件可以存在多份,它们之间有优先级关系。你可以为某个文件夹单独提供一份配置文件, 例如在当前文件夹下的 .cargo/config.toml
文件,也可以提供一个全局的默认配置,为 $HOME/.cargo/config.toml
文件或者不带 .toml
扩展名的文件。 Cargo 对 .toml
扩展名的支持是在 1.39 版本中添加的,已经是首选形式了。如果这两种形式的配置文件都存在,Cargo 将优先使用不带扩展名的文件,并打印 warning,所以推荐使用 onfig.toml
配置文件。
下面是一份配置示例:
toml
[cargo-new]
// 可以配置默认的名字和 email,这些会出现在新项目的 Cargo.toml 中
name = "..."
email = "..."
[build]
jobs = 1 // 并行执行的 rustc 程序数量
rustflags = ["..", ".."] // 编译时传递给 rustc 的额外命令行参数
[term]
verbose = false // 执行命令时是否打印详细信息
color = 'auto' // 控制台内的彩色显示
[alias] // 设置命令别名
b = "build"
t = "test"
r = "run"
rr = "run --release"
更详细的信息请参考官方文档:cargo-Configuration。
32.2.2 workspace
cargo 的 workspace 概念,是为了解决多 crate 的互相协调问题而存在的。假设现在我们有一个比较大的项目。 我们把它拆分成了多个 crate 来组织,就会面临一个问题:不同的 crate 会有各自不同的Cargo.toml
,编译的时候它们会各自产生不同的Cargo.lock
文件,我们无法保证所有的 crate 对同样的依赖项使用的是同样的版本号。
为了让不同的 crate 之间能共享一些信息,cargo 提供了一个 workspace 的概念。一个 workspace 可以包含多个项目; 所有的项目共享一个Cargo.lock
文件,共享同一个输出目录;一个 workspace 内的所有项目的公共依赖项都是同样的版本,输出的目标文件都在同一个文件夹内。
workspace 同样是用Cargo.toml
来管理的。我们可以把所有的项目都放到一个文件夹下面。在这个文件夹下写一个Cargo.toml
来管理这里的所有项目。 Cargo.toml
文件中要写一个[workspace]
的配置,比如:
toml
[workspace]
members = [
"project1", "lib1"
]
整个文件夹的目录结构如下:
sh
├── Cargo.lock
├── Cargo.toml
├── project1
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── lib1
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── target
我们可以在 workspace 的根目录执行cargo build
等命令。请注意,虽然每个 crate 都有自己的Cargo.toml
文件,可以各自配置自己的依赖项,但是每个 crate 下面不再会各自生成一个Cargo.lock
文件,而是统一在 workspace 下生成一个Cargo.lock
文件。如果多个 crate 都依赖一个外部库,那么它们必然都是依赖的同一个版本。
32.2.3 build.rs
cargo 工具还允许用户在正式编译开始前执行一些自定义的逻辑。 方法是在根目录下创建build.rs
文件,自定义逻辑就写在build.rs
文件里面。 在执行cargo build
的时候,cargo 会先把这个build.rs
编译成一个可执行程序,然后运行这个程序,做完后再开始编译真正的 crate。
build.rs
一般用于下面这些情况:
- 提前调用外部编译工具,比如调用
gcc
编译一个 C 库; - 在操作系统中查找 C 库的位置;
- 根据某些配置,自动生成源码;
- 执行某些平台相关的配置。
build.rs
里面甚至可以再依赖其他的库。build.rs
时无权访问dependencies
和dev-dependencies
部分中列出的依赖项。 我们需要在 build-dependencies
中指定需要的依赖,可以通过 cargo add --build rand
来安装:
toml
[build-dependencies]
rand = "1.0"
build.rs
里面如果需要读取当前 crate 的一些信息,可以通过环境变量来操作。 cargo 在执行这个程序之前就预先设置好了一些环境变量,比较常用的有下面几种。
CARGO_MANIFEST_DIR
当前 crate 的Cargo.toml
文件的路径。
CARGO_PKG_NAME
当前 crate 的名字。
OUT_DIR
build.rs
的输出路径。如果要在 build.rs
中生成代码,那么生成的代码就要存在这个文件夹下。
HOST
当前 rustc 编译器的平台特性。
OPT_LEVEL
优化级别。
更多的环境变量请参考 cargo 的标准文档。
下面还是用一个完整的示例演示一下build.rs
功能如何使用。
构建时注入环境变量
再来个示例,让我们通过将构建的信息自动注入到环境变量中: 首先,让我们创建一个新的 git 标签:
sh
git tag -a v0.1.2 -m 'new version'
在 main.rs
中编写:
rust
static VERSION: &'static str = env!("GIT_VERSION");
static COMMIT: &'static str = env!("GIT_COMMIT");
static DATE: &'static str = env!("GIT_DATE");
fn main() {
println!("Hello, world!");
println!("version: {}", VERSION);
println!("commit: {}", COMMIT);
println!("date: {}", DATE);
}
接着运行 cargo add --build chrono
添加 chrono 库到 build.rs
的依赖项中,来格式化日期:
toml
[build-dependencies]
chrono = "0.4.23"
然后,编写 build.rs
:
rust
// build.rs
use chrono::{DateTime, Utc};
use std::process::Command;
fn main() {
let output = Command::new("git")
.args(["describe", "--tags", "--abbrev=0"])
.output()
.unwrap();
let git_tag = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_VERSION={}", git_tag);
let output = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
let git_commit = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_COMMIT={}", git_commit);
let now: DateTime<Utc> = Utc::now();
let build_date = now.to_rfc3339();
println!("cargo:rustc-env=GIT_DATE={}", build_date);
}
这个字符串的开头部分 cargo:rustc-env=
表示告诉 Cargo 这是一个用于定义环境变量的指令。将变量 git_commit
的值设置给 GIT_COMMIT
环境变量
当 Cargo 运行构建过程时,它会捕捉到这些以 cargo:rustc-env=
开头的指令,并根据其指示设置相应的环境变量。 这样,在编译期间就可以使用这些环境变量来传递一些额外的信息给程序。
需要注意的是,这行代码必须运行在 Cargo 构建系统的上下文中,通常是在项目的构建脚本(build.rs
)中使用。
编译和运行它:
Hello, world!
version: v0.1.2
commit: f6b7fc65ef00e3b2d27886ab9ecf243b4ef6549b
date: 2023-10-17T14:38:43.833297+00:00
这正是我们想要的!
构建时生成 rs 文件
假设我们现在要把当前项目最新的 commit id 记录到可执行程序里面。这种需求就必须使用自动代码生成来完成。 也可以通过前面的环境变量来完成,这里我们通过另一种方式实现。
首先新建一个项目with_commit_hash
:
rust
cargo new –bin with_commit_hash
接着在项目文件夹下创建一个build.rs
的文件。
我们希望能在编译过程中生成一份源代码文件,里面记录了一个常量,类似这样:
rust
const CURRENT_COMMIT_ID : &’static str = "123456789ABCDEF";
查找当前 git 的最新 commit id 可以通过命令git rev-parse HEAD
来完成。所以,我们的build.rs
可以这样实现:
rust
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::process::Command;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("commit_id.rs");
let mut f = File::create(&dest_path).unwrap();
let commit = Command::new("git")
.arg("rev-parse")
.arg("HEAD")
.output()
.expect("Failed to execute git command");
let commit = String::from_utf8(commit.stdout).expect("Invalid utf8 string");
let output = format!(r#"pub const CURRENT_COMMIT_ID : &'static str = "{}";"#, commit);
f.write_all(output.as_bytes()).unwrap();
}
输出路径是通过读取OUT_DIR
环境变量获得的。利用标准库里面的 Command 类型,我们可以调用外部的进程,并获得它的标准输出结果。 最后再构造出我们想要的源码字符串,写入到目标文件中。
生成了这份代码之后,我们怎么使用呢?在main.rs
里面,可以通过宏直接把这部分源码包含到项目中来:
rust
include!(concat!(env!("OUT_DIR"), "/commit_id.rs"));
fn main() {
println!("Current commit id is: {}", CURRENT_COMMIT_ID);
}
这个include!
宏可以直接把目标文件中的内容在编译阶段复制到当前位置。这样main
函数就可以访问CURRENT_COMMIT_ID
这个常量了。 大家要记得在当前项目使用 git 命令新建几个 commit。然后编译、执行,可见在可执行程序中包含最新 commit id 这个任务就完全自动化起来了。