精通 Rust 构建脚本与条件编译:完整开发者指南

发布: (2025年12月22日 GMT+8 07:55)
11 min read
原文: Dev.to

Source: Mastering Rust Build Scripts and Conditional Compilation – The Complete Developer’s Guide

请提供您想要翻译的文章正文内容,我将按照您的要求把它译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!

📚 Explore My Books

作为畅销作者,我邀请您在 Amazon 上浏览我的书籍。
别忘了在 Medium 上关注我。谢谢——您的支持意义非凡!

🏗️ 建造房子 vs. 编写 Rust 程序

想象一下你在建造一座房子。你不会在空旷的田地里直接开始钉木板。首先你会 测量土地检查水源,并 浇筑基础。框架结构随后才会搭建。

编写 Rust 程序的过程类似。在正式开始编译之前,你通常需要进行一些准备工作:在系统上查找库、生成代码,或检查目标平台。这时 Rust 的构建脚本build.rs)就派上用场——它们就是你的基础施工队。

什么是 build.rs

build.rs 是一个小型、独立的 Rust 程序,会在你的主 crate 编译之前 在后台 运行。Cargo(Rust 的构建工具和包管理器)会自动编译并执行这个脚本。它的主要任务是 与 Cargo 通信,通过向控制台打印 Cargo 能理解的特殊指令,向 Cargo 低声传达如何构建最终产物的步骤。

📦 为什么使用构建脚本?个人示例

我曾经写过一个需要与 PostgreSQL 数据库通信的工具。优秀的 libpq C 库可以完成这项工作,而我的纯 Rust 代码需要链接到这个已经存在的库。

  • 在我朋友的 macOS 机器上,libpq 位于 /usr/local/lib
  • 在我的 Linux 服务器上,它位于 /usr/lib/x86_64-linux-gnu

在 Rust 源码中硬编码路径几乎是不可能的。解决办法是什么?编写一个 build.rs 脚本,在宿主系统上搜索 libpq 并告诉 Cargo 去哪里找到它。

用于链接系统库的最小 build.rs

// build.rs
fn main() {
    // 告诉 Cargo 让 rustc 链接系统 libpq
    println!("cargo:rustc-link-lib=pq");

    // 可选:如果 wrapper.h 发生变化,重新运行构建
    println!("cargo:rerun-if-changed=wrapper.h");
}

其中的语句

println!("cargo:rustc-link-lib=pq");

是“魔法”:它并不是给人看的,而是给 Cargo 的指令,Cargo 会把它转换为相应的链接器标志。

🛠️ 构建脚本可以生成 Rust 代码

除了链接库之外,构建脚本还能在编译时生成 Rust 代码
例如,你可能有一个数据文件(colors.txt),想把它转换成 Rust 的 enum。在编译时完成这一步可以把验证工作从运行时提前到编译时。

📄 示例数据文件

red   #FF0000
green #00FF00
blue  #0000FF

🛠️ build.rs – 生成 colors.rs

// build.rs
use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;

fn main() {
    // Directory where Cargo asks us to place generated files
    let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
    let dest_path = Path::new(&out_dir).join("colors.rs");
    let mut file = fs::File::create(&dest_path).expect("Unable to create file");

    // Read the source data file
    let color_data = fs::read_to_string("colors.txt").expect("Unable to read colors.txt");

    // Write the generated Rust source
    writeln!(&mut file, "// Auto‑generated file from build.rs").unwrap();
    writeln!(&mut file, "#[derive(Debug)]").unwrap();
    writeln!(&mut file, "pub enum Color {{").unwrap();

    for line in color_data.lines() {
        // Split each line into name and hex value
        let parts: Vec<&str> = line.split_whitespace().collect();
        if parts.len() == 2 {
            let name = parts[0];
            // let _hex = parts[1]; // Could be used for extra features
            writeln!(&mut file, "    {},", name).unwrap();
        }
    }

    writeln!(&mut file, "}}").unwrap();
}

关键点

  • OUT_DIR 是 Cargo 为生成的产物创建的目录。
  • include!(后面会用到)会把生成的文件引入到 crate 中。
  • 每当任意源文件或数据文件发生变化时,脚本会自动重新运行。

📦 生成的 colors.rs

// Auto‑generated file from build.rs
#[derive(Debug)]
pub enum Color {
    red,
    green,
    blue,
}

🚀 在 main.rs 中使用生成的代码

// src/main.rs
// Include the code generated by build.rs
include!(concat!(env!("OUT_DIR"), "/colors.rs"));

fn main() {
    let my_color = Color::red;
    println!("My color is {:?}", my_color);

    // The following line would cause a compile‑time error because `yellow` does not exist:
    // let other_color = Color::yellow;
}

colors.txt 被更新(例如,添加 yellow #FFFF00)时,构建脚本会再次运行,重新生成 colors.rsColor::yellow 便会自动可用。

🏷️ 条件编译 – 架构师

如果说构建脚本是 基础工人,那么条件编译(#[cfg(...)])就是 架构师,它会为山间小屋和海边别墅绘制不同的设计图,而这些图都来源于同一份蓝图。(讨论在本段之后仍在继续。)

它的工作方式类似编译器大门口的守卫。它检查一个条件,只有当条件为真时,才会让其后面的代码进入最终的二进制文件。

最常见的用途是针对不同的操作系统。假设我需要获取当前用户的主目录。在类 Unix 系统(Linux、macOS)上,这通常是 HOME 环境变量;在 Windows 上,则是 HOMEDRIVEHOMEPATH 的组合。我可以写一个在运行时检查操作系统的函数,但这会带来一点点开销以及代码必须始终评估的分支。相反,我可以让编译器只为目标平台构建相应的代码。

fn get_home_dir() -> Option<String> {
    #[cfg(target_os = "windows")]
    {
        std::env::var("HOMEDRIVE")
            .and_then(|drive| std::env::var("HOMEPATH").map(|path| drive + &path))
            .ok()
    }

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    {
        std::env::var("HOME").ok()
    }

    #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
    {
        compile_error!("This OS is not supported for finding home directory.");
    }
}

当为 Windows 编译时,只有 #[cfg(target_os = "windows")] 块会被保留;其余块会被丢弃。其他平台同理。

有时你需要在表达式内部作出决定。这时 cfg! 宏派上用场——它在编译时返回 truefalse

fn print_greeting() {
    let greeting = if cfg!(target_os = "windows") {
        "Howdy from Windows!"
    } else if cfg!(target_family = "unix") {
        "Hello from Unix!"
    } else {
        "Greetings, unknown system!"
    };
    println!("{}", greeting);
}

编译器在编译期间求值 cfg!,随后死代码消除会剔除未使用的分支。

构建脚本 + 自定义 cfg 标志

构建脚本可以发现环境信息,并将其作为 自定义 条件编译标志传递给主代码。

// build.rs
fn main() {
    // 尝试使用 pkg-config 找到 libpq
    if pkg_config::probe_library("libpq").is_ok() {
        println!("cargo:rustc-link-lib=pq");
        // 告诉主代码:“我们找到了系统的 libpq!”
        println!("cargo:rustc-cfg=has_system_libpq");
    }
}

println!("cargo:rustc-cfg=has_system_libpq"); 这行代码定义了一个自定义配置标志。

// src/lib.rs or src/main.rs

#[cfg(has_system_libpq)]
use some_system_libpq_binding::Connection;

#[cfg(not(has_system_libpq))]
use pure_rust_postgres::Connection as FallbackConnection;

pub struct Database {
    #[cfg(has_system_libpq)]
    conn: Connection,
    #[cfg(not(has_system_libpq))]
    conn: FallbackConnection,
}

impl Database {
    pub fn connect(connection_string: &str) -> Result<Self, Box<dyn std::error::Error>> {
        #[cfg(has_system_libpq)]
        {
            let conn = Connection::establish(connection_string)?;
            Ok(Database { conn })
        }
        #[cfg(not(has_system_libpq))]
        {
            let conn = FallbackConnection::connect(connection_string)?;
            Ok(Database { conn })
        }
    }
}

在拥有 libpq 的机器上,has_system_libpq 标志会被设置,从而启用高效的系统原生绑定。否则,会使用纯 Rust 的回退实现,所有选择都在编译时完成,没有运行时开销。

将 Feature 当作 cfg 标志

Feature 用来组织可选依赖和条件代码。

# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"

[features]
default = []                # 没有默认特性
json_output = ["serde_json"] # 启用此特性会拉入 serde_json crate

[depend

Source:

A user can enable the feature with:

cargo build --features "json_output"

101 Books

101 Books 是一家由作者 Aarav Joshi 共同创立的 AI 驱动出版公司。通过利用先进的 AI 技术,我们将出版成本控制得极低——部分书籍的定价低至 $4——让优质知识惠及每个人。

  • 📚 在亚马逊查看我们的图书 Golang Clean Code
  • 🔔 敬请关注更新和精彩新闻。
  • 🔎 选购图书时,搜索 Aarav Joshi 可发现更多我们的作品。
  • 🎁 使用以上链接可享受 特别折扣

我们的创作

探索我们的项目:

我们在 Medium

# Conditional Compilation in Rust

A user runs

```bash
cargo build --features "json_output"

to get the JSON version. Otherwise, they get the simple text output. This is how large libraries offer modular functionality without forcing all users to download every possible dependency.

A word of caution from experience:
Conditional compilation is incredibly useful, but it can make your code harder to test. If you have a block of code guarded by #[cfg(target_os = "windows")], you can’t test it on your Linux laptop. Sometimes, for integration tests, you might want to compile and run tests for all targets on a CI server. Over‑using cfg can also make the logical flow of your code harder to follow, as chunks of it become invisible depending on how you’re compiling.

The philosophy behind these systems is what I find most compelling. They move decisions from runtime to compile time where possible. They turn what is often a messy, script‑heavy build process in other languages into a declarative, integrated, and type‑safe part of your Rust project.

  • My build.rs is just another Rust file, subject to the same safety and tooling.
  • My conditional compilation is checked by the compiler, catching typos in feature names or cfg predicates.

It allows you to write one codebase that is honest about the differences in the world—different OSes, different hardware, different sets of required features—and then lets the compiler assemble the exact right program for a given situation. It’s not about hiding complexity, but about managing it in a structured, reliable, and efficient way.

Think of it as starting with a plan for all possible houses; the build system, guided by your scripts and conditions, gathers the right materials and builds the one you actually need for the plot of land you’re on.


📘 Check out my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!

Back to Blog

相关文章

阅读更多 »

虚空

早期经历 你好。这将是我的第一篇文章,希望它能成为我与 Rust 编程语言纠缠的系列文章之一。鉴于我……

我在 Rust 的第一周学到的东西

如何学习 Rust?一些常被引用的 Rust 学习资源包括:- The Rust Programming Language https://doc.rust-lang.org/stable/book/ – 官方 Rust 书籍。