From eaeb4166da7ce8c3b2bd617f034d3ebbd43c4405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:38:16 +0200 Subject: [PATCH] change watch command to work with arbitrary commands --- Cargo.toml | 2 +- README.md | 7 +- src/cli.rs | 4 +- src/subcommands/build.rs | 16 ++--- src/subcommands/clean.rs | 14 ++-- src/subcommands/watch.rs | 148 +++++++++++++++++++++++++++------------ 6 files changed, 128 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ae33e0e..7db1b65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ watch = ["dep:notify-debouncer-mini", "dep:ctrlc"] zip = ["shulkerbox/zip"] [dependencies] -clap = { version = "4.5.4", features = ["derive"] } +clap = { version = "4.5.4", features = ["derive", "env"] } colored = "2.1.0" serde = { version = "1.0.197", features = ["derive"] } thiserror = "1.0.58" diff --git a/README.md b/README.md index 94dac03..ceadc53 100644 --- a/README.md +++ b/README.md @@ -43,19 +43,22 @@ Where [PATH] is the path of the project folder to clean [default: `.`] Options: - `--output ` The output directory, overrides the `DATAPACK_DIR` environment variable +- `--all` Clean all files in the output directory, not only the ones generated by shulkerscript +- `--force` Required for `--all` to prevent accidental deletion of files Environment variables: - `DATAPACK_DIR` The output directory [default: `./dist`] ### Watch for changes ```bash -shulkerscript watch [OPTIONS] [SUBCOMMAND] +shulkerscript watch [OPTIONS] [PATH] ``` -Where [SUBCOMMAND] is either `build` or `package` [default: `build`] +Where [PATH] is the path of the project folder to watch [default: `.`] Options: - `--no-initial` Do not run the command initially - `--debounce-time ` The time to wait in ms after the last change before running the command [default: `2000`] +- `--execute ` The commands (cli subcommands or shell commands) to execute in the project when changes have been detected [multi-arg, default: `build`] ## Contributing diff --git a/src/cli.rs b/src/cli.rs index 1f451e2..08cb1f3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use crate::subcommands::{self, BuildArgs, CleanArgs, InitArgs}; use clap::{Parser, Subcommand}; use color_eyre::eyre::Result; -#[derive(Debug, Parser)] +#[derive(Debug, Clone, Parser)] #[command(version, about, long_about = None)] pub struct Args { #[command(subcommand)] @@ -22,7 +22,7 @@ pub enum Command { /// This will remove the `dist` directory. Clean(CleanArgs), #[cfg(feature = "watch")] - /// Watch for changes and execute command. + /// Watch for changes and execute commands. Watch(subcommands::WatchArgs), #[cfg(feature = "lang-debug")] /// Build the project and dump the intermediate state. diff --git a/src/subcommands/build.rs b/src/subcommands/build.rs index 9f6575a..6c79896 100644 --- a/src/subcommands/build.rs +++ b/src/subcommands/build.rs @@ -5,10 +5,11 @@ use shulkerbox::virtual_fs::{VFile, VFolder}; use crate::{ config::ProjectConfig, error::Error, - terminal_output::{print_error, print_info, print_warning}, + terminal_output::{print_error, print_info, print_success, print_warning}, }; use std::{ - env, fs, + borrow::Cow, + fs, path::{Path, PathBuf}, }; @@ -18,8 +19,7 @@ pub struct BuildArgs { #[clap(default_value = ".")] pub path: PathBuf, /// The path of the directory to place the compiled datapack. - /// Overrides the `DATAPACK_DIR` environment variable. - #[clap(short, long)] + #[clap(short, long, env = "DATAPACK_DIR")] pub output: Option, /// The path of a folder which files and subfolders will be copied to the root of the datapack. /// Overrides the `assets` field in the pack.toml file. @@ -52,9 +52,9 @@ pub fn build(_verbose: bool, args: &BuildArgs) -> Result<()> { let path = args.path.as_path(); let dist_path = args .output - .clone() - .or_else(|| env::var("DATAPACK_DIR").ok().map(PathBuf::from)) - .unwrap_or_else(|| path.join("dist")); + .as_ref() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(path.join("dist"))); let and_package_msg = if args.zip { " and packaging" } else { "" }; @@ -124,7 +124,7 @@ pub fn build(_verbose: bool, args: &BuildArgs) -> Result<()> { #[cfg(not(feature = "zip"))] output.place(&dist_path)?; - print_info(format!( + print_success(format!( "Finished building{and_package_msg} project to {}", dist_path.absolutize_from(path)?.display() )); diff --git a/src/subcommands/clean.rs b/src/subcommands/clean.rs index 6c963e7..f1452fa 100644 --- a/src/subcommands/clean.rs +++ b/src/subcommands/clean.rs @@ -1,4 +1,4 @@ -use std::{env, path::PathBuf}; +use std::{borrow::Cow, path::PathBuf}; use color_eyre::eyre::Result; use path_absolutize::Absolutize; @@ -11,7 +11,7 @@ pub struct CleanArgs { #[clap(default_value = ".")] pub path: PathBuf, /// The path of the directory where the compiled datapacks are placed. - #[clap(short, long)] + #[clap(short, long, env = "DATAPACK_DIR")] pub output: Option, /// Clean the whole output folder #[clap(short, long)] @@ -25,9 +25,9 @@ pub fn clean(verbose: bool, args: &CleanArgs) -> Result<()> { let path = args.path.as_path(); let dist_path = args .output - .clone() - .or_else(|| env::var("DATAPACK_DIR").ok().map(PathBuf::from)) - .unwrap_or_else(|| path.join("dist")); + .as_ref() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(path.join("dist"))); let mut delete_paths = Vec::new(); @@ -35,7 +35,7 @@ pub fn clean(verbose: bool, args: &CleanArgs) -> Result<()> { if args.all { if args.force { - delete_paths.push(dist_path.clone()); + delete_paths.push(dist_path.clone().into_owned()); } else { print_error("You must use the --force flag to clean the whole output folder.") } @@ -69,7 +69,7 @@ pub fn clean(verbose: bool, args: &CleanArgs) -> Result<()> { if verbose { print_info(format!("Deleting {:?}, as it is empty", dist_path)); } - std::fs::remove_dir(&dist_path)?; + std::fs::remove_dir(dist_path.as_ref())?; } print_success("Project cleaned successfully."); diff --git a/src/subcommands/watch.rs b/src/subcommands/watch.rs index 846513e..b806641 100644 --- a/src/subcommands/watch.rs +++ b/src/subcommands/watch.rs @@ -1,64 +1,75 @@ -use std::{path::Path, thread, time::Duration}; +use std::{ + env, io, iter, + path::PathBuf, + process::{self, ExitStatus}, + thread, + time::Duration, +}; -use clap::Subcommand; +use clap::Parser; use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult}; -use super::BuildArgs; use crate::{ - cli::Command, + cli::Args, error::Result, - terminal_output::{print_error, print_info}, + terminal_output::{print_error, print_info, print_warning}, }; #[derive(Debug, clap::Args, Clone)] pub struct WatchArgs { + /// The path of the project to watch. + #[clap(default_value = ".")] + pub path: PathBuf, /// Do not run the command when starting, only after changes are detected. #[clap(short, long)] - no_inital: bool, + pub no_inital: bool, /// The time to wait in ms before running the command after changes are detected. #[clap(short, long, default_value = "2000")] - debounce_time: u64, - /// The command to run when changes are detected. - #[command(subcommand)] - cmd: Option, + pub debounce_time: u64, + /// The commands to run in the project directory when changes are detected. + #[clap(short = 'x', long, default_value = "build .")] + pub execute: Vec, } -#[derive(Debug, Clone, Subcommand)] -pub enum WatchSubcommand { - /// Build the project. - Build(BuildArgs), +#[derive(Debug, Clone)] +enum WatchCommand { + Internal(Args), + External(String), } -impl From for Command { - fn from(value: WatchSubcommand) -> Self { - match value { - WatchSubcommand::Build(args) => Command::Build(args), - } +pub fn watch(_verbose: bool, args: &WatchArgs) -> Result<()> { + print_info(format!("Watching project at {}", args.path.display())); + + let commands = args + .execute + .iter() + .map(|cmd| { + let split = cmd.split_whitespace(); + let prog_name = std::env::args() + .next() + .unwrap_or(env!("CARGO_PKG_NAME").to_string()); + if let Ok(args) = + Args::try_parse_from(iter::once(prog_name.as_str()).chain(split.clone())) + { + WatchCommand::Internal(args) + } else { + WatchCommand::External(cmd.to_owned()) + } + }) + .collect::>(); + + if env::set_current_dir(args.path.as_path()).is_err() { + print_warning("Failed to change working directory to project path. Commands may not work."); } -} - -pub fn watch(verbose: bool, args: &WatchArgs) -> Result<()> { - let cmd = Command::from( - args.cmd - .to_owned() - .unwrap_or_else(|| WatchSubcommand::Build(BuildArgs::default())), - ); - - let project_path = match &args.cmd { - Some(WatchSubcommand::Build(args)) => args.path.as_path(), - None => Path::new("."), - }; #[allow(clippy::collapsible_if)] if !args.no_inital { - if cmd.run(verbose).is_err() { - print_error("Command failed to run initially"); - } + run_cmds(&commands, true); } ctrlc::set_handler(move || { print_info("Stopping watcher..."); - std::process::exit(0); + process::exit(0); }) .expect("Error setting Ctrl-C handler"); @@ -66,11 +77,9 @@ pub fn watch(verbose: bool, args: &WatchArgs) -> Result<()> { Duration::from_millis(args.debounce_time), move |res: DebounceEventResult| { if res.is_ok() { - if cmd.run(verbose).is_err() { - print_error("Command failed to run"); - } + run_cmds(&commands, false) } else { - std::process::exit(1); + process::exit(1); } }, ) @@ -78,17 +87,17 @@ pub fn watch(verbose: bool, args: &WatchArgs) -> Result<()> { let watcher = debouncer.watcher(); watcher - .watch(project_path.join("src").as_path(), RecursiveMode::Recursive) + .watch(args.path.join("src").as_path(), RecursiveMode::Recursive) .expect("Failed to watch project src"); watcher .watch( - project_path.join("pack.png").as_path(), + args.path.join("pack.png").as_path(), RecursiveMode::NonRecursive, ) .expect("Failed to watch project pack.png"); watcher .watch( - project_path.join("pack.toml").as_path(), + args.path.join("pack.toml").as_path(), RecursiveMode::NonRecursive, ) .expect("Failed to watch project pack.toml"); @@ -97,3 +106,56 @@ pub fn watch(verbose: bool, args: &WatchArgs) -> Result<()> { thread::sleep(Duration::from_secs(60)); } } + +fn run_cmds(cmds: &[WatchCommand], initial: bool) { + if initial { + print_info("Running commands initially..."); + } else { + print_info("Changes have been detected. Running commands..."); + } + for (index, cmd) in cmds.iter().enumerate() { + match cmd { + WatchCommand::Internal(args) => { + if args.run().is_err() { + print_error(format!("Error running command: {}", index + 1)); + print_error("Not running further commands."); + break; + } + } + WatchCommand::External(cmd) => { + let status = run_shell_cmd(cmd); + match status { + Ok(status) if !status.success() => { + print_error(format!( + "Command {} exited unsuccessfully with status code {}", + index + 1, + status.code().unwrap_or(1) + )); + print_error("Not running further commands."); + break; + } + Ok(_) => {} + Err(_) => { + print_error(format!("Error running command: {}", index + 1)); + print_error("Not running further commands."); + break; + } + } + } + } + } +} + +fn run_shell_cmd(cmd: &str) -> io::Result { + let mut command = if cfg!(target_os = "windows") { + let mut command = process::Command::new("cmd"); + command.arg("/C"); + command + } else { + let mut command = process::Command::new(env::var("SHELL").unwrap_or("sh".to_string())); + command.arg("-c"); + command + }; + + command.arg(cmd).status() +}