如果缺少 include_bytes!(…) 目标,则回退到替代值

Falling back to alternative value if include_bytes!(…) target is missing

我的包有一个使用 include_bytes!(…) to bundle a copy of some precomputed values into the compiled binary. This is an optimization, but isn't strictly necessary: the program is capable of calculating these values at run time if the bundled data slice .is_empty().

的二进制目标

程序需要能够在没有这些数据的情况下构建。但是,如果目标文件不存在,include_bytes!("data/computed.bin") 会导致生成错误。

error: couldn't read src/data/computed.bin: No such file or directory (os error 2)

目前,我有一个 Bash 构建脚本,它使用 touch data/computed.bin 确保文件在构建之前存在。但是,我不想依赖特定于平台的解决方案,例如 Bash;我希望能够使用 cargo build.

在任何支持的平台上构建这个项目

我的 Rust 程序如何从一个文件中 include_bytes!(…)include_str!(…) 如果它退出,但如果文件不存在则优雅地回退到替代值或行为,同时仅使用标准 Cargo 构建工具?

我们可以使用 build script 来确保包含的文件在输出包尝试包含它之前存在。但是,构建脚本只能写入当前构建的唯一输出目录,所以我们不能直接在源目录中创建缺少的输入文件。

error: failed to verify package tarball

Caused by:
  Source directory was modified by build.rs during cargo publish. Build scripts should not modify anything outside of OUT_DIR.

相反,我们的构建脚本可以在构建目录中创建 file-to-include,复制源数据(如果存在),并且我们可以更新我们的包代码以包含来自构建目录而不是来自源目录。构建路径将在构建期间在 OUT_DIR 环境变量中可用,因此我们可以从构建脚本中的 std::env::var("OUT_DIR") 和包其余部分中的 env!("OUT_DIR") 访问它。

//! build.rs

use std::{fs, io};

fn main() {
    let out_dir = std::env::var("OUT_DIR").unwrap();

    fs::create_dir_all(&format!("{}/src/data", out_dir))
        .expect("unable to create data directory");

    let path = format!("src/data/computed.bin", name);
    let out_path = format!("{}/{}", out_dir, path);

    let mut out_file = fs::OpenOptions::new()
        .append(true)
        .create(true)
        .open(&out_path)
        .expect("unable to open/create data file");

    if let Ok(mut source_file) = fs::File::open(&path) {
        io::copy(&mut source_file, &mut out_file).expect("failed to copy data after opening");
    }
}
//! src/foo.rs

fn precomputed_data() -> Option<&'static [u8]> {
    let data = include_bytes!(concat!(env!("OUT_DIR"), "/src/data/computed.bin")).as_ref();
    if !data.is_empty() {
        Some(data)
    } else {
        None
    }
}

虽然使用构建脚本(如 )可以工作,但我不喜欢该解决方案:

  • 构建脚本复制文件 – 根据文件大小,这可能非常昂贵。尽管可以改用硬链接来解决这个问题。
  • 空文件可能是完美的数据 – 该解决方案会将空文件误检测为丢失。但是,根据用例,空文件实际上可能完全有效。
  • 它非常冗长 – 这将一个简单的 include_bytes! 变成了大约 20 行的构建脚本,并且在包含处理 data.is_empty()案例.
  • 一般人很难理解这里发生的事情 reader – 为什么这个脚本包含来自 $OUT_DIR 的内容? reader 可能需要一些时间才能了解这里可能涉及构建脚本。
  • 扩展性不佳 – 如果需要选择性地包含多个文件,那么大多数问题会变得更糟。

因此我决定编写程序宏包 include_optional 来解决这个问题(目前只适用于 nightly Rust,因为它依赖于一些不稳定的特性)。

有了这个,这个问题的解决方案就是一个班轮:

use include_optional::include_bytes_optional;

fn precomputed_data() -> Option<&'static [u8]> {
    include_bytes_optional!("./computed.bin")
}

还有包装include_str!include!的宏。