use crate::errors::*;
use clap::{Arg, ArgGroup, ErrorKind};
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use std::str::FromStr;
use which::which_in;
const ENV_VAR_INTERNAL_START_SERVER: &str = "SCCACHE_START_SERVER";
#[derive(Debug, Clone)]
pub enum StatsFormat {
Text,
Json,
}
impl StatsFormat {
fn as_str(&self) -> &'static str {
match self {
Self::Text => "text",
Self::Json => "json",
}
}
fn values() -> &'static [Self] {
&[Self::Text, Self::Json]
}
}
impl FromStr for StatsFormat {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
match s {
"text" => Ok(Self::Text),
"json" => Ok(Self::Json),
_ => bail!("Unrecognized stats format: {:?}", s),
}
}
}
impl Default for StatsFormat {
fn default() -> Self {
Self::Text
}
}
pub enum Command {
ShowStats(StatsFormat),
InternalStartServer,
StartServer,
StopServer,
ZeroStats,
DistStatus,
DistAuth,
PackageToolchain(PathBuf, PathBuf),
Compile {
exe: OsString,
cmdline: Vec<OsString>,
cwd: PathBuf,
env_vars: Vec<(OsString, OsString)>,
},
}
fn flag_infer_long_and_short(name: &'static str) -> Arg<'static> {
flag_infer_long(name).short(name.chars().next().expect("Name needs at least one char"))
}
fn flag_infer_long(name: &'static str) -> Arg<'static> {
Arg::new(name).long(name)
}
fn get_clap_command() -> clap::Command<'static> {
clap::Command::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.trailing_var_arg(true)
.max_term_width(110)
.after_help(concat!(
"Enabled features:\n",
" S3: ",
cfg!(feature = "s3"),
"\n",
" Redis: ",
cfg!(feature = "redis"),
"\n",
" Memcached: ",
cfg!(feature = "memcached"),
"\n",
" GCS: ",
cfg!(feature = "gcs"),
"\n",
" Azure: ",
cfg!(feature = "azure"),
"\n"
))
.args(&[
flag_infer_long_and_short("show-stats").help("show cache statistics"),
flag_infer_long("start-server").help("start background server"),
flag_infer_long("stop-server").help("stop background server"),
flag_infer_long_and_short("zero-stats").help("zero statistics counters"),
flag_infer_long("dist-auth").help("authenticate for distributed compilation"),
flag_infer_long("dist-status").help("show status of the distributed client"),
flag_infer_long("package-toolchain")
.help("package toolchain for distributed compilation")
.number_of_values(2)
.value_names(&["EXE", "OUT"]),
flag_infer_long("stats-format")
.help("set output format of statistics")
.value_name("FMT")
.possible_values(StatsFormat::values().iter().map(StatsFormat::as_str))
.default_value(StatsFormat::default().as_str()),
Arg::new("CMD")
.multiple_occurrences(true)
.use_value_delimiter(false),
])
.group(
ArgGroup::new("one_and_only_one")
.args(&[
"dist-auth",
"dist-status",
"show-stats",
"start-server",
"stop-server",
"zero-stats",
"package-toolchain",
"CMD",
])
.required(true),
)
}
pub fn try_parse() -> Result<Command> {
trace!("parse");
let cwd =
env::current_dir().context("sccache: Couldn't determine current working directory")?;
let internal_start_server = env::var(ENV_VAR_INTERNAL_START_SERVER).as_deref() == Ok("1");
let mut args: Vec<_> = env::args_os().collect();
if !internal_start_server {
if let Ok(exe) = env::current_exe() {
match exe
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_lowercase())
{
Some(ref e) if e == env!("CARGO_PKG_NAME") => {}
_ => {
if let (Some(path), Some(exe_filename)) = (env::var_os("PATH"), exe.file_name())
{
match which_in(exe_filename, Some(&path), &cwd) {
Ok(ref full_path)
if full_path.canonicalize()? == exe.canonicalize()? =>
{
if let Some(dir) = full_path.parent() {
let path = env::join_paths(
env::split_paths(&path).filter(|p| p != dir),
)
.ok();
if let Ok(full_path) = which_in(exe_filename, path, &cwd) {
args[0] = full_path.into();
}
}
}
Ok(full_path) => args[0] = full_path.into(),
Err(_) => {}
}
args.insert(0, env!("CARGO_PKG_NAME").into());
}
}
}
}
}
let matches_result = get_clap_command().try_get_matches_from(args);
match (internal_start_server, matches_result) {
(true, Err(e)) => {
if e.kind() == ErrorKind::MissingRequiredArgument {
Ok(Command::InternalStartServer)
} else {
Err(e.into())
}
}
(false, Err(e)) => Err(e.into()),
(true, Ok(_)) => {
bail!("`{ENV_VAR_INTERNAL_START_SERVER}=1` can't be used with other commands");
}
(false, Ok(matches)) => {
if matches.is_present("show-stats") {
let fmt = matches
.value_of_t("stats-format")
.expect("There is a default value");
Ok(Command::ShowStats(fmt))
} else if matches.is_present("start-server") {
Ok(Command::StartServer)
} else if matches.is_present("stop-server") {
Ok(Command::StopServer)
} else if matches.is_present("zero-stats") {
Ok(Command::ZeroStats)
} else if matches.is_present("dist-auth") {
Ok(Command::DistAuth)
} else if matches.is_present("dist-status") {
Ok(Command::DistStatus)
} else if matches.is_present("package-toolchain") {
let mut toolchain_values: Vec<PathBuf> =
matches.values_of_t("package-toolchain")?;
let maybe_out = toolchain_values.pop();
let maybe_exe = toolchain_values.pop();
match (maybe_exe, maybe_out) {
(Some(exe), Some(out)) => Ok(Command::PackageToolchain(exe, out)),
_ => unreachable!("clap should enforce two values"),
}
} else if matches.is_present("CMD") {
let mut env_vars = env::vars_os().collect::<Vec<_>>();
if env::var_os("RUNNING_UNDER_RR").is_some() {
env_vars.retain(|(k, _v)| k != "LD_PRELOAD" && k != "RUNNING_UNDER_RR");
}
let cmd: Vec<OsString> = matches.values_of_t("CMD")?;
match cmd.as_slice() {
[exe, cmdline @ ..] => Ok(Command::Compile {
exe: exe.to_owned(),
cmdline: cmdline.to_owned(),
cwd,
env_vars,
}),
_ => unreachable!("clap should enforce at least one value in cmd"),
}
} else {
unreachable!("Either the arg group or env variable should provide a command");
}
}
}
}