1use {
2 crate::{config::QuasarConfig, error::CliResult, style, toolchain, utils},
3 std::{
4 fs,
5 path::{Path, PathBuf},
6 process::{Command, Stdio},
7 time::Instant,
8 },
9};
10
11pub fn run(debug: bool, watch: bool, features: Option<String>) -> CliResult {
12 if watch {
13 return run_watch(debug, features);
14 }
15
16 run_once(debug, features.as_deref())
17}
18
19fn run_once(debug: bool, features: Option<&str>) -> CliResult {
20 let config = QuasarConfig::load()?;
21 let start = Instant::now();
22
23 crate::idl::generate(Path::new("."), config.has_typescript_tests())?;
24
25 let sp = style::spinner("Building...");
26
27 let output = if config.is_solana_toolchain() {
28 let mut cmd = Command::new("cargo");
29 cmd.arg("build-sbf");
30 if debug {
31 cmd.arg("--debug");
32 }
33 if let Some(f) = features {
34 cmd.args(["--features", f]);
35 }
36 cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).output()
37 } else {
38 if !toolchain::has_sbpf_linker() {
39 sp.finish_and_clear();
40 eprintln!("\n {}", style::fail("sbpf-linker not found on PATH."));
41 eprintln!();
42 eprintln!(" Install platform-tools first:");
43 eprintln!(
44 " {}",
45 style::bold("git clone https://github.com/anza-xyz/platform-tools")
46 );
47 eprintln!(" {}", style::bold("cd platform-tools"));
48 eprintln!(" {}", style::bold("cargo install-with-gallery"));
49 std::process::exit(1);
50 }
51
52 let mut cmd = Command::new("cargo");
53 if debug {
54 cmd.env("RUSTFLAGS", "-C link-arg=--btf -C debuginfo=2");
55 }
56 cmd.arg("build-bpf");
57 if let Some(f) = features {
58 cmd.args(["--features", f]);
59 }
60 cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).output()
61 };
62
63 sp.finish_and_clear();
64
65 match output {
66 Ok(o) if o.status.success() => {
67 let elapsed = start.elapsed();
68
69 if !config.is_solana_toolchain() {
70 let program = config.module_name();
71 let src = PathBuf::from("target")
72 .join("bpfel-unknown-none")
73 .join("release")
74 .join(format!("lib{}.so", program));
75 let dest_dir = PathBuf::from("target").join("deploy");
76 fs::create_dir_all(&dest_dir)?;
77 let dest = dest_dir.join(format!("lib{}.so", program));
78 fs::copy(&src, &dest).map_err(|e| {
79 eprintln!(
80 " {}",
81 style::fail(&format!("failed to copy {}: {e}", src.display()))
82 );
83 e
84 })?;
85 }
86
87 let stderr = String::from_utf8_lossy(&o.stderr);
89 let warnings = extract_warnings(&stderr);
90 if !warnings.is_empty() {
91 eprintln!();
92 for line in &warnings {
93 eprintln!(" {line}");
94 }
95 }
96
97 let so_path = utils::find_so(&config, false);
98 let size_info = so_path
99 .and_then(|p| {
100 let meta = fs::metadata(&p).ok()?;
101 let new_size = meta.len();
102 let delta = size_delta(&p, new_size);
103 save_last_size(&p, new_size);
104 Some(format!(
105 " ({}{delta})",
106 style::dim(&style::human_size(new_size))
107 ))
108 })
109 .unwrap_or_default();
110
111 println!(
112 " {}",
113 style::success(&format!(
114 "Build complete in {}{size_info}",
115 style::bold(&style::human_duration(elapsed))
116 ))
117 );
118 Ok(())
119 }
120 Ok(o) => {
121 let elapsed = start.elapsed();
122 let stderr = String::from_utf8_lossy(&o.stderr);
123 print_build_errors(&stderr, elapsed);
124 std::process::exit(o.status.code().unwrap_or(1));
125 }
126 Err(e) => {
127 eprintln!(
128 " {}",
129 style::fail(&format!("failed to run build command: {e}"))
130 );
131 std::process::exit(1);
132 }
133 }
134}
135
136pub fn profile_build() -> Result<PathBuf, crate::error::CliError> {
139 let config = QuasarConfig::load()?;
140 let start = Instant::now();
141
142 crate::idl::generate(Path::new("."), config.has_typescript_tests())?;
143
144 let sp = style::spinner("Profile build...");
145
146 let output = if config.is_solana_toolchain() {
147 let mut cmd = Command::new("cargo");
148 cmd.arg("build-sbf").arg("--debug");
149 cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).output()
150 } else {
151 if !toolchain::has_sbpf_linker() {
152 sp.finish_and_clear();
153 eprintln!("\n {}", style::fail("sbpf-linker not found on PATH."));
154 eprintln!();
155 eprintln!(" Install platform-tools first:");
156 eprintln!(
157 " {}",
158 style::bold("git clone https://github.com/anza-xyz/platform-tools")
159 );
160 eprintln!(" {}", style::bold("cd platform-tools"));
161 eprintln!(" {}", style::bold("cargo install-with-gallery"));
162 std::process::exit(1);
163 }
164
165 let existing_flags = read_target_rustflags();
167 let mut all_flags = existing_flags;
168 all_flags.extend([
169 "-C".to_string(),
170 "link-arg=--btf".to_string(),
171 "-C".to_string(),
172 "debuginfo=2".to_string(),
173 ]);
174
175 let encoded = all_flags.join("\x1f");
177 let mut cmd = Command::new("cargo");
178 cmd.env("CARGO_ENCODED_RUSTFLAGS", encoded);
179 cmd.arg("build-bpf");
180 cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).output()
181 };
182
183 sp.finish_and_clear();
184
185 match output {
186 Ok(o) if o.status.success() => {
187 let elapsed = start.elapsed();
188 let program = config.module_name();
189 let profile_dir = PathBuf::from("target").join("profile");
190 fs::create_dir_all(&profile_dir)?;
191
192 let src = if config.is_solana_toolchain() {
194 utils::find_so(&config, false).unwrap_or_else(|| {
197 PathBuf::from("target")
198 .join("sbf-solana-solana")
199 .join("release")
200 .join(format!("{}.so", program))
201 })
202 } else {
203 PathBuf::from("target")
204 .join("bpfel-unknown-none")
205 .join("release")
206 .join(format!("lib{}.so", program))
207 };
208
209 let dest = profile_dir.join(format!("{}.so", program));
210 fs::copy(&src, &dest).map_err(|e| {
211 eprintln!(
212 " {}",
213 style::fail(&format!("failed to copy {}: {e}", src.display()))
214 );
215 e
216 })?;
217
218 let size = fs::metadata(&dest).map(|m| m.len()).unwrap_or(0);
219 println!(
220 " {}",
221 style::success(&format!(
222 "Profile build in {} ({})",
223 style::bold(&style::human_duration(elapsed)),
224 style::dim(&style::human_size(size))
225 ))
226 );
227
228 Ok(dest)
229 }
230 Ok(o) => {
231 let elapsed = start.elapsed();
232 let stderr = String::from_utf8_lossy(&o.stderr);
233 print_build_errors(&stderr, elapsed);
234 std::process::exit(o.status.code().unwrap_or(1));
235 }
236 Err(e) => {
237 eprintln!(
238 " {}",
239 style::fail(&format!("failed to run build command: {e}"))
240 );
241 std::process::exit(1);
242 }
243 }
244}
245
246fn run_watch(debug: bool, features: Option<String>) -> CliResult {
247 if let Err(e) = run_once(debug, features.as_deref()) {
248 eprintln!(" {}", style::fail(&format!("{e}")));
249 }
250
251 loop {
252 let baseline = collect_mtimes(Path::new("src"));
253 loop {
254 std::thread::sleep(std::time::Duration::from_secs(1));
255 let current = collect_mtimes(Path::new("src"));
256 if current != baseline {
257 if let Err(e) = run_once(debug, features.as_deref()) {
258 eprintln!(" {}", style::fail(&format!("{e}")));
259 }
260 break;
261 }
262 }
263 }
264}
265
266fn extract_warnings(stderr: &str) -> Vec<String> {
272 let mut warnings = Vec::new();
273 let mut capture = false;
274
275 for line in stderr.lines() {
276 if line.starts_with("warning") {
277 if line.contains("warnings emitted")
278 || line.contains("warning emitted")
279 || line.contains("user-defined alias")
280 || line.contains("shadowing")
281 {
282 continue;
283 }
284 capture = true;
285 warnings.push(line.to_string());
286 } else if capture {
287 if line.starts_with(" ") || line.starts_with(" -->") || line.is_empty() {
288 warnings.push(line.to_string());
289 } else {
290 capture = false;
291 }
292 }
293 }
294
295 warnings
296}
297
298fn print_build_errors(stderr: &str, elapsed: std::time::Duration) {
301 let mut errors: Vec<String> = Vec::new();
302 let mut capture = false;
303
304 for line in stderr.lines() {
305 if line.starts_with("error") || line.starts_with("warning") {
307 if line.contains("warnings emitted") || line.contains("warning emitted") {
309 continue;
310 }
311 if line.contains("user-defined alias") || line.contains("shadowing") {
313 continue;
314 }
315 capture = true;
316 errors.push(line.to_string());
317 } else if capture {
318 if line.starts_with(" ")
320 || line.starts_with(" -->")
321 || line.starts_with("Caused by:")
322 || line.is_empty()
323 {
324 errors.push(line.to_string());
325 } else {
326 capture = false;
327 }
328 }
329 }
330
331 if errors.is_empty() {
332 if !stderr.is_empty() {
334 eprint!("{stderr}");
335 }
336 eprintln!(
337 " {}",
338 style::fail(&format!(
339 "build failed in {}",
340 style::bold(&style::human_duration(elapsed))
341 ))
342 );
343 return;
344 }
345
346 eprintln!();
347 for line in &errors {
348 eprintln!(" {line}");
349 }
350 eprintln!();
351
352 let err_count = errors.iter().filter(|l| l.starts_with("error")).count();
354 let warn_count = errors.iter().filter(|l| l.starts_with("warning")).count();
355
356 let mut summary = String::new();
357 if err_count > 0 {
358 summary.push_str(&format!(
359 "{err_count} error{}",
360 if err_count == 1 { "" } else { "s" }
361 ));
362 }
363 if warn_count > 0 {
364 if !summary.is_empty() {
365 summary.push_str(", ");
366 }
367 summary.push_str(&format!(
368 "{warn_count} warning{}",
369 if warn_count == 1 { "" } else { "s" }
370 ));
371 }
372
373 eprintln!(
374 " {}",
375 style::fail(&format!(
376 "build failed in {} ({summary})",
377 style::bold(&style::human_duration(elapsed))
378 ))
379 );
380}
381
382const LAST_SIZE_FILE: &str = "target/.quasar-last-size";
387
388fn size_delta(so_path: &Path, new_size: u64) -> String {
389 let key = so_path.to_string_lossy();
390 let last = fs::read_to_string(LAST_SIZE_FILE)
391 .ok()
392 .and_then(|contents| {
393 contents
394 .lines()
395 .find(|l| l.starts_with(&*key))
396 .and_then(|l| l.rsplit_once(' '))
397 .and_then(|(_, s)| s.parse::<u64>().ok())
398 });
399
400 let Some(prev) = last else {
401 return String::new();
402 };
403
404 if new_size == prev {
405 return String::new();
406 }
407
408 let diff = new_size as i64 - prev as i64;
409 if diff > 0 {
410 format!(
411 ", {}",
412 style::color(196, &format!("+{}", style::human_size(diff as u64)))
413 )
414 } else {
415 format!(
416 ", {}",
417 style::color(83, &format!("-{}", style::human_size((-diff) as u64)))
418 )
419 }
420}
421
422fn save_last_size(so_path: &Path, size: u64) {
423 let key = so_path.to_string_lossy();
424 let entry = format!("{key} {size}");
425
426 let mut lines: Vec<String> = fs::read_to_string(LAST_SIZE_FILE)
428 .unwrap_or_default()
429 .lines()
430 .filter(|l| !l.starts_with(&*key))
431 .map(String::from)
432 .collect();
433 lines.push(entry);
434 let _ = fs::write(LAST_SIZE_FILE, lines.join("\n"));
435}
436
437fn read_target_rustflags() -> Vec<String> {
439 let config_path = Path::new(".cargo").join("config.toml");
440 let contents = match fs::read_to_string(&config_path) {
441 Ok(c) => c,
442 Err(_) => return vec![],
443 };
444 let value: toml::Value = match contents.parse() {
445 Ok(v) => v,
446 Err(_) => return vec![],
447 };
448 value
449 .get("target")
450 .and_then(|t| t.get("bpfel-unknown-none"))
451 .and_then(|t| t.get("rustflags"))
452 .and_then(|f| f.as_array())
453 .map(|arr| {
454 arr.iter()
455 .filter_map(|v| v.as_str().map(String::from))
456 .collect()
457 })
458 .unwrap_or_default()
459}
460
461pub fn collect_mtimes(dir: &Path) -> Vec<(PathBuf, std::time::SystemTime)> {
462 let mut times = Vec::new();
463 if let Ok(entries) = fs::read_dir(dir) {
464 for entry in entries.flatten() {
465 let path = entry.path();
466 if path.is_dir() {
467 times.extend(collect_mtimes(&path));
468 } else if path.extension().is_some_and(|e| e == "rs") {
469 if let Ok(meta) = fs::metadata(&path) {
470 if let Ok(mtime) = meta.modified() {
471 times.push((path, mtime));
472 }
473 }
474 }
475 }
476 }
477 times.sort_by(|a, b| a.0.cmp(&b.0));
478 times
479}