pub mod opt;
pub use bindgen;
use std::collections::BTreeSet;
use std::env;
use std::fmt::Display;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use regex::Regex;
use semver::{BuildMetadata, Prerelease, Version};
pub use crate::opt::{
Addressing, Architecture, MathLib, OptimizationOpt, TargetISA, TargetOS, CPU,
};
pub fn compile_library(lib: &str, files: &[&str]) {
let mut cfg = Config::new();
for f in files {
cfg.file(*f);
}
cfg.compile(lib)
}
macro_rules! exit_failure {
($fmt:expr) => {{
eprintln!($fmt);
std::process::exit(libc::EXIT_FAILURE);
}};
($fmt:expr, $($arg:tt)*) => {{
eprintln!($fmt, $($arg)*);
std::process::exit(libc::EXIT_FAILURE);
}}
}
pub struct Config {
ispc_version: Version,
ispc_files: Vec<PathBuf>,
include_paths: Vec<PathBuf>,
out_dir: Option<PathBuf>,
debug: Option<bool>,
opt_level: Option<u32>,
target: Option<String>,
cargo_metadata: bool,
defines: Vec<(String, Option<String>)>,
math_lib: MathLib,
addressing: Option<Addressing>,
optimization_opts: BTreeSet<OptimizationOpt>,
cpu_target: Option<CPU>,
force_alignment: Option<u32>,
no_omit_frame_ptr: bool,
no_stdlib: bool,
no_cpp: bool,
quiet: bool,
werror: bool,
woff: bool,
wno_perf: bool,
instrument: bool,
enable_llvm_intrinsics: bool,
target_isa: Option<Vec<TargetISA>>,
architecture: Option<Architecture>,
target_os: Option<TargetOS>,
bindgen_builder: bindgen::Builder,
}
impl Config {
pub fn new() -> Config {
let cmd_output = Command::new("ispc")
.arg("--version")
.output()
.expect("Failed to find ISPC compiler in PATH");
if !cmd_output.status.success() {
exit_failure!("Failed to get ISPC version, is it in your PATH?");
}
let ver_string = String::from_utf8_lossy(&cmd_output.stdout);
let re = Regex::new(r"(\d+\.\d+\.\d+)").unwrap();
let ispc_ver = Version::parse(
re.captures_iter(&ver_string)
.next()
.expect("Failed to parse ISPC version")
.get(1)
.expect("Failed to parse ISPC version")
.as_str(),
)
.expect("Failed to parse ISPC version");
Config {
ispc_version: ispc_ver,
ispc_files: Vec::new(),
include_paths: Vec::new(),
out_dir: None,
debug: None,
opt_level: None,
target: None,
cargo_metadata: true,
defines: Vec::new(),
math_lib: MathLib::ISPCDefault,
addressing: None,
optimization_opts: BTreeSet::new(),
cpu_target: None,
force_alignment: None,
no_omit_frame_ptr: false,
no_stdlib: false,
no_cpp: false,
quiet: false,
werror: false,
woff: false,
wno_perf: false,
instrument: false,
enable_llvm_intrinsics: false,
target_isa: None,
architecture: None,
target_os: None,
bindgen_builder: Default::default(),
}
}
pub fn file<P: AsRef<Path>>(&mut self, file: P) -> &mut Config {
self.ispc_files.push(file.as_ref().to_path_buf());
self
}
pub fn out_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Config {
self.out_dir = Some(dir.as_ref().to_path_buf());
self
}
pub fn debug(&mut self, debug: bool) -> &mut Config {
self.debug = Some(debug);
self
}
pub fn opt_level(&mut self, opt_level: u32) -> &mut Config {
self.opt_level = Some(opt_level);
self
}
pub fn target(&mut self, target: &str) -> &mut Config {
self.target = Some(target.to_string());
self
}
pub fn add_define(&mut self, define: &str, value: Option<&str>) -> &mut Config {
self.defines
.push((define.to_string(), value.map(|s| s.to_string())));
self
}
pub fn addressing(&mut self, addressing: Addressing) -> &mut Config {
self.addressing = Some(addressing);
self
}
pub fn math_lib(&mut self, math_lib: MathLib) -> &mut Config {
self.math_lib = math_lib;
self
}
pub fn optimization_opt(&mut self, opt: OptimizationOpt) -> &mut Config {
self.optimization_opts.insert(opt);
self
}
pub fn cpu(&mut self, cpu: CPU) -> &mut Config {
self.cpu_target = Some(cpu);
self
}
pub fn force_alignment(&mut self, alignment: u32) -> &mut Config {
self.force_alignment = Some(alignment);
self
}
pub fn include_path<P: AsRef<Path>>(&mut self, path: P) -> &mut Config {
self.include_paths.push(path.as_ref().to_path_buf());
self
}
pub fn no_omit_frame_pointer(&mut self) -> &mut Config {
self.no_omit_frame_ptr = true;
self
}
pub fn no_stdlib(&mut self) -> &mut Config {
self.no_stdlib = true;
self
}
pub fn no_cpp(&mut self) -> &mut Config {
self.no_cpp = true;
self
}
pub fn quiet(&mut self) -> &mut Config {
self.quiet = true;
self
}
pub fn werror(&mut self) -> &mut Config {
self.werror = true;
self
}
pub fn woff(&mut self) -> &mut Config {
self.woff = true;
self
}
pub fn wno_perf(&mut self) -> &mut Config {
self.wno_perf = true;
self
}
pub fn instrument(&mut self) -> &mut Config {
let min_ver = Version {
major: 1,
minor: 9,
patch: 1,
pre: Prerelease::EMPTY,
build: BuildMetadata::EMPTY,
};
if self.ispc_version < min_ver {
exit_failure!(
"Error: instrumentation is not supported on ISPC versions \
older than 1.9.1 as it generates a non-C compatible header"
);
}
self.instrument = true;
self
}
pub fn enable_llvm_intrinsics(&mut self) -> &mut Config {
self.enable_llvm_intrinsics = true;
self
}
pub fn target_isa(&mut self, target: TargetISA) -> &mut Config {
self.target_isa = Some(vec![target]);
self
}
pub fn target_isas(&mut self, targets: Vec<TargetISA>) -> &mut Config {
self.target_isa = Some(targets);
self
}
pub fn target_arch(&mut self, arch: Architecture) -> &mut Config {
self.architecture = Some(arch);
self
}
pub fn target_os(&mut self, os: TargetOS) -> &mut Config {
self.target_os = Some(os);
self
}
pub fn cargo_metadata(&mut self, metadata: bool) -> &mut Config {
self.cargo_metadata = metadata;
self
}
pub fn bindgen_builder(&mut self, builder: bindgen::Builder) -> &mut Self {
self.bindgen_builder = builder;
self
}
pub fn compile(&self, lib: &str) {
let dst = self.get_out_dir();
let build_dir = self.get_build_dir();
let default_args = self.default_args();
let mut objects = vec![];
let mut headers = vec![];
for s in &self.ispc_files {
let fname = s
.file_stem()
.expect("ISPC source files must be files")
.to_str()
.expect("ISPC source file names must be valid UTF-8");
self.print(&format!("cargo:rerun-if-changed={}", s.display()));
let ispc_fname = String::from(fname) + "_ispc";
let object = build_dir.join(ispc_fname.clone()).with_extension("o");
let header = build_dir.join(ispc_fname.clone()).with_extension("h");
let deps = build_dir.join(ispc_fname.clone()).with_extension("idep");
let output = Command::new("ispc")
.args(&default_args)
.arg(s)
.arg("-o")
.arg(&object)
.arg("-h")
.arg(&header)
.arg("-MMM")
.arg(&deps)
.output()
.unwrap();
if !output.stderr.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
for l in stderr.lines() {
self.print(&format!("cargo:warning=(ISPC) {l}"));
}
}
if !output.status.success() {
exit_failure!("Failed to compile ISPC source file {}", s.display());
}
objects.push(object);
headers.push(header);
let deps_list = File::open(deps)
.unwrap_or_else(|_| panic!("Failed to open dependencies list for {}", s.display()));
let reader = BufReader::new(deps_list);
for d in reader.lines() {
let dep_name = d.unwrap();
self.print(&format!("cargo:rerun-if-changed={dep_name}"));
}
if let Some(ref t) = self.target_isa {
if t.len() > 1 {
for isa in t.iter() {
let isa_fname = ispc_fname.clone() + "_" + &isa.lib_suffix();
let isa_obj = build_dir.join(isa_fname).with_extension("o");
objects.push(isa_obj);
}
}
}
}
let libfile = lib.to_owned() + &self.get_target();
if !self.assemble(&libfile, &objects).success() {
exit_failure!("Failed to assemble ISPC objects into library {lib}");
}
self.print(&format!("cargo:rustc-link-lib=static={libfile}"));
let bindgen_header = self.generate_bindgen_header(lib, &headers);
let bindings = self
.bindgen_builder
.clone()
.header(bindgen_header.to_str().unwrap());
let bindgen_file = dst.join(lib).with_extension("rs");
let generated_bindings = match bindings.generate() {
Ok(b) => b.to_string(),
Err(_) => exit_failure!("Failed to generating Rust bindings to {}", lib),
};
let mut file = match File::create(bindgen_file) {
Ok(f) => f,
Err(e) => exit_failure!("Failed to open bindgen mod file for writing: {}", e),
};
file.write_all("#[allow(non_camel_case_types,dead_code,non_upper_case_globals,non_snake_case,improper_ctypes)]\n"
.as_bytes()).unwrap();
file.write_all(format!("pub mod {lib} {{\n").as_bytes())
.unwrap();
file.write_all(generated_bindings.as_bytes()).unwrap();
file.write_all(b"}").unwrap();
self.print(&format!("cargo:rustc-link-search=native={}", dst.display()));
self.print(&format!("cargo:rustc-env=ISPC_OUT_DIR={}", dst.display()));
}
pub fn ispc_version(&self) -> &Version {
&self.ispc_version
}
#[cfg(unix)]
fn assemble(&self, lib: &str, objects: &[PathBuf]) -> ExitStatus {
Command::new("ar")
.arg("crus")
.arg(format!("lib{lib}.a"))
.args(objects)
.current_dir(self.get_out_dir())
.status()
.unwrap()
}
#[cfg(windows)]
fn assemble(&self, lib: &str, objects: &[PathBuf]) -> ExitStatus {
let target = self.get_target();
let mut lib_cmd = cc::windows_registry::find_tool(&target, "lib.exe")
.expect("Failed to find lib.exe for MSVC toolchain, aborting")
.to_command();
lib_cmd
.arg(format!("/OUT:{lib}.lib"))
.args(objects)
.current_dir(self.get_out_dir())
.status()
.unwrap()
}
fn generate_bindgen_header(&self, lib: &str, headers: &[PathBuf]) -> PathBuf {
let bindgen_header = self
.get_build_dir()
.join(format!("_{lib}_ispc_bindgen_header.h"));
let mut include_file = File::create(&bindgen_header).unwrap();
writeln!(include_file, "#include <stdint.h>").unwrap();
writeln!(include_file, "#include <stdbool.h>").unwrap();
for h in headers {
writeln!(include_file, "#include \"{}\"", h.display()).unwrap();
}
bindgen_header
}
fn default_args(&self) -> Vec<String> {
let mut ispc_args = Vec::new();
let opt_level = self.get_opt_level();
if self.get_debug() {
ispc_args.push(String::from("-g"));
}
if let Some(ref c) = self.cpu_target {
ispc_args.push(c.to_string());
if *c != CPU::Generic || (*c == CPU::Generic && opt_level != 0) {
ispc_args.push(String::from("-O") + &opt_level.to_string());
} else {
self.print(
&"cargo:warning=ispc-rs: Omitting -O0 on CPU::Generic target, ispc bug 1223",
);
}
} else {
ispc_args.push(String::from("-O") + &opt_level.to_string());
}
if cfg!(unix) {
ispc_args.push(String::from("--pic"));
}
let target = self.get_target();
if target.starts_with("i686") {
ispc_args.push(String::from("--arch=x86"));
} else if target.starts_with("x86_64") {
ispc_args.push(String::from("--arch=x86-64"));
} else if target.starts_with("aarch64") {
ispc_args.push(String::from("--arch=aarch64"));
}
for d in &self.defines {
match d.1 {
Some(ref v) => ispc_args.push(format!("-D{}={}", d.0, v)),
None => ispc_args.push(format!("-D{}", d.0)),
}
}
ispc_args.push(self.math_lib.to_string());
if let Some(ref s) = self.addressing {
ispc_args.push(s.to_string());
}
if let Some(ref f) = self.force_alignment {
ispc_args.push(String::from("--force-alignment=") + &f.to_string());
}
for o in &self.optimization_opts {
ispc_args.push(o.to_string());
}
for p in &self.include_paths {
ispc_args.push(format!("-I{}", p.display()));
}
if self.no_omit_frame_ptr {
ispc_args.push(String::from("--no-omit-frame-pointer"));
}
if self.no_stdlib {
ispc_args.push(String::from("--nostdlib"));
}
if self.no_cpp {
ispc_args.push(String::from("--nocpp"));
}
if self.quiet {
ispc_args.push(String::from("--quiet"));
}
if self.werror {
ispc_args.push(String::from("--werror"));
}
if self.woff {
ispc_args.push(String::from("--woff"));
}
if self.wno_perf {
ispc_args.push(String::from("--wno-perf"));
}
if self.instrument {
ispc_args.push(String::from("--instrument"));
}
if self.enable_llvm_intrinsics {
ispc_args.push(String::from("--enable-llvm-intrinsics"));
}
if let Some(ref t) = self.target_isa {
let mut isa_str = String::from("--target=");
isa_str.push_str(&t[0].to_string());
for isa in t.iter().skip(1) {
isa_str.push_str(&format!(",{}", isa));
}
ispc_args.push(isa_str);
} else if target.starts_with("aarch64") {
ispc_args.push(String::from("--target=neon-i32x4"));
}
if let Some(ref a) = self.architecture {
ispc_args.push(a.to_string());
}
if let Some(ref o) = self.target_os {
ispc_args.push(o.to_string());
}
ispc_args
}
fn get_out_dir(&self) -> PathBuf {
let p = self
.out_dir
.clone()
.unwrap_or_else(|| env::var_os("OUT_DIR").map(PathBuf::from).unwrap());
if p.is_relative() {
env::current_dir().unwrap().join(p)
} else {
p
}
}
fn get_build_dir(&self) -> PathBuf {
env::var_os("OUT_DIR").map(PathBuf::from).unwrap()
}
fn get_debug(&self) -> bool {
self.debug
.unwrap_or_else(|| env::var("DEBUG").map(|x| x == "true").unwrap())
}
fn get_opt_level(&self) -> u32 {
self.opt_level.unwrap_or_else(|| {
let opt = env::var("OPT_LEVEL").unwrap();
opt.parse::<u32>().unwrap()
})
}
fn get_target(&self) -> String {
self.target
.clone()
.unwrap_or_else(|| env::var("TARGET").unwrap())
}
fn print<T: Display>(&self, s: &T) {
if self.cargo_metadata {
println!("{s}");
}
}
}
impl Default for Config {
fn default() -> Config {
Config::new()
}
}