Rust 之二 各组件工具的配置、使用及源码、构建(一)
概述
Rust 是一种预编译静态类型(ahead-of-time compiled)的编程语言,除了有基本的 Rust 编程语言标准之外,Rust 官方还提供了一系列适用于各个平台的开发辅助工具以及编译工具链来帮助我们处理 Rust 代码。
rustup
rustup 是 Rust 官方提供的所有相关开发辅助工具以及编译工具链的安装及管理器,使用 rustup 这个工具就可以自动为我们下载安装官方的 Rust 各工具的预编译发行版,并且可以在不同的发行版之间自由切换。
使用
rustup 是一个命令行工具,对应的可执行程序名为 rustup,详细的使用方法见下图中的命令行参数说明即可。
-
为了方便管理各种工具,rustup 引入了 component、profile、proxy 等基本概念来组织和管理所有相关工具的发布,详见之前博文 Rust 之一 组件介绍、版本发布、开发环境搭建 中的详细介绍!
-
rustup本身就是代理的原型,它自身没有代理!
配置
rustup 支持通过环境变量和配置文件来加载相关配置。rustup默认的工作目录为 $RUSTUP_HOME/.rustup,其中的目录说明如下:
-
downloads:存储下载的工具链和组件归档文件,命名格式为<文件名>-<哈希值>.tar.gz。这些文件在工具链安装完成后不会自动删除,可通过rustup cache clean手动清理。 -
update-hashes:存储每个工具链的更新哈希值,用于快速检查工具链是否有更新。每个文件对应一个工具链,内容为该工具链的最新版本哈希值。 -
tmp:用于存储安装过程中的临时文件和元数据。rustup 会自动管理此目录,通常无需手动干预。 -
toolchains:采用命名空间-目标三元组的格式每个安装的编译工具链一个子目录来存储已安装的工具链。每个工具链子目录包含完整的 Rust工具链,其内部结构如下:
bin:包含 Rust 编译器和工具(rustc、cargo、rustdoc等)lib:Rust 标准库和运行时组件share:文档和辅助资源src:源码(可选,通过rustup component add rust-src安装)
-
settings.toml:核心配置文件,采用 TOML 格式,存储了 rustup 的全局设置
环境变量
-
RUSTUP_HOME:指定了编译工具链的存放位置- 默认值:对于 Linux 系统是
~/.rustup,对于 Windows 系统是%USERPROFILE%/.rustup - 通过定义
RUSTUP_HOME=/usr/local/rustup就可以更改默认存放位置
- 默认值:对于 Linux 系统是
-
RUSTUP_DIST_SERVER:指定了下载 Rust 相关静态资源的根 URL。- 默认值:https://static.rust-lang.org
- 之前叫
RUSTUP_DIST_ROOT,现已弃用! - 通过定义
RUSTUP_DIST_SERVER="https://rsproxy.cn"就可以更换 rustup 的下载源
-
RUSTUP_UPDATE_ROOT指定了rustup自更新的地址- 默认值:https://static.rust-lang.org/rustup
- 通过定义
RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup"就可以更换 rustup 的更新源
-
RUSTUP_TOOLCHAIN:用于覆盖所有 Rust 工具调用所使用的工具链- 应安装此名称的工具链,否则调用将失败。
-
RUSTUP_VERSION: 覆盖执行rustup-init.sh或rustup self update时下载的 rustup 版本。- 默认值:无
- 示例
RUSTUP_VERSION=nightly
-
其他配置项见 https://rust-lang.github.io/rustup/environment-variables.html
配置文件
rustup 有一个 TOML 格式的配置文件 ${RUSTUP_HOME}/settings.toml,目前其中存放了默认编译工具链等信息以及 overrides 的配置信息。在 Unix 操作系统上,对于某些设置会放到 /etc/rustup/settings.toml 文件中。
源码
rustup 的源码仓库为 https://github.com/rust-lang/rustup,可以直接 git clone https://github.com/rust-lang/rustup.git 获取。rustup 是 multirust 的继承者。Rustup 最初是 multiust-rs,由 Diggory Blake 将 multirust 从 shell 脚本改写为 Rust,现在由 Rust 项目维护。
-
rustup源码中实现了根据名字的不同而实现不一样的功能,.cargo/bin中的各个工具以及我们安装使用的rustup-init实际上都是它改成了不同名字。 详见src\bin\rustup-init.rs中函数main()➜run_rustup()➜run_rustup_inner()的内容
-
对于类 Unix 系统,官方推荐的
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh安装方法实际上就是下载并执行源码中的rustup-init.sh这个脚本 -
源码下的
doc文件夹中是文档的源码,分别对应 The rustup book 和 The Rustup developer guide。文档使用的是 mdBook 文档系统,详细介绍参见后文的 mdBook 章节的介绍
-
源码下的
www文件夹中是 https://rustup.rs/ 网站的源码,这个网站貌似很少使用。-
直接在
www文件夹执行python -m http.server 8000可以本地部署 -
这个不是 https://rust-lang.org/ 的源码,https://rust-lang.org/ 的源码是独立仓库 https://github.com/rust-lang/www.rust-lang.org
-
构建
rustup 的源码本身就是一个标准的 Rust 项目工程(在 Rust 之三 项目管理(Package、Crate、Workspace 等)、文档规范、编码规范 中详细介绍),因此,我们可以使用 cargo build 命令一键编译出名为 rustup-init 可执行程序,这个是 rustup-init 实际上就是 rustup,详见下面的说明。
- 如果在 Windows 下构建,则必须要安装 Visual Studio Build Tools 2017 or later(必选 Workloads select Visual C++ build tools)、NASM、CMake、ninja
cargo
Cargo 是 Rust 的包管理器,可以用于依赖包的下载、编译、更新、分发等操作。同时, Cargo 还是一个命令行项目管理工具,它有一套自己的项目结构(在 Rust 之三 项目管理(Package、Crate、Workspace 等)、文档规范、编码规范 中详细介绍),可以帮助我们自动调用编译工具链来编译 Rust 项目源码。
使用
cargo 是一个命令行工具,默认位于用户目录的 .cargo/bin 目录下,对应的可执行程序名为 cargo,详细的使用方法见下图中的命令行参数说明即可。
-
当我们调用
cargo命令时实际上是通过.cargo/bin/cargo这个代理来调用.rustup/toolchains/发行版/bin/cargo这个真正的可执行程序。
-
绝大多数情况下,我们都是使用
cargo命令来编译项目,而不是直接使用rustc
-
cargo管理的项目的基本单位被称为Package。一个 Package 中至少包含一个 Crate;一个 Package 中最多只能包含一个库 Library Crate;一个 Package 中可以包含任意多个 Binary Crate,详见之后的博文 Rust 之三 项目管理(Package、Crate、Workspace 等)、文档规范、编码规范。
子命令机制
cargo 除了其内置的 build、check、doc、run、test、remove 等子命令之外,其还提供了一种扩展子命令的方法:每个 cargo <subcommand> 对应一个可执行文件 cargo-<subcommand>,Cargo 自动查找 cargo-<subcommand> 并将命令行参数传递给 cargo-<subcommand>。
cargo xtask build
^ ^ ^
| | └─ 参数传递给 xtask
| └─ 自定义子命令
└─ Cargo 主程序
当我们执行非内置子命令,例如 cargo <cmd> [options] 时 ,cargo 解析命令行参数,识别第一个参数 <cmd>,然后检查 <cmd> 是否是 cargo 的内置命令(如 build, run, test 等),当不是内置命令时,cargo 会查找名为 cargo-<cmd> 的可执行文件,查找优先级:
- 首先查找
~/.cargo/bin是否存在 PATH 中,如果不在强制插入到最前面 - 按定义顺序在 PATH 环境变量中的
cargo-<cmd>子命令 - 当前工作目录中找
target/release/<cmd> - 当前工作目录中找
target/debug/<cmd>
cargo-binutils
cargo-binutils 是由 Rust 官方的嵌入式工作组(Rust on Embedded Devices Working Group)中的工具组(Embedded WG Tools team)维护的一个非官方项目,这个项目可以为我们的 cargo 命令增加一些子命令,使用这些子命令可以直接调用指定编译工具链对应的 LLVM 的相关工具。
安装
-
cargo install cargo-binutils -
rustup component add llvm-tools
示例
当安装了 cargo-binutils 和 llvm-tools 之后,我们就可以直接使用如下命令了(cargo xxx 就是通过直接查找 cargo-xxx 可执行程序来实现的):
cargo objcopy用于将 ELF 转换为 BINcargo nm输出符号表cargo objdump反汇编
cargo build
cargo build 会编译我们的项目代码,结果会被放入项目根目录下的 target 文件夹中,target 目录的结构取决于是否使用 --target 标志为特定的平台构建。target 的位置可以通过设置 CARGO_TARGET_DIR 环境变量、build.target-dir 配置项以及 --target-dir 命令行参数这三种方式来更改。
-
cargo build --verbose可以看到调用rustc的详细参数 -
编译库时,Cargo 会自动使用
rustc的--crate-type lib选项,进而生成.rlib的库文件;编译可执行程序时,Cargo 会自动使用rustc的--crate-type bin,进而生成可执行文件 -
对于每个 Crate 的编译,Cargo 都会为
rustc指定--extern选项,给出当前 crate 将使用的每个库的文件名,Rust 会将代码静态链接到最终的可执行文件中
不使用 --target
若不使用 --target 标志,则 Cargo 会根据宿主机架构进行构建,构建结果会放入项目根目录下的 target 目录中,target 下每个子目录中包含了相应的发布配置 profile 的构建结果,例如 release、debug 是自带的 profile,前者往往用于生产环境,因为会做大量的性能优化,而后者则用于开发环境,此时的编译效率和报错信息是最好的。
| 目录 | 描述 |
|---|---|
| target/debug/ | 包含了 dev profile 的构建输出(cargo build 或 cargo build --debug) |
| target/release/ | release profile 的构建输出,cargo build --release |
| target/foo/ | 自定义 foo profile 的构建输出,cargo build --profile=foo |
Cargo 还会创建几个用于构建过程的其它类型目录,它们的目录结构只应该被 Cargo 自身使用,因此可能会在未来发生变化:
| 目录 | 描述 |
|---|---|
| target/debug/deps | 依赖和其它输出成果 |
| target/debug/incremental | rustc 增量编译的输出,该缓存可以用于提升后续的编译速度 |
| target/debug/build/ | 构建脚本的输出 |
还有一些命令会在 target 下生成自己的独立目录:
| 目录 | 描述 |
|---|---|
| target/doc/ | 包含通过 cargo doc 生成的文档 |
| target/package/ | 包含 cargo package 或 cargo publish 生成的输出 |
除此之外,我们还可以在项目根目录的 Cargo.toml 中定义自己想要的 profile ,例如用于测试环境的 profile:test,用于预发环境的 profile:pre-prod 等。出于历史原因:
- dev 和 test profile 的构建结果都存放在 debug 目录下
- release 和 bench profile 则存放在 release 目录下
- 用户定义的 profile 存在同名的目录下
使用 --target
当使用 --target XXX 为特定的平台编译后,输出会放在 target/XXX/ 目录中。target/XXX/ 目录中就是上面说的不使用 --target 时的目录结构,在 target/XXX 下的 profile 文件夹中包含编译后的最终成果:
| 目录 | 描述 |
|---|---|
| target/XXX/debug/ | 包含编译后的输出,例如二进制可执行文件、库对象( library target ) |
| target/XXX/debug/examples/ | 包含示例对象( example target ) |
配置
cargo 支持通过环境变量和配置文件来加载相关配置。cargo 默认的工作目录为 $CARGO_HOME/.cargo,其中的目录说明如下:
-
bin:包含了通过cargo install xxx(只有拥有二进制目标文件的包能够被安装,并会被安装到.cargo/bin中) 或rustup下载的包编译出的可执行文件。可以(默认)将该目录加入到$PATH环境变量中,以实现对这些可执行文件的直接访问 -
registry:这个是 Cargo 下周的各种 Crate 的缓存目录,包含了注册中心(例如 crates.io)的元数据和 Packagesindex: 包含了注册中心中所有可用包的元数据( 版本、依赖等 )cache: 保存了已下载的依赖,这些依赖包以 gzip 的压缩档案形式保存,后缀名为.cratesrc: 若一个已下载的.crate被一个 package 所需要,该档案会被解压缩到此目录下,最终 rustc 可以在其中找到所需的 .rs 文件
环境变量
-
CARGO_HOME:定义了 cargo 默认所在的目录,其中包含了 cargo 可执行程序、配置文件、缓存文件等等,我们通过修改这个环境变量就可以重定位 Cargo 的位置。-
Windows 平台下默认值是
%USERPROFILE%\.cargo;Unix 类系统下默认值是$HOME/.cargo -
当我们使用 Cargo 构建项目时,下载的各种 crates 就位于
.cargo/registry目录中
-
-
CARGO_TARGET_DIR:定义了构建输出目录,默认是target
配置文件
cargo 在工作时会自动读取 Windows 平台下的 %USERPROFILE%\.cargo\config.toml 或者类 Unix 系统下的 $HOME/.cargo/config.toml 或者当前项目目录下的 .cargo/config.toml 中的配置信息(在 cargo 1.38 及以前的配置版本使用文件名 config)。
-
当前项目目录下的
.cargo/config.toml优先级高于系统用户目录下的配置文件! -
config.toml中的内容最常用的就是更改软件包的镜像源地址。在 crates.io 之外添加新的注册服务,配置如下:[registries] ustc = { index = "https://mirrors.ustc.edu.cn/crates.io-index/" }对于这种方式,我们的项目的 Cargo.toml 中的依赖包引入方式也得对应的改
[dependencies] time = { registry = "ustc" } -
直接使用新注册服务来替代默认的 crates.io,配置如下
[source.crates-io] replace-with = 'ustc' [source.ustc] registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"示例:
[source.crates-io] registry = "https://github.com/rust-lang/crates.io-index" replace-with = "ustc" # 或 rustcc、sjtu、tuna # 根据 replace-with 配置的镜像源,以下只需配置一个对应的源即可 # 中国科大 [source.ustc] registry = "git://mirrors.ustc.edu.cn/crates.io-index" # rustcc 社区 [source.rustcc] registry = "https://code.aliyun.com/rustcc/crates.io-index.git" # 上海交通大学 [source.sjtu] registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index" # 清华大学 [source.tuna] registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
源码
cargo 的源码仓库为 https://github.com/rust-lang/cargo,直接使用 git clone https://github.com/rust-lang/cargo.git 获取。
子命令机制
cargo 会自动查找 .cargo/bin/ 中以 cargo- 开头的其他可执行文件,并把他们作为子命令来使用!基于 Cargo 源码分析(位于 cargo/src/bin/cargo/ 目录),以下是关键实现细节:
主入口流程 (main.rs)
fn main() {
// 1. 初始化全局上下文
let mut gctx = GlobalContext::default()?;
// 2. 委托给 cli::main 处理
cli::main(&mut gctx)
}
命令解析流程 (cli.rs::main())
pub fn main(gctx: &mut GlobalContext) -> CliResult {
// 1. 使用 clap 解析命令行参数
let args = cli(gctx).try_get_matches()?;
// 2. 展开别名(递归处理别名链)
let (expanded_args, global_args) = expand_aliases(gctx, args, vec![])?;
// 3. 提取子命令
let (cmd, subcommand_args) = match expanded_args.subcommand() {
Some((cmd, args)) => (cmd, args),
_ => {
cli(gctx).print_help()?;
return Ok(());
}
};
// 4. 推断执行类型(内置/清单命令/外部)
let exec = Exec::infer(cmd)?;
// 5. 配置全局上下文
configure_gctx(gctx, &expanded_args, Some(subcommand_args), global_args, Some(&exec))?;
// 6. 执行命令
exec.exec(gctx, subcommand_args)?;
Ok(())
}
命令推断逻辑 (cli.rs::Exec::infer())
enum Exec {
Builtin(commands::Exec), // 内置命令
Manifest(String), // 清单命令(cargo-script)
External(String), // 外部子命令
}
impl Exec {
/// 命令优先级:
/// 1. 内置命令 xor 清单命令
/// 2. 别名(在 expand_aliases 中处理)
/// 3. 外部子命令
fn infer(cmd: &str) -> CargoResult<Self> {
if let Some(exec) = commands::builtin_exec(cmd) {
Ok(Self::Builtin(exec))
} else if commands::run::is_manifest_command(cmd) {
Ok(Self::Manifest(cmd.to_owned()))
} else {
Ok(Self::External(cmd.to_owned()))
}
}
}
内置命令检查 (commands/mod.rs::builtin_exec())
pub fn builtin_exec(cmd: &str) -> Option<Exec> {
let f = match cmd {
"add" => add::exec,
"bench" => bench::exec,
"build" => build::exec,
"check" => check::exec,
// ... 所有内置命令
_ => return None, // 不是内置命令
};
Some(f)
}
外部子命令查找 (main.rs::find_external_subcommand())
fn find_external_subcommand(gctx: &GlobalContext, cmd: &str) -> Option<PathBuf> {
// 构造可执行文件名: cargo-{cmd}{.exe}
let command_exe = format!("cargo-{}{}", cmd, env::consts::EXE_SUFFIX);
// 在搜索目录中查找
search_directories(gctx)
.iter()
.map(|dir| dir.join(&command_exe))
.find(|file| is_executable(file)) // 找到第一个可执行的文件
}
搜索目录列表 (main.rs::search_directories())
这是查找路径的核心实现:
fn search_directories(gctx: &GlobalContext) -> Vec<PathBuf> {
// 1. 从 PATH 环境变量获取所有目录
let mut path_dirs = if let Some(val) = gctx.get_env_os("PATH") {
env::split_paths(&val).collect()
} else {
vec![]
};
// 2. 获取 ~/.cargo/bin 路径
let home_bin = gctx.home().clone().into_path_unlocked().join("bin");
// 3. 如果 PATH 中不包含 ~/.cargo/bin,则将其插入到最前面
// 这样 ~/.cargo/bin 具有最高优先级
// 参见: https://github.com/rust-lang/cargo/issues/11020
if !path_dirs.iter().any(|p| p == &home_bin) {
path_dirs.insert(0, home_bin);
};
path_dirs
}
查找优先级:
~/.cargo/bin/如果不在 PATH 中,强制插入到第一位- 按 PATH 中定义的顺序依次查找
- 最后在项目目录中查找编译后的二进制文件在
./target/debug/或./target/release/
执行外部子命令 (main.rs::execute_external_subcommand())
fn execute_external_subcommand(gctx: &GlobalContext, cmd: &str, args: &[&OsStr]) -> CliResult {
// 1. 查找外部子命令
let path = find_external_subcommand(gctx, cmd);
let command = match path {
Some(command) => command,
None => {
// 2. 未找到时,生成友好的错误消息
let suggestions = list_commands(gctx);
let did_you_mean = closest_msg(cmd, suggestions.keys(), |c| c, "command");
return Err(CliError::new(
anyhow::format_err!(
"no such command: `{cmd}`{did_you_mean}\n\n\
help: view all installed commands with `cargo --list`\n\
help: find a package to install `{cmd}` with `cargo search cargo-{cmd}`"
),
101
));
}
};
// 3. 执行找到的外部命令
execute_subcommand(gctx, Some(&command), args)
}
fn execute_subcommand(
gctx: &GlobalContext,
cmd_path: Option<&PathBuf>,
args: &[&OsStr],
) -> CliResult {
let cargo_exe = gctx.cargo_exe()?;
let mut cmd = match cmd_path {
Some(cmd_path) => ProcessBuilder::new(cmd_path), // 外部命令
None => ProcessBuilder::new(&cargo_exe), // 内置命令
};
// 设置 CARGO 环境变量,传递参数
cmd.env(cargo::CARGO_ENV, cargo_exe).args(args);
// 继承 jobserver(用于并行构建控制)
if let Some(client) = gctx.jobserver_from_env() {
cmd.inherit_jobserver(client);
}
// 使用 exec_replace() 替换当前进程
cmd.exec_replace()?;
Ok(())
}
可执行文件检查
#[cfg(unix)]
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
use std::os::unix::prelude::*;
fs::metadata(path)
.map(|metadata| {
// 检查是否是文件且具有执行权限(任一执行位)
metadata.is_file() && metadata.permissions().mode() & 0o111 != 0
})
.unwrap_or(false)
}
#[cfg(windows)]
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
// Windows 只检查是否是文件
path.as_ref().is_file()
}
文档源码
源码的 /src/doc 目录下是文档 The Cargo Book 的源码,使用的是 mdBook 文档系统,详细介绍参见后文的 mdBook 章节的介绍。
构建
cargo 的源码本身就是一个 Rust 项目工程,因此,我们可以使用 cargo build 命令一键编译出名为 cargo 可执行程序。
参考
- https://aws.github.io/aws-lc-rs/requirements/windows.html
- https://blog.csdn.net/fj_Author/article/details/132594546
- https://www.andy-pearce.com/blog/posts/2023/May/uncovering-rust-build-and-packaging/
- https://aws.github.io/aws-lc-rs/requirements/index.html
更多推荐



所有评论(0)