pub mod command;
pub mod detect;
pub mod error;
pub mod kicad;
pub mod output;
#[cfg(test)]
mod tests;
use std::path::{Path, PathBuf};
use std::process::{ExitStatus, Stdio};
pub use command::{
add_command, build_command, dev_command, export_command, install_command, search_command,
};
pub use detect::detect_runner;
pub use error::RunnerError;
pub use kicad::{convert_kicad, is_kicad_target};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunnerKind {
Local(PathBuf),
Global(PathBuf),
}
impl RunnerKind {
pub fn description(&self) -> String {
match self {
Self::Local(p) => format!("local executable ({})", p.display()),
Self::Global(p) => format!("global executable ({})", p.display()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Runner {
kind: RunnerKind,
}
pub fn detect() -> Result<Runner, RunnerError> {
Runner::detect()
}
impl Runner {
pub fn new(kind: RunnerKind) -> Self {
Self { kind }
}
pub fn detect() -> Result<Self, RunnerError> {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let path_var = std::env::var_os("PATH");
detect_runner(¤t_dir, path_var.as_deref())
}
pub fn kind(&self) -> &RunnerKind {
&self.kind
}
pub fn dev(&self, file: &Path, port: Option<u16>) -> Result<ExitStatus, RunnerError> {
let (working_dir, target_file) = resolve_hardware_context(file);
let mut cmd = dev_command(&self.kind, &target_file, port);
if let Some(ref dir) = working_dir {
cmd.current_dir(dir);
}
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
cmd.status().map_err(|source| RunnerError::Io {
command: command::format_command(&cmd),
source,
})
}
pub fn export(
&self,
file: &Path,
format: &str,
output: Option<&Path>,
) -> Result<(), RunnerError> {
let abs_output = output.map(|p| {
if p.is_absolute() {
p.to_path_buf()
} else {
std::env::current_dir()
.map(|cd| cd.join(p))
.unwrap_or_else(|_| p.to_path_buf())
}
});
if let Some(ref p) = abs_output {
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).ok();
}
}
let mut cmd = export_command(&self.kind, file, format, abs_output.as_deref());
let out = cmd.output().map_err(|source| RunnerError::Io {
command: command::format_command(&cmd),
source,
})?;
if !out.status.success() {
let details = command::extract_output_details(&out);
return Err(RunnerError::CommandFailed {
command: command::format_command(&cmd),
code: out.status.code(),
stderr: details.clone(),
help: details,
});
}
Ok(())
}
pub fn build(&self, file: &Path, out_file: Option<&Path>) -> Result<PathBuf, RunnerError> {
let mut cmd = build_command(&self.kind, file);
let out = cmd.output().map_err(|source| RunnerError::Io {
command: command::format_command(&cmd),
source,
})?;
if !out.status.success() {
let details = command::extract_output_details(&out);
return Err(RunnerError::CommandFailed {
command: command::format_command(&cmd),
code: out.status.code(),
stderr: details.clone(),
help: details,
});
}
let stdout = String::from_utf8_lossy(&out.stdout);
let produced = output::resolve_circuit_json(file, &stdout)?;
if let Some(dest) = out_file {
output::copy_output(&produced, dest)?;
Ok(dest.to_path_buf())
} else {
Ok(produced)
}
}
pub fn add(&self, packages: &[String], cwd: Option<&Path>) -> Result<(), RunnerError> {
let mut cmd = command::add_command(&self.kind, packages);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = cmd.status().map_err(|source| RunnerError::Io {
command: command::format_command(&cmd),
source,
})?;
if !status.success() {
return Err(RunnerError::CommandFailed {
command: command::format_command(&cmd),
code: status.code(),
stderr: "failed to add package(s)".into(),
help: "check package names or registry connectivity".into(),
});
}
Ok(())
}
pub fn install(&self, cwd: Option<&Path>) -> Result<(), RunnerError> {
let mut cmd = command::install_command(&self.kind);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = cmd.status().map_err(|source| RunnerError::Io {
command: command::format_command(&cmd),
source,
})?;
if !status.success() {
return Err(RunnerError::CommandFailed {
command: command::format_command(&cmd),
code: status.code(),
stderr: "failed to install dependencies".into(),
help: "check network connection or package.json validity".into(),
});
}
Ok(())
}
pub fn search(&self, query: &[String]) -> Result<(), RunnerError> {
let mut cmd = command::search_command(&self.kind, query);
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = cmd.status().map_err(|source| RunnerError::Io {
command: command::format_command(&cmd),
source,
})?;
if !status.success() {
return Err(RunnerError::CommandFailed {
command: command::format_command(&cmd),
code: status.code(),
stderr: "search command failed".into(),
help: "check network connection".into(),
});
}
Ok(())
}
}
fn resolve_hardware_context(file: &Path) -> (Option<PathBuf>, PathBuf) {
if let Some(parent) = file.parent() {
if !parent.as_os_str().is_empty() && parent.join("package.json").is_file() {
let filename = file
.file_name()
.map(PathBuf::from)
.unwrap_or_else(|| file.to_path_buf());
return (Some(parent.to_path_buf()), filename);
}
}
if Path::new("hardware").join("package.json").is_file() {
if let Ok(rel) = file.strip_prefix("hardware") {
return (Some(PathBuf::from("hardware")), rel.to_path_buf());
}
}
(None, file.to_path_buf())
}