Rust 的构建脚本是什么?今天一次性搞懂它

3 阅读5分钟

Rust 的构建脚本是什么?今天一次性搞懂它

对于刚刚接触 Rust 的程序员来说,大概率会在一些成熟项目中看到一个特殊的文件 build.rs,它是干什么的呢?今天我们一次性搞懂它。

build.rs 是什么

简单来说,build.rs,也就是构建脚本,是 Rust 提供的编译期执行脚本,它本质上是一个独立的 Rust 程序,会在 Cargo 编译你的项目(即编译 src 目录下的代码)之前被编译并执行。

它的执行流程很简单,当 Cargo 检测到项目根目录如果存在 build.rs 文件,那么就会先编译 build.rs 生成可执行文件并执行,然后才会去编译项目代码。

为什么要 Rust 设计 build.rs

Rust 设计 build.rs 最根本的原因是为了打造统一的构建流程。Rust 是一门注重工程化的编程语言,需要一套统一的构建机制来处理复杂场景,比如:链接 C/C++ 库、检测系统环境、生成 codegen 文件等。在其他编程语言中,开发者需要可能需要编写 shell 脚本、使用 CMake 等工具,这会导致构建流程碎片化,也可能会存在着跨平台兼容性的问题。

build.rs 作为 Rust 原生支持的构建脚本,与 Cargo 无缝集成,能统一处理各类构建需求。所有的复杂构建场景都能在 build.rs 中统一实现,减少跨平台兼容性问题,极大的降低了工程化的维护成本。

Cargo 与 build.rs 的无缝集成

build.rs 之所以能成为 Rust 构建流程的核心组成部分,关键在于其与 Cargo 构建工具的无缝集成。Cargo 默认会把根目录的 build.rs 文件自动识别为构建脚本,在编译项目核心代码前执行该脚本。同时 Cargo 还支持自定义脚本名称,可在 Cargo.toml 中配置:

[package]
# 自定义构建脚本名称
build = "custom_build_name.rs"
# 禁用构建脚本
# build = false

build.rs 无法直接使用 dependenciesdev-dependencies 下的库,如需单独添加到 build-dependencies

cargo add anyhow --build

Cargo 在执行 build.rs 前,会自动注入一系列环境变量,供 build.rs 获取项目、构建相关的信息,比如 OUT_DIR(编译输出目录)、TARGET(编译目标平台)等这些最常用的环境变量。

而同样的,build.rs 可通过 println! 输出以 cargo: 开头的指令,Cargo 会解析这些指令并调整后续的构建行为,常用指令包括:

  • cargo:rerun-if-changed=文件路径:仅当指定文件变化时重新编译 build.rs(增量构建)。
  • cargo:rerun-if-env-changed=环境变量名:与上一条指令类似,仅当指定环境变量变化时重新编译。
  • cargo:rustc-link-lib=库名:链接系统库或第三方库(常用于 FFI 场景)。
  • cargo:info=日志信息:输出构建日志(通过 cargo build -vv 查看)。

这里我们提供一个 build.rs 打印环境变量的示例供你参考:

use std::env;

fn main() {
    // 当 build.rs 发生变更时,重新编译 build.rs
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:info==== 打印所有环境变量 ===");
    for (key, value) in env::vars() {
        println!("cargo:info={}={}", key, value);
    }
}

执行以下命令查看效果:

# 清除编译缓存
cargo clean
# 编译并查看详细日志
cargo build -vv

一个完整的开发示例

Prost 是 Rust 生态中最主流的 Protobuf 实现,它提供了编译期代码生成和运行时序列化/反序列化能力,而其代码生成过程,正是通过 build.rs 在编译期完成的,下面我们将一步步实现这个示例。

环境准备

首先需要安装 Protobuf 官方编译器 protoc,prost 依赖它解析 .proto 文件。

# windows
winget install Google.Protobuf
# macOS(Homebrew)
brew install protobuf
# Linux(Ubuntu)
sudo apt install protobuf-compiler

新建项目并配置依赖

执行 cargo new rust-prost-demo 后进入项目目录修改 Cargo.toml,添加相关依赖:

[package]
name = "rust-prost-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
prost = "0.14"
prost-types = "0.14"

[build-dependencies]
prost-build = "0.14.1"

定义 Protobuf 文件

创建 protos/items.proto 文件,定义一个简单的服装信息结构:

syntax = "proto3";

// 定义包名,生成的 Rust 代码会对应这个包名
package snazzy.items;

message Shirt {
    enum Size {
        SMALL = 0;
        MEDIUM = 1;
        LARGE = 2;
    }
    string color = 1;
    Size size = 2;
}

编写 build.rs 脚本

在项目根目录创建 build.rs 文件,核心逻辑是:调用 prost-build 编译 protos/items.proto 文件,生成对应的 Rust 代码。

use std::io::Result;

fn main() -> Result<()> {
    println!("cargo::rerun-if-changed=protos/items.proto");
    println!("cargo::rerun-if-changed=build.rs");

    prost_build::Config::new()
        // 可指定输出目录,这里不展示
        //.out_dir("path_to")
        .compile_protos(&["protos/items.proto"], &["protos/"])?;

    Ok(())
}

接下来,执行 cargo build 命令后,就可以去看编译后的 Rust 文件了,完整路径为:

target/[debug/release]/build/rust-prost-demo-xxxxxxx/out/snazzy.items.rs

在项目中使用生成的代码

由于默认生成的代码不在 src 目录当中,所以我们需要使用 include!() 宏引入生成的代码,生成的代码文件名与 proto 包名一致,即 snazzy.items.rs

pub mod snazzy {
    pub mod items {
        include!(concat!(env!("OUT_DIR"), "/snazzy.items.rs"));
    }
}

use prost::Message;
use snazzy::items;

fn main() {
    let mut shirt = items::Shirt::default();
    shirt.color = String::from("red");
    shirt.set_size(items::shirt::Size::Large);
    println!("{:?}", shirt);

    // 序列化
    let mut buf = Vec::new();
    shirt.encode(&mut buf).expect("序列化失败");
    println!("序列化后的字节数组:{:?}", buf);

    // 反序列化
    let decoded_shirt = items::Shirt::decode(&buf[..]).expect("反序列化失败");
    println!("反序列化后:{:?}", decoded_shirt);
}

运行测试

执行 cargo run 后就会看到以下输出:

Shirt { color: "red", size: Large }
序列化后的字节数组:[10, 3, 114, 101, 100, 16, 2]
反序列化后:Shirt { color: "red", size: Large }

总结

看到这里,相信你已经彻底搞懂 build.rs 是什么了,它是 Rust 为编译期自定义需求而设计的功能特性,打造统一构建流程、跨平台适配与生态集成,完美契合了 Rust 高效工程化的设计理念。