‹ back home

Injecting project versions into builds

2024-12-16 #development #git #rust

Applications typically need to embed their version into binaries, for things like --version to work as expected. The exact version will usually be stored in version control (e.g.: git).

The recipe presented here ensures that source tarballs include the right version string (even tarballs auto-generated by code forges), and builds done from a full git checkout use the output of git-describe.

The examples below work for a Rust project, but the general solution is applicable to any language.

Including the version into tarballs

Most code forges provide tarballs for tags and individual commits. These are generated using git-archive. Git provides an export-subst feature which allows us to replace a string with the output of git-describe, effectively injecting the corresponding version into the tarball.

First of all, we’ll create a .gitattributes file which includes a file where the replacement will be executed, and the export-subst attribute. E.g.:

/build.rs export-subst

This will replace the string $Format:%(describe)$ in build.rs with the corresponding version.

Injecting the version during builds

The above merely replaces the string into a file when generating tarballs, but we still need to consider builds done using a full git checkout of the repository (which will have the un-replaced string).

When building from a git checkout, we can determine the right version string using git describe --tags.

The following example uses a replaced version, if it has been replaced, and otherwise falls back to git describe:

use std::process::Command;

fn main() {
    if std::env::var("PIMSYNC_VERSION").is_err() {
        let version = "$Format:%(describe)$"; // Will be replaced by git-archive.
        let version = if version.starts_with('$') {
            match Command::new("git").args(["describe", "--tags"]).output() {
                Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_owned(),
                Ok(o) => panic!("git-describe exited non-zero: {}", o.status),
                Err(err) => panic!("failed to execute git-describe: {err}"),
            }
        } else {
            String::from(version)
        };
        println!("cargo:rustc-env=PIMSYNC_VERSION={version}");
    }
}

As you’ll notice, I basically inject the version into an PIMSYNC_VERSION variable. This is only done if PIMSYNC_VERSION is unset, allowing anyone compiling to override the version by setting the PIMSYNC_VERSION variable.

Using the environment variable

Finally, the application needs to use this variable as a version. This is done by simply loading the environment variable into a &str:

pub const VERSION: &str = env!("PIMSYNC_VERSION");

The env! macro evaluates an environment variable at compile time. This would fail if the variable is unset, but the variable is set unconditionally. If it is unset, it is because someone has been tinkering with the build system and broke it.

Caveats

There are none. This relies on code-forge auto-generated tarballs, using functionality in git itself. You don’t need to manually produce tarballs, and builds done from any commit or tag will reflect their source properly.

Have comments or want to discuss this topic?
Send an email to my public inbox: ~whynothugo/public-inbox@lists.sr.ht.
Or feel free to reply privately by email: hugo@whynothugo.nl.

— § —