use argparse::SpecialMap;
use error::{ErrorKind, Result, ResultExt};
use lookup::*;
use shim::move_gcov_files;
use utils::{CommandExt, clean_dir, set_executable};
use cov::IntoStringLossy;
use serde_json::from_reader;
use shell_escape::escape;
use tempfile::TempDir;
use std::borrow::Cow;
use std::collections::HashMap;
use std::env::current_exe;
use std::ffi::{OsStr, OsString};
use std::fs::{File, canonicalize, create_dir, create_dir_all};
use std::io::{self, Write};
use std::iter::once;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
include!(concat!(env!("OUT_DIR"), "/host.rs"));
#[derive(Debug)]
pub struct Cargo<'a> {
cargo_path: OsString,
rustc_path: String,
rustdoc_path: String,
manifest_path: PathBuf,
cov_build_path: PathBuf,
target: &'a str,
profiler_lib_path: Cow<'static, str>,
profiler_lib_name: Cow<'a, str>,
forward_args: Vec<&'a OsStr>,
workspace_packages: Vec<String>,
}
impl<'a> Cargo<'a> {
pub fn new(special_args: SpecialMap<'a>, forward_args: Vec<&'a OsStr>) -> Result<Cargo<'a>> {
let cargo_path = find_cargo();
let rustc_path = find_rustc("RUSTC");
let rustdoc_path = find_rustc("RUSTDOC");
let manifest_path = match special_args.get("manifest-path") {
Some(p) => canonicalize(p)?,
None => locate_project(&cargo_path).chain_err(|| "Cargo.toml not found")?,
};
let metadata = parse_metadata(&cargo_path, &manifest_path).chain_err(|| "Cannot parse workspace metadata")?;
let mut cov_build_path = metadata.target_directory.or_else(|| find_target_path(&manifest_path)).ok_or(ErrorKind::TargetDirectoryNotFound)?;
cov_build_path.push("cov");
cov_build_path.push("build");
create_dir_all(&cov_build_path).chain_err(|| "Cannot prepare coverage build directory")?;
let mut workspace_packages = metadata.workspace_members;
for pkg_id in &mut workspace_packages {
let space_index = pkg_id.find(' ').unwrap_or_else(|| pkg_id.len());
pkg_id.truncate(space_index);
}
let target = special_args.get("target").and_then(|s| s.to_str()).unwrap_or(HOST);
let (profiler_lib_path, profiler_lib_name) = match special_args.get("profiler") {
Some(&path) => {
let (p, n) = split_profiler_lib(Path::new(path)).chain_err(|| "Cannot parse user-provided profiler library")?;
(Cow::Owned(canonicalize(p)?.into_string_lossy()), Cow::Borrowed(n))
},
None => {
if supports_built_in_profiler(&rustc_path, target) {
(Cow::Borrowed("@native"), Cow::Borrowed("@native"))
} else {
let (p, n) = find_native_profiler_lib(target).chain_err(|| "Native profiler library not found")?;
(Cow::Owned(canonicalize(p)?.into_string_lossy()), Cow::Owned(n))
}
},
};
debug!("Profiler: -L {} -l {}", profiler_lib_path, profiler_lib_name);
Ok(Cargo {
cargo_path,
rustc_path,
rustdoc_path,
manifest_path,
cov_build_path,
target,
profiler_lib_path,
profiler_lib_name,
forward_args,
workspace_packages,
})
}
pub fn into_cov_build_path(self) -> PathBuf {
self.cov_build_path
}
fn prepare_cov_build_path(&self) -> Result<()> {
let self_path = match current_exe() {
Ok(path) => escape(Cow::Owned(path.into_string_lossy())).into_owned(),
Err(_) => "cargo-cov".to_owned(),
};
create_dir_all(self.cov_build_path.join("gcno"))?;
create_dir_all(self.cov_build_path.join("gcda"))?;
let rustc_shim = self.write_shim(&self_path, "rustc-shim.bat")?;
let rustdoc_shim = self.write_shim(&self_path, "rustdoc-shim.bat")?;
let test_runner = self.write_shim(&self_path, "test-runner.bat")?;
let test_runner_slice: &[&Path] = &[&test_runner];
let target = if let Ok(true) = supports_target_runner(&self.cargo_path) {
once((
self.target,
CargoConfigTarget {
runner: test_runner_slice,
},
)).collect()
} else {
HashMap::new()
};
let config_bytes = ::toml::to_vec(&CargoConfig {
build: CargoConfigBuild {
target_dir: ".",
rustc: &rustc_shim,
rustdoc: &rustdoc_shim,
incremental: false,
},
target,
})?;
let mut config_path = self.cov_build_path.join(".cargo");
create_dir_all(&config_path)?;
config_path.push("config");
let mut config = File::create(config_path)?;
config.write_all(&config_bytes)?;
Ok(())
}
fn write_shim(&self, self_path: &str, shim_name: &str) -> io::Result<PathBuf> {
#[cfg(unix)]
const HEADER: &str = "#!/bin/sh";
#[cfg(unix)]
const FORWARD_ARGS: &str = "\"$@\"";
#[cfg(windows)]
const HEADER: &str = "@echo off";
#[cfg(windows)]
const FORWARD_ARGS: &str = "%*";
let shim_path = self.cov_build_path.join(shim_name);
let mut file = File::create(&shim_path)?;
set_executable(&file)?;
write!(file, "{}\n{} {} {}", HEADER, self_path, shim_name, FORWARD_ARGS)?;
Ok(shim_path)
}
}
bitflags! {
pub struct CleanTargets: u8 {
const BUILD_GCDA = 1;
const BUILD_GCNO = 2;
const BUILD_EXTERNAL = 4;
const REPORT = 8;
}
}
impl<'a> Cargo<'a> {
pub fn forward(self, subcommand: &str) -> Result<()> {
self.prepare_cov_build_path()?;
let mut cmd = Command::new(self.cargo_path);
cmd.current_dir(&self.cov_build_path)
.env("COV_RUSTC", self.rustc_path)
.env("COV_RUSTDOC", self.rustdoc_path)
.env("COV_BUILD_PATH", &self.cov_build_path)
.env("COV_PROFILER_LIB_PATH", &*self.profiler_lib_path)
.env("COV_PROFILER_LIB_NAME", &*self.profiler_lib_name)
.arg(subcommand)
.arg("--manifest-path")
.arg(self.manifest_path);
if self.target != HOST {
cmd.args(&["--target", self.target]);
}
cmd.args(self.forward_args);
progress!("Delegate", "{:?}", cmd);
cmd.ensure_success("cargo")?;
if subcommand == "test" || subcommand == "run" {
move_gcov_files(&self.cov_build_path, OsStr::new("gcda"))?;
}
Ok(())
}
pub fn clean(&self, clean_targets: CleanTargets) -> Result<()> {
fn do_clean(folder: &Path) -> Result<()> {
progress!("Remove", "{}", folder.display());
clean_dir(folder)?;
Ok(())
}
if clean_targets.contains(CleanTargets::BUILD_EXTERNAL) {
do_clean(&self.cov_build_path)?;
} else {
if clean_targets.contains(CleanTargets::BUILD_GCDA) {
do_clean(&self.cov_build_path.join("gcda"))?
}
if clean_targets.contains(CleanTargets::BUILD_GCNO) {
let mut cmd = Command::new(&self.cargo_path);
cmd.current_dir(&self.cov_build_path)
.env("RUSTC", &self.rustc_path) .args(&["clean", "--manifest-path"])
.arg(&self.manifest_path);
if self.target != HOST {
cmd.args(&["--target", self.target]);
}
for pkg_name in &self.workspace_packages {
cmd.args(&["-p", pkg_name]);
}
progress!("Delegate", "{:?}", cmd);
cmd.ensure_success("cargo")?;
do_clean(&self.cov_build_path.join("gcno"))?
}
}
if clean_targets.contains(CleanTargets::REPORT) {
do_clean(&self.cov_build_path.with_file_name("report"))?;
}
Ok(())
}
}
fn locate_project(cargo_path: &OsStr) -> Result<PathBuf> {
let child = Command::new(cargo_path) .stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.arg("locate-project")
.spawn()?;
let project_location: ProjectLocation = from_reader(child.stdout.expect("stdout"))?;
Ok(project_location.root)
}
#[derive(Debug, Deserialize)]
struct ProjectLocation {
root: PathBuf,
}
#[derive(Debug, Deserialize)]
struct Metadata {
workspace_members: Vec<String>,
target_directory: Option<PathBuf>,
}
fn parse_metadata(cargo_path: &OsStr, manifest_path: &Path) -> io::Result<Metadata> {
let child = Command::new(cargo_path) .stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.args(&["metadata", "--no-deps", "--format-version", "1", "--manifest-path"])
.arg(manifest_path)
.spawn()?;
let metadata = from_reader(child.stdout.expect("stdout"))?;
Ok(metadata)
}
fn find_target_path(manifest_path: &Path) -> Option<PathBuf> {
let mut base = manifest_path.to_owned();
while base.pop() {
base.push("target");
if base.is_dir() {
return Some(base);
}
base.set_file_name("Cargo.lock");
let has_cargo_lock = base.is_file();
if has_cargo_lock {
base.set_file_name("target");
return Some(base);
}
base.pop();
}
None
}
#[derive(Debug, Serialize)]
struct CargoConfig<'a> {
target: HashMap<&'a str, CargoConfigTarget<'a>>,
build: CargoConfigBuild<'a>,
}
#[derive(Debug, Serialize)]
struct CargoConfigBuild<'a> {
#[serde(rename = "target-dir")]
target_dir: &'a str,
rustc: &'a Path,
rustdoc: &'a Path,
incremental: bool,
}
#[derive(Debug, Serialize)]
struct CargoConfigTarget<'a> {
runner: &'a [&'a Path],
}
fn supports_built_in_profiler(rustc: &str, target: &str) -> bool {
let dir = TempDir::new().expect("created temporary directory");
let result = Command::new(rustc)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.args(&[
"-",
"-Zprofile",
"--crate-name",
"___",
"--crate-type",
"lib",
"--target",
target,
"--out-dir",
])
.arg(dir.path())
.status()
.map(|s| s.success())
.unwrap_or(false);
debug!("supports_built_in_profiler({:?}, {:?}) = {}", rustc, target, result);
result
}
fn supports_target_runner(cargo: &OsStr) -> Result<bool> {
use std::io::Write;
let dir = TempDir::new()?;
let mut cargo_config_path = dir.path().join(".cargo");
create_dir(&cargo_config_path)?;
cargo_config_path.push("config");
let mut file = File::create(cargo_config_path)?;
write!(file, "[target.{}]\nrunner = \"echo\"", HOST)?;
drop(file);
let manifest_path = dir.path().join("Cargo.toml");
let mut file = File::create(manifest_path)?;
write!(
file,
r#"
#![cfg(test)] /* Note: this file doubles as `Cargo.toml` and `lib.rs`
[package]
name = "check_runner"
version = "0.0.0"
[lib]
path = "Cargo.toml"
# */
"#
)?;
drop(file);
let result = Command::new(cargo) .stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.current_dir(dir.path())
.arg("build")
.status()
.map(|s| s.success())
.unwrap_or(false);
debug!("supports_target_runner({:?}) = {}", cargo, result);
Ok(result)
}