Skip to content

Commit d46ba28

Browse files
authored
Merge pull request #591 from Dstack-TEE/worktree-vmm-ui-rust-build
vmm: build console UI from build.rs
2 parents 35db0ec + 89bc013 commit d46ba28

6 files changed

Lines changed: 175 additions & 17667 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ node_modules/
1111
.claude/settings.local.json
1212
__pycache__
1313
.planning/
14+
/vmm/src/console_v1.html

vmm/build.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
use std::{
6+
env, fs,
7+
path::{Path, PathBuf},
8+
process::Command,
9+
time::SystemTime,
10+
};
11+
12+
fn main() {
13+
if let Err(err) = build_console() {
14+
panic!("failed to build vmm console: {err}");
15+
}
16+
}
17+
18+
fn build_console() -> Result<(), Box<dyn std::error::Error>> {
19+
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
20+
let ui_dir = manifest_dir.join("ui");
21+
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
22+
let output = out_dir.join("console_v1.html");
23+
24+
emit_rerun_if_changed(&manifest_dir.join("build.rs"))?;
25+
emit_rerun_tree(&ui_dir)?;
26+
27+
ensure_command("node", &["--version"], "Node.js")?;
28+
ensure_command(
29+
npm_cmd(),
30+
&["--version"],
31+
"npm (normally bundled with Node.js)",
32+
)?;
33+
34+
if should_run_npm_ci(&ui_dir)? {
35+
run(
36+
npm_cmd(),
37+
&["ci"],
38+
&ui_dir,
39+
&[],
40+
"Install VMM UI dependencies with `npm ci`",
41+
)?;
42+
}
43+
44+
let output_str = output
45+
.to_str()
46+
.ok_or("OUT_DIR path contains non-UTF-8 characters")?;
47+
run(
48+
"node",
49+
&["build.mjs"],
50+
&ui_dir,
51+
&[("DSTACK_UI_OUT", output_str)],
52+
"Build VMM UI",
53+
)?;
54+
55+
if !output.exists() {
56+
return Err(format!(
57+
"UI build succeeded but {} was not created",
58+
output.display()
59+
)
60+
.into());
61+
}
62+
63+
Ok(())
64+
}
65+
66+
fn emit_rerun_tree(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
67+
if !path.exists() {
68+
return Ok(());
69+
}
70+
println!("cargo:rerun-if-changed={}", path.display());
71+
for entry in fs::read_dir(path)? {
72+
let entry = entry?;
73+
let child = entry.path();
74+
if entry.file_type()?.is_dir() {
75+
let name = entry.file_name();
76+
let name = name.to_string_lossy();
77+
if matches!(name.as_ref(), "node_modules" | "build" | "dist") {
78+
continue;
79+
}
80+
emit_rerun_tree(&child)?;
81+
} else {
82+
emit_rerun_if_changed(&child)?;
83+
}
84+
}
85+
Ok(())
86+
}
87+
88+
fn emit_rerun_if_changed(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
89+
println!("cargo:rerun-if-changed={}", path.display());
90+
Ok(())
91+
}
92+
93+
fn ensure_command(
94+
program: &str,
95+
args: &[&str],
96+
display_name: &str,
97+
) -> Result<(), Box<dyn std::error::Error>> {
98+
match Command::new(program).args(args).output() {
99+
Ok(output) if output.status.success() => Ok(()),
100+
Ok(output) => {
101+
let stderr = String::from_utf8_lossy(&output.stderr);
102+
let stdout = String::from_utf8_lossy(&output.stdout);
103+
Err(format!(
104+
"{display_name} is required to build vmm/ui. Please install it first. `{program} {}` failed. stdout: {} stderr: {}",
105+
args.join(" "),
106+
stdout.trim(),
107+
stderr.trim(),
108+
)
109+
.into())
110+
}
111+
Err(err) => Err(format!(
112+
"{display_name} is required to build vmm/ui. Please install Node.js and npm first: {err}"
113+
)
114+
.into()),
115+
}
116+
}
117+
118+
fn should_run_npm_ci(ui_dir: &Path) -> Result<bool, Box<dyn std::error::Error>> {
119+
let package_json = ui_dir.join("package.json");
120+
let package_lock = ui_dir.join("package-lock.json");
121+
let marker = ui_dir.join("node_modules/.package-lock.json");
122+
123+
if !marker.exists() {
124+
return Ok(true);
125+
}
126+
127+
let marker_time = modified_time(&marker)?;
128+
Ok(modified_time(&package_json)? > marker_time || modified_time(&package_lock)? > marker_time)
129+
}
130+
131+
fn modified_time(path: &Path) -> Result<SystemTime, Box<dyn std::error::Error>> {
132+
Ok(fs::metadata(path)?.modified()?)
133+
}
134+
135+
fn run(
136+
program: &str,
137+
args: &[&str],
138+
cwd: &Path,
139+
envs: &[(&str, &str)],
140+
what: &str,
141+
) -> Result<(), Box<dyn std::error::Error>> {
142+
let mut command = Command::new(program);
143+
command
144+
.current_dir(cwd)
145+
.args(args)
146+
.envs(envs.iter().copied());
147+
let status = command.status()?;
148+
if !status.success() {
149+
return Err(format!("{what} failed with exit status {status}").into());
150+
}
151+
Ok(())
152+
}
153+
154+
fn npm_cmd() -> &'static str {
155+
if cfg!(windows) {
156+
"npm.cmd"
157+
} else {
158+
"npm"
159+
}
160+
}

0 commit comments

Comments
 (0)