✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Rust语言通关之路
景天的主页:景天科技苑
文章目录
- Cargo构建脚本
- 一、什么是构建脚本(build.rs)
- 1.1 在 Rust 中,build.rs 是一种特殊的构建脚本(Build Script)
- 1.2 创建构建脚本
- 二、build.rs 的执行机制
- 三、构建脚本的基本结构
- 四、常见的 cargo: 指令及其用途
- 4.1 生成代码
- 4.2 构建 C/C++ 代码(构建时依赖)
- 4.3 链接系统库
- 4.4 读取环境变量
- 五、构建脚本的限制和注意事项
- 六、替代方案
- 七、结论
Cargo构建脚本
Cargo是Rust的包管理器和构建系统,它是Rust生态系统中最强大的工具之一。除了基本的依赖管理和编译功能外,Cargo还提供了一个强大的特性——构建脚本(build scripts)。构建脚本允许开发者在编译过程的不同阶段执行自定义操作,为复杂的构建需求提供了灵活的解决方案
一、什么是构建脚本(build.rs)
1.1 在 Rust 中,build.rs 是一种特殊的构建脚本(Build Script)
它在构建过程的预处理阶段执行,属于 Cargo 的扩展机制。它主要用于:
生成代码
构建或链接本地库
编译 C/C++ 依赖(通过 cc crate)
生成代码或绑定(如 FFI、代码生成工具)
传递配置到 Rust 源码中(如系统环境变量)
检查编译平台特性,进行条件编译
生成版本信息、构建时间等
构建脚本本质上就是一个 Rust 可执行程序,默认命名为 build.rs,位于 crate 根目录下。
也可以在Cargo.toml的package里面,通过build参数指定文件名
1.2 创建构建脚本
要在项目中添加构建脚本,只需在项目根目录(与Cargo.toml同级)创建一个名为build.rs的文件:
my_project/
├── Cargo.toml
├── build.rs
└── src/
└── main.rs
Cargo会自动检测到这个文件并在构建过程中使用它。
二、build.rs 的执行机制
在编译 crate 前,Cargo 会:
检查是否存在 build.rs;
执行 build.rs;
将其输出写入 OUT_DIR;
根据构建脚本的输出(例如环境变量、生成的文件)进行后续构建。
三、构建脚本的基本结构
一个最简单的 build.rs 文件:
fn main() {println!("cargo:rerun-if-changed=build.rs");println!("cargo:rustc-env=BUILD_MODE=debug");
}
说明:
所有 println! 的输出需以 cargo: 前缀开头;
Cargo 解析这些关键指令,影响构建流程;
cargo:rustc-env 设置的是环境变量;
cargo:rerun-if-changed 通知 Cargo 只有当指定文件变化时才重新运行 build.rs。
四、常见的 cargo: 指令及其用途
指令
cargo:rustc-link-lib=xxx 链接系统库(如 C 库)
cargo:rustc-link-search=path 添加库搜索路径
cargo:rustc-env=KEY=VAL 设置 Rust 编译时的环境变量
cargo:rerun-if-changed=path 文件变动时重新执行构建脚本
cargo:rerun-if-env-changed=VAR 指定环境变量变动时重构
cargo:warning=msg 构建输出警告信息
cargo:rustc-cfg=feature_name 给 Rust 编译器添加 cfg(feature_name) 条件编译标记
4.1 生成代码
构建脚本最常见的用途之一是生成Rust代码。这可以通过以下步骤实现:
在构建脚本中生成代码
将生成的代码写入文件
告诉Cargo在编译时包含这个文件
编写build.rs
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;fn main() {//获取OUT_DIR环境变量的值,该变量由Cargo提供,指向目标目录let out_dir = env::var("OUT_DIR").unwrap();//在目标目录中创建一个文件let dest_path = Path::new(&out_dir).join("generated_code.rs");let mut f = File::create(&dest_path).unwrap();//在文件中写入Rust代码//可以使用writeln!宏来写入代码//也可以通过f.write_all(&[u8])来写入字节// writeln!(&mut f, "pub fn generated_function() -> i32 {{").unwrap();// writeln!(&mut f, " 42").unwrap();// writeln!(&mut f, "}}").unwrap();f.write_all(b"pub fn generated_function() -> i32 {42}").unwrap();//告诉Cargo在build.rs文件改变时重新运行println!("cargo:rerun-if-changed=build.rs");
}
然后在你的主代码中可以使用include!宏来包含生成的代码:
//包含生成的函数
include!(concat!(env!("OUT_DIR"), "/generated_code.rs"));fn main() {//调用生成的函数println!("The answer is: {}", generated_function());
}
生成的代码在target/debug/build下面
4.2 构建 C/C++ 代码(构建时依赖)
使用 cc crate 可以方便地将 C 代码编译为静态库并链接到 Rust。
构建脚本可以有自己的依赖项,这些依赖项在Cargo.toml的[build-dependencies]部分指定:
[build-dependencies]
cc = "1.0"
然后在构建脚本中使用依赖:
use cc;fn main() {//编译C代码cc::Build::new().file("src/native/add.c").compile("libadd.a");//告诉Cargo在C代码改变时重新运行println!("cargo:rerun-if-changed=src/native/add.c");
}
c代码:
注意路径,可以自定义路径
#include <stdio.h>;
int add(int a, int b)
{return a + b;
}
Rust调用:
//引入C中定义的函数
unsafe extern "C" {unsafe fn add(a: i32, b: i32) -> i32;
}fn main() {unsafe {println!("C add: {}", add(2, 3));}
}
注意,引入C函数之后,需要定义函数参数和返回值,逻辑不用定义
4.3 链接系统库
构建脚本另一个常见用途是链接系统库。例如,如果你想链接一个名为foo的系统库:
fn main() {println!("cargo:rustc-link-lib=foo");
}你也可以指定库的类型:
fn main() {// 静态链接println!("cargo:rustc-link-lib=static=foo");// 动态链接println!("cargo:rustc-link-lib=dylib=foo");// 框架(在macOS上)println!("cargo:rustc-link-lib=framework=foo");
}
4.4 读取环境变量
构建脚本可以根据操作系统、环境变量等做出决策。
在 Rust 的构建脚本(build.rs)中,你可以通过读取 Cargo 提供的环境变量,识别目标操作系统,并执行不同的构建逻辑。
最常用的是 TARGET 变量,它可以告诉你当前编译目标的平台三元组,如:
x86_64-unknown-linux-gnu
x86_64-pc-windows-msvc
aarch64-apple-darwin
🧠 常用 TARGET 值参考
平台 目标字符串示例
Linux x86_64-unknown-linux-gnu
Windows x86_64-pc-windows-msvc
macOS aarch64-apple-darwin / x86_64-apple-darwin
iOS aarch64-apple-ios
Android aarch64-linux-android
WebAssembly wasm32-unknown-unknown
🧪 示例:为不同平台生成不同文件
use std::{ env, fs::write, path::Path };fn main() {let target = env::var("TARGET").unwrap();let out_dir = env::var("OUT_DIR").unwrap();let dest_path = Path::new(&out_dir).join("platform.rs");let platform_code = if target.contains("windows") {r#"pub fn platform() { println!("Windows 构建"); }"#} else if target.contains("apple") {r#"pub fn platform() { println!("macOS 构建"); }"#} else if target.contains("linux") {r#"pub fn platform() { println!("Linux 构建"); }"#} else {r#"pub fn platform() { println!("未知平台"); }"#};write(&dest_path, platform_code).unwrap();println!("cargo:rerun-if-changed=build.rs");
}
在主代码中使用:
include!(concat!(env!("OUT_DIR"), "/platform.rs"));fn main() {platform();
}
五、构建脚本的限制和注意事项
- 构建时间:构建脚本会增加项目的编译时间
- 可移植性:构建脚本中的平台特定代码可能会影响项目的可移植性
- 安全性:构建脚本可以执行任意代码,下载依赖时要小心
- 确定性构建:构建脚本可能导致非确定性构建(如嵌入构建时间)
- 错误处理:构建脚本中的错误会导致整个构建失败
六、替代方案
在某些情况下,可能有比构建脚本更好的选择:
过程宏(proc macros):对于代码生成,过程宏可能是更好的选择
构建时依赖:对于简单的代码生成,可以在build.rs中使用include!而不是过程宏
第三方构建系统:对于非常复杂的构建需求,可能需要考虑CMake或Meson等构建系统
七、结论
Cargo构建脚本是Rust构建系统中一个强大而灵活的特性,它为开发者提供了在构建过程中执行自定义操作的能力。通过本教程,你应该已经掌握了构建脚本的基本用法和高级技巧,能够处理各种复杂的构建需求。
记住,虽然构建脚本功能强大,但应该谨慎使用。在可能的情况下,优先使用更简单、更标准的Cargo特性。只有在真正需要时才使用构建脚本,并确保它们尽可能高效和可维护。