quick_search/crates/quicksearch-gui/build.rs
= cf40196546
Some checks failed
CI / linux (push) Successful in 2m49s
CI / windows-cross (push) Failing after 1m20s
CI / release (push) Has been skipped
Added index-preserving pruning. Added windows installer to CI, added automated GUI screenshot generation for documentation.
2026-08-04 23:33:28 -04:00

247 lines
8.8 KiB
Rust

//! Build identity for the two binaries.
//!
//! Bakes the commit the tree was built from into `QS_COMMIT`, and on Windows
//! compiles a VERSIONINFO resource so the `.exe` reports a version in
//! Explorer's Properties rather than nothing at all.
//!
//! The version number itself is not handled here: `env!("CARGO_PKG_VERSION")`
//! already carries `[workspace.package] version`, which is the one source of
//! truth the CI tag check, build-deb.sh and build-installer.sh all read.
//!
//! No dependencies on purpose — a build script that pulled a crate in would
//! land in Cargo.lock, and every build in this repo runs `--locked`.
use std::path::PathBuf;
use std::process::Command;
/// What the version reads as when there is no git and no `QS_COMMIT` — an
/// unpacked source tarball, say. Never a build failure.
const UNKNOWN: &str = "unknown";
/// Abbreviated-hash length, matching `git rev-parse --short=7` and the hashes
/// the forge shows.
const SHORT_LEN: usize = 7;
fn main() {
let commit = resolve_commit();
println!("cargo::rustc-env=QS_COMMIT={commit}");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
emit_version_resource(&commit);
}
}
// ------------------------------------------------------------- commit ----
/// The short commit hash: `QS_COMMIT` first, then git, then [`UNKNOWN`].
///
/// CI sets `QS_COMMIT` from the event's SHA rather than letting this shell out
/// to git, because `actions/checkout` leaves a shallow clone owned by another
/// user — the SHA the runner already knows is both cheaper and more trustworthy
/// than anything read back out of that tree.
fn resolve_commit() -> String {
println!("cargo::rerun-if-env-changed=QS_COMMIT");
watch_git_head();
let supplied = std::env::var("QS_COMMIT").unwrap_or_default();
let supplied = supplied.trim();
if !supplied.is_empty() {
if let Some(hash) = short_hash(supplied) {
return hash;
}
println!(
"cargo::warning=QS_COMMIT is not a commit hash ({supplied:?}); \
falling back to git"
);
}
git_commit().unwrap_or_else(|| UNKNOWN.to_string())
}
/// The first [`SHORT_LEN`] characters, lowercased, or `None` when `raw` is not
/// a hex hash. Accepts a full 40-character SHA (what CI passes) and an already
/// abbreviated one alike.
fn short_hash(raw: &str) -> Option<String> {
if raw.is_empty() || !raw.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
// `raw` is ASCII here, so slicing by byte cannot split a character.
Some(raw[..raw.len().min(SHORT_LEN)].to_ascii_lowercase())
}
fn git_commit() -> Option<String> {
let out = git(&["rev-parse", &format!("--short={SHORT_LEN}"), "HEAD"])?;
short_hash(out.trim())
}
/// Rebuild when HEAD moves.
///
/// Without this cargo only reruns the script when a file in the package
/// changes, so committing anything outside this crate would leave the previous
/// hash baked into the binary.
fn watch_git_head() {
let Some(git_dir) = git_dir() else { return };
let head = git_dir.join("HEAD");
watch(&head);
// On a branch, HEAD itself only changes on checkout — the ref it names is
// what moves on commit. A detached HEAD holds the hash directly and needs
// nothing more. The reflog covers the case where the branch ref is packed
// and so has no loose file to watch.
if let Ok(contents) = std::fs::read_to_string(&head) {
if let Some(reference) = contents.trim().strip_prefix("ref:") {
watch(&git_dir.join(reference.trim()));
}
}
watch(&git_dir.join("logs").join("HEAD"));
}
/// Watch `path`, but only if it exists: cargo treats a `rerun-if-changed` path
/// it cannot stat as permanently dirty, which would recompile this crate on
/// every single build.
fn watch(path: &std::path::Path) {
if path.exists() {
println!("cargo::rerun-if-changed={}", path.display());
}
}
fn git_dir() -> Option<PathBuf> {
let path = PathBuf::from(git(&["rev-parse", "--absolute-git-dir"])?.trim());
path.is_dir().then_some(path)
}
/// Run git in the crate directory, `None` on any failure — a missing git, a
/// tree that is not a repository, and a repository git refuses to trust all
/// mean the same thing here.
fn git(args: &[&str]) -> Option<String> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
let out = Command::new("git")
.arg("-C")
.arg(manifest_dir)
.args(args)
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8(out.stdout).ok())
.flatten()
}
// ------------------------------------------------ windows VERSIONINFO ----
/// The binaries this crate builds, with the description Explorer shows in the
/// Properties dialog and in the details pane.
const BINARIES: [(&str, &str); 2] = [
("quicksearch", "QuickSearch"),
("quicksearch-cli", "QuickSearch terminal search"),
];
/// Compile a VERSIONINFO resource per binary and link it in.
///
/// The string values mirror `packaging/quicksearch.nsi` so the app and the
/// installer that ships it never disagree about who published what.
fn emit_version_resource(commit: &str) {
// rust-toolchain.toml lists x86_64-pc-windows-gnu and nothing else, and
// windres is what compiles a .rc there. An MSVC target would need rc.exe
// and a different invocation, so it is skipped rather than half-supported.
if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("gnu") {
return;
}
let out_dir = PathBuf::from(env("OUT_DIR"));
let version = env("CARGO_PKG_VERSION");
// Windows insists on exactly four numeric fields. Cargo splits the version
// for us, so unlike build-installer.sh there is no suffix to strip.
let quad = format!(
"{},{},{},0",
env("CARGO_PKG_VERSION_MAJOR"),
env("CARGO_PKG_VERSION_MINOR"),
env("CARGO_PKG_VERSION_PATCH"),
);
let quad_text = quad.replace(',', ".");
for (bin, description) in BINARIES {
let rc = format!(
r#"1 VERSIONINFO
FILEVERSION {quad}
PRODUCTVERSION {quad}
FILEOS 0x4L
FILETYPE 0x1L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "Jeremy <jeremy@karsttech.com>"
VALUE "FileDescription", "{description}"
VALUE "FileVersion", "{quad_text}"
VALUE "InternalName", "{bin}"
VALUE "LegalCopyright", "GPL-3.0-or-later"
VALUE "OriginalFilename", "{bin}.exe"
VALUE "ProductName", "QuickSearch"
VALUE "ProductVersion", "{version} ({commit})"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
"#
);
let rc_path = out_dir.join(format!("{bin}.rc"));
let res_path = out_dir.join(format!("{bin}.res"));
std::fs::write(&rc_path, rc).expect("OUT_DIR is writable");
compile_resource(&rc_path, &res_path);
// Per-binary, because OriginalFilename differs between the two. Linked
// as a plain object rather than through a static library: nothing
// references a resource by symbol, so an archive member holding one
// would be dropped as unused.
println!("cargo::rustc-link-arg-bin={bin}={}", res_path.display());
}
}
fn compile_resource(rc: &std::path::Path, res: &std::path::Path) {
println!("cargo::rerun-if-env-changed=QS_WINDRES");
let explicit = std::env::var("QS_WINDRES").ok();
let candidates: Vec<&str> = match &explicit {
Some(tool) => vec![tool.as_str()],
// The cross compiler's windres first, then the plain name for a native
// mingw shell where the tools are unprefixed.
None => vec!["x86_64-w64-mingw32-windres", "windres"],
};
let mut attempts = Vec::new();
for tool in &candidates {
match Command::new(tool)
.arg("-O")
.arg("coff")
.arg(rc)
.arg("-o")
.arg(res)
.output()
{
Ok(out) if out.status.success() => return,
Ok(out) => attempts.push(format!(
"{tool}: exited {}{}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
)),
Err(e) => attempts.push(format!("{tool}: {e}")),
}
}
// A hard error, not a warning. A Windows build already needs mingw for the
// linker, so this only fires on a genuinely broken toolchain — and silently
// shipping an .exe with no version is exactly what this exists to prevent.
panic!(
"could not compile the Windows version resource. Install \
binutils-mingw-w64-x86-64 (or set QS_WINDRES to a resource compiler). \
Tried:\n {}",
attempts.join("\n ")
);
}
fn env(key: &str) -> String {
std::env::var(key).unwrap_or_else(|_| panic!("cargo sets {key} for build scripts"))
}