use std::{ io, path::Path, process::{Command, ExitStatus, Output}, }; #[derive(Debug, thiserror::Error)] pub enum Error { /// no compiler specified, all defaults failed #[error("couldn't find a C compiler")] NoCompiler, /// user specified a compiler and it didn't work #[error("couldn't execute `{cmd}`: {src}")] BadCompiler { cmd: String, src: io::Error }, /// user specified a compiler and it's nonsense #[error("compiler can't be empty")] EmptyCompiler, /// compiler ran but failed for some reason #[error("compiler failed with {status}\n\n{}", String::from_utf8_lossy(.stderr))] ExecFailed { status: ExitStatus, stderr: Vec }, } fn to_res(Output { status, stderr, .. }: Output) -> Result<(), Error> { status .success() .then_some(()) .ok_or(Error::ExecFailed { status, stderr }) } // there's probably a platform dependent better order for searching these. too bad ! const DEFAULT_COMPILERS: [&str; 3] = ["cc", "gcc", "clang"]; pub struct Build<'a> { compiler: Option<&'a str>, dir: Option<&'a Path>, inputs: Vec<&'a str>, } impl<'a> Build<'a> { pub fn new() -> Self { Self { compiler: None, dir: None, inputs: Vec::new(), } } pub fn compile(self, output_path: &Path) -> Result<(), Error> { if let Some(compiler) = self.compiler { let mut parts = compiler.split_whitespace(); let mut cmd = Command::new(parts.next().ok_or(Error::EmptyCompiler)?); cmd.args(parts); return match self.try_compile(cmd, output_path) { Err(e) => Err(Error::BadCompiler { cmd: compiler.to_owned(), src: e, }), Ok(output) => to_res(output), }; } for cc in DEFAULT_COMPILERS { let cmd = Command::new(cc); if let Ok(output) = self.try_compile(cmd, output_path) { return to_res(output); } } Err(Error::NoCompiler) } pub fn compiler(&mut self, compiler: Option<&'a str>) -> &mut Self { self.compiler = compiler; self } pub fn dir(&mut self, path: &'a Path) -> &mut Self { self.dir = Some(path); self } pub fn input_files(&mut self, files: &[&'a str]) -> &mut Self { for file in files { self.inputs.push(*file); } self } fn try_compile(&self, mut cmd: Command, output_path: &Path) -> io::Result { cmd.arg("-fpic").arg("-shared").arg("-std=c11"); if let Some(path) = self.dir { cmd.current_dir(path); } cmd.args(&self.inputs); cmd.arg("-o").arg(output_path); cmd.output() } }