use std::{
collections::{HashMap, HashSet},
iter::FromIterator,
path::{PathBuf, MAIN_SEPARATOR},
time::Duration,
};
use cargo_metadata::{MetadataCommand, Node, Package, PackageId};
use clap::{value_t, values_t, ArgMatches, ErrorKind};
use log::{debug, warn};
use watchexec::{
config::{Config, ConfigBuilder},
run::OnBusyUpdate,
Shell,
};
pub fn set_commands(builder: &mut ConfigBuilder, matches: &ArgMatches) -> bool {
let mut commands: Vec<String> = Vec::new();
let features = value_t!(matches, "features", String).ok();
let subcommand_cargo = {
let (name, args) = matches.subcommand();
if name.is_empty() {
None
} else if let Some(args) = args {
let mut cargo_cmd = vec![name.to_string()];
cargo_cmd.extend(values_t!(args, "args", String).unwrap_or_else(|e| {
if e.kind == ErrorKind::ArgumentNotFound {
Vec::new()
} else {
e.exit()
}
}));
Some(cargo_cmd.join(" "))
} else {
Some(name.to_string())
}
};
if matches.is_present("cmd:cargo") || subcommand_cargo.is_some() {
let normal_cargos = values_t!(matches, "cmd:cargo", String).unwrap_or_else(|e| {
if e.kind == ErrorKind::ArgumentNotFound {
Vec::new()
} else {
e.exit()
}
});
for cargo in normal_cargos
.into_iter()
.chain(subcommand_cargo.into_iter())
{
let mut cmd: String = "cargo ".into();
let cargo = cargo.trim_start();
if let Some(features) = features.as_ref() {
if cargo.starts_with('b')
|| cargo.starts_with("check")
|| cargo.starts_with("doc")
|| cargo.starts_with('r')
|| cargo.starts_with("test")
|| cargo.starts_with("install")
{
let word_boundary = cargo
.find(|c: char| c.is_whitespace())
.unwrap_or(cargo.len());
let (subcommand, args) = cargo.split_at(word_boundary);
cmd.push_str(subcommand);
cmd.push_str(" --features ");
cmd.push_str(features);
cmd.push(' ');
cmd.push_str(args)
} else {
cmd.push_str(cargo);
}
} else {
cmd.push_str(cargo);
}
commands.push(cmd);
}
}
if matches.is_present("cmd:shell") {
for shell in values_t!(matches, "cmd:shell", String).unwrap_or_else(|e| e.exit()) {
commands.push(shell);
}
}
if matches.is_present("cmd:trail") {
debug!("trailing command is present, ignore all other command options");
if matches
.value_of("use-shell")
.map_or(false, |shell| shell.eq_ignore_ascii_case("none"))
{
commands = values_t!(matches, "cmd:trail", String).unwrap_or_else(|e| e.exit());
} else {
commands = vec![values_t!(matches, "cmd:trail", String)
.unwrap_or_else(|e| e.exit())
.into_iter()
.map(|arg| shell_escape::escape(arg.into()))
.collect::<Vec<_>>()
.join(" ")];
}
}
let default_command = if commands.is_empty() {
let mut cmd: String = "cargo check".into();
if let Some(features) = features.as_ref() {
cmd.push_str(" --features ");
cmd.push_str(features);
}
commands.push(cmd);
true
} else {
false
};
debug!("Commands: {:?}", commands);
builder.cmd(commands);
default_command
}
pub fn set_ignores(builder: &mut ConfigBuilder, matches: &ArgMatches) {
if matches.is_present("ignore-nothing") {
debug!("Ignoring nothing");
builder.no_vcs_ignore(true);
builder.no_ignore(true);
return;
}
let novcs = matches.is_present("no-vcs-ignores");
builder.no_vcs_ignore(novcs);
debug!("Load Git/VCS ignores: {:?}", !novcs);
let noignore = matches.is_present("no-dot-ignores");
builder.no_ignore(noignore);
debug!("Load .ignore ignores: {:?}", !noignore);
let mut list = vec![
format!("*{}.DS_Store", MAIN_SEPARATOR),
"*.sw?".into(),
"*.sw?x".into(),
"#*#".into(),
".#*".into(),
".*.kate-swp".into(),
format!("*{s}.hg{s}**", s = MAIN_SEPARATOR),
format!("*{s}.git{s}**", s = MAIN_SEPARATOR),
format!("*{s}.svn{s}**", s = MAIN_SEPARATOR),
"*.db".into(),
"*.db-*".into(),
format!("*{s}*.db-journal{s}**", s = MAIN_SEPARATOR),
format!("*{s}target{s}**", s = MAIN_SEPARATOR),
"rustc-ice-*.txt".into(),
];
debug!("Default ignores: {:?}", list);
if matches.is_present("ignore") {
for ignore in values_t!(matches, "ignore", String).unwrap_or_else(|e| e.exit()) {
#[cfg(windows)]
let ignore = ignore.replace('/', &MAIN_SEPARATOR.to_string());
list.push(ignore);
}
}
debug!("All ignores: {:?}", list);
builder.ignores(list);
}
pub fn set_debounce(builder: &mut ConfigBuilder, matches: &ArgMatches) {
if matches.is_present("delay") {
let debounce = value_t!(matches, "delay", f32).unwrap_or_else(|e| e.exit());
debug!("File updates debounce: {} seconds", debounce);
let d = Duration::from_millis((debounce * 1000.0) as u64);
builder.poll_interval(d).debounce(d);
}
}
fn find_local_deps(filter_platform: Option<String>) -> Result<Vec<PathBuf>, String> {
let options: Vec<String> = filter_platform
.map(|platform| format!("--filter-platform={}", platform))
.into_iter()
.collect();
let metadata = MetadataCommand::new()
.other_options(options)
.exec()
.map_err(|e| format!("Failed to execute `cargo metadata`: {}", e))?;
let resolve = match metadata.resolve {
None => return Ok(Vec::new()),
Some(resolve) => resolve,
};
let id_to_node =
HashMap::<PackageId, &Node>::from_iter(resolve.nodes.iter().map(|n| (n.id.clone(), n)));
let id_to_package = HashMap::<PackageId, &Package>::from_iter(
metadata.packages.iter().map(|p| (p.id.clone(), p)),
);
let mut pkgids_seen = HashSet::new();
let mut pkgids_to_check = Vec::new();
match resolve.root {
Some(root) => pkgids_to_check.push(root),
None => pkgids_to_check.extend_from_slice(&metadata.workspace_members),
};
let mut local_deps = HashSet::new();
while !pkgids_to_check.is_empty() {
let current_pkgid = pkgids_to_check.pop().unwrap();
if !pkgids_seen.insert(current_pkgid.clone()) {
continue;
}
let pkg = match id_to_package.get(¤t_pkgid) {
None => continue,
Some(&pkg) => pkg,
};
if pkg.source.is_some() {
continue;
}
let mut path = pkg.manifest_path.clone();
path.pop();
local_deps.insert(path.into_std_path_buf());
if let Some(node) = id_to_node.get(¤t_pkgid) {
for dep in &node.deps {
pkgids_to_check.push(dep.pkg.clone());
}
}
}
Ok(local_deps.into_iter().collect::<Vec<PathBuf>>())
}
pub fn set_watches(builder: &mut ConfigBuilder, matches: &ArgMatches, only_default_command: bool) {
let mut watches = Vec::new();
if matches.is_present("watch") {
for watch in values_t!(matches, "watch", String).unwrap_or_else(|e| e.exit()) {
watches.push(watch.into());
}
}
if watches.is_empty() {
if !matches.is_present("skip-local-deps") {
let filter_platform = if only_default_command {
Some(crate::rustc::host_triple())
} else {
None
};
match find_local_deps(filter_platform) {
Ok(dirs) => {
if dirs.is_empty() {
debug!("Found no local deps");
} else {
watches = dirs;
}
}
Err(err) => {
eprintln!("Finding local deps failed: {}", err);
}
}
}
watches.push(".".into());
}
debug!("Watches: {:?}", watches);
builder.paths(watches);
}
pub fn get_options(matches: &ArgMatches) -> Config {
let mut builder = ConfigBuilder::default();
builder
.poll(matches.is_present("poll"))
.clear_screen(matches.is_present("clear"))
.run_initially(!matches.is_present("postpone"))
.no_environment(!matches.is_present("env-changes"))
.use_process_group(!matches.is_present("no-process-group"));
builder.on_busy_update(if matches.is_present("no-restart") {
OnBusyUpdate::Queue
} else if matches.is_present("watch-when-idle") {
OnBusyUpdate::DoNothing
} else {
OnBusyUpdate::Restart
});
builder.shell(if let Some(s) = matches.value_of("use-shell") {
if s.eq_ignore_ascii_case("powershell") {
Shell::Powershell
} else if s.eq_ignore_ascii_case("none") {
if matches.is_present("cmd:trail") {
Shell::None
} else {
warn!("--use-shell=none is non-sensical for cargo-watch with -x/-s, ignoring");
default_shell()
}
} else if s.eq_ignore_ascii_case("cmd") {
cmd_shell(s.into())
} else {
Shell::Unix(s.into())
}
} else {
default_shell()
});
set_ignores(&mut builder, matches);
set_debounce(&mut builder, matches);
let only_default_command = set_commands(&mut builder, matches);
set_watches(&mut builder, matches, only_default_command);
let mut args = builder.build().unwrap();
args.once = matches.is_present("once");
debug!("Watchexec arguments: {:?}", args);
args
}
#[cfg(windows)]
fn default_shell() -> Shell {
Shell::Cmd
}
#[cfg(not(windows))]
fn default_shell() -> Shell {
Shell::default()
}
#[cfg(windows)]
fn cmd_shell(_: String) -> Shell {
Shell::Cmd
}
#[cfg(not(windows))]
fn cmd_shell(s: String) -> Shell {
Shell::Unix(s)
}