use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{self, Child, Command};
use std::thread::{self, JoinHandle};
use std::{env, fmt};
#[derive(Clone, Debug)]
pub enum BuildMode {
CArchive,
CShared,
}
impl fmt::Display for BuildMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BuildMode::CArchive => write!(f, "c-archive"),
BuildMode::CShared => write!(f, "c-shared"),
}
}
}
impl Default for BuildMode {
fn default() -> Self {
Self::CArchive
}
}
#[derive(Clone, Debug, Default)]
pub struct Build {
files: Vec<PathBuf>,
env: HashMap<OsString, OsString>,
out_dir: Option<PathBuf>,
buildmode: BuildMode,
compiler: PathBuf,
goarch: Option<OsString>,
goos: Option<OsString>,
cargo_metadata: bool,
}
#[derive(Clone, Debug)]
enum ErrorKind {
EnvVarNotFound,
EnvVarValueUnknown,
ToolNotFound,
ToolExecError,
}
#[derive(Clone, Debug)]
pub struct Error {
kind: ErrorKind,
message: String,
}
impl Error {
fn new(kind: ErrorKind, message: &str) -> Self {
Self {
kind,
message: message.to_owned(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl Build {
pub fn new() -> Self {
Self {
files: Vec::new(),
env: HashMap::new(),
out_dir: None,
buildmode: BuildMode::CArchive,
compiler: PathBuf::from("go"),
goarch: None,
goos: None,
cargo_metadata: true,
}
}
pub fn file<P: AsRef<Path>>(&mut self, p: P) -> &mut Build {
self.files.push(p.as_ref().to_path_buf());
self
}
pub fn files<P>(&mut self, p: P) -> &mut Build
where
P: IntoIterator,
P::Item: AsRef<Path>,
{
for file in p.into_iter() {
self.file(file);
}
self
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Build
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.env
.insert(key.as_ref().to_owned(), val.as_ref().to_owned());
self
}
pub fn out_dir<P: AsRef<Path>>(&mut self, out_dir: P) -> &mut Build {
self.out_dir = Some(out_dir.as_ref().to_owned());
self
}
pub fn buildmode(&mut self, buildmode: BuildMode) -> &mut Build {
self.buildmode = buildmode;
self
}
pub fn compiler<P: AsRef<Path>>(&mut self, compiler: P) -> &mut Build {
self.compiler = compiler.as_ref().to_owned();
self
}
pub fn goarch<T: AsRef<OsStr>>(&mut self, arch: T) -> &mut Build {
self.goarch = Some(arch.as_ref().to_owned());
self
}
pub fn goos<T: AsRef<OsStr>>(&mut self, os: T) -> &mut Build {
self.goos = Some(os.as_ref().to_owned());
self
}
pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Build {
self.cargo_metadata = cargo_metadata;
self
}
pub fn try_compile(&self, lib_name: &str) -> Result<(), Error> {
let gnu_lib_name = self.get_gnu_lib_name(lib_name);
let dst = self.get_out_dir()?;
let out = dst.join(&gnu_lib_name);
for file in &self.files {
self.println(&format!("cargo:rerun-if-changed={}", file.display()));
}
let ccompiler = cc::Build::new().try_get_compiler().map_err(|e| {
Error::new(
ErrorKind::ToolNotFound,
&format!("could not find c compiler: {}", e),
)
})?;
let mut command = process::Command::new(&self.compiler);
command.arg("build");
command.args(&["-buildmode", &self.buildmode.to_string()]);
command.args(&["-o", &out.display().to_string()]);
command.args(self.files.iter());
command.env("CGO_ENABLED", "1");
command.env("CC", ccompiler.path());
let goarch = self
.goarch
.as_ref()
.map(|g| Ok(g.to_owned()))
.unwrap_or_else(|| self.get_goarch())?;
command.env("GOARCH", goarch);
let goos = self
.goarch
.as_ref()
.map(|g| Ok(g.to_owned()))
.unwrap_or_else(|| self.get_goos())?;
command.env("GOOS", goos);
command.envs(&self.env);
run(&mut command, lib_name)?;
match self.buildmode {
BuildMode::CArchive => {
self.println(&format!("cargo:rustc-link-lib=static={}", lib_name))
}
BuildMode::CShared => self.println(&format!("cargo:rustc-link-lib=dylib={}", lib_name)),
}
self.println(&format!("cargo:rustc-link-search=native={}", dst.display()));
Ok(())
}
pub fn compile(&self, output: &str) {
if let Err(e) = self.try_compile(output) {
fail(&e.message);
}
}
fn get_out_dir(&self) -> Result<PathBuf, Error> {
let path = match self.out_dir.clone() {
Some(p) => p,
None => env::var_os("OUT_DIR").map(PathBuf::from).ok_or_else(|| {
Error::new(
ErrorKind::EnvVarNotFound,
"Environment vairable OUT_DIR not defined.",
)
})?,
};
Ok(path)
}
fn get_gnu_lib_name(&self, lib_name: &str) -> String {
let mut gnu_lib_name = String::with_capacity(5 + lib_name.len());
gnu_lib_name.push_str("lib");
gnu_lib_name.push_str(&lib_name);
match self.buildmode {
BuildMode::CArchive => gnu_lib_name.push_str(".a"),
BuildMode::CShared => {
if cfg!(windows) {
gnu_lib_name.push_str(".dll")
} else {
gnu_lib_name.push_str(".so")
}
}
}
gnu_lib_name
}
fn get_goarch(&self) -> Result<OsString, Error> {
let arch = env::var("CARGO_CFG_TARGET_ARCH").map_err(|_| {
Error::new(
ErrorKind::EnvVarNotFound,
"Cannot find CARGO_CFG_TARGET_ARCH env var",
)
})?;
let goarch = match arch.as_str() {
"x86" => "386",
"x86_64" => "amd64",
"mips" => "mips",
"powerpc" => "ppc",
"powerpc64" => "ppc64",
"arm" => "arm",
"aarch64" => "arm64",
a => {
return Err(Error::new(
ErrorKind::EnvVarValueUnknown,
&format!("Unknown arch {}", a),
))
}
};
Ok(goarch.into())
}
fn get_goos(&self) -> Result<OsString, Error> {
let arch = env::var("CARGO_CFG_TARGET_OS").map_err(|_| {
Error::new(
ErrorKind::EnvVarNotFound,
"Cannot find CARGO_CFG_TARGET_OS env var",
)
})?;
let goos = match arch.as_str() {
"windows" => "windows",
"macos" => "darwin",
"ios" => "darwin",
"linux" => "linux",
"android" => "android",
"freebsd" => "freebsd",
"dragonfly" => "dragonfly",
"openbsd" => "openbsd",
"netbsd" => "netbsd",
o => {
return Err(Error::new(
ErrorKind::EnvVarValueUnknown,
&format!("Unknown os {}", o),
))
}
};
Ok(goos.into())
}
fn println(&self, s: &str) {
if self.cargo_metadata {
println!("{}", s);
}
}
}
fn run(cmd: &mut Command, program: &str) -> Result<(), Error> {
let (mut child, print) = spawn(cmd, program)?;
let status = child.wait().map_err(|_| {
Error::new(
ErrorKind::ToolExecError,
&format!(
"Failed to wait on spawned child process, command {:?} with args {:?}",
cmd, program
),
)
})?;
print.join().unwrap();
println!("{}", status);
if status.success() {
Ok(())
} else {
Err(Error::new(
ErrorKind::ToolExecError,
&format!(
"Command {:?} with args {:?} did not execute successfully (status code {}).",
cmd, program, status
),
))
}
}
fn spawn(cmd: &mut Command, program: &str) -> Result<(Child, JoinHandle<()>), Error> {
match cmd.stderr(process::Stdio::piped()).spawn() {
Ok(mut child) => {
let stderr = BufReader::new(child.stderr.take().unwrap());
let print = thread::spawn(move || {
for line in stderr.split(b'\n').filter_map(|l| l.ok()) {
print!("cargo:warning=");
io::stdout().write_all(&line).unwrap();
println!();
}
});
Ok((child, print))
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Err(Error::new(
ErrorKind::ToolNotFound,
&format!("Failed to find tool. Is {} installed?", program),
)),
Err(_) => Err(Error::new(
ErrorKind::ToolExecError,
&format!("Command {:?} with args {:?} failed to start.", cmd, program),
)),
}
}
fn fail(s: &str) -> ! {
let _ = writeln!(io::stderr(), "\n\nerror occurred: {}\n\n", s);
std::process::exit(1);
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}