Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

My package has a binary target that uses 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().

The program needs to be able to build without this data. However, include_bytes!("data/computed.bin") causes a build error if the target file does not exist.

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

Currently, I have a Bash build script that uses touch data/computed.bin to ensure the file exists before building. However, I don't want to depend on platform-specific solutions like Bash; I want to be able to build this project on any supported platform using cargo build.

How can my Rust program include_bytes!(…) or include_str!(…) from a file if it exits, but gracefully fall back to an alternative value or behaviour if the file doesn't exist, while only using the standard Cargo build tools?

like image 476
Jeremy Avatar asked May 07 '19 02:05

Jeremy


1 Answers

We can use a build script to ensure that the included file exists before out package tries to include it. However, build scripts can only write to the current build's unique output directory, so we can't just create the missing input files in the source directory directly.

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.

Instead, our build script can create the file-to-include in the build directory, copying the source data if it exists, and we can update our package code to include this data from the build directory instead of from the source directory. The build path will be available in the OUT_DIR environment variable during the build, so we can access it from std::env::var("OUT_DIR") in our build script and from env!("OUT_DIR") in the rest of our package.

//! 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
    }
}
like image 131
Jeremy Avatar answered Sep 21 '22 11:09

Jeremy