起因
最近在写一个适用于 Rust 操作系统的组件,过程中犯了一个非常粗心的错误,导致这两天一直在了解 Makefile 文件与 build.rs 文件是如何共同作用从而构建和编译 Rust 项目的,由于在此之前确实不太了解这块内容,遂将最近所学记录一下,以便之后查阅。
先来讲讲这个因粗心导致的问题,我的目录结构如下:
.
|_os //某操作系统内核代码
|_src
|_build.rs
|_Cargo.toml
|_Makefile
|_tool //一个脚本工具
|_src
|_main.rs
|_Cargo.toml
|_Makefile
|_...
tool
是我自己写的一个 crate,其他都是某开源操作系统的源码,在 os
目录下 make run
即可编译运行,我需要在 make run
后先运行 tool
向内核中注入一些代码,再编译内核。而 os
需要在 no_std
环境及 RISC-V 架构下运行,tool
则是在正常服务器环境下运行的,于是我向 build.rs
和 Makefile
中添加了这些内容:
// os/build.rs
let status = Command::new("cargo")
.args(&[
"run",
"--manifest-path",
"../tool/Cargo.toml",
"--target",
"x86_64-unknown-linux-gnu",
])
.status()
.expect("Failed to run tool");
if !status.success() {
panic!("trace_event_tool failed");
}
// os/makefile
build: ... tool ...
tool:
@echo "Building and running tool"
cd ../tool && cargo run --target x86_64-unknown-linux-gnu
运行后一直报各种各样的错,包括但不限于:无法编译 tool
的依赖库,找不到 tool
的链接文件,不支持 tool
中某个依赖库引入 std …
我觉得很奇怪,按道理说 tool
应该是运行在服务器环境上的,不受 os
的环境条件限制,而且它只是一个在构建过程中读取配置文件并修改代码的工具,并不涉及裸机编程或需要特定内存布局的场景,不应该需要自定义链接文件,且在 tool
目录下单独运行是能成功的,所以一定是 build.rs
和 Makefile
中的代码出了问题,我猜想是哪里导致 tool
运行在 riscv64gc-unknown-none-elf
架构下,然后就在调试,改了很多地方都没有成功,真的是很苦闷,后来有一次尝试删除了 build.rs
中增加的内容,竟然可以成功运行,但是只运行了 os
本身,没有先运行 tool
,于是我就去检查 makefile
文件,然后发现…我修改的竟然一直是根目录下的 makefile
而不是 os
目录下的 makefile
!!!
发现这一点之后突然感觉这些调试时间都错付了,这个错误简直太粗心。于是我将原先放错位置的 Makefile
代码放到 os/Makefile
中,成功运行。于是发现根本不需要修改 build.rs
,同样都是构建和编译项目的工具,什么时候应该修改哪个?针对这个问题,我进行了一些学习
Makefile
Makefile
是一种构建自动化工具,广泛应用于各种编程语言和项目,它可以用于定义如何编译和链接程序的规则,它的主要功能有:
- 指定编译规则:定义如何从源文件生成目标文件和可执行文件;
- 管理依赖关系:确保在源文件发生变化时重新编译相关文件;
- 自动化常见任务:如清理临时文件、运行测试等。
Makefile
的基本语法就不在这里阐述了,网上有很多教程,而且大家也很少会自己写,网上 copy 一个或者用 gpt 生成一个都是很好的提高生产力的手段。它的关键就是适用于定义复杂的构建过程,特别是多个文件和目标之间存在依赖关系的情况,通过变量和条件语句从而实现灵活的配置,然后用户使用 make
命令执行构建过程。
build.rs
build.rs
是 Rust 项目中的一个特殊构建脚本,用于在编译过程中执行一些自定义的构建逻辑。主要用途有:
- 生成代码:如自动生成绑定代码、构建过程中的代码注入等;
- 配置依赖:根据环境变量或配置文件生成依赖配置;
- 执行前置任务:如编译前生成或检查资源文件。
在 build.rs
中,可以使用 Rust 标准库中的各种功能来实现所需的构建逻辑,比较常用的是使用 println!("cargo:rerun-if-changed=path/to/file")
可以通知 Cargo 在指定文件发生变化时重新运行构建脚本。
Rust 语言圣经的 9.3.10 章节(https://course.rs/cargo/reference/build-script/intro.html)讲述了更多关于 build.rs
的内容。
两者关系
Makefile
通常用于管理整个项目的构建过程,特别是跨平台、跨语言或者需要调用多种编译工具的项目,能够统一管理各种构建步骤和依赖;build.rs
会在cargo build
过程中自动被调用,主要用于单个 Rust crate 的构建,尤其是那些需要在编译过程中动态生成或配置的部分;Makefile
中可以调用cargo build
,从而间接运行build.rs
。
结论
所以假设有一个 Rust 项目,其中需要在构建前执行一些额外的操作(例如通过 tool
修改代码),就可以通过 Makefile
先调用 tool
,无论 tool
的架构是否与原项目一致,然后再调用 cargo build
,触发 os
crate 的编译,包括 build.rs
中的自定义构建逻辑。Makefile
文件与 build.rs
文件共同作用,实现灵活而强大的构建过程。