Skip to content

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 xxxCargo.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 设置控制对你代码的优化数量,范围为03。dev 的默认 opt-level0,因为我们再开发过程中,经常会进行构建输出,所以我们减少对代码的优化,而加快开发速度。

更多信息查看 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时无权访问dependenciesdev-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 这个任务就完全自动化起来了。

Released under the MIT License