use cache::{
Cache,
Storage,
hash_key,
};
use compiler::{
clang,
gcc,
msvc,
};
use filetime::FileTime;
use futures::{self,Future};
use log::LogLevel::{Debug,Trace};
use mock_command::{
CommandChild,
CommandCreatorSync,
RunCommand,
exit_status,
};
use sha1;
use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt;
use std::fs::{self,File};
use std::io::prelude::*;
use std::io::{
self,
BufReader,
Error,
ErrorKind,
};
use std::path::Path;
use std::process::{self,Stdio};
use std::str;
use std::thread;
use std::time::Duration;
use tempdir::TempDir;
#[derive(Debug, PartialEq, Clone)]
pub enum CompilerKind {
Gcc,
Clang,
Msvc {
includes_prefix: String,
},
}
impl CompilerKind {
pub fn parse_arguments(&self, arguments: &[String]) -> CompilerArguments {
match *self {
CompilerKind::Gcc => gcc::parse_arguments(arguments, gcc::argument_takes_value),
CompilerKind::Clang => gcc::parse_arguments(arguments, clang::argument_takes_value),
CompilerKind::Msvc { .. } => msvc::parse_arguments(arguments),
}
}
pub fn preprocess<T : CommandCreatorSync>(&self, creator: T, compiler: &Compiler, parsed_args: &ParsedArguments, cwd: &str) -> io::Result<process::Output> {
match *self {
CompilerKind::Gcc | CompilerKind::Clang => {
gcc::preprocess(creator, compiler, parsed_args, cwd)
},
CompilerKind::Msvc { ref includes_prefix } => msvc::preprocess(creator, compiler, parsed_args, cwd, includes_prefix),
}
}
pub fn compile<T : CommandCreatorSync>(&self, creator: T, compiler: &Compiler, preprocessor_output: Vec<u8>, parsed_args: &ParsedArguments, cwd: &str) -> io::Result<(Cacheable, process::Output)> {
match *self {
CompilerKind::Gcc => gcc::compile(creator, compiler, preprocessor_output, parsed_args, cwd),
CompilerKind::Clang => clang::compile(creator, compiler, preprocessor_output, parsed_args, cwd),
CompilerKind::Msvc { .. } => msvc::compile(creator, compiler, preprocessor_output, parsed_args, cwd),
}
}
}
#[allow(dead_code)]
#[derive(Debug, PartialEq)]
pub struct ParsedArguments {
pub input: String,
pub extension: String,
pub depfile: Option<String>,
pub outputs: HashMap<&'static str, String>,
pub preprocessor_args: Vec<String>,
pub common_args: Vec<String>,
}
impl ParsedArguments {
pub fn output_file(&self) -> Cow<str> {
self.outputs.get("obj").and_then(|o| Path::new(o).file_name().map(|f| f.to_string_lossy())).unwrap_or(Cow::Borrowed("Unknown filename"))
}
}
#[derive(Debug, PartialEq)]
pub enum CompilerArguments {
Ok(ParsedArguments),
CannotCache,
NotCompilation,
}
#[derive(Clone)]
pub struct Compiler {
pub executable: String,
pub mtime: FileTime,
pub digest: String,
pub kind: CompilerKind,
}
#[derive(Debug, PartialEq)]
pub enum MissType {
Normal,
CacheReadError,
ForcedRecache,
}
pub struct CacheWriteInfo {
pub object_file: String,
pub duration: Duration,
}
pub type CacheWriteResult = Result<CacheWriteInfo, String>;
pub type CacheWriteFuture = futures::BoxFuture<CacheWriteResult, futures::Canceled>;
pub enum CompileResult {
Error,
CacheHit,
CacheMiss(MissType, CacheWriteFuture),
NotCacheable,
CompileFailed,
}
impl fmt::Debug for CompileResult {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
&CompileResult::Error => write!(f, "CompileResult::Error"),
&CompileResult::CacheHit => write!(f, "CompileResult::CacheHit"),
&CompileResult::CacheMiss(ref m, _) => write!(f, "CompileResult::CacheMiss({:?}, _)", m),
&CompileResult::NotCacheable => write!(f, "CompileResult::NotCacheable"),
&CompileResult::CompileFailed => write!(f, "CompileResult::CompileFailed"),
}
}
}
impl PartialEq<CompileResult> for CompileResult {
fn eq(&self, other: &CompileResult) -> bool {
match (self, other) {
(&CompileResult::Error, &CompileResult::Error) => true,
(&CompileResult::CacheHit, &CompileResult::CacheHit) => true,
(&CompileResult::CacheMiss(ref m, _), &CompileResult::CacheMiss(ref n, _)) => m == n,
(&CompileResult::NotCacheable, &CompileResult::NotCacheable) => true,
(&CompileResult::CompileFailed, &CompileResult::CompileFailed) => true,
_ => false,
}
}
}
#[derive(Debug, PartialEq)]
pub enum Cacheable {
Yes,
No,
}
#[derive(Debug, PartialEq)]
pub enum CacheControl {
Default,
ForceRecache,
}
impl Compiler {
pub fn new(executable: &str, kind: CompilerKind) -> io::Result<Compiler> {
let attr = try!(fs::metadata(executable));
let f = try!(File::open(executable));
let mut m = sha1::Sha1::new();
let mut reader = BufReader::new(f);
loop {
let mut buffer = [0; 1024];
let count = try!(reader.read(&mut buffer[..]));
if count == 0 {
break;
}
m.update(&buffer[..count]);
}
Ok(Compiler {
executable: executable.to_owned(),
mtime: FileTime::from_last_modification_time(&attr),
digest: m.digest().to_string(),
kind: kind,
})
}
pub fn parse_arguments(&self, arguments: &[String]) -> CompilerArguments {
if log_enabled!(Debug) {
let cmd_str = arguments.join(" ");
debug!("parse_arguments: `{}`", cmd_str);
}
let parsed_args = self.kind.parse_arguments(arguments);
match parsed_args {
CompilerArguments::Ok(_) => debug!("parse_arguments: Ok"),
CompilerArguments::CannotCache => debug!("parse_arguments: CannotCache"),
CompilerArguments::NotCompilation => debug!("parse_arguments: NotCompilation"),
};
parsed_args
}
pub fn get_cached_or_compile<T: CommandCreatorSync>(&self, creator: T, storage: &Storage, arguments: &[String], parsed_args: &ParsedArguments, cwd: &str, cache_control: CacheControl) -> io::Result<(CompileResult, process::Output)> {
let out_file = parsed_args.output_file();
if log_enabled!(Debug) {
let cmd_str = arguments.join(" ");
debug!("[{}]: get_cached_or_compile: {}", out_file, cmd_str);
}
let preprocessor_result = try!(self.kind.preprocess(creator.clone(), self, parsed_args, cwd).map_err(|e| { debug!("[{}]: preprocessor failed: {:?}", out_file, e); e }));
if !preprocessor_result.status.success() {
return Ok((CompileResult::Error, preprocessor_result));
}
trace!("[{}]: Preprocessor output is {} bytes", out_file, preprocessor_result.stdout.len());
let key = hash_key(self, arguments, &preprocessor_result.stdout);
trace!("[{}]: Hash key: {}", out_file, key);
let pwd = Path::new(cwd);
let outputs = parsed_args.outputs.iter()
.map(|(key, path)| (key, pwd.join(path)))
.collect::<HashMap<_, _>>();
let cache_status = if cache_control == CacheControl::ForceRecache {
Cache::Recache
} else {
storage.get(&key)
};
match cache_status {
Cache::Hit(mut entry) => {
debug!("[{}]: Cache hit!", out_file);
for (key, path) in &outputs {
let mut f = try!(File::create(path));
try!(entry.get_object(key, &mut f));
}
let mut stdout = io::Cursor::new(vec!());
let mut stderr = io::Cursor::new(vec!());
entry.get_object("stdout", &mut stdout).unwrap_or(());
entry.get_object("stderr", &mut stderr).unwrap_or(());
Ok((CompileResult::CacheHit,
process::Output {
status: exit_status(0),
stdout: stdout.into_inner(),
stderr: stderr.into_inner(),
}))
},
res @ Cache::Miss | res @ Cache::Recache | res @ Cache::Error(_) => {
let miss_type = match res {
Cache::Miss => { debug!("[{}]: Cache miss!", out_file); MissType::Normal }
Cache::Recache => { debug!("[{}]: Cache recache!", out_file); MissType::ForcedRecache }
Cache::Error(e) => {
debug!("[{}]: Cache read error: {:?}", out_file, e);
MissType::CacheReadError
}
Cache::Hit(_) => MissType::Normal,
};
let process::Output { stdout, .. } = preprocessor_result;
let (cacheable, compiler_result) = try!(self.kind.compile(creator, self, stdout, parsed_args, cwd));
if compiler_result.status.success() {
if cacheable == Cacheable::Yes {
debug!("[{}]: Compiled, storing in cache", out_file);
let mut entry = try!(storage.start_put(&key));
for (key, path) in &outputs {
let mut f = try!(File::open(&path));
try!(entry.put_object(key, &mut f)
.or_else(|e| {
error!("[{}]: Failed to put object `{:?}` in storage: {:?}", out_file, path, e);
Err(e)
}));
}
if !compiler_result.stdout.is_empty() {
try!(entry.put_object("stdout", &mut io::Cursor::new(&compiler_result.stdout)));
}
if !compiler_result.stderr.is_empty() {
try!(entry.put_object("stderr", &mut io::Cursor::new(&compiler_result.stderr)));
}
let out_file = out_file.into_owned();
let future = storage.finish_put(&key, entry)
.map(move |res| {
match res {
Ok(_) => debug!("[{}]: Stored in cache successfully!", out_file),
Err(ref e) => debug!("[{}]: Cache write error: {:?}", out_file, e),
}
res.map(|duration| CacheWriteInfo {
object_file: out_file,
duration: duration,
})
}).boxed();
Ok((CompileResult::CacheMiss(miss_type, future), compiler_result))
} else {
debug!("[{}]: Compiled but not cacheable", out_file);
Ok((CompileResult::NotCacheable, compiler_result))
}
} else {
debug!("[{}]: Compiled but failed, not storing in cache", out_file);
Ok((CompileResult::CompileFailed, compiler_result))
}
}
}
}
}
fn write_file(path : &Path, contents: &[u8]) -> io::Result<()> {
let mut f = try!(File::create(path));
f.write_all(contents)
}
pub fn detect_compiler_kind<T : CommandCreatorSync>(mut creator : T, executable : &str) -> Option<CompilerKind> {
trace!("detect_compiler");
TempDir::new("sccache")
.and_then(|dir| {
let src = dir.path().join("testfile.c");
try!(write_file(&src, b"#if defined(_MSC_VER)
msvc
#elif defined(__clang__)
clang
#elif defined(__GNUC__)
gcc
#endif
"));
let args = vec!(OsString::from("-E"), OsString::from(&src));
if log_enabled!(Trace) {
let va = args.iter().map(|a| a.to_str().unwrap()).collect::<Vec<&str>>();
trace!("compiler: {}, args: '{}'", executable, va.join(" "));
}
creator.new_command_sync(&executable)
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.and_then(|child| child.wait_with_output())
.and_then(|output| {
str::from_utf8(&output.stdout)
.or_else(|_| Err(Error::new(ErrorKind::Other, "Failed to parse output")))
.and_then(|stdout| {
for line in stdout.lines() {
if line == "gcc" {
debug!("Found GCC");
return Ok(Some(CompilerKind::Gcc));
} else if line == "clang" {
debug!("Found clang");
return Ok(Some(CompilerKind::Clang));
} else if line == "msvc" {
debug!("Found MSVC");
let prefix = try!(msvc::detect_showincludes_prefix(&mut creator, &executable));
trace!("showIncludes prefix: '{}'", prefix);
return Ok(Some(CompilerKind::Msvc {
includes_prefix: prefix,
}));
}
}
Ok(None)
})
})
}).unwrap_or_else(|e| {
warn!("Failed to run compiler: {}", e);
None
})
}
pub fn get_compiler_info<T : CommandCreatorSync>(creator : T, executable : &str) -> Option<Compiler> {
detect_compiler_kind(creator, executable)
.and_then(|kind| {
Compiler::new(executable, kind)
.or_else(|e| {
error!("Failed to create Compiler: {}", e);
Err(())
})
.ok()
})
}
pub fn wait_with_input_output<T: CommandChild + 'static>(mut child: T, input: Option<Vec<u8>>) -> io::Result<process::Output> {
let stdin = input.and_then(|i| {
child.take_stdin().map(|mut stdin| {
thread::spawn(move || {
stdin.write_all(&i)
})
})
});
fn read<R>(mut input: R) -> thread::JoinHandle<io::Result<Vec<u8>>>
where R: Read + Send + 'static
{
thread::spawn(move || {
let mut ret = Vec::new();
input.read_to_end(&mut ret).map(|_| ret)
})
}
stdin.and_then(|t| t.join().unwrap().ok());
let stdout = child.take_stdout().map(read);
let stderr = child.take_stderr().map(read);
let status = try!(child.wait());
let stdout = stdout.and_then(|t| t.join().unwrap().ok());
let stderr = stderr.and_then(|t| t.join().unwrap().ok());
Ok(process::Output {
status: status,
stdout: stdout.unwrap_or_default(),
stderr: stderr.unwrap_or_default(),
})
}
pub fn run_input_output<C: RunCommand>(mut command: C, input: Option<Vec<u8>>) -> io::Result<process::Output> {
command.stdin(if input.is_some() { Stdio::piped() } else { Stdio::inherit() })
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.and_then(|child| wait_with_input_output(child, input))
}
#[cfg(test)]
mod test {
use super::*;
use cache::disk::DiskCache;
use mock_command::*;
use std::fs::{self,File};
use std::io::Write;
use test::utils::*;
#[test]
fn test_detect_compiler_kind_gcc() {
let creator = new_creator();
next_command(&creator, Ok(MockChild::new(exit_status(0), "foo\nbar\ngcc", "")));
assert_eq!(Some(CompilerKind::Gcc), detect_compiler_kind(creator.clone(), "/foo/bar"));
}
#[test]
fn test_detect_compiler_kind_clang() {
let creator = new_creator();
next_command(&creator, Ok(MockChild::new(exit_status(0), "clang\nfoo", "")));
assert_eq!(Some(CompilerKind::Clang), detect_compiler_kind(creator.clone(), "/foo/bar"));
}
#[test]
fn test_detect_compiler_kind_msvc() {
use env_logger;
match env_logger::init() {
Ok(_) => {},
Err(_) => {},
}
let creator = new_creator();
let f = TestFixture::new();
let srcfile = f.touch("stdio.h").unwrap();
let mut s = srcfile.to_str().unwrap();
if s.starts_with("\\\\?\\") {
s = &s[4..];
}
let prefix = String::from("blah: ");
let stdout = format!("{}{}\r\n", prefix, s);
next_command(&creator, Ok(MockChild::new(exit_status(0), "foo\nmsvc\nbar", "")));
next_command(&creator, Ok(MockChild::new(exit_status(0), &stdout, &String::new())));
assert_eq!(Some(CompilerKind::Msvc { includes_prefix: prefix }), detect_compiler_kind(creator.clone(), "/foo/bar"));
}
#[test]
fn test_detect_compiler_kind_unknown() {
let creator = new_creator();
next_command(&creator, Ok(MockChild::new(exit_status(0), "something", "")));
assert_eq!(None, detect_compiler_kind(creator.clone(), "/foo/bar"));
}
#[test]
fn test_detect_compiler_kind_process_fail() {
let creator = new_creator();
next_command(&creator, Ok(MockChild::new(exit_status(1), "", "")));
assert_eq!(None, detect_compiler_kind(creator.clone(), "/foo/bar"));
}
#[test]
fn test_get_compiler_info() {
let creator = new_creator();
let f = TestFixture::new();
next_command(&creator, Ok(MockChild::new(exit_status(0), "gcc", "")));
let c = get_compiler_info(creator.clone(),
f.bins[0].to_str().unwrap()).unwrap();
assert_eq!(f.bins[0].to_str().unwrap(), c.executable);
assert_eq!("da39a3ee5e6b4b0d3255bfef95601890afd80709", c.digest);
assert_eq!(CompilerKind::Gcc, c.kind);
}
#[test]
fn test_compiler_get_cached_or_compile_uncached() {
use env_logger;
match env_logger::init() {
Ok(_) => {},
Err(_) => {},
}
let creator = new_creator();
let f = TestFixture::new();
let storage = DiskCache::new(&f.tempdir.path());
next_command(&creator, Ok(MockChild::new(exit_status(0), "gcc", "")));
let c = get_compiler_info(creator.clone(),
f.bins[0].to_str().unwrap()).unwrap();
next_command(&creator, Ok(MockChild::new(exit_status(0), "preprocessor output", "")));
const COMPILER_STDOUT : &'static [u8] = b"compiler stdout";
const COMPILER_STDERR : &'static [u8] = b"compiler stderr";
let obj = f.tempdir.path().join("foo.o");
let o = obj.clone();
next_command_calls(&creator, move || {
match File::create(&o)
.and_then(|mut f| f.write_all(b"file contents")) {
Ok(_) => Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR)),
Err(e) => Err(e),
}
});
let cwd = f.tempdir.path().to_str().unwrap();
let arguments = stringvec!["-c", "foo.c", "-o", "foo.o"];
let parsed_args = match c.parse_arguments(&arguments) {
CompilerArguments::Ok(parsed) => parsed,
o @ _ => panic!("Bad result from parse_arguments: {:?}", o),
};
let (cached, res) = c.get_cached_or_compile(creator.clone(), &storage, &arguments, &parsed_args, cwd, CacheControl::Default).unwrap();
assert_eq!(true, fs::metadata(&obj).and_then(|m| Ok(m.len() > 0)).unwrap());
assert!(match cached {
CompileResult::CacheMiss(MissType::Normal, _) => true,
_ => false,
});
assert_eq!(exit_status(0), res.status);
assert_eq!(COMPILER_STDOUT, res.stdout.as_slice());
assert_eq!(COMPILER_STDERR, res.stderr.as_slice());
fs::remove_file(&obj).unwrap();
next_command(&creator, Ok(MockChild::new(exit_status(0), "preprocessor output", "")));
let (cached, res) = c.get_cached_or_compile(creator.clone(), &storage, &arguments, &parsed_args, cwd, CacheControl::Default).unwrap();
assert_eq!(true, fs::metadata(&obj).and_then(|m| Ok(m.len() > 0)).unwrap());
assert_eq!(CompileResult::CacheHit, cached);
assert_eq!(exit_status(0), res.status);
assert_eq!(COMPILER_STDOUT, res.stdout.as_slice());
assert_eq!(COMPILER_STDERR, res.stderr.as_slice());
}
#[test]
fn test_compiler_get_cached_or_compile_cached() {
use env_logger;
match env_logger::init() {
Ok(_) => {},
Err(_) => {},
}
let creator = new_creator();
let f = TestFixture::new();
let storage = DiskCache::new(&f.tempdir.path());
next_command(&creator, Ok(MockChild::new(exit_status(0), "gcc", "")));
let c = get_compiler_info(creator.clone(),
f.bins[0].to_str().unwrap()).unwrap();
next_command(&creator, Ok(MockChild::new(exit_status(0), "preprocessor output", "")));
const COMPILER_STDOUT : &'static [u8] = b"compiler stdout";
const COMPILER_STDERR : &'static [u8] = b"compiler stderr";
let obj = f.tempdir.path().join("foo.o");
let o = obj.clone();
next_command_calls(&creator, move || {
match File::create(&o)
.and_then(|mut f| f.write_all(b"file contents")) {
Ok(_) => Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR)),
Err(e) => Err(e),
}
});
let cwd = f.tempdir.path().to_str().unwrap();
let arguments = stringvec!["-c", "foo.c", "-o", "foo.o"];
let parsed_args = match c.parse_arguments(&arguments) {
CompilerArguments::Ok(parsed) => parsed,
o @ _ => panic!("Bad result from parse_arguments: {:?}", o),
};
let (cached, res) = c.get_cached_or_compile(creator.clone(), &storage, &arguments, &parsed_args, cwd, CacheControl::Default).unwrap();
assert_eq!(true, fs::metadata(&obj).and_then(|m| Ok(m.len() > 0)).unwrap());
assert!(match cached {
CompileResult::CacheMiss(MissType::Normal, _) => true,
_ => false,
});
assert_eq!(exit_status(0), res.status);
assert_eq!(COMPILER_STDOUT, res.stdout.as_slice());
assert_eq!(COMPILER_STDERR, res.stderr.as_slice());
fs::remove_file(&obj).unwrap();
next_command(&creator, Ok(MockChild::new(exit_status(0), "preprocessor output", "")));
let (cached, res) = c.get_cached_or_compile(creator.clone(), &storage, &arguments, &parsed_args, cwd, CacheControl::Default).unwrap();
assert_eq!(true, fs::metadata(&obj).and_then(|m| Ok(m.len() > 0)).unwrap());
assert_eq!(CompileResult::CacheHit, cached);
assert_eq!(exit_status(0), res.status);
assert_eq!(COMPILER_STDOUT, res.stdout.as_slice());
assert_eq!(COMPILER_STDERR, res.stderr.as_slice());
}
#[test]
fn test_compiler_get_cached_or_compile_force_recache() {
use env_logger;
match env_logger::init() {
Ok(_) => {},
Err(_) => {},
}
let creator = new_creator();
let f = TestFixture::new();
let storage = DiskCache::new(&f.tempdir.path());
next_command(&creator, Ok(MockChild::new(exit_status(0), "gcc", "")));
let c = get_compiler_info(creator.clone(),
f.bins[0].to_str().unwrap()).unwrap();
const COMPILER_STDOUT: &'static [u8] = b"compiler stdout";
const COMPILER_STDERR: &'static [u8] = b"compiler stderr";
let obj = f.tempdir.path().join("foo.o");
for _ in 0..2 {
next_command(&creator, Ok(MockChild::new(exit_status(0), "preprocessor output", "")));
let o = obj.clone();
next_command_calls(&creator, move || {
match File::create(&o)
.and_then(|mut f| f.write_all(b"file contents")) {
Ok(_) => Ok(MockChild::new(exit_status(0), COMPILER_STDOUT, COMPILER_STDERR)),
Err(e) => Err(e),
}
});
}
let cwd = f.tempdir.path().to_str().unwrap();
let arguments = stringvec!["-c", "foo.c", "-o", "foo.o"];
let parsed_args = match c.parse_arguments(&arguments) {
CompilerArguments::Ok(parsed) => parsed,
o @ _ => panic!("Bad result from parse_arguments: {:?}", o),
};
let (cached, res) = c.get_cached_or_compile(creator.clone(), &storage, &arguments, &parsed_args, cwd, CacheControl::Default).unwrap();
assert_eq!(true, fs::metadata(&obj).and_then(|m| Ok(m.len() > 0)).unwrap());
assert!(match cached {
CompileResult::CacheMiss(MissType::Normal, _) => true,
_ => false,
});
assert_eq!(exit_status(0), res.status);
assert_eq!(COMPILER_STDOUT, res.stdout.as_slice());
assert_eq!(COMPILER_STDERR, res.stderr.as_slice());
fs::remove_file(&obj).unwrap();
let (cached, res) = c.get_cached_or_compile(creator.clone(), &storage, &arguments, &parsed_args, cwd, CacheControl::ForceRecache).unwrap();
assert_eq!(true, fs::metadata(&obj).and_then(|m| Ok(m.len() > 0)).unwrap());
assert!(match cached {
CompileResult::CacheMiss(MissType::ForcedRecache, _) => true,
_ => false,
});
assert_eq!(exit_status(0), res.status);
assert_eq!(COMPILER_STDOUT, res.stdout.as_slice());
assert_eq!(COMPILER_STDERR, res.stderr.as_slice());
}
}