What you're looking for is called a build.rs
build script. Build scripts are used for more complex compile-time code generation than macros are capable of. For instance, my own makepass password generator uses a build script to handle turning a words dictionary into a rust source file, so that the words can be built directly into the binary.
For your specific use case, you might do something like this. Note that the build script typically lives in the project root, rather than the src
directory.
// build.rs
use std::{
env,
fmt::{self, Display, Formatter},
fs::File,
io::{BufWriter, Write},
path::Path,
};
fn main() {
// your original code used `std::env::args`. It's not possible to pass
// command line arguments to a build script, so instead I'm using an
// environment variable called LIST, containing a space-separated list
// of ints
let list: Vec<i32> = env::var("LIST")
.expect("no variable called LIST")
.split_whitespace()
.map(|item| item.parse().expect("LIST contained an invalid number"))
.collect();
// When generating rust code in build.rs, use the OUT_DIR variable as a
// directory that contains it; cargo uses it to help preserve generated
// code (so that it's only regenerated when necessary)
let path = env::var("OUT_DIR").expect("no variable called OUT_DIR");
let path = Path::new(&path).join("output.rs");
let output_file = File::create(&path).expect("Failed to create `output.rs`");
write!(BufWriter::new(output_file), "{}", FooWriter { data: &list })
.expect("failed to write to output.rs");
// This directive informs cargo that $LIST is a build-time dependency, so
// it rebuilds this crate if the variable changes
println!("cargo:rerun-if-env-changed=LIST");
}
// This struct is used to implement the recursive write that's required
struct FooWriter<'a> {
data: &'a [i32],
}
impl Display for FooWriter<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.data.split_first() {
None => write!(f, "None"),
Some((item, tail)) => {
write!(
f,
"Some(Box::new(Foo {{ a: {}, b: {} }}))",
item,
FooWriter { data: tail }
)
}
}
}
}
// src/main.rs
#[derive(Debug)]
struct Foo {
a: i32,
b: Option<Box<Foo>>,
}
fn main() {
// include! performs a textual include of a file at build time.
// the contents of the file are loaded directly into this
// source file, as though via copy-paste.
// concat! is a simple compile time string concatenation
// env! gets the value of an environment variable at compile
// time and makes it available as an &'static str
let my_data = include!(concat!(env!("OUT_DIR"), "/output.rs"));
println!("{:#?}", my_data)
}
This program will fail to build if the LIST
environment variable doesn't exist at build time. Here's how it looks when LIST
does exist:
$ env LIST="1 2 3" cargo build
Compiling rust-crate v0.1.0 (/Users/nathanwest/Documents/Repos
< ... warnings ... >
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
$ ./target/debug/rust-crate
Some(
Foo {
a: 1,
b: Some(
Foo {
a: 2,
b: Some(
Foo {
a: 3,
b: None,
},
),
},
),
},
)