use std::{
ffi::OsString,
io::{self, Write},
path::PathBuf,
};
use anyhow::{bail, Result};
use camino::Utf8PathBuf;
use crate::{
cargo::Workspace,
cli::{self, Args, Subcommand},
env,
metadata::{Metadata, PackageId},
process::ProcessBuilder,
regex_vec::{RegexVec, RegexVecBuilder},
term,
};
pub(crate) struct Context {
pub(crate) ws: Workspace,
pub(crate) args: Args,
pub(crate) workspace_members: WorkspaceMembers,
pub(crate) build_script_re: RegexVec,
pub(crate) current_dir: PathBuf,
pub(crate) current_exe: PathBuf,
pub(crate) llvm_cov: PathBuf,
pub(crate) llvm_profdata: PathBuf,
pub(crate) llvm_cov_flags: Option<String>,
pub(crate) llvm_profdata_flags: Option<String>,
}
impl Context {
pub(crate) fn new(mut args: Args) -> Result<Self> {
let show_env = args.subcommand == Subcommand::ShowEnv;
let ws = Workspace::new(&args.manifest, args.target.as_deref(), args.doctests, show_env)?;
cli::merge_config_to_args(&ws, &mut args.target, &mut args.verbose, &mut args.color);
term::set_coloring(&mut args.color);
term::verbose::set(args.verbose != 0);
args.cov.html |= args.cov.open;
if args.cov.output_dir.is_some() && !args.cov.show() {
args.cov.output_dir = None;
}
{
let _guard = term::warn::ignore();
if args.cov.disable_default_ignore_filename_regex {
warn!("--disable-default-ignore-filename-regex option is unstable");
}
if args.doc {
warn!("--doc option is unstable");
} else if args.doctests {
warn!("--doctests option is unstable");
}
}
if args.target.is_some() {
info!(
"when --target option is used, coverage for proc-macro and build script will \
not be displayed because cargo does not pass RUSTFLAGS to them"
);
}
if args.cov.output_dir.is_none() && args.cov.html {
args.cov.output_dir = Some(ws.output_dir.clone());
}
if !matches!(args.subcommand, Subcommand::Report | Subcommand::Clean)
&& env::var_os("CARGO_LLVM_COV_SHOW_ENV").is_some()
{
warn!(
"cargo-llvm-cov subcommands other than report and clean may not work correctly \
in context where environment variables are set by show-env; consider using \
normal {} commands",
if args.subcommand == Subcommand::Nextest { "cargo-nextest" } else { "cargo" }
);
}
let mut rustlib: Utf8PathBuf = ws.rustc_print("target-libdir")?.into();
rustlib.pop(); rustlib.push("bin");
let (llvm_cov, llvm_profdata): (PathBuf, PathBuf) = match (
env::var_os("LLVM_COV").map(PathBuf::from),
env::var_os("LLVM_PROFDATA").map(PathBuf::from),
) {
(Some(llvm_cov), Some(llvm_profdata)) => (llvm_cov, llvm_profdata),
(llvm_cov_env, llvm_profdata_env) => {
if llvm_cov_env.is_some() {
warn!("setting only LLVM_COV environment variable may not work properly; consider setting both LLVM_COV and LLVM_PROFDATA environment variables");
} else if llvm_profdata_env.is_some() {
warn!("setting only LLVM_PROFDATA environment variable may not work properly; consider setting both LLVM_COV and LLVM_PROFDATA environment variables");
}
let llvm_cov: PathBuf =
rustlib.join(format!("llvm-cov{}", env::consts::EXE_SUFFIX)).into();
let llvm_profdata: PathBuf =
rustlib.join(format!("llvm-profdata{}", env::consts::EXE_SUFFIX)).into();
if !llvm_cov.exists() || !llvm_profdata.exists() {
let sysroot: Utf8PathBuf = ws.rustc_print("sysroot")?.into();
let toolchain = sysroot.file_name().unwrap();
if cmd!("rustup", "toolchain", "list")
.read()
.map_or(false, |t| t.contains(toolchain))
{
let mut cmd = cmd!(
"rustup",
"component",
"add",
"llvm-tools-preview",
"--toolchain",
toolchain
);
let ask = match env::var_os("CARGO_LLVM_COV_SETUP") {
None => true,
Some(ref v) if v == "yes" => false,
Some(v) => {
if v != "no" {
warn!(
"CARGO_LLVM_COV_SETUP must be yes or no, but found `{v:?}`"
);
}
bail!(
"failed to find llvm-tools-preview, please install llvm-tools-preview \
with `rustup component add llvm-tools-preview --toolchain {toolchain}`",
);
}
};
ask_to_run(
&mut cmd,
ask,
"install the `llvm-tools-preview` component for the selected toolchain",
)?;
} else {
bail!(
"failed to find llvm-tools-preview, please install llvm-tools-preview, or set LLVM_COV and LLVM_PROFDATA environment variables",
);
}
}
(llvm_cov_env.unwrap_or(llvm_cov), llvm_profdata_env.unwrap_or(llvm_profdata))
}
};
let workspace_members =
WorkspaceMembers::new(&args.exclude, &args.exclude_from_report, &ws.metadata);
if workspace_members.included.is_empty() {
bail!("no crates to be measured for coverage");
}
let build_script_re = pkg_hash_re(&ws, &workspace_members.included);
let mut llvm_cov_flags = env::var("LLVM_COV_FLAGS")?;
if llvm_cov_flags.is_none() {
llvm_cov_flags = env::var("CARGO_LLVM_COV_FLAGS")?;
if llvm_cov_flags.is_some() {
warn!("CARGO_LLVM_COV_FLAGS is deprecated; consider using LLVM_COV_FLAGS instead");
}
}
let mut llvm_profdata_flags = env::var("LLVM_PROFDATA_FLAGS")?;
if llvm_profdata_flags.is_none() {
llvm_profdata_flags = env::var("CARGO_LLVM_PROFDATA_FLAGS")?;
if llvm_profdata_flags.is_some() {
warn!("CARGO_LLVM_PROFDATA_FLAGS is deprecated; consider using LLVM_PROFDATA_FLAGS instead");
}
}
Ok(Self {
ws,
args,
workspace_members,
build_script_re,
current_dir: env::current_dir().unwrap(),
current_exe: match env::current_exe() {
Ok(exe) => exe,
Err(e) => {
let exe = format!("cargo-llvm-cov{}", env::consts::EXE_SUFFIX);
warn!("failed to get current executable, assuming {exe} in PATH as current executable: {e}");
exe.into()
}
},
llvm_cov,
llvm_profdata,
llvm_cov_flags,
llvm_profdata_flags,
})
}
pub(crate) fn process(&self, program: impl Into<OsString>) -> ProcessBuilder {
let mut cmd = cmd!(program);
if self.args.verbose > 1 {
cmd.display_env_vars();
}
cmd
}
pub(crate) fn cargo(&self) -> ProcessBuilder {
self.ws.cargo(self.args.verbose)
}
}
fn pkg_hash_re(ws: &Workspace, pkg_ids: &[PackageId]) -> RegexVec {
let mut re = RegexVecBuilder::new("^(", ")-[0-9a-f]+$");
for id in pkg_ids {
re.or(&ws.metadata.packages[id].name);
}
re.build().unwrap()
}
pub(crate) struct WorkspaceMembers {
pub(crate) excluded: Vec<PackageId>,
pub(crate) included: Vec<PackageId>,
}
impl WorkspaceMembers {
fn new(exclude: &[String], exclude_from_report: &[String], metadata: &Metadata) -> Self {
let mut excluded = vec![];
let mut included = vec![];
if !exclude.is_empty() || !exclude_from_report.is_empty() {
for id in &metadata.workspace_members {
if exclude.contains(&metadata.packages[id].name)
|| exclude_from_report.contains(&metadata.packages[id].name)
{
excluded.push(id.clone());
} else {
included.push(id.clone());
}
}
} else {
for id in &metadata.workspace_members {
included.push(id.clone());
}
}
Self { excluded, included }
}
}
fn ask_to_run(cmd: &mut ProcessBuilder, ask: bool, text: &str) -> Result<()> {
let is_ci = env::var_os("CI").is_some() || env::var_os("TF_BUILD").is_some();
if ask && !is_ci {
let mut buf = String::new();
print!("I will run {cmd} to {text}.\nProceed? [Y/n] ");
io::stdout().flush()?;
io::stdin().read_line(&mut buf)?;
match buf.trim().to_lowercase().as_str() {
"" | "y" | "yes" => {}
"n" | "no" => bail!("aborting as per your request"),
a => bail!("invalid answer `{}`", a),
};
} else {
info!("running {} to {}", cmd, text);
}
cmd.run()?;
Ok(())
}