-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.rs
More file actions
338 lines (303 loc) · 14 KB
/
Copy pathbuild.rs
File metadata and controls
338 lines (303 loc) · 14 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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//! Build script: embeds git version information at compile time.
//!
//! Produces `$OUT_DIR/build_info.rs` with constants that `src/build_info.rs`
//! includes. Every git operation is independently fallible — no `.git`
//! directory (crates.io tarball), shallow clones (no tags), and sparse
//! checkouts are all handled gracefully by falling back to `None`.
//!
//! Scenarios:
//! cargo build — .git directory at repo root, full info
//! cargo install --git=... — .git *file* in checkout (gitdir: pointer),
//! git -C follows it transparently
//! cargo install gloam — crates.io tarball, no .git at all,
//! all git values are None
//! shallow clone / CI — .git exists but tags may be missing,
//! GIT_DESCRIBE falls back to None
use std::io::Write;
use std::{env, fs, io, path::Path, path::PathBuf, process::Command};
use flate2::Compression;
use flate2::write::DeflateEncoder;
/// Run a git command in `repo_dir`. Using `-C` lets git walk up to find
/// `.git` naturally and handles gitdir files (worktrees, cargo git checkouts).
fn git(repo_dir: &Path, args: &[&str]) -> Option<String> {
Command::new("git")
.arg("-C")
.arg(repo_dir)
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
/// Check whether a git command exits successfully (ignoring output).
fn git_ok(repo_dir: &Path, args: &[&str]) -> Option<bool> {
Command::new("git")
.arg("-C")
.arg(repo_dir)
.args(args)
.output()
.ok()
.map(|o| o.status.success())
}
/// Find the git repo root for the crate being built.
///
/// Uses `git rev-parse --show-toplevel` from CARGO_MANIFEST_DIR, which
/// lets git itself determine whether we're inside a repo.
///
/// Returns None if:
/// - git isn't installed
/// - we're not in a repo (crates.io tarball)
/// - the detected repo root is an unrelated repo (e.g. the user's home
/// directory under dotfiles management, which contains the cargo
/// registry extraction path)
///
/// The "is this our repo?" check verifies that the repo root contains a
/// Cargo.toml with our package name. This is more robust than a simple
/// `starts_with` ancestor check, which passes when `cargo install` extracts
/// into `~/.cargo/registry/src/` inside a home-directory git repo.
fn find_repo_root(manifest_dir: &Path) -> Option<PathBuf> {
let toplevel = git(manifest_dir, &["rev-parse", "--show-toplevel"])?;
let root = PathBuf::from(&toplevel);
// The repo root must contain a Cargo.toml that declares our package.
// This rejects unrelated repos that happen to be ancestors of the
// extraction directory (e.g. dotfiles repos tracking ~/).
let cargo_toml = root.join("Cargo.toml");
let pkg_name = env::var("CARGO_PKG_NAME").unwrap_or_default();
match fs::read_to_string(&cargo_toml) {
Ok(contents) => {
// Verify this Cargo.toml declares our package. Whitespace
// around `=` varies (some authors align values), so we check
// that a line contains `name`, `=`, and `"<pkg_name>"` in
// that order rather than matching a single exact string.
let quoted_name = format!("\"{}\"", pkg_name);
let is_ours = contents.lines().any(|line| {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("name") {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
return rest.trim().starts_with("ed_name);
}
}
false
});
if !is_ours {
return None;
}
}
Err(_) => return None,
}
Some(root)
}
/// Write `contents` to `path`, but only if the file doesn't already exist
/// with identical content. Avoids triggering unnecessary rebuilds.
fn write_if_changed(path: &Path, contents: &[u8]) -> io::Result<()> {
if let Ok(existing) = fs::read(path)
&& existing == contents
{
return Ok(());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, contents)
}
fn opt_str(v: &Option<String>) -> String {
match v {
Some(s) => format!("Some({:?})", s),
None => "None".to_string(),
}
}
/// Recursively collect every file under `dir` into `out`.
fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> io::Result<()> {
for entry in fs::read_dir(dir)? {
let path = entry?.path();
if path.is_dir() {
collect_files(&path, out)?;
} else {
out.push(path);
}
}
Ok(())
}
/// Compress `data` with raw DEFLATE at maximum effort. This runs once per
/// build, so ratio matters and speed does not — level 9 is free here.
fn deflate(data: &[u8]) -> Vec<u8> {
let mut enc = DeflateEncoder::new(Vec::new(), Compression::new(9));
enc.write_all(data).expect("deflate write");
enc.finish().expect("deflate finish")
}
/// Embed every file under `bundled/` as a raw-DEFLATE blob in OUT_DIR,
/// mirroring the source layout with a `.deflate` suffix. `src/bundled.rs`
/// inflates these at runtime, trading ~6 MiB of uncompressed XML in the binary
/// for ~0.65 MiB of blobs plus a one-time inflate of only the files used.
fn emit_bundled_blobs(manifest_dir: &Path) {
let bundled_dir = manifest_dir.join("bundled");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bundled");
// A file added to or removed from bundled/ changes the directory's mtime.
println!("cargo:rerun-if-changed={}", bundled_dir.display());
let mut files = Vec::new();
collect_files(&bundled_dir, &mut files).expect("reading bundled/");
for src in files {
println!("cargo:rerun-if-changed={}", src.display());
let rel = src.strip_prefix(&bundled_dir).unwrap();
let data = fs::read(&src).expect("reading bundled file");
let mut dest = out_dir.join(rel);
let name = format!("{}.deflate", dest.file_name().unwrap().to_string_lossy());
dest.set_file_name(name);
write_if_changed(&dest, &deflate(&data)).expect("writing bundled blob");
}
}
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let repo_root = find_repo_root(&manifest_dir);
// Compress the bundled specs/headers into OUT_DIR for src/bundled.rs.
emit_bundled_blobs(&manifest_dir);
// Tell cargo when to re-run. We must resolve the *real* git directory —
// when .git is a file (worktrees, `cargo install --git`), the actual HEAD,
// refs, and packed-refs live inside the gitdir target, not next to the .git
// file. Cargo treats non-existent rerun-if-changed paths as "always
// changed", so we must only emit paths that actually exist.
//
// IMPORTANT: do NOT watch the `.git` directory itself — cargo watches
// directories recursively, and git constantly touches files inside `.git/`
// (index, lock files, FETCH_HEAD, etc.) that have nothing to do with the
// commit or tag state. Only watch the specific files that change when
// the version-relevant state changes.
//
// If no repo is found at all, emit just `build.rs` to prevent cargo's
// default "rerun on any file change" behaviour.
if let Some(ref root) = repo_root {
// `git rev-parse --git-dir` follows .git files and returns the real
// directory (e.g. ~/.cargo/git/db/gloam-xxxx/...). The result may be
// relative to the repo root, so resolve it.
let git_dir = git(root, &["rev-parse", "--git-dir"])
.map(|p| {
let path = PathBuf::from(p);
if path.is_absolute() {
path
} else {
root.join(path)
}
})
.unwrap_or_else(|| root.join(".git"));
// HEAD changes on commit, checkout, rebase, etc.
// packed-refs changes when refs are packed.
// refs/heads and refs/tags change on commit/tag operations.
for name in &["HEAD", "packed-refs", "refs/heads", "refs/tags"] {
let path = git_dir.join(name);
if path.exists() {
println!("cargo:rerun-if-changed={}", path.display());
}
}
// Also watch .git itself if it's a *file* (gitdir pointer) — if the
// pointer changes, we need to re-resolve. But NOT if it's a
// directory, because that triggers recursive watching.
let git_entry = root.join(".git");
if git_entry.is_file() {
println!("cargo:rerun-if-changed={}", git_entry.display());
}
} else {
// No git — just watch build.rs so we don't re-run on every build.
println!("cargo:rerun-if-changed=build.rs");
}
// Each git query is independent — any can fail without affecting others.
let (git_describe, git_sha, git_sha_short, git_branch, git_dirty, git_commit_year) =
match repo_root {
Some(ref root) => (
// --tags: match lightweight tags too (not just annotated).
// --dirty: append "-dirty" if the worktree has uncommitted changes.
// Deliberately no --always: if no tag is reachable (shallow
// clone, tags not fetched), we want this to fail so we fall
// through to the PKG_VERSION+sha format instead of emitting
// a bare SHA as the version string.
git(root, &["describe", "--tags", "--dirty"]),
git(root, &["rev-parse", "HEAD"]),
git(root, &["rev-parse", "--short", "HEAD"]),
git(root, &["symbolic-ref", "--short", "-q", "HEAD"]),
// diff-index exits 0 if clean, 1 if dirty. Covers tracked files;
// for untracked files we'd need status --porcelain, but untracked
// files aren't a meaningful "dirty" signal for version stamping.
git_ok(root, &["diff-index", "--quiet", "HEAD", "--"]).map(|clean| !clean),
// Year of HEAD commit — used for copyright years in generated files.
// %as gives the author date in YYYY-MM-DD format.
git(root, &["show", "-s", "--format=%as", "HEAD"])
.and_then(|s| s.split('-').next().map(str::to_string)),
),
None => (None, None, None, None, None, None),
};
// Build year: prefer git commit year (accurate for releases), fall back to
// the current calendar year (correct for crates.io installs where no git
// info is available).
let build_year: u16 = git_commit_year
.as_deref()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| {
// std::time is available without extra deps. SystemTime → days → year.
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
// Approximate: 365.25 days/year, close enough for a copyright year.
(1970 + secs / 31_557_600) as u16
});
// Assemble a human-readable version string:
// With git describe: "v0.3.1-7-gabcdef1" or "v0.3.1-7-gabcdef1-dirty"
// SHA only (no tags): "0.3.1+abcdef1" or "0.3.1+abcdef1.dirty"
// No git at all: "0.3.1"
let pkg_version = env::var("CARGO_PKG_VERSION").unwrap();
let version_string = if let Some(ref desc) = git_describe {
desc.clone()
} else if let Some(ref sha) = git_sha_short {
let dirty_suffix = if git_dirty == Some(true) {
".dirty"
} else {
""
};
format!("{pkg_version}+{sha}{dirty_suffix}")
} else {
pkg_version.clone()
};
// Expose the target triple to integration tests via env!("TARGET").
// The cc crate needs this to find the correct compiler.
println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let dest = out_dir.join("build_info.rs");
// Pre-format values as Rust literals to avoid nested format!() calls.
let version_string_lit = format!("{:?}", version_string);
let pkg_version_lit = format!("{:?}", pkg_version);
let git_describe_lit = opt_str(&git_describe);
let git_sha_lit = opt_str(&git_sha);
let git_sha_short_lit = opt_str(&git_sha_short);
let git_branch_lit = opt_str(&git_branch);
let git_dirty_lit = match git_dirty {
Some(b) => format!("Some({b})"),
None => "None".to_string(),
};
let rs = format!(
r#"// @generated by build.rs — do not edit.
/// Composite version string for display. Prefers `git describe` output
/// when available, falls back to `CARGO_PKG_VERSION` + short SHA, or just
/// `CARGO_PKG_VERSION` if no git info is available at all.
pub const VERSION: &str = {version_string_lit};
/// Cargo package version from Cargo.toml (always present).
pub const PKG_VERSION: &str = {pkg_version_lit};
/// Full output of `git describe --tags --dirty --always`, if available.
/// Examples: "v0.3.1", "v0.3.1-7-gabcdef1", "v0.3.1-dirty", "abcdef1".
pub const GIT_DESCRIBE: Option<&str> = {git_describe_lit};
/// Full commit SHA, if available.
pub const GIT_SHA: Option<&str> = {git_sha_lit};
/// Abbreviated commit SHA, if available.
pub const GIT_SHA_SHORT: Option<&str> = {git_sha_short_lit};
/// Current branch name, if on a branch (None for detached HEAD).
pub const GIT_BRANCH: Option<&str> = {git_branch_lit};
/// Whether the working tree had uncommitted changes at build time.
pub const GIT_DIRTY: Option<bool> = {git_dirty_lit};
/// Year for copyright notices. From the git commit date when available,
/// otherwise the calendar year at build time.
pub const BUILD_YEAR: u16 = {build_year};
"#,
);
write_if_changed(&dest, rs.as_bytes()).expect("failed to write build_info.rs");
}