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.