#![forbid(unsafe_code)]
#![allow(clippy::needless_doctest_main)]
use std::{
env,
ffi::{OsStr, OsString},
fmt::Write,
path::{Path, PathBuf},
process,
};
#[derive(Clone, Debug)]
pub struct Build {
build_mode: BuildMode,
cargo_metadata: bool,
change_dir: Option<PathBuf>,
goarch: Option<String>,
goos: Option<String>,
ldflags: Option<OsString>,
module_mode: Option<ModuleMode>,
out_dir: Option<PathBuf>,
packages: Vec<PathBuf>,
trimpath: bool,
}
impl Default for Build {
fn default() -> Self {
Self::new()
}
}
impl Build {
pub fn new() -> Self {
Build {
build_mode: BuildMode::default(),
cargo_metadata: true,
change_dir: None,
goarch: None,
goos: None,
ldflags: None,
module_mode: None,
out_dir: None,
packages: Vec::default(),
trimpath: false,
}
}
pub fn build_mode(&mut self, build_mode: BuildMode) -> &mut Self {
self.build_mode = build_mode;
self
}
pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Self {
self.cargo_metadata = cargo_metadata;
self
}
pub fn change_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
self.change_dir = Some(dir.as_ref().to_owned());
self
}
pub fn goarch(&mut self, goarch: &str) -> &mut Self {
self.goarch = Some(goarch.to_owned());
self
}
pub fn goos(&mut self, goos: &str) -> &mut Self {
self.goos = Some(goos.to_owned());
self
}
pub fn ldflags<P: AsRef<OsStr>>(&mut self, ldflags: P) -> &mut Self {
self.ldflags = Some(ldflags.as_ref().to_os_string());
self
}
pub fn module_mode(&mut self, module_mode: ModuleMode) -> &mut Self {
self.module_mode = Some(module_mode);
self
}
pub fn out_dir<P: AsRef<Path>>(&mut self, out_dir: P) -> &mut Self {
self.out_dir = Some(out_dir.as_ref().to_owned());
self
}
pub fn package<P: AsRef<Path>>(&mut self, package: P) -> &mut Self {
self.packages.push(package.as_ref().to_owned());
self
}
pub fn trimpath(&mut self, trimpath: bool) -> &mut Self {
self.trimpath = trimpath;
self
}
pub fn build(&self, output: &str) {
if let Err(err) = self.try_build(output) {
eprintln!("\n\nerror occurred: {}\n", err);
process::exit(1);
}
}
pub fn try_build(&self, output: &str) -> Result<(), Error> {
let goarch = match &self.goarch {
None => goarch_from_env()?,
Some(goarch) => goarch.to_owned(),
};
let goos = match &self.goos {
None => goos_from_env()?,
Some(goos) => goos.to_owned(),
};
let lib_name = self.format_lib_name(output);
let out_dir = match &self.out_dir {
Some(out_dir) => out_dir.clone(),
None => get_env_var("OUT_DIR")?.into(),
};
let out_path = out_dir.join(lib_name);
let mut cmd = process::Command::new("go");
cmd.env("CGO_ENABLED", "1")
.env("GOOS", goos)
.env("GOARCH", goarch)
.env("CC", get_cc())
.env("CXX", get_cxx())
.arg("build");
if let Some(change_dir) = &self.change_dir {
cmd.args([&"-C".into(), change_dir]);
}
if let Some(ldflags) = &self.ldflags {
cmd.args([&"-ldflags".into(), ldflags]);
}
if let Some(module_mode) = &self.module_mode {
cmd.args(["-mod", &module_mode.to_string()]);
}
if self.trimpath {
cmd.arg("-trimpath");
}
cmd.args(["-buildmode", &self.build_mode.to_string()]);
cmd.args(["-o".into(), out_path]);
for package in &self.packages {
cmd.arg(package);
}
let build_output = match cmd.output() {
Ok(build_output) => build_output,
Err(err) => {
return Err(Error::new(
ErrorKind::ToolExecError,
&format!("failed to execute go command: {}", err),
));
}
};
if self.cargo_metadata {
let link_kind = match self.build_mode {
BuildMode::CArchive => "static",
BuildMode::CShared => "dylib",
};
println!("cargo:rustc-link-lib={}={}", link_kind, output);
println!("cargo:rustc-link-search=native={}", out_dir.display());
}
if build_output.status.success() {
return Ok(());
}
let mut message = format!(
"failed to build Go library ({}). Build output:",
build_output.status
);
let mut push_output = |stream_name, bytes| {
let string = String::from_utf8_lossy(bytes);
let string = string.trim();
if string.is_empty() {
return;
}
write!(&mut message, "\n=== {stream_name}:\n{string}").unwrap();
};
push_output("stdout", &build_output.stdout);
push_output("stderr", &build_output.stderr);
Err(Error::new(ErrorKind::ToolExecError, &message))
}
fn format_lib_name(&self, output: &str) -> PathBuf {
let mut lib = String::with_capacity(output.len() + 7);
lib.push_str("lib");
lib.push_str(output);
lib.push_str(match self.build_mode {
BuildMode::CArchive => {
if cfg!(windows) {
".lib"
} else {
".a"
}
}
BuildMode::CShared => {
if cfg!(windows) {
".dll"
} else {
".so"
}
}
});
lib.into()
}
}
#[derive(Clone, Debug, Default)]
pub enum BuildMode {
#[default]
CArchive,
CShared,
}
impl std::fmt::Display for BuildMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::CArchive => "c-archive",
Self::CShared => "c-shared",
})
}
}
#[derive(Clone, Debug)]
pub enum ModuleMode {
Mod,
ReadOnly,
Vendor,
}
impl std::fmt::Display for ModuleMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Mod => "mod",
Self::ReadOnly => "readonly",
Self::Vendor => "vendor",
})
}
}
#[derive(Clone, Debug)]
enum ErrorKind {
EnvVarNotFound,
InvalidGOARCH,
InvalidGOOS,
ToolExecError,
}
#[derive(Clone, Debug)]
pub struct Error {
kind: ErrorKind,
message: String,
}
impl Error {
fn new(kind: ErrorKind, message: &str) -> Self {
Error {
kind,
message: message.to_owned(),
}
}
}
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}: {}", self.kind, self.message)
}
}
fn get_cc() -> PathBuf {
cc::Build::new().get_compiler().path().to_path_buf()
}
fn get_cxx() -> PathBuf {
cc::Build::new()
.cpp(true)
.get_compiler()
.path()
.to_path_buf()
}
fn goarch_from_env() -> Result<String, Error> {
let target_arch = get_env_var("CARGO_CFG_TARGET_ARCH")?;
let goarch = match target_arch.as_str() {
"x86" => "386",
"x86_64" => "amd64",
"powerpc64" => "ppc64",
"aarch64" => "arm64",
"mips" | "mips64" | "arm" => &target_arch,
_ => {
return Err(Error::new(
ErrorKind::InvalidGOARCH,
&format!("unexpected target arch {}", target_arch),
))
}
};
Ok(goarch.to_string())
}
fn goos_from_env() -> Result<String, Error> {
let target_os = get_env_var("CARGO_CFG_TARGET_OS")?;
let goos = match target_os.as_str() {
"macos" => "darwin",
"windows" | "ios" | "linux" | "android" | "freebsd" | "dragonfly" | "openbsd"
| "netbsd" => &target_os,
_ => {
return Err(Error::new(
ErrorKind::InvalidGOOS,
&format!("unexpected target os {}", target_os),
))
}
};
Ok(goos.to_string())
}
fn get_env_var(key: &str) -> Result<String, Error> {
env::var(key).map_err(|_| {
Error::new(
ErrorKind::EnvVarNotFound,
&format!("could not find environment variable {}", key),
)
})
}