forked from zeroclaw-labs/zeroclaw
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.rs
More file actions
181 lines (163 loc) Β· 6.19 KB
/
build.rs
File metadata and controls
181 lines (163 loc) Β· 6.19 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
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::SystemTime;
fn main() {
let dist_dir = Path::new("web/dist");
let web_dir = Path::new("web");
// Tell Cargo to re-run this script when web sources or bundled assets change.
println!("cargo:rerun-if-changed=web/src");
println!("cargo:rerun-if-changed=web/public");
println!("cargo:rerun-if-changed=web/index.html");
println!("cargo:rerun-if-changed=docs/assets/zeroclaw-trans.png");
println!("cargo:rerun-if-changed=web/package.json");
println!("cargo:rerun-if-changed=web/package-lock.json");
println!("cargo:rerun-if-changed=web/tsconfig.json");
println!("cargo:rerun-if-changed=web/tsconfig.app.json");
println!("cargo:rerun-if-changed=web/tsconfig.node.json");
println!("cargo:rerun-if-changed=web/vite.config.ts");
println!("cargo:rerun-if-changed=web/dist");
// Attempt to build the web frontend if npm is available and web/dist is
// missing or stale. The build is best-effort: when Node.js is not
// installed (e.g. CI containers, cross-compilation, minimal dev setups)
// we fall back to the existing stub/empty dist directory so the Rust
// build still succeeds.
let needs_build = web_build_required(web_dir, dist_dir);
if needs_build && web_dir.join("package.json").exists() {
if let Ok(npm) = which_npm() {
eprintln!("cargo:warning=Building web frontend (web/dist is missing or stale)...");
// npm ci / npm install
let install_status = Command::new(&npm)
.args(["ci", "--ignore-scripts"])
.current_dir(web_dir)
.status();
match install_status {
Ok(s) if s.success() => {}
Ok(s) => {
// Fall back to `npm install` if `npm ci` fails (no lockfile, etc.)
eprintln!("cargo:warning=npm ci exited with {s}, trying npm install...");
let fallback = Command::new(&npm)
.args(["install"])
.current_dir(web_dir)
.status();
if !matches!(fallback, Ok(s) if s.success()) {
eprintln!("cargo:warning=npm install failed β skipping web build");
ensure_dist_dir(dist_dir);
return;
}
}
Err(e) => {
eprintln!("cargo:warning=Could not run npm: {e} β skipping web build");
ensure_dist_dir(dist_dir);
return;
}
}
// npm run build
let build_status = Command::new(&npm)
.args(["run", "build"])
.current_dir(web_dir)
.status();
match build_status {
Ok(s) if s.success() => {
eprintln!("cargo:warning=Web frontend built successfully.");
}
Ok(s) => {
eprintln!(
"cargo:warning=npm run build exited with {s} β web dashboard may be unavailable"
);
}
Err(e) => {
eprintln!(
"cargo:warning=Could not run npm build: {e} β web dashboard may be unavailable"
);
}
}
}
}
ensure_dist_dir(dist_dir);
ensure_dashboard_assets(dist_dir);
}
fn web_build_required(web_dir: &Path, dist_dir: &Path) -> bool {
let Some(dist_mtime) = latest_modified(dist_dir) else {
return true;
};
[
web_dir.join("src"),
web_dir.join("public"),
web_dir.join("index.html"),
web_dir.join("package.json"),
web_dir.join("package-lock.json"),
web_dir.join("tsconfig.json"),
web_dir.join("tsconfig.app.json"),
web_dir.join("tsconfig.node.json"),
web_dir.join("vite.config.ts"),
]
.into_iter()
.filter_map(|path| latest_modified(&path))
.any(|mtime| mtime > dist_mtime)
}
fn latest_modified(path: &Path) -> Option<SystemTime> {
let metadata = fs::metadata(path).ok()?;
if metadata.is_file() {
return metadata.modified().ok();
}
if !metadata.is_dir() {
return None;
}
let mut latest = metadata.modified().ok();
let entries = fs::read_dir(path).ok()?;
for entry in entries.flatten() {
if let Some(child_mtime) = latest_modified(&entry.path()) {
latest = Some(match latest {
Some(current) if current >= child_mtime => current,
_ => child_mtime,
});
}
}
latest
}
/// Ensure the dist directory exists so `rust-embed` does not fail at compile
/// time even when the web frontend is not built.
fn ensure_dist_dir(dist_dir: &Path) {
if !dist_dir.exists() {
std::fs::create_dir_all(dist_dir).expect("failed to create web/dist/");
}
}
fn ensure_dashboard_assets(dist_dir: &Path) {
// The Rust gateway serves `web/dist/` via rust-embed under `/_app/*`.
// Some builds may end up with missing/blank logo assets, so we ensure the
// expected image is always present in `web/dist/` at compile time.
let src = Path::new("docs/assets/zeroclaw-trans.png");
if !src.exists() {
eprintln!(
"cargo:warning=docs/assets/zeroclaw-trans.png not found; skipping dashboard asset copy"
);
return;
}
let dst = dist_dir.join("zeroclaw-trans.png");
if let Err(e) = fs::copy(src, &dst) {
eprintln!("cargo:warning=Failed to copy zeroclaw-trans.png into web/dist/: {e}");
}
}
/// Locate the `npm` binary on the system PATH.
fn which_npm() -> Result<String, ()> {
let cmd = if cfg!(target_os = "windows") {
"where"
} else {
"which"
};
Command::new(cmd)
.arg("npm")
.output()
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout)
.ok()
.map(|s| s.lines().next().unwrap_or("npm").trim().to_string())
} else {
None
}
})
.ok_or(())
}