1use std::process::Command;
2use std::sync::{Arc, Mutex};
3
4use anyhow::Result;
5use crossbuild_core::{
6 diagnostics::{Diagnostic, DiagnosticSink},
7 error::CrossBuildError,
8 model::{BuildPlan, ExecutionMode, RunReport},
9};
10
11pub struct Runner;
13
14impl Runner {
15 pub fn run(
16 plan: &BuildPlan,
17 sink: &mut dyn DiagnosticSink,
18 ) -> Result<RunReport, CrossBuildError> {
19 let command_repr = plan.command.to_string();
20
21 if plan.request.execution_mode == ExecutionMode::DryRun {
22 sink.emit(Diagnostic::info(
23 "CB1000",
24 format!("dry run: {command_repr}"),
25 ));
26 return Ok(RunReport {
27 executed: false,
28 command: command_repr,
29 working_directory: plan.command.current_dir.clone(),
30 exit_code: None,
31 duration_ms: 0,
32 });
33 }
34
35 sink.emit(Diagnostic::info(
36 "CB1001",
37 format!("running {command_repr}"),
38 ));
39
40 let start = std::time::Instant::now();
41
42 let mut command = Command::new(&plan.command.program);
43 command.current_dir(&plan.command.current_dir);
44 command.args(&plan.command.args);
45 command.envs(&plan.command.env);
46
47 command.stdout(std::process::Stdio::piped());
49 command.stderr(std::process::Stdio::piped());
50
51 let mut child = command.spawn().map_err(|source| {
52 if source.kind() == std::io::ErrorKind::NotFound {
53 CrossBuildError::CargoUnavailable {
54 program: plan.command.program.clone(),
55 }
56 } else {
57 CrossBuildError::Io { path: None, source }
58 }
59 })?;
60
61 let stdout = child.stdout.take().expect("stdout was piped at spawn");
63 let stderr = child.stderr.take().expect("stderr was piped at spawn");
64
65 let collected: Arc<Mutex<Vec<Diagnostic>>> = Arc::new(Mutex::new(Vec::new()));
66
67 let out_diags = collected.clone();
68 let stdout_handle = std::thread::spawn(move || {
69 use std::io::{BufRead, BufReader};
70 let reader = BufReader::new(stdout);
71 for line in reader.lines().map_while(Result::ok) {
72 if !line.trim().is_empty() {
73 out_diags
74 .lock()
75 .expect("diagnostic lock not poisoned")
76 .push(Diagnostic::info("CB1010", line));
77 }
78 }
79 });
80
81 let err_diags = collected.clone();
82 let stderr_handle = std::thread::spawn(move || {
83 use std::io::{BufRead, BufReader};
84 let reader = BufReader::new(stderr);
85 for line in reader.lines().map_while(Result::ok) {
86 if !line.trim().is_empty() {
87 err_diags
88 .lock()
89 .expect("diagnostic lock not poisoned")
90 .push(Diagnostic::warning("CB1011", line));
91 }
92 }
93 });
94
95 let status = child
96 .wait()
97 .map_err(|source| CrossBuildError::Io { path: None, source })?;
98
99 let _ = stdout_handle.join();
100 let _ = stderr_handle.join();
101
102 for diag in collected
103 .lock()
104 .expect("diagnostic lock not poisoned")
105 .drain(..)
106 {
107 sink.emit(diag);
108 }
109
110 let exit_code = status.code();
111 let duration_ms = start.elapsed().as_millis() as u64;
112
113 if !status.success() {
114 return Err(CrossBuildError::BuildFailed {
115 command: command_repr,
116 exit_code,
117 });
118 }
119
120 Ok(RunReport {
121 executed: true,
122 command: command_repr,
123 working_directory: plan.command.current_dir.clone(),
124 exit_code,
125 duration_ms,
126 })
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crossbuild_core::model::{
134 BuildPlan, BuildRequest, CommandLine, ExecutionMode, PlanStep, TargetTriple,
135 };
136 use std::path::PathBuf;
137
138 #[test]
139 fn dry_run_works() {
140 let plan = BuildPlan {
141 request: BuildRequest::new(
142 PathBuf::from("Cargo.toml"),
143 TargetTriple::parse("x86_64-unknown-linux-gnu").unwrap(),
144 )
145 .with_execution_mode(ExecutionMode::DryRun),
146 host: crossbuild_core::platform::detect_host().unwrap(),
147 target: crossbuild_core::platform::assess_target(
148 &TargetTriple::parse("x86_64-unknown-linux-gnu").unwrap(),
149 &crossbuild_core::platform::detect_host().unwrap(),
150 ),
151 command: CommandLine::new("cargo", PathBuf::from(".")),
152 steps: vec![PlanStep::InvokeCargo],
153 provider_actions: vec![],
154 cargo_config: None,
155 cache_key: "test".to_string(),
156 };
157
158 let mut sink = crossbuild_core::diagnostics::StderrDiagnosticSink::new(false);
159 let report = Runner::run(&plan, &mut sink).unwrap();
160 assert!(!report.executed);
161 }
162}