Repository navigation
Expand file tree
/
Copy pathbuild.rs
More file actions
200 lines (184 loc) · 8.47 KB
/
Copy pathbuild.rs
File metadata and controls
200 lines (184 loc) · 8.47 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
//! Makes `rust-embed` see a changed asset tree, records the commit being
//! built, and installs the git hooks.
//!
//! The derive expands to one `include_bytes!` per file, and rustc records
//! those in dep-info, so *editing* an embedded asset already rebuilds. Files
//! that appear or disappear do not: they were never in the previous expansion,
//! so nothing names them and Cargo considers the crate fresh.
//!
//! The failure that costs a release: build once, run `scripts/fetch-vendor.sh`
//! for the first time, build again. Nothing recompiles, and the binary ships
//! without the 37 MB of vendored runtime. It boots, serves the page, and then
//! fails in the browser with a Python runtime that will not start.
//!
//! Cargo walks a directory given to `rerun-if-changed` in full, so the one
//! line covers the tree.
use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=web");
set_build_commit();
install_git_hooks();
}
/// The released binary must identify the source it was built from even when
/// the rolling `latest` tag has since moved. CI supplies its checkout SHA;
/// local builds use HEAD, and source archives without either say `unknown`.
/// Both are seven characters wide, because `--short` alone follows
/// `core.abbrev`, and a setting below seven would fail `hex_sha`.
fn set_build_commit() {
println!("cargo:rerun-if-env-changed=GITHUB_SHA");
let root = std::env::var_os("CARGO_MANIFEST_DIR")
.map(std::path::PathBuf::from)
.filter(|root| root.join(".git").exists());
if let Some(root) = root.as_deref() {
watch_git_head(root);
}
let commit = std::env::var("GITHUB_SHA")
.ok()
.and_then(|sha| hex_sha(&sha).map(|sha| sha[..7].to_ascii_lowercase()))
.or_else(|| {
root.as_deref()
.and_then(|root| git_output(root, &["rev-parse", "--short=7", "HEAD"]))
.and_then(|sha| hex_sha(&sha).map(str::to_ascii_lowercase))
})
.unwrap_or_else(|| "unknown".to_string());
println!("cargo:rustc-env=CODETRIAL_BUILD_COMMIT={commit}");
}
fn hex_sha(sha: &str) -> Option<&str> {
let sha = sha.trim();
(sha.len() >= 7 && sha.bytes().all(|byte| byte.is_ascii_hexdigit())).then_some(sha)
}
fn git_output(root: &std::path::Path, args: &[&str]) -> Option<String> {
let output = Command::new("git")
.args(args)
.current_dir(root)
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout).ok()
}
/// HEAD holds the commit in a detached checkout and names a ref on a branch.
/// Watch that ref too, so the next local commit rebuilds the identifier even
/// when none of the source files changed.
fn watch_git_head(root: &std::path::Path) {
let Some(head) = git_path(root, "HEAD") else {
return;
};
println!("cargo:rerun-if-changed={}", head.display());
if let Some(reference) = git_output(root, &["symbolic-ref", "-q", "HEAD"])
&& let Some(path) = git_path(root, reference.trim())
{
if path.is_file() {
println!("cargo:rerun-if-changed={}", path.display());
} else {
// A packed or unborn branch has no loose ref yet. A commit creates
// one without changing packed-refs, so watch the nearest existing
// parent as well as packed-refs.
if let Some(parent) = path.ancestors().skip(1).find(|parent| parent.is_dir()) {
println!("cargo:rerun-if-changed={}", parent.display());
}
if let Some(packed) = git_path(root, "packed-refs")
&& packed.is_file()
{
println!("cargo:rerun-if-changed={}", packed.display());
}
}
}
}
/// Joined onto `root` rather than asked for as absolute. `--path-format` needs
/// git 2.31, and an older one, such as the git in the Bullseye image the Linux
/// release builds in, echoes the unknown flag back as output and still exits 0.
/// The answer is relative to the working directory, which is `root`, or
/// already absolute, which `join` keeps as it is.
fn git_path(root: &std::path::Path, name: &str) -> Option<std::path::PathBuf> {
git_output(root, &["rev-parse", "--git-path", name]).map(|path| root.join(path.trim()))
}
/// Links `scripts/git-*.sh` into the hooks directory of a checkout.
///
/// A hook nobody installed is a hook nobody runs, and the first person to find
/// that out is whoever reads a commit message the rules would have caught. A
/// build is the one step every contributor takes, so it is where installation
/// belongs; `make hooks` stays for anyone who wants it on its own.
///
/// Never fails the build. A checkout can be read-only, a hooks directory can
/// belong to somebody else's tooling, and a Windows build has no `sh`. None of
/// that is a reason to refuse to compile, so a failure is a warning and the
/// build goes on.
fn install_git_hooks() {
let root = std::env::var_os("CARGO_MANIFEST_DIR").map(std::path::PathBuf::from);
let Some(root) = root else { return };
// A worktree has a `.git` file rather than a directory, and both mean the
// same thing here: this is a checkout, so it has hooks worth installing.
if !root.join(".git").exists() {
return;
}
// Watched so that adding a hook script, or deleting an installed hook,
// reruns this. Without the second path Cargo would keep the build script
// fresh and a removed hook would stay removed until something else forced a
// rerun.
//
// The price, stated because it will otherwise look like a bug. A build
// script rerun makes the crate dirty, and Cargo follows the installed
// symlinks, so editing a hook script counts as changing this directory:
// measured at 10.3s against 0.68s for a no-op build. The first build after
// a fresh clone also pays one extra rebuild, because installing the hooks
// is itself a change to the directory being watched. Both are the price of
// a hook that comes back after somebody deletes it; `make hooks` is the
// same install without the watch.
println!("cargo:rerun-if-changed=scripts/install-git-hooks.sh");
// Asked of git rather than assumed to be `.git/hooks`: a linked worktree
// keeps its hooks with the common directory, and core.hooksPath moves them
// anywhere. Watching the wrong directory is a watch that never fires.
//
// Not the whole of scripts/, though adding a hook there is the one change
// this does not notice. That directory holds every gate script in the
// repository and each edit to one would rebuild the binary; `make hooks`
// covers the rare case at no standing cost.
if let Some(hooks) = git_path(&root, "hooks")
&& hooks.is_dir()
{
println!("cargo:rerun-if-changed={}", hooks.display());
}
let installer = root.join("scripts/install-git-hooks.sh");
if !installer.is_file() {
return;
}
// Git for Windows carries a shell, but nothing guarantees it is on PATH,
// and a Windows symlink needs a privilege the build does not have. Silent
// rather than a warning: the release job builds a Windows target on every
// push, and a warning nobody can act on teaches people to skim them.
if cfg!(windows) {
return;
}
match Command::new("sh")
.arg(&installer)
.current_dir(&root)
.output()
{
Ok(output) if output.status.success() => {
// Cargo swallows a build script's stdout, which is right for the
// four "HOOK installed" lines and wrong for the two the installer
// prints when it needs a person: a hook it refused to overwrite,
// and a core.hooksPath that sends hooks somewhere else. Those two
// reach the terminal or nothing does.
for line in String::from_utf8_lossy(&output.stdout).lines() {
let line = line.trim();
if line.starts_with("KEEP") || line.starts_with("NOTE") {
println!("cargo::warning=git hooks: {line}");
}
}
}
Ok(output) => warn_hooks(&String::from_utf8_lossy(&output.stderr)),
Err(error) => warn_hooks(&error.to_string()),
}
}
fn warn_hooks(reason: &str) {
let reason = reason.trim().replace('\n', "; ");
let reason = if reason.is_empty() {
"scripts/install-git-hooks.sh failed".to_string()
} else {
reason
};
println!("cargo::warning=could not install the git hooks ({reason}); run make hooks");
}