-
Notifications
You must be signed in to change notification settings - Fork 171
Expand file tree
/
Copy pathbuild.rs
More file actions
244 lines (228 loc) · 11.7 KB
/
Copy pathbuild.rs
File metadata and controls
244 lines (228 loc) · 11.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
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
//! build.rs — injects git build metadata into the binary and, under the
//! optional `bundled-assets` feature, guards the compile-time embed
//! precondition.
//!
//! Exposes `GIT_HASH` and `GIT_BRANCH` (consumed via `env!()` in `main.rs`).
//! The frontend is built by Vite into `static-dist/` at the repo root and
//! served directly by the web layer (`interfaces::web`); when
//! `bundled-assets` is on, `src/interfaces/web/embedded.rs` bakes that
//! directory into the binary at compile time via `rust-embed`.
use std::env;
use std::path::Path;
use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
// `sqlx::migrate!()` (src/infrastructure/db.rs) embeds every file under
// `migrations/` at COMPILE time. Nothing else tells cargo those files
// are inputs, so editing one and rebuilding silently kept the old
// embedded copy — the binary and the schema disagreeing, with no error
// until something downstream hit the difference. The only workaround
// was touching db.rs, which you have to remember every time.
//
// A directory here means "watch everything beneath it", so adding,
// editing or renaming a .sql re-triggers the embed on its own. Same
// mechanism `bundled_assets_guard` uses for `static-dist`.
println!("cargo:rerun-if-changed=migrations");
git_status();
bundled_assets_guard();
}
// ═══════════════════════════════════════════════════════════════════════════════
// Bundled-assets precondition guard
//
// When `--features bundled-assets` is on, `rust-embed`'s `#[folder = "static-dist/"]`
// scans that directory at compile time and errors with a not-very-helpful
// "No such file or directory" if it's missing. Users hit this first when they
// try `cargo build --release --features bundled-assets` before running the
// frontend build — we intercept it here with a clear, actionable message.
//
// Also emits `cargo:rerun-if-changed=static-dist/` so a fresh frontend build
// re-triggers the embed step without needing `cargo clean` — matches what a
// dev on the bundled feature would expect after `just fe-build`.
// ═══════════════════════════════════════════════════════════════════════════════
fn bundled_assets_guard() {
if env::var("CARGO_FEATURE_BUNDLED_ASSETS").is_err() {
return;
}
println!("cargo:rerun-if-changed=static-dist");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let dist = Path::new(&manifest_dir).join("static-dist");
let index = dist.join("index.html");
if !index.exists() {
// `cargo:warning=` prefixes surface these in the terminal even
// when cargo's default output is quiet; the panic below turns
// them into a compile-time error so the missing prerequisite
// can't slip past a distracted dev.
println!("cargo:warning=`bundled-assets` feature requires static-dist/ at the repo root.");
println!("cargo:warning=Build the SvelteKit SPA first: (cd frontend && npm run build)");
println!("cargo:warning=Or via the workspace shortcut: just fe-build");
panic!(
"build.rs: missing {}/index.html — see the cargo:warning lines above",
dist.display()
);
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// Grab git values
// Supports GitHub; CI vars are honoured (extend if moving to GitLab/CircleCI/…).
// ═══════════════════════════════════════════════════════════════════════════════
fn git_status() {
// Resolve the actual git directory. Two layouts to handle:
//
// * Normal checkout — `.git` is a directory; `git_dir` = ".git",
// and everything (HEAD, refs, packed-refs) lives inside it.
//
// * `git worktree add` checkout — `.git` is a FILE with contents
// `gitdir: /path/to/main/.git/worktrees/<name>`. The per-worktree
// HEAD lives at that resolved path; branch refs + packed-refs are
// SHARED across worktrees and live in the main repo's `.git/`
// (the "common dir"). `.git/HEAD` inside the worktree checkout
// literally does not exist.
//
// Cargo's documented behaviour for `rerun-if-changed=<path>` when
// `<path>` doesn't exist: **re-run the build script on every
// incremental build**. On a worktree the naive `.git/HEAD` watch
// therefore forces build.rs to run every `cargo build`, re-emits
// GIT_HASH, invalidates main.rs, and triggers a full re-link. That
// was the "cargo build always takes 30-60 s even with no changes"
// symptom on worktrees.
//
// `resolve_git_dir` handles both shapes and gives us the ACTUAL
// paths we should watch. `git_common_dir` (for shared refs) is
// distinct from `git_dir` (per-worktree HEAD) in the worktree
// case, identical in the normal-checkout case.
let git_dir = resolve_git_dir(".git").unwrap_or_else(|| ".git".to_string());
let git_common_dir = resolve_git_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
// Watch ONLY the files whose contents encode "which commit are we
// on" — HEAD (branch pointer OR raw SHA when detached) plus the
// specific ref file for the current branch. Watching a directory
// (`refs/heads`) misfires on every ref added/removed via `git
// fetch`, `git gc`, `git branch`, and IDE git integrations —
// bumping the dir mtime, re-running build.rs, re-emitting
// GIT_HASH, and forcing a full re-link.
let head_path = format!("{git_dir}/HEAD");
if std::path::Path::new(&head_path).exists() {
println!("cargo:rerun-if-changed={head_path}");
}
if let Some(current_branch_ref) = current_branch_ref_path(&git_dir) {
// Branch refs live in `git_common_dir` (shared across
// worktrees), not per-worktree `git_dir`.
let ref_path = format!("{git_common_dir}/{current_branch_ref}");
if std::path::Path::new(&ref_path).exists() {
println!("cargo:rerun-if-changed={ref_path}");
}
}
// Packed refs — git occasionally packs loose refs (auto-gc, or
// `git pack-refs`), moving current-branch content OUT of
// `refs/heads/<name>` and INTO `packed-refs`. Without watching
// this file, a `commit` after a pack would go undetected until
// the branch was re-checked-out.
let packed_refs = format!("{git_common_dir}/packed-refs");
if std::path::Path::new(&packed_refs).exists() {
println!("cargo:rerun-if-changed={packed_refs}");
}
let git_hash = first_env(&["GITHUB_SHA", "CI_COMMIT_SHA", "CIRCLE_SHA1", "GIT_COMMIT"])
.or_else(|| git(&["rev-parse", "HEAD"]))
.unwrap_or_else(|| "unknown".into());
println!("cargo:rustc-env=GIT_HASH={git_hash}");
let git_branch = first_env(&[
"GITHUB_HEAD_REF", // GitHub: PR source branch (empty on push)
"GITHUB_REF_NAME", // GitHub: branch/tag on push
"CI_COMMIT_REF_NAME", // GitLab
"CIRCLE_BRANCH", // CircleCI
"GIT_BRANCH", // Jenkins
])
.or_else(|| git(&["rev-parse", "--abbrev-ref", "HEAD"]))
.filter(|b| b != "HEAD") // detached HEAD is not a real branch name
.unwrap_or_else(|| "unknown".into());
println!("cargo:rustc-env=GIT_BRANCH={git_branch}");
// CI builds: rerun if the injected env changes
for k in [
"GITHUB_SHA",
"GITHUB_HEAD_REF",
"GITHUB_REF_NAME",
"CI_COMMIT_SHA",
"CI_COMMIT_REF_NAME",
"CIRCLE_SHA1",
"CIRCLE_BRANCH",
"GIT_COMMIT",
"GIT_BRANCH",
] {
println!("cargo:rerun-if-env-changed={k}");
}
// Only nag on CI builds — the warning fires every time build.rs
// runs (branch switch, commit on current branch, first build).
// On a local dev loop it becomes noise. CI is where "which commit
// built this artifact" is load-bearing (release provenance,
// release-note automation).
if env::var("CI").is_ok() {
println!("cargo:warning=OxiCloud built with git hash: {git_hash} and branch: {git_branch}");
}
}
/// Resolve `.git` (or whatever path was passed) to the ACTUAL git
/// directory. Handles both:
///
/// * Normal checkout — `.git` is a directory → return it verbatim.
/// * Worktree — `.git` is a text file containing
/// `gitdir: /absolute/or/relative/path` → follow the pointer.
///
/// Returns `None` when the path is neither (unusual — permission issue
/// or repository-less build); caller falls back to the literal `.git`
/// name (which won't exist, so no watches fire — fine for CI where
/// build.rs runs once anyway).
fn resolve_git_dir(path: &str) -> Option<String> {
let meta = std::fs::metadata(path).ok()?;
if meta.is_dir() {
return Some(path.to_string());
}
// `.git` is a file — parse the `gitdir:` pointer written by
// `git worktree add`. Format is stable across git versions:
// gitdir: /abs/path/to/main/.git/worktrees/<name>\n
let contents = std::fs::read_to_string(path).ok()?;
let target = contents.trim().strip_prefix("gitdir: ")?.trim();
// Path may be absolute or (rarely) relative to the checkout root.
// std::path handles both transparently for our purposes.
Some(target.to_string())
}
/// Resolve the "git common dir" — the shared refs store. In a normal
/// checkout it equals `git_dir`. In a worktree, `git_dir` is
/// `main/.git/worktrees/<name>` and the common dir is `main/.git` —
/// where all branch refs and packed-refs actually live.
///
/// The pointer is a `commondir` file inside the worktree's git_dir
/// containing a path (usually relative, `../..` to escape the
/// `worktrees/<name>/` prefix).
fn resolve_git_common_dir(git_dir: &str) -> Option<String> {
let commondir_marker = format!("{git_dir}/commondir");
let contents = std::fs::read_to_string(&commondir_marker).ok()?;
let target = contents.trim();
// `commondir` is usually relative to git_dir; resolve it.
let joined = std::path::Path::new(git_dir).join(target);
// Canonicalise so downstream string-comparisons don't trip on
// `../..` versus the real path.
joined
.canonicalize()
.ok()
.and_then(|p| p.to_str().map(str::to_owned))
.or_else(|| joined.to_str().map(str::to_owned))
}
/// Parse `<git_dir>/HEAD` to find the specific ref file the current
/// branch points at (e.g. contents `ref: refs/heads/feat/foo` →
/// return `Some("refs/heads/feat/foo")`). Returns `None` for
/// detached HEAD (raw SHA in HEAD, no branch file to watch) or an
/// unreadable HEAD; in either case the bare HEAD watch above still
/// catches the state we care about.
fn current_branch_ref_path(git_dir: &str) -> Option<String> {
let head = std::fs::read_to_string(format!("{git_dir}/HEAD")).ok()?;
head.trim().strip_prefix("ref: ").map(str::to_owned)
}
fn git(args: &[&str]) -> Option<String> {
let out = Command::new("git").args(args).output().ok()?;
out.status.success().then_some(())?;
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
(!s.is_empty()).then_some(s)
}
fn first_env(keys: &[&str]) -> Option<String> {
keys.iter()
.find_map(|k| env::var(k).ok())
.filter(|s| !s.is_empty())
}