use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Child, ExitStatus, Output, Stdio};
use std::thread::JoinHandle;
use std::time::{Duration, Instant, SystemTime};
use super::process::{extract_output_details, format_command};
use super::{command, output, resolve_hardware_context, Runner, RunnerError};
struct SpawnedBuild {
child: Child,
command: String,
started: SystemTime,
}
impl Runner {
pub fn build(&self, file: &Path, out_file: Option<&Path>) -> Result<PathBuf, RunnerError> {
let mut spawned = self.spawn_build(file)?;
let out = collect_output(&mut spawned.child, &spawned.command)?;
let produced = fresh_circuit_json(file, &out, spawned.started);
if !out.status.success() && produced.is_err() {
let details = extract_output_details(&out);
return Err(RunnerError::CommandFailed {
command: spawned.command,
code: out.status.code(),
stderr: details.clone(),
help: details,
});
}
let produced_path = produced?;
match out_file {
Some(dest) => {
output::copy_output(&produced_path, dest)?;
Ok(dest.to_path_buf())
}
None => Ok(produced_path),
}
}
fn spawn_build(&self, file: &Path) -> Result<SpawnedBuild, RunnerError> {
let (working_dir, target_file) = resolve_hardware_context(file);
let mut cmd = command::build_command(&self.kind, &target_file);
if let Some(ref dir) = working_dir {
cmd.current_dir(dir);
}
cmd.stdin(Stdio::null());
cmd.env("CI", "1");
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let command = format_command(&cmd);
let started = SystemTime::now() - Duration::from_secs(2);
let child = cmd.spawn().map_err(|source| RunnerError::Io {
command: command.clone(),
source,
})?;
Ok(SpawnedBuild {
child,
command,
started,
})
}
}
fn collect_output(child: &mut Child, command: &str) -> Result<Output, RunnerError> {
let stdout_handle = drain(child.stdout.take());
let stderr_handle = drain(child.stderr.take());
let status = wait_with_timeout(child, command)?;
Ok(Output {
status,
stdout: stdout_handle.join().unwrap_or_default(),
stderr: stderr_handle.join().unwrap_or_default(),
})
}
fn drain<R: Read + Send + 'static>(pipe: Option<R>) -> JoinHandle<Vec<u8>> {
std::thread::spawn(move || {
let mut buf = Vec::new();
if let Some(mut pipe) = pipe {
let _ = pipe.read_to_end(&mut buf);
}
buf
})
}
fn wait_with_timeout(child: &mut Child, command: &str) -> Result<ExitStatus, RunnerError> {
let start = Instant::now();
let timeout = build_timeout();
let result = loop {
match child.try_wait() {
Ok(Some(status)) => break Ok(status),
Ok(None) if start.elapsed() >= timeout => break Err(timeout_error(command, timeout)),
Ok(None) => std::thread::sleep(Duration::from_millis(50)),
Err(source) => {
break Err(RunnerError::Io {
command: command.to_string(),
source,
})
}
}
};
if result.is_err() {
let _ = child.kill();
let _ = child.wait();
}
result
}
fn timeout_error(command: &str, timeout: Duration) -> RunnerError {
RunnerError::CommandFailed {
command: command.to_string(),
code: None,
stderr: format!("compilation timed out after {} seconds", timeout.as_secs()),
help: "raise FUSIBLE_BUILD_TIMEOUT_SECS or set `routingDisabled` on <board>".to_string(),
}
}
fn fresh_circuit_json(
file: &Path,
out: &Output,
started: SystemTime,
) -> Result<PathBuf, RunnerError> {
let stdout_str = String::from_utf8_lossy(&out.stdout);
let path = output::resolve_circuit_json(file, &stdout_str)?;
let fresh = std::fs::metadata(&path)
.and_then(|m| m.modified())
.is_ok_and(|modified| modified >= started);
if fresh {
Ok(path)
} else {
Err(RunnerError::OutputNotFound { path })
}
}
fn build_timeout() -> Duration {
let secs = std::env::var("FUSIBLE_BUILD_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.filter(|s| *s > 0)
.unwrap_or(120);
Duration::from_secs(secs)
}