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
fn main() {
// The `dashboard` feature embeds `web/dist/` (the built SPA) via rust-embed. Make sure
// it's available at compile time — see [`dashboard::ensure`].
#[cfg(feature = "dashboard")]
dashboard::ensure();
// License notices are embedded only for release distribution, behind the
// `license-notice` feature, which keeps the build-time license gathering out of a normal
// `cargo build` / `cargo test` / `cargo install`. With it, `notalawyer_build::build()`
// resolves the dependency graph and gathers each crate's license into $OUT_DIR/notalawyer,
// reading `about.toml` (the SPDX accept-list) and possibly fetching license texts over the
// network at build time.
#[cfg(feature = "license-notice")]
{
println!("cargo:rerun-if-changed=about.toml");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-changed=Cargo.lock");
notalawyer_build::build();
}
}
#[cfg(feature = "dashboard")]
mod dashboard {
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
/// Make `web/dist/` (the SPA `rust-embed`'s `#[folder]` embeds) available at compile time.
///
/// The folder must EXIST for the derive to compile (it errors otherwise), and a non-empty
/// one is what makes `bambu serve` actually serve the dashboard rather than the built-in
/// fallback page (see `src/server/assets.rs`). Resolution, best-first:
///
/// 1. **Already built** — the release matrix downloads a pre-built `web/dist` before
/// compiling, and a dev keeps one from `pnpm -C web build`. If `web/dist/index.html`
/// exists we use it and stop (no toolchain needed).
/// 2. **Build it** — otherwise (a crates.io tarball or a fresh git checkout — neither
/// ships `web/dist`, only the frontend source) run a best-effort
/// `pnpm install --frozen-lockfile && pnpm build`.
/// 3. **Fallback** — any constrained environment (docs.rs has no network; an offline or
/// `BAMBU_SKIP_WEB_BUILD=1` build; a host without node/pnpm; a failed build) keeps the
/// empty dir and the runtime fallback page rather than failing the whole crate build.
/// Set `BAMBU_WEB_BUILD_STRICT=1` to turn a failed/again-skipped build into a hard
/// error instead — used by release/CI verification, where the SPA must be present.
pub fn ensure() {
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let dist = manifest.join("web/dist");
// rust-embed's derive needs the folder to exist even when it ends up empty.
let _ = std::fs::create_dir_all(&dist);
// Rebuild the SPA only when its real inputs change — never on node_modules/dist or
// Playwright (test) churn, which must not retrigger the Rust build.
for p in [
"web/package.json",
"web/pnpm-lock.yaml",
"web/pnpm-workspace.yaml",
"web/index.html",
"web/vite.config.ts",
"web/tsconfig.json",
"web/src",
] {
println!("cargo:rerun-if-changed={p}");
}
for e in [
"DOCS_RS",
"CARGO_NET_OFFLINE",
"BAMBU_SKIP_WEB_BUILD",
"BAMBU_WEB_BUILD_STRICT",
] {
println!("cargo:rerun-if-env-changed={e}");
}
// 1. A pre-built SPA wins — no toolchain needed (the common crates.io / release path).
if dist.join("index.html").exists() {
return;
}
let strict = env::var("BAMBU_WEB_BUILD_STRICT").as_deref() == Ok("1");
// 3a. Don't shell out to a package manager in constrained environments.
if env::var_os("DOCS_RS").is_some()
|| env::var("CARGO_NET_OFFLINE").as_deref() == Ok("true")
|| env::var("BAMBU_SKIP_WEB_BUILD").as_deref() == Ok("1")
{
give_up(strict, "skipping the SPA build (docs.rs/offline/opt-out)");
return;
}
if !have("node") || !have("pnpm") {
give_up(
strict,
"node/pnpm not found (install them, or use a release binary, for the full dashboard)",
);
return;
}
// 2. Best-effort build.
let built = run(
"pnpm",
&["-C", "web", "install", "--frozen-lockfile"],
&manifest,
) && run("pnpm", &["-C", "web", "build"], &manifest)
&& dist.join("index.html").exists();
if !built {
give_up(strict, "SPA build failed");
}
}
/// Either hard-fail (strict, for release/CI) or warn and let the runtime fallback page
/// stand in. The message explains which path was taken.
fn give_up(strict: bool, why: &str) {
if strict {
panic!("bambu dashboard: {why} and BAMBU_WEB_BUILD_STRICT=1");
}
println!("cargo:warning=bambu dashboard: {why}; embedding the fallback page");
}
fn have(cmd: &str) -> bool {
Command::new(cmd)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn run(cmd: &str, args: &[&str], dir: &Path) -> bool {
Command::new(cmd)
.args(args)
.current_dir(dir)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
}