-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.rs
More file actions
161 lines (146 loc) · 5.7 KB
/
build.rs
File metadata and controls
161 lines (146 loc) · 5.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! Build script for autoprat - generates version information.
//!
//! ## Version Generation Algorithm
//!
//! This build script generates version strings using the following algorithm:
//!
//! 1. Try `git describe --tags --always --dirty`
//! - If successful and contains 'v' or '-g': use the result directly
//! - Example: `v1.0.0` (exact tag)
//! - Example: `v1.0.0-5-g1a2b3c4d-dirty` (5 commits after v1.0.0 tag)
//!
//! 2. If no tags exist (git describe returns just a commit hash):
//! - Generate pseudo-version: `v{CARGO_PKG_VERSION}-{timestamp}-{commit}[+dirty]`
//! - Where:
//! - `CARGO_PKG_VERSION`: version from Cargo.toml (e.g., "0.1.0")
//! - `timestamp`: commit timestamp for clean builds, build timestamp for dirty builds
//! - `commit`: 12-character commit SHA
//! - `+dirty`: suffix if working directory has uncommitted changes
//! - Example: `v0.1.0-20250610203045-2adb30a27442+dirty`
//!
//! 3. Smart timestamp selection:
//! - Clean builds: Use commit timestamp (deterministic, same commit = same version)
//! - Dirty builds: Use build timestamp (shows when you compiled your changes)
//!
//! 4. Fallback (if git commands fail):
//! - Generate pseudo-version using current build timestamp
//!
//! This provides meaningful version information for development workflows while
//! respecting semantic versioning when tags are present.
use std::{env, process::Command};
use chrono::Utc;
fn main() {
["src", "build.rs", "Cargo.toml", "Cargo.lock"]
.iter()
.for_each(|path| println!("cargo:rerun-if-changed={path}"));
let build_info = generate_human_readable_version();
println!("cargo:rustc-env=BUILD_INFO_HUMAN={build_info}");
}
/// Executes a git command and returns the trimmed stdout as a String.
fn git_command(args: &[&str]) -> Option<String> {
Command::new("git")
.args(args)
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
/// Gets the Rust toolchain version
fn get_rustc_version() -> Option<String> {
Command::new("rustc")
.arg("--version")
.output()
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|s| s.trim().to_string())
}
/// Checks if the working directory has uncommitted changes.
/// Returns None if git is not available or not in a git repository.
///
/// Filters out .cargo-ok which is created by `cargo install --git` in the
/// source checkout directory. This file is not part of the actual source
/// and should not trigger the "dirty" flag during installation, whilst
/// preserving detection of real uncommitted changes during development.
fn is_git_dirty() -> Option<bool> {
git_command(&["status", "--porcelain"]).map(|output| {
output.lines().any(|line| {
let path = &line[3..]; // Skip the status prefix
// Ignore .cargo-ok file created by cargo install
path != ".cargo-ok"
})
})
}
/// Returns a human-readable Git version string for embedding in build
/// metadata.
///
/// This function attempts to describe the current Git commit using:
/// ```sh
/// git describe --tags --always --dirty
/// ```
///
/// If no tags exist, it generates a pseudo-version using the
/// Cargo.toml version:
/// v{CARGO_PKG_VERSION}-<timestamp>-<commit>+dirty.
fn get_git_version() -> Option<String> {
git_command(&["describe", "--tags", "--always", "--dirty"])
.map(|desc| {
// If git describe returned just a hash (no tags),
// generate pseudo-version.
if !desc.contains('v') && !desc.contains("-g") {
generate_pseudo_version()
} else {
desc
}
})
.or_else(|| Some(generate_pseudo_version()))
}
/// Generates a pseudo-version using Cargo.toml version:
/// v{version}-<timestamp>-<commit>+dirty.
fn generate_pseudo_version() -> String {
let commit_hash =
git_command(&["rev-parse", "--short=12", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
let is_dirty = is_git_dirty();
// Use commit timestamp for clean builds, build timestamp for
// dirty builds, or build timestamp if git is unavailable.
let timestamp = match is_dirty {
Some(true) => {
// For dirty builds, show when the binary was built (more
// relevant).
Utc::now().format("%Y%m%d%H%M%S").to_string()
}
Some(false) => {
// For clean builds, show when the commit was made
// (deterministic).
git_command(&["log", "-1", "--format=%ct"])
.and_then(|s| s.parse::<i64>().ok())
.and_then(|timestamp| chrono::DateTime::from_timestamp(timestamp, 0))
.map(|dt| dt.format("%Y%m%d%H%M%S").to_string())
.unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S").to_string())
}
None => {
// No git available, use build timestamp.
Utc::now().format("%Y%m%d%H%M%S").to_string()
}
};
let dirty_suffix = match is_dirty {
Some(true) => "+dirty",
Some(false) => "",
None => "", // No git available, don't mark as dirty
};
let version = env!("CARGO_PKG_VERSION");
format!("v{version}-{timestamp}-{commit_hash}{dirty_suffix}")
}
/// Generates human-readable version info.
fn generate_human_readable_version() -> String {
let components = [
Some(env!("CARGO_PKG_VERSION").to_string()),
get_git_version().map(|v| format!("({v})")),
get_rustc_version(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
components.join(" ")
}