Start over for watchexec 2.0

This commit is contained in:
Félix Saparelli 2022-01-22 17:49:55 +13:00
parent 093b443dbc
commit 2ce78572d1
No known key found for this signature in database
GPG Key ID: B948C4BAE44FC474
10 changed files with 1807 additions and 929 deletions

1481
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,33 +13,71 @@ homepage = "https://watchexec.github.io/#cargo-watch"
repository = "https://github.com/watchexec/cargo-watch"
readme = "README.md"
edition = "2018"
rust-version = "1.51.1"
edition = "2021"
resolver = "2"
rust-version = "1.58.0"
exclude = ["/.github"]
[[bin]]
name = "cargo-watch"
[dependencies]
camino = "1.0.4"
clap = "2.33.1"
log = "0.4.14"
shell-escape = "0.1.5"
stderrlog = "0.5.1"
watchexec = "1.16.1"
# camino = "1.0.4"
console-subscriber = { version = "0.1.0", optional = true }
dunce = "1.0.2"
futures = "0.3.17"
miette = { version = "3.2.0", features = ["fancy"] }
# shell-escape = "0.1.5"
tracing = "0.1.26"
watchexec = "2.0.0-pre.6"
[dependencies.clap]
version = "2.33.3"
default-features = false
features = ["wrap_help"]
[dependencies.tokio]
version = "1.15.0"
features = [
"fs",
"io-std",
"parking_lot",
"process",
"rt",
"rt-multi-thread",
"signal",
"sync",
]
[dependencies.tracing-subscriber]
version = "0.3.6"
features = [
"env-filter",
"fmt",
]
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1.26"
[target.'cfg(not(target_os="freebsd"))'.dependencies]
notify-rust = "4.5.2"
[build-dependencies]
embed-resource = "1.6.1"
[dev-dependencies]
assert_cmd = "1.0.1"
insta = "1.7.1"
predicates = "2.0.0"
wait-timeout = "0.2.0"
[features]
dev-console = ["console-subscriber"]
[profile.release]
lto = true
panic = "abort"
debug = 1 # for stack traces
lto = "fat"
codegen-units = 1
[package.metadata.binstall]

View File

@ -1,238 +1,225 @@
use clap::{App, AppSettings, Arg, ArgMatches, ErrorKind, SubCommand};
use std::{env, process};
use std::{
env,
ffi::OsString,
fs::File,
io::{BufRead, BufReader},
path::Path,
};
pub fn parse() -> ArgMatches<'static> {
let footnote = "You can use the `-- command` style instead, note you'll need to use full commands, it won't prefix `cargo` for you.\n\nBy default, your entire project is watched, except for the target/ and .git/ folders, and your .ignore and .gitignore files are used to filter paths.".to_owned();
use clap::{crate_version, App, Arg, ArgMatches};
use miette::{Context, IntoDiagnostic, Result};
#[cfg(windows)] let footnote = format!("{}\n\nOn Windows, patterns given to -i have forward slashes (/) automatically converted to backward ones (\\) to ease command portability.", footnote);
let mut app = App::new(env!("CARGO_PKG_NAME"))
.bin_name("cargo")
.version(env!("CARGO_PKG_VERSION"))
.help_message("")
.version_message("")
.setting(AppSettings::ArgsNegateSubcommands)
.setting(AppSettings::DisableHelpSubcommand)
.setting(AppSettings::DontCollapseArgsInUsage)
.setting(AppSettings::GlobalVersion)
.setting(AppSettings::StrictUtf8)
.setting(AppSettings::SubcommandRequired)
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("watch")
.author(env!("CARGO_PKG_HOMEPAGE"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.usage("cargo watch [FLAGS] [OPTIONS]")
.help_message("Display this message")
.version_message("Display version information")
.arg(
Arg::with_name("once")
.long("testing-only--once")
.hidden(true),
)
.arg(
Arg::with_name("clear")
.short("c")
.long("clear")
.help("Clear the screen before each run"),
)
.arg(
Arg::with_name("log:debug")
.long("debug")
.help("Show debug output"),
)
.arg(
Arg::with_name("log:info")
.long("why")
.help("Show paths that changed"),
)
.arg(
Arg::with_name("ignore-nothing")
.long("ignore-nothing")
.help("Ignore nothing, not even target/ and .git/"),
)
.arg(
Arg::with_name("no-gitignore")
.long("no-gitignore")
.help("Dont use .gitignore files"),
)
.arg(
Arg::with_name("no-ignore")
.long("no-ignore")
.help("Dont use .ignore files"),
)
.arg(
Arg::with_name("no-restart")
.long("no-restart")
.help("Dont restart command while its still running"),
)
.arg(
Arg::with_name("packages:all")
.long("all")
.conflicts_with("packages:one")
.hidden(true)
.help("Reserved for workspace support"),
)
.arg(
Arg::with_name("poll")
.long("poll")
.help("Force use of polling for file changes"),
)
.arg(
Arg::with_name("postpone")
.long("postpone")
.help("Postpone first run until a file changes"),
)
.arg(
Arg::with_name("watch-when-idle")
.long("watch-when-idle")
.help("Ignore events emitted while the commands run. Will become default behaviour in 8.0."),
)
.arg(
Arg::with_name("features")
.long("features")
.takes_value(true)
.help("List of features passed to cargo invocations"),
)
.arg(
Arg::with_name("log:quiet")
.short("q")
.long("quiet")
.help("Suppress output from cargo-watch itself"),
)
.arg(
Arg::with_name("cmd:cargo")
.short("x")
.long("exec")
.takes_value(true)
.value_name("cmd")
.multiple(true)
.empty_values(false)
.min_values(1)
.number_of_values(1)
.help("Cargo command(s) to execute on changes [default: check]"),
)
.arg(
Arg::with_name("cmd:shell")
.short("s")
.long("shell")
.takes_value(true)
.value_name("cmd")
.multiple(true)
.empty_values(false)
.min_values(1)
.number_of_values(1)
.help("Shell command(s) to execute on changes"),
)
.arg(
Arg::with_name("delay")
.short("d")
.long("delay")
.takes_value(true)
.empty_values(false)
.default_value("0.5")
.help("File updates debounce delay in seconds"),
)
.arg(
Arg::with_name("ignore")
.short("i")
.long("ignore")
.takes_value(true)
.value_name("pattern")
.multiple(true)
.empty_values(false)
.min_values(1)
.number_of_values(1)
.help("Ignore a glob/gitignore-style pattern"),
)
.arg(
Arg::with_name("packages:one")
.short("p")
.long("package")
.takes_value(true)
.value_name("spec")
.multiple(true)
.empty_values(false)
.min_values(1)
.hidden(true)
.help("Reserved for workspace support"),
)
.arg(
Arg::with_name("watch")
.short("w")
.long("watch")
.takes_value(true)
.multiple(true)
.empty_values(false)
.min_values(1)
.number_of_values(1)
.default_value(".")
.help("Watch specific file(s) or folder(s)"),
)
.arg(
Arg::with_name("use-shell")
.long("use-shell")
.takes_value(true)
.help(if cfg!(windows) {
"Use a different shell. Try --use-shell=powershell, which will become the default in 8.0."
} else {
"Use a different shell. E.g. --use-shell=bash"
}),
)
.arg(
Arg::with_name("workdir")
.short("C")
.long("workdir")
.takes_value(true)
.help("Change working directory before running command [default: crate root]"),
)
.arg(
Arg::with_name("notif")
.help("Send a desktop notification when watchexec notices a change (experimental, behaviour may change)")
.short("N")
.long("notify")
.hidden(cfg!(target_os="freebsd"))
)
.arg(
Arg::with_name("rust-backtrace")
.help("Inject RUST_BACKTRACE=VALUE (generally you want to set it to 1) into the environment")
.short("B")
.takes_value(true)
)
.arg(
Arg::with_name("cmd:trail")
.raw(true)
.help("Full command to run. -x and -s will be ignored!"),
)
.after_help(footnote.as_str()),
);
// Allow invocation of cargo-watch with both `cargo-watch watch ARGS`
// (as invoked by cargo) and `cargo-watch ARGS`.
let mut args: Vec<String> = env::args().collect();
args.insert(1, "watch".into());
let matches = match app.get_matches_from_safe_borrow(args) {
Ok(matches) => matches,
Err(err) => {
match err.kind {
ErrorKind::HelpDisplayed => {
println!("{}", err);
process::exit(0);
}
ErrorKind::VersionDisplayed => {
// Unlike HelpDisplayed, VersionDisplayed emits the output
// by itself (clap-rs/clap#1390). It also does so without a
// trailing newline, so we print one ourselves.
println!();
process::exit(0);
}
_ => app.get_matches(),
}
}
};
matches.subcommand.unwrap().matches
trait Clap3Compat {
/// Does nothing for clap2, but remove this trait for clap3, and get cool new option groups!
fn help_heading(self, _heading: impl Into<Option<&'static str>>) -> Self
where
Self: Sized,
{
self
}
}
impl Clap3Compat for Arg<'_, '_> {}
const OPTSET_FILTERING: &str = "Filtering options:";
const OPTSET_COMMAND: &str = "Command options:";
const OPTSET_CONFIG: &str = "Config file options:";
const OPTSET_DEBUGGING: &str = "Debugging options:";
const OPTSET_OUTPUT: &str = "Output options:";
const OPTSET_BEHAVIOUR: &str = "Behaviour options:";
pub fn get_args() -> Result<ArgMatches<'static>> {
let app = App::new("watchexec")
.version(crate_version!())
.about("Execute commands when watched files change")
.after_help("Use @argfile as first argument to load arguments from the file `argfile` (one argument per line) which will be inserted in place of the @argfile (further arguments on the CLI will override or add onto those in the file).")
.arg(Arg::with_name("config-file")
.help_heading(Some(OPTSET_CONFIG))
.help("Config file(s) to use")
.multiple(true)
.short("C")
.long("config"))
.arg(Arg::with_name("command")
.help_heading(Some(OPTSET_COMMAND))
.help("Command to execute")
.multiple(true)
.required(true))
.arg(Arg::with_name("paths")
.help_heading(Some(OPTSET_FILTERING))
.help("Watch a specific file or directory")
.short("w")
.long("watch")
.value_name("path")
.number_of_values(1)
.multiple(true)
.takes_value(true))
.arg(Arg::with_name("clear")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Clear screen before executing command")
.short("c")
.long("clear"))
.arg(Arg::with_name("on-busy-update")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Select the behaviour to use when receiving events while the command is running. Current default is queue, will change to do-nothing in 2.0.")
.takes_value(true)
.possible_values(&["do-nothing", "queue", "restart", "signal"])
.long("on-busy-update"))
.arg(Arg::with_name("restart")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Restart the process if it's still running. Shorthand for --on-busy-update=restart")
.short("r")
.long("restart"))
.arg(Arg::with_name("signal")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Specify the signal to send when using --on-busy-update=signal")
.short("s")
.long("signal")
.takes_value(true)
.value_name("signal")
.default_value("SIGTERM")
.hidden(cfg!(windows)))
.arg(Arg::with_name("kill")
.help_heading(Some(OPTSET_BEHAVIOUR))
.hidden(true)
.short("k")
.long("kill"))
.arg(Arg::with_name("debounce")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Set the timeout between detected change and command execution, defaults to 50ms")
.takes_value(true)
.value_name("milliseconds")
.short("d")
.long("debounce"))
.arg(Arg::with_name("verbose")
.help_heading(Some(OPTSET_DEBUGGING))
.help("Print debugging messages (-v, -vv, -vvv, -vvvv; use -vvv for bug reports)")
.multiple(true)
.short("v")
.long("verbose"))
.arg(Arg::with_name("print-events")
.help_heading(Some(OPTSET_DEBUGGING))
.help("Print events that trigger actions")
.long("print-events")
.alias("changes-only")) // --changes-only is deprecated (remove at v2)
.arg(Arg::with_name("no-vcs-ignore")
.help_heading(Some(OPTSET_FILTERING))
.help("Skip auto-loading of VCS (Git, etc) ignore files")
.long("no-vcs-ignore"))
.arg(Arg::with_name("no-project-ignore")
.help_heading(Some(OPTSET_FILTERING))
.help("Skip auto-loading of project ignore files (.gitignore, .ignore, etc)")
.long("no-project-ignore")
.alias("no-ignore")) // --no-ignore is deprecated (remove at v2)
.arg(Arg::with_name("no-default-ignore")
.help_heading(Some(OPTSET_FILTERING))
.help("Skip auto-ignoring of commonly ignored globs")
.long("no-default-ignore"))
.arg(Arg::with_name("no-global-ignore")
.help_heading(Some(OPTSET_FILTERING))
.help("Skip auto-loading of global or environment-wide ignore files")
.long("no-global-ignore"))
.arg(Arg::with_name("postpone")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Wait until first change to execute command")
.short("p")
.long("postpone"))
.arg(Arg::with_name("poll")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Force polling mode (interval in milliseconds)")
.long("force-poll")
.value_name("interval"))
.arg(Arg::with_name("shell")
.help_heading(Some(OPTSET_COMMAND))
.help(if cfg!(windows) {
"Use a different shell, or `none`. Try --shell=powershell, which will become the default in 2.0."
} else {
"Use a different shell, or `none`. Defaults to `sh` (until 2.0, where that will change to `$SHELL`). E.g. --shell=bash"
})
.takes_value(true)
.long("shell"))
// -n short form will not be removed, and instead become a shorthand for --shell=none
.arg(Arg::with_name("no-shell")
.help_heading(Some(OPTSET_COMMAND))
.help("Do not wrap command in a shell. Deprecated: use --shell=none instead.")
.short("n")
.long("no-shell"))
.arg(Arg::with_name("no-environment")
.help_heading(Some(OPTSET_OUTPUT))
.help("Do not set WATCHEXEC_*_PATH environment variables for the command")
.long("no-environment"))
.arg(Arg::with_name("no-process-group")
.help_heading(Some(OPTSET_COMMAND))
.help("Do not use a process group when running the command")
.long("no-process-group"))
.arg(Arg::with_name("once").short("1").hidden(true))
.arg(Arg::with_name("watch-when-idle")
.help_heading(Some(OPTSET_BEHAVIOUR))
.help("Deprecated alias for --on-busy-update=do-nothing, which will become the default in 2.0.")
.short("W")
.hidden(true)
.long("watch-when-idle"))
.arg(Arg::with_name("notif")
.help_heading(Some(OPTSET_OUTPUT))
.help("Send a desktop notification when the command ends")
.short("N")
.long("notify"))
.arg(
Arg::with_name("extensions")
.help_heading(Some(OPTSET_FILTERING))
.help("Comma-separated list of file extensions to watch (e.g. js,css,html)")
.short("e")
.long("exts")
.takes_value(true),
)
.arg(
Arg::with_name("filter")
.help_heading(Some(OPTSET_FILTERING))
.help("Ignore all modifications except those matching the pattern")
.short("f")
.long("filter")
.number_of_values(1)
.multiple(true)
.takes_value(true)
.value_name("pattern"),
)
.arg(
Arg::with_name("ignore")
.help_heading(Some(OPTSET_FILTERING))
.help("Ignore modifications to paths matching the pattern")
.short("i")
.long("ignore")
.number_of_values(1)
.multiple(true)
.takes_value(true)
.value_name("pattern"),
)
.arg(
Arg::with_name("no-meta")
.help_heading(Some(OPTSET_FILTERING))
.help("Ignore metadata changes")
.long("no-meta"),
);
let mut raw_args: Vec<OsString> = env::args_os().collect();
if let Some(first) = raw_args.get(1).and_then(|s| s.to_str()) {
if let Some(arg_path) = first.strip_prefix('@').map(Path::new) {
let arg_file = BufReader::new(
File::open(arg_path)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to open argument file {:?}", arg_path))?,
);
let mut more_args: Vec<OsString> = arg_file
.lines()
.map(|l| l.map(OsString::from).into_diagnostic())
.collect::<Result<_>>()?;
more_args.insert(0, raw_args.remove(0));
more_args.extend(raw_args.into_iter().skip(1));
raw_args = more_args;
}
}
Ok(app.get_matches_from(raw_args))
}

5
src/config.rs Normal file
View File

@ -0,0 +1,5 @@
mod init;
mod runtime;
pub use init::init;
pub use runtime::runtime;

22
src/config/init.rs Normal file
View File

@ -0,0 +1,22 @@
use std::convert::Infallible;
use clap::ArgMatches;
use miette::Result;
use watchexec::{config::InitConfig, handler::SyncFnHandler};
pub fn init(_args: &ArgMatches<'static>) -> Result<InitConfig> {
let mut config = InitConfig::default();
config.on_error(SyncFnHandler::from(
|data| -> std::result::Result<(), Infallible> {
if cfg!(debug_assertions) {
eprintln!("[[{:?}]]", data);
} else {
eprintln!("[[{}]]", data);
}
Ok(())
},
));
Ok(config)
}

251
src/config/runtime.rs Normal file
View File

@ -0,0 +1,251 @@
use std::{convert::Infallible, env::current_dir, path::Path, str::FromStr, time::Duration};
use clap::ArgMatches;
use miette::{IntoDiagnostic, Result};
use notify_rust::Notification;
use watchexec::{
action::{Action, Outcome, PostSpawn, PreSpawn},
command::Shell,
config::RuntimeConfig,
event::ProcessEnd,
fs::Watcher,
handler::SyncFnHandler,
paths::summarise_events_to_env,
signal::{process::SubSignal, source::MainSignal},
};
pub fn runtime(args: &ArgMatches<'static>) -> Result<RuntimeConfig> {
let mut config = RuntimeConfig::default();
config.command(
args.values_of_lossy("command")
.expect("(clap) Bug: command is not present")
.iter(),
);
config.pathset(match args.values_of_os("paths") {
Some(paths) => paths.map(|os| Path::new(os).to_owned()).collect(),
None => vec![current_dir().into_diagnostic()?],
});
config.action_throttle(Duration::from_millis(
args.value_of("debounce")
.unwrap_or("50")
.parse()
.into_diagnostic()?,
));
if let Some(interval) = args.value_of("poll") {
config.file_watcher(Watcher::Poll(Duration::from_millis(
interval.parse().into_diagnostic()?,
)));
}
if args.is_present("no-process-group") {
config.command_grouped(false);
}
config.command_shell(if args.is_present("no-shell") {
Shell::None
} else if let Some(s) = args.value_of("shell") {
if s.eq_ignore_ascii_case("powershell") {
Shell::Powershell
} else if s.eq_ignore_ascii_case("none") {
Shell::None
} else if s.eq_ignore_ascii_case("cmd") {
cmd_shell(s.into())
} else {
Shell::Unix(s.into())
}
} else {
default_shell()
});
let clear = args.is_present("clear");
let notif = args.is_present("notif");
let mut on_busy = args
.value_of("on-busy-update")
.unwrap_or("queue")
.to_owned();
if args.is_present("restart") {
on_busy = "restart".into();
}
if args.is_present("watch-when-idle") {
on_busy = "do-nothing".into();
}
let mut signal = args
.value_of("signal")
.map(SubSignal::from_str)
.transpose()
.into_diagnostic()?
.unwrap_or(SubSignal::Terminate);
if args.is_present("kill") {
signal = SubSignal::ForceStop;
}
let print_events = args.is_present("print-events");
let once = args.is_present("once");
config.on_action(move |action: Action| {
let fut = async { Ok::<(), Infallible>(()) };
if print_events {
for (n, event) in action.events.iter().enumerate() {
eprintln!("[EVENT {}] {}", n, event);
}
}
if once {
action.outcome(Outcome::both(Outcome::Start, Outcome::wait(Outcome::Exit)));
return fut;
}
let signals: Vec<MainSignal> = action.events.iter().flat_map(|e| e.signals()).collect();
let has_paths = action
.events
.iter()
.flat_map(|e| e.paths())
.next()
.is_some();
if signals.contains(&MainSignal::Terminate) {
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
return fut;
}
if signals.contains(&MainSignal::Interrupt) {
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
return fut;
}
if !has_paths {
if !signals.is_empty() {
let mut out = Outcome::DoNothing;
for sig in signals {
out = Outcome::both(out, Outcome::Signal(sig.into()));
}
action.outcome(out);
return fut;
}
let completion = action.events.iter().flat_map(|e| e.completions()).next();
if let Some(status) = completion {
let (msg, printit) = match status {
Some(ProcessEnd::ExitError(code)) => {
(format!("Command exited with {}", code), true)
}
Some(ProcessEnd::ExitSignal(sig)) => {
(format!("Command killed by {:?}", sig), true)
}
Some(ProcessEnd::ExitStop(sig)) => {
(format!("Command stopped by {:?}", sig), true)
}
Some(ProcessEnd::Continued) => ("Command continued".to_string(), true),
Some(ProcessEnd::Exception(ex)) => {
(format!("Command ended by exception {:#x}", ex), true)
}
Some(ProcessEnd::Success) => ("Command was successful".to_string(), false),
None => ("Command completed".to_string(), false),
};
if printit {
eprintln!("[[{}]]", msg);
}
if notif {
Notification::new()
.summary("Watchexec: command ended")
.body(&msg)
.show()
.map(drop)
.unwrap_or_else(|err| {
eprintln!("[[Failed to send desktop notification: {}]]", err);
});
}
action.outcome(Outcome::DoNothing);
return fut;
}
}
let when_running = match (clear, on_busy.as_str()) {
(_, "do-nothing") => Outcome::DoNothing,
(true, "restart") => {
Outcome::both(Outcome::Stop, Outcome::both(Outcome::Clear, Outcome::Start))
}
(false, "restart") => Outcome::both(Outcome::Stop, Outcome::Start),
(_, "signal") => Outcome::Signal(signal),
(true, "queue") => Outcome::wait(Outcome::both(Outcome::Clear, Outcome::Start)),
(false, "queue") => Outcome::wait(Outcome::Start),
_ => Outcome::DoNothing,
};
let when_idle = if clear {
Outcome::both(Outcome::Clear, Outcome::Start)
} else {
Outcome::Start
};
action.outcome(Outcome::if_running(when_running, when_idle));
fut
});
let no_env = args.is_present("no-environment");
config.on_pre_spawn(move |prespawn: PreSpawn| async move {
if !no_env {
let envs = summarise_events_to_env(prespawn.events.iter());
if let Some(mut command) = prespawn.command().await {
for (k, v) in envs {
command.env(format!("WATCHEXEC_{}_PATH", k), v);
}
}
}
Ok::<(), Infallible>(())
});
config.on_post_spawn(SyncFnHandler::from(move |postspawn: PostSpawn| {
if notif {
Notification::new()
.summary("Watchexec: change detected")
.body(&format!("Running `{}`", postspawn.command.join(" ")))
.show()
.map(drop)
.unwrap_or_else(|err| {
eprintln!("[[Failed to send desktop notification: {}]]", err);
});
}
Ok::<(), Infallible>(())
}));
Ok(config)
}
// until 2.0, then Powershell
#[cfg(windows)]
fn default_shell() -> Shell {
Shell::Cmd
}
#[cfg(not(windows))]
fn default_shell() -> Shell {
Shell::default()
}
// because Shell::Cmd is only on windows
#[cfg(windows)]
fn cmd_shell(_: String) -> Shell {
Shell::Cmd
}
#[cfg(not(windows))]
fn cmd_shell(s: String) -> Shell {
Shell::Unix(s)
}

View File

@ -1,50 +1,59 @@
use camino::Utf8PathBuf;
use stderrlog::Timestamp;
use watchexec::{error::Result, run::watch};
use std::env::var;
use miette::{IntoDiagnostic, Result};
use watchexec::{event::Event, Watchexec};
mod args;
mod options;
mod root;
mod watch;
mod config;
// mod filterer;
fn main() -> Result<()> {
let matches = args::parse();
#[cfg(target_env = "musl")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
let debug = matches.is_present("log:debug");
let info = matches.is_present("log:info");
let quiet = matches.is_present("log:quiet");
let testing = matches.is_present("once");
#[tokio::main]
async fn main() -> Result<()> {
#[cfg(feature = "dev-console")]
console_subscriber::init();
stderrlog::new()
.quiet(quiet)
.show_module_names(debug)
.verbosity(if debug {
3
} else if info {
2
} else {
1
})
.timestamp(if testing {
Timestamp::Off
} else {
Timestamp::Millisecond
})
.init()
.unwrap();
root::change_dir(
matches
.value_of("workdir")
.map(Utf8PathBuf::from)
.unwrap_or_else(root::project_root),
);
if let Some(b) = matches.value_of("rust-backtrace") {
std::env::set_var("RUST_BACKTRACE", b);
if var("RUST_LOG").is_ok() && cfg!(not(feature = "dev-console")) {
tracing_subscriber::fmt::init();
}
let opts = options::get_options(&matches);
let handler = watch::CwHandler::new(opts, quiet, matches.is_present("notif"))?;
watch(&handler)
let args = args::get_args()?;
{
let verbosity = args.occurrences_of("verbose");
let mut builder = tracing_subscriber::fmt().with_env_filter(match verbosity {
0 => "cargo-watch=warn",
1 => "watchexec=debug,cargo-watch=debug",
2 => "watchexec=trace,cargo-watch=trace",
_ => "trace",
});
if verbosity > 2 {
use tracing_subscriber::fmt::format::FmtSpan;
builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
}
if verbosity > 3 {
builder.pretty().try_init().ok();
} else {
builder.try_init().ok();
}
}
let init = config::init(&args)?;
let runtime = config::runtime(&args)?;
// runtime.filterer(filterer::new(&args).await?);
let wx = Watchexec::new(init, runtime)?;
if !args.is_present("postpone") {
wx.send_event(Event::default()).await?;
}
wx.main().await.into_diagnostic()??;
Ok(())
}

View File

@ -1,262 +0,0 @@
use std::{env, path::MAIN_SEPARATOR, time::Duration};
use clap::{value_t, values_t, ArgMatches};
use log::{debug, warn};
use watchexec::{
config::{Config, ConfigBuilder},
run::OnBusyUpdate,
Shell,
};
pub fn set_commands(builder: &mut ConfigBuilder, matches: &ArgMatches) {
let mut commands: Vec<String> = Vec::new();
// --features are injected just after applicable cargo subcommands
// and before the remaining arguments
let features = value_t!(matches, "features", String).ok();
if matches.is_present("cmd:trail") {
debug!("trailing command is present, ignore all other command options");
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(" ")];
} else {
let command_order = env::args().filter_map(|arg| match arg.as_str() {
"-x" | "--exec" => Some("cargo"),
"-s" | "--shell" => Some("shell"),
_ => None,
});
let mut cargos = if matches.is_present("cmd:cargo") {
values_t!(matches, "cmd:cargo", String).unwrap_or_else(|e| e.exit())
} else {
Vec::new()
}
.into_iter();
let mut shells = if matches.is_present("cmd:shell") {
values_t!(matches, "cmd:shell", String).unwrap_or_else(|e| e.exit())
} else {
Vec::new()
}
.into_iter();
for c in command_order {
match c {
"cargo" => {
commands.push(cargo_command(
cargos
.next()
.expect("Argument-order mismatch, this is a bug"),
&features,
));
}
"shell" => {
commands.push(
shells
.next()
.expect("Argument-order mismatch, this is a bug"),
);
}
_ => {}
}
}
}
// Default to `cargo check`
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);
}
debug!("Commands: {:?}", commands);
builder.cmd(commands);
}
fn cargo_command(cargo: String, features: &Option<String>) -> String {
let mut cmd = String::from("cargo ");
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")
{
// Split command into first word and the arguments
let word_boundary = cargo
.find(|c: char| c.is_whitespace())
.unwrap_or_else(|| cargo.len());
// Find returns the byte index, and split_at takes a byte offset.
// This means the splitting is unicode-safe.
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);
}
cmd
}
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-gitignore");
builder.no_vcs_ignore(novcs);
debug!("Load Git/VCS ignores: {:?}", !novcs);
let noignore = matches.is_present("no-ignore");
builder.no_ignore(noignore);
debug!("Load .ignore ignores: {:?}", !noignore);
let mut list = vec![
// Mac
format!("*{}.DS_Store", MAIN_SEPARATOR),
// Vim
"*.sw?".into(),
"*.sw?x".into(),
// Emacs
"#*#".into(),
".#*".into(),
// Kate
".*.kate-swp".into(),
// VCS
format!("*{s}.hg{s}**", s = MAIN_SEPARATOR),
format!("*{s}.git{s}**", s = MAIN_SEPARATOR),
format!("*{s}.svn{s}**", s = MAIN_SEPARATOR),
// SQLite
"*.db".into(),
"*.db-*".into(),
format!("*{s}*.db-journal{s}**", s = MAIN_SEPARATOR),
// Rust
format!("*{s}target{s}**", s = MAIN_SEPARATOR),
];
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);
}
}
pub fn set_watches(builder: &mut ConfigBuilder, matches: &ArgMatches) {
let mut opts = Vec::new();
if matches.is_present("watch") {
for watch in values_t!(matches, "watch", String).unwrap_or_else(|e| e.exit()) {
opts.push(watch.into());
}
}
if opts.is_empty() {
opts.push(".".into());
}
debug!("Watches: {:?}", opts);
builder.paths(opts);
}
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(true);
// TODO in 8.0: remove --watch-when-idle and switch --no-restart behaviour to DoNothing
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") {
warn!("--use-shell=none is non-sensical for cargo-watch, ignoring");
default_shell()
} else if s.eq_ignore_ascii_case("cmd") {
cmd_shell(s.into())
} else {
Shell::Unix(s.into())
}
} else {
// in 8.0, just rely on default watchexec behaviour
default_shell()
});
set_ignores(&mut builder, matches);
set_debounce(&mut builder, matches);
set_watches(&mut builder, matches);
set_commands(&mut builder, matches);
let mut args = builder.build().unwrap();
args.once = matches.is_present("once");
debug!("Watchexec arguments: {:?}", args);
args
}
// until 8.0
#[cfg(windows)]
fn default_shell() -> Shell {
Shell::Cmd
}
#[cfg(not(windows))]
fn default_shell() -> Shell {
Shell::default()
}
// because Shell::Cmd is only on windows
#[cfg(windows)]
fn cmd_shell(_: String) -> Shell {
Shell::Cmd
}
#[cfg(not(windows))]
fn cmd_shell(s: String) -> Shell {
Shell::Unix(s)
}

View File

@ -1,27 +0,0 @@
use camino::Utf8PathBuf;
use clap::{Error, ErrorKind};
use log::debug;
use std::{env::set_current_dir, process::Command};
pub fn project_root() -> Utf8PathBuf {
Command::new("cargo")
.arg("locate-project")
.arg("--message-format")
.arg("plain")
.output()
.map_err(|err| err.to_string())
.and_then(|out| String::from_utf8(out.stdout).map_err(|err| err.to_string()))
.map(Utf8PathBuf::from)
.and_then(|path| {
path.parent()
.ok_or_else(|| String::from("project root does not exist"))
.map(ToOwned::to_owned)
})
.unwrap_or_else(|err| Error::with_description(&err, ErrorKind::Io).exit())
}
pub fn change_dir(dir: Utf8PathBuf) {
debug!("change directory to: {}", dir);
set_current_dir(dir)
.unwrap_or_else(|err| Error::with_description(&err.to_string(), ErrorKind::Io).exit())
}

View File

@ -1,80 +0,0 @@
use watchexec::{
config::Config,
error::Result,
pathop::PathOp,
run::{ExecHandler, Handler},
};
pub struct CwHandler {
cmd: String,
once: bool,
quiet: bool,
notify: bool,
inner: ExecHandler,
}
impl Handler for CwHandler {
fn args(&self) -> Config {
self.inner.args()
}
fn on_manual(&self) -> Result<bool> {
if self.once {
Ok(true)
} else {
self.start();
self.inner.on_manual()
}
}
fn on_update(&self, ops: &[PathOp]) -> Result<bool> {
self.start();
self.inner.on_update(ops).map(|o| {
#[cfg(not(target_os = "freebsd"))]
if self.notify {
notify_rust::Notification::new()
.summary("Cargo Watch observed a change")
.body("Cargo Watch has seen a change, the command may have restarted.")
.show()
.map(drop)
.unwrap_or_else(|err| {
log::warn!("Failed to send desktop notification: {}", err);
});
}
o
})
}
}
impl CwHandler {
pub fn new(mut args: Config, quiet: bool, notify: bool) -> Result<Self> {
let cmd = args.cmd.join(" && ");
let mut final_cmd = cmd.clone();
if !quiet {
#[cfg(unix)]
final_cmd.push_str(r#"; echo "[Finished running. Exit status: $?]""#);
#[cfg(windows)]
final_cmd.push_str(r#" & echo "[Finished running. Exit status: %ERRORLEVEL%]""#);
#[cfg(not(any(unix, windows)))]
final_cmd.push_str(r#" ; echo "[Finished running]""#);
// ^ could be wrong depending on the platform, to be fixed on demand
}
args.cmd = vec![final_cmd];
Ok(Self {
once: args.once,
cmd,
inner: ExecHandler::new(args)?,
quiet,
notify,
})
}
fn start(&self) {
if !self.quiet {
println!("[Running '{}']", self.cmd);
}
}
}