Embedding Version in Binaries
The real power of versionator isn't just tracking version in source control—it's getting that version into your compiled binary so you can always identify exactly what's running in production.
Why Binary Embedding Matters
Consider this scenario: You're debugging a production issue at 2 AM. You SSH into a server and find a binary called myapp. What version is it?
Without embedded version info:
$ ./myapp --version
# ???
$ ls -la myapp
-rwxr-xr-x 1 deploy deploy 12345678 Jan 15 10:30 myapp
# Timestamp? Maybe helpful, maybe not.
With embedded version info:
$ ./myapp --version
myapp v2.1.3 (commit: abc1234, built: 2024-01-15T10:30:00Z)
You instantly know: the exact version, the exact commit, and when it was built.
Two Approaches
Every language falls into one of two categories:
| Category | Languages | Mechanism |
|---|---|---|
| Compiled | Go, Rust, C, C++, Java, Kotlin, C#, Swift | Inject values at compile time |
| Interpreted | Python, JavaScript, TypeScript, Ruby, PHP | Generate source file at build time |
Both approaches achieve the same result: version info baked into the final artifact.
Live Demos
All examples below are runnable. Each includes a justfile with just run to see the embedded version in action.
Go
Location: examples/go/
Go's linker injects string values via -ldflags:
package main
import "fmt"
// VERSION will be set by the linker during build
var VERSION = "0.0.0"
func main() {
fmt.Printf("Sample Go Application\n")
fmt.Printf("Version: %s\n", VERSION)
}
build:
VERSION=$$(versionator version); \
go build -ldflags "-X main.VERSION=$$VERSION" -o sample-app .
Run it:
$ cd examples/go && just run
Getting version from versionator...
Building sample application with version: 0.0.13
Build completed: sample-app
./sample-app
Sample Go Application
Version: 0.0.13
Source code: main.go | justfile
Rust
Location: examples/rust/
Rust reads environment variables at compile time with option_env!():
fn main() {
// VERSION will be set by the compiler during build via environment variable
let version = option_env!("VERSION").unwrap_or("0.0.0");
println!("Sample Rust Application");
println!("Version: {}", version);
}
build:
VERSION=$$(versionator version); \
VERSION="$$VERSION" rustc -o sample-app main.rs
Run it:
$ cd examples/rust && just run
Getting version from versionator...
Building sample application with version: 0.0.13
Build completed: sample-app
./sample-app
Sample Rust Application
Version: 0.0.13
Source code: main.rs | justfile
C
Location: examples/c/
C uses preprocessor defines (-D) to inject values:
#include <stdio.h>
// VERSION will be set by the compiler during build
#ifndef VERSION
#define VERSION "0.0.0"
#endif
int main() {
printf("Sample C Application\n");
printf("Version: %s\n", VERSION);
return 0;
}
build:
VERSION=$$(versionator version); \
gcc -DVERSION="\"$$VERSION\"" -o sample-app main.c
Run it:
$ cd examples/c && just run
Getting version from versionator...
Building sample application with version: 0.0.13
Build completed: sample-app
./sample-app
Sample C Application
Version: 0.0.13
Source code: main.c | justfile
C++
Location: examples/cpp/
Same approach as C—preprocessor defines:
#include <iostream>
// VERSION will be set by the compiler during build
#ifndef VERSION
#define VERSION "0.0.0"
#endif
int main() {
std::cout << "Sample C++ Application" << std::endl;
std::cout << "Version: " << VERSION << std::endl;
return 0;
}
build:
VERSION=$$(versionator version); \
g++ -DVERSION="\"$$VERSION\"" -o sample-app main.cpp
Run it:
$ cd examples/cpp && just run
Getting version from versionator...
Building sample application with version: 0.0.13
Build completed: sample-app
./sample-app
Sample C++ Application
Version: 0.0.13
Source code: main.cpp | justfile
Java
Location: examples/java/
Java generates a source file from a template at build time:
package app;
import static app.BuildTime.VERSION;
public class Main {
public static void main(String[] args) {
System.out.println("Sample Java Application");
System.out.println("Version: " + VERSION);
}
}
The Makefile generates BuildTime.java from a template:
build:
VERSION=$$(versionator version); \
sed -e "s/@VERSION@/$${VERSION}/g" BuildTime.java.tmpl > BuildTime.java; \
javac Main.java BuildTime.java
Run it:
$ cd examples/java && just run
Getting version from versionator...
Generating BuildTime.java from template...
Building sample application with version: 0.0.13
Build completed: app/Main.class app/BuildTime.class
java app.Main
Sample Java Application
Version: 0.0.13
Source code: app/Main.java | app/BuildTime.tmpl.java | justfile
Kotlin
Location: examples/kotlin/
Kotlin generates a Version.kt object at build time:
package app
import version.Version
fun main() {
println("Sample Kotlin Application")
println("Version: ${Version.VERSION}")
}
version-file:
versionator output emit kotlin --output Version.kt
build: version-file
kotlinc Main.kt Version.kt -include-runtime -d sample-app.jar
Run it:
$ cd examples/kotlin && just run
Generating Version.kt using versionator emit...
Building Kotlin application...
Build completed: sample-app.jar
java -jar sample-app.jar
Sample Kotlin Application
Version: 0.0.16
Source code: Main.kt | justfile
C#
Location: examples/csharp/
C# generates a Version.cs static class at build time:
using Version;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Sample C# Application");
Console.WriteLine($"Version: {VersionInfo.Version}");
}
}
version-file:
versionator output emit csharp --output Version.cs
build: version-file
dotnet build -c Release -o out
Run it:
$ cd examples/csharp && just run
Generating Version.cs using versionator emit...
Building C# application...
Build completed: out/SampleApp.dll
dotnet out/SampleApp.dll
Sample C# Application
Version: 0.0.16
Source code: Program.cs | justfile
Swift
Location: examples/swift/
Swift generates a Version.swift file with global constants:
print("Sample Swift Application")
print("Version: \(VERSION)")
version-file:
versionator output emit swift --output Version.swift
build: version-file
swiftc -o sample-app main.swift Version.swift
Run it:
$ cd examples/swift && just run
Generating Version.swift using versionator emit...
Building Swift application...
Build completed: sample-app
./sample-app
Sample Swift Application
Version: 0.0.16
Source code: main.swift | justfile
Python
Location: examples/python/
Python uses versionator output emit to generate a _version.py module:
"""Sample application entry point."""
from . import __version__
def main():
print("Sample Python Application")
print(f"Version: {__version__}")
if __name__ == "__main__":
main()
version-file:
versionator output emit python --output mypackage/_version.py
run: version-file
python -m mypackage.main
Run it:
$ cd examples/python && just run
Generating _version.py using versionator emit...
Version 0.0.13 written to mypackage/_version.py
python3 -m mypackage.main
Sample Python Application
Version: 0.0.13
Source code: mypackage/main.py | justfile
JavaScript
Location: examples/javascript/
JavaScript generates a version.js module:
import { VERSION } from './version.js';
function main() {
console.log('Sample JavaScript Application');
console.log(`Version: ${VERSION}`);
}
main();
version-file:
versionator output emit js --output src/version.js
run: version-file
node src/index.js
Run it:
$ cd examples/javascript && just run
Generating version.js using versionator emit...
Version 0.0.13 written to src/version.js
node src/index.js
Sample JavaScript Application
Version: 0.0.13
Source code: src/index.js | justfile
TypeScript
Location: examples/typescript/
TypeScript generates a typed version.ts module:
import { VERSION } from './version.js';
function main(): void {
console.log('Sample TypeScript Application');
console.log(`Version: ${VERSION}`);
}
main();
version-file:
versionator output emit ts --output src/version.ts
build: version-file
npx tsc
run: build
node dist/index.js
Run it:
$ cd examples/typescript && just run
Generating version.ts using versionator emit...
Version 0.0.13 written to src/version.ts
Building TypeScript package...
Build completed!
node dist/index.js
Sample TypeScript Application
Version: 0.0.13
Source code: src/index.ts | justfile
Ruby
Location: examples/ruby/
Ruby generates a version.rb module with a Versionator namespace:
require_relative "mypackage/version"
module Mypackage
def self.hello
puts "Sample Ruby Application"
puts "Version: #{Versionator::VERSION}"
end
end
version-file:
versionator output emit ruby --output lib/mypackage/version.rb
run: version-file
ruby -I lib -e "require 'mypackage'; Mypackage.hello"
Run it:
$ cd examples/ruby && just run
Generating version.rb using versionator emit...
Version 0.0.13 written to lib/mypackage/version.rb
ruby -I lib -e "require 'mypackage'; Mypackage.hello"
Sample Ruby Application
Version: 0.0.13
Source code: lib/mypackage.rb | justfile
PHP
PHP generates a version class:
versionator output emit php --output src/Version.php
Generated file:
<?php
namespace MyApp;
class Version {
public const VERSION = "1.2.3";
public const MAJOR = 1;
public const MINOR = 2;
public const PATCH = 3;
}
JSON / YAML
For configuration files or API responses:
# JSON
versionator output emit json --output version.json
# YAML
versionator output emit yaml --output version.yml
JSON output:
{
"version": "1.2.3",
"major": 1,
"minor": 2,
"patch": 3
}
Docker / Containers
Location: examples/docker/
Container images embed version info in two places:
- The binary inside (using the language-specific approach above)
- OCI image labels (for image inspection without running)
# Build arguments
ARG VERSION=dev
ARG GIT_COMMIT=unknown
ARG BUILD_DATE=unknown
FROM golang:1.21-alpine AS builder
ARG VERSION
ARG GIT_COMMIT
ARG BUILD_DATE
WORKDIR /app
COPY . .
# Inject version at compile time
RUN go build -ldflags "\
-X main.Version=${VERSION} \
-X main.GitCommit=${GIT_COMMIT} \
-X main.BuildDate=${BUILD_DATE}" \
-o /app/sample-app
FROM alpine:3.19
ARG VERSION
ARG GIT_COMMIT
ARG BUILD_DATE
# OCI Image Labels
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.revision="${GIT_COMMIT}"
LABEL org.opencontainers.image.created="${BUILD_DATE}"
COPY --from=builder /app/sample-app /usr/local/bin/sample-app
ENTRYPOINT ["sample-app"]
docker-build:
VERSION=$$(versionator version); \
COMMIT=$$(versionator output version -t "{{ShortHash}}"); \
DATE=$$(versionator output version -t "{{BuildDateTimeUTC}}"); \
docker build \
--build-arg VERSION=$$VERSION \
--build-arg GIT_COMMIT=$$COMMIT \
--build-arg BUILD_DATE=$$DATE \
-t sample-app:$$VERSION .
Run it:
$ cd examples/docker && just show-version
Version from versionator:
VERSION=0.0.13
GIT_COMMIT=ba4ecb3
BUILD_DATE=2026-03-08T18:52:29Z
$ just docker-build
Building Docker image with:
VERSION=0.0.13
GIT_COMMIT=ba4ecb3
BUILD_DATE=2026-03-08T18:52:29Z
...
$ just docker-run
Running sample-app:0.0.13
Sample Docker Application
Version: 0.0.13 (commit: ba4ecb3, built: 2026-03-08T18:52:29Z)
Source code: main.go | Dockerfile | justfile
Running All Demos
From the repository root:
# Build versionator first
just build
# Run all examples
for dir in examples/*/; do
echo "=== $dir ==="
(cd "$dir" && just run 2>/dev/null || echo "skipped")
done
The Pattern
Every example follows the same pattern:
- Makefile calls
versionator output versionto get the current version - Build step injects that version (compile-time for compiled languages, file generation for interpreted)
- Application displays the embedded version at runtime
The version is baked in. It doesn't read from a file at runtime. It doesn't query an API. It's part of the binary itself.
Template Variables
Use these versionator template variables for richer version info:
| Variable | Use Case | Example |
|---|---|---|
{{MajorMinorPatch}} | Clean version | 1.2.3 |
{{Prefix}}{{MajorMinorPatch}} | Prefixed version | v1.2.3 |
{{ShortHash}} | Git commit (7 chars) | abc1234 |
{{BuildDateTimeUTC}} | ISO 8601 timestamp | 2024-01-15T10:30:00Z |
{{BranchName}} | Current branch | main |
See Template Variables for the complete reference.
Custom Templates
The examples above use built-in templates via versionator output emit <lang>. For custom namespaces, additional fields, or different file structures, use custom templates with --template-file.
Dump and Customize
# Dump Python template
versionator output emit dump python > custom_python.tmpl
# Edit custom_python.tmpl...
# Use custom template
versionator output emit --template-file custom_python.tmpl --output _version.py
Custom Template Examples
Each interpreted language has a -custom example demonstrating the --template-file workflow:
| Language | Built-in Template | Custom Template |
|---|---|---|
| Python | examples/python/ | examples/python-custom/ |
| JavaScript | examples/javascript/ | examples/javascript-custom/ |
| TypeScript | examples/typescript/ | examples/typescript-custom/ |
| Ruby | examples/ruby/ | examples/ruby-custom/ |
Built-in examples use versionator output emit <lang> — simple, zero configuration.
Custom examples use versionator output emit --template-file — for custom namespaces (e.g., Mypackage::VERSION instead of Versionator::VERSION) or additional fields like GIT_HASH and BUILD_DATE.
Best Practices
- Add generated files to .gitignore: Don't commit version files
- Generate at build time: Run emit in build scripts, not manually
- Use appropriate approach: Inject for compiled, generate for interpreted
- Include in CI: Ensure version files are generated in CI/CD
See Also
- CI/CD Integration - Automate version injection in pipelines
- Makefiles and Just - Build tool integration
- Template Variables - All available template variables