Skip to main content

crossbuild_runner/
lib.rs

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
11/// Executes build plans.
12pub 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        // Set up output capture
48        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        // Capture and forward output
62        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}