Generating a versions.rs at build time

The C ABI of librsvg exports a few uint_t variables like rsvg_major_version, rsvg_minor_version, etc.

We are in the process of moving this to Rust so that the entire librsvg.so can be built with cargo-c (right now all the code in the library is in Rust, and is wrapped by stub functions in C. Those variables are also in C files).

Ideally those version numbers would be defined once, in the configure.ac script, and propagated down to a generated versions.rs source file. (Commentary: librsvg is fine with not letting cargo build run standalone for the C library; we need to do some postprocessing for it anyway, which is not cargo-c's business.)

I see two options:

  1. Set some VERSION_MAJOR, VERSION_MINOR environment variables in the build scripts. Have a build.rs pick these up and generate a versions.rs in the build directory, which is later included somewhere in the library.
  2. Have a versions.rs.in onto which the build scripts do textual substitution to generate versions.rs in the source directory.

I suppose (1) is the "conventionally build.rs way", and (2) is the "keep the existing build scripts mostly unchanged".

Has anyone had to do something similar to this? Any preferred approaches? Thanks!

1 Like

You could be totally unprincipled and just parse configure.ac from your build.rs script.

let mut file = File::open("../configure.ac").expect("builds must take place within the librsvg source tree");
let major_regex = Regex::new(r#"^m4_define\(\[rsvg_major_version\],\[(\d+)\]\)"#).unwrap();
let minor_regex = Regex::new(r#"^m4_define\(\[rsvg_minor_version\],\[(\d+)\]\)"#).unwrap();
let micro_regex = Regex::new(r#"^m4_define\(\[rsvg_micro_version\],\[(\d+)\]\)"#).unwrap();
let mut major = None;
let mut minor = None;
let mut micro = None;
for line in BufRead::new(file).lines() {
    if let Some(nums) = major_regex.captures(line) {
        major = Some(nums.get(1).expect("major_regex has one capture group").as_str());
    } else if let Some(nums) = minor_regex.captures(line) {
        minor = Some(nums.get(1).expect("minor_regex has one capture group").as_str());
    } else if let Some(nums) = micro_regex.captures(line) {
        micro = Some(nums.get(1).expect("micro_regex has one capture group").as_str());
    }
}
mem::drop(file);
let mut file = File::create(format!("{}/version.rs", env::get("OUT_DIR"))).expect("open version.rs for writing");
file.write_all(format!(
  r#"fn major_version() -> u32 { {} } fn minor_version() -> u32 { {} } fn micro_version() -> u32 { {} }"#,
  major.expect("major"),
  minor.expect("minor"),
  micro.expect("micro"),
).as_bytes()).expect("write_all");

A lot of Rust-based websites do something similar to include the git commit in a generated source file, like this, because they use continuous deployment and don't really have version numbers.

If the values you need are already in environment variables, you can use env! to import them as strs at compile time, without needing anything in build.rs.

This is pure evil and I love it :smile: Thank you!

Oh, yeah, env! was my first approach, but unfortunately I need the values as numbers. (For historical reasons they are numbers in the C code; for a new library I'd probably just export the version as a string.)

That’s doable, too, but is complicated by const fn being relatively weak right now:

const VERSION: &[u8] = "2.13.4".as_bytes();

const fn get_ver_field(mut x:u8)->u16 {
    let mut i:usize = 0;

    while x > 0 {
        if VERSION[i] == '.' as u8 { x -= 1; }
        i += 1;
    }

    let mut result:u16 = 0;

    while i < VERSION.len() && VERSION[i] != '.' as u8 {
        result *= 10;
        result += VERSION[i] as u16 - '0' as u16;
        i += 1;
    }

    result
}
    
pub static MAJOR:u16 = get_ver_field(0);
pub static MINOR:u16 = get_ver_field(1);
pub static PATCH:u16 = get_ver_field(2);

fn main() {
    println!("{}.{}.{}", MAJOR,MINOR,PATCH);
}

(Playground)

1 Like