change watch command to work with arbitrary commands

This commit is contained in:
Moritz Hölting 2024-06-13 10:38:16 +02:00
parent 0284eccfcd
commit eaeb4166da
6 changed files with 128 additions and 63 deletions

View File

@ -24,7 +24,7 @@ watch = ["dep:notify-debouncer-mini", "dep:ctrlc"]
zip = ["shulkerbox/zip"] zip = ["shulkerbox/zip"]
[dependencies] [dependencies]
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive", "env"] }
colored = "2.1.0" colored = "2.1.0"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
thiserror = "1.0.58" thiserror = "1.0.58"

View File

@ -43,19 +43,22 @@ Where [PATH] is the path of the project folder to clean [default: `.`]
Options: Options:
- `--output <OUTPUT>` The output directory, overrides the `DATAPACK_DIR` environment variable - `--output <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: Environment variables:
- `DATAPACK_DIR` The output directory [default: `./dist`] - `DATAPACK_DIR` The output directory [default: `./dist`]
### Watch for changes ### Watch for changes
```bash ```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: Options:
- `--no-initial` Do not run the command initially - `--no-initial` Do not run the command initially
- `--debounce-time <DEBOUNCE_TIME>` The time to wait in ms after the last change before running the command [default: `2000`] - `--debounce-time <DEBOUNCE_TIME>` The time to wait in ms after the last change before running the command [default: `2000`]
- `--execute <COMMAND>` The commands (cli subcommands or shell commands) to execute in the project when changes have been detected [multi-arg, default: `build`]
## Contributing ## Contributing

View File

@ -2,7 +2,7 @@ use crate::subcommands::{self, BuildArgs, CleanArgs, InitArgs};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
#[derive(Debug, Parser)] #[derive(Debug, Clone, Parser)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
pub struct Args { pub struct Args {
#[command(subcommand)] #[command(subcommand)]
@ -22,7 +22,7 @@ pub enum Command {
/// This will remove the `dist` directory. /// This will remove the `dist` directory.
Clean(CleanArgs), Clean(CleanArgs),
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
/// Watch for changes and execute command. /// Watch for changes and execute commands.
Watch(subcommands::WatchArgs), Watch(subcommands::WatchArgs),
#[cfg(feature = "lang-debug")] #[cfg(feature = "lang-debug")]
/// Build the project and dump the intermediate state. /// Build the project and dump the intermediate state.

View File

@ -5,10 +5,11 @@ use shulkerbox::virtual_fs::{VFile, VFolder};
use crate::{ use crate::{
config::ProjectConfig, config::ProjectConfig,
error::Error, error::Error,
terminal_output::{print_error, print_info, print_warning}, terminal_output::{print_error, print_info, print_success, print_warning},
}; };
use std::{ use std::{
env, fs, borrow::Cow,
fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -18,8 +19,7 @@ pub struct BuildArgs {
#[clap(default_value = ".")] #[clap(default_value = ".")]
pub path: PathBuf, pub path: PathBuf,
/// The path of the directory to place the compiled datapack. /// The path of the directory to place the compiled datapack.
/// Overrides the `DATAPACK_DIR` environment variable. #[clap(short, long, env = "DATAPACK_DIR")]
#[clap(short, long)]
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
/// The path of a folder which files and subfolders will be copied to the root of the datapack. /// 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. /// 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 path = args.path.as_path();
let dist_path = args let dist_path = args
.output .output
.clone() .as_ref()
.or_else(|| env::var("DATAPACK_DIR").ok().map(PathBuf::from)) .map(Cow::Borrowed)
.unwrap_or_else(|| path.join("dist")); .unwrap_or_else(|| Cow::Owned(path.join("dist")));
let and_package_msg = if args.zip { " and packaging" } else { "" }; 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"))] #[cfg(not(feature = "zip"))]
output.place(&dist_path)?; output.place(&dist_path)?;
print_info(format!( print_success(format!(
"Finished building{and_package_msg} project to {}", "Finished building{and_package_msg} project to {}",
dist_path.absolutize_from(path)?.display() dist_path.absolutize_from(path)?.display()
)); ));

View File

@ -1,4 +1,4 @@
use std::{env, path::PathBuf}; use std::{borrow::Cow, path::PathBuf};
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use path_absolutize::Absolutize; use path_absolutize::Absolutize;
@ -11,7 +11,7 @@ pub struct CleanArgs {
#[clap(default_value = ".")] #[clap(default_value = ".")]
pub path: PathBuf, pub path: PathBuf,
/// The path of the directory where the compiled datapacks are placed. /// The path of the directory where the compiled datapacks are placed.
#[clap(short, long)] #[clap(short, long, env = "DATAPACK_DIR")]
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
/// Clean the whole output folder /// Clean the whole output folder
#[clap(short, long)] #[clap(short, long)]
@ -25,9 +25,9 @@ pub fn clean(verbose: bool, args: &CleanArgs) -> Result<()> {
let path = args.path.as_path(); let path = args.path.as_path();
let dist_path = args let dist_path = args
.output .output
.clone() .as_ref()
.or_else(|| env::var("DATAPACK_DIR").ok().map(PathBuf::from)) .map(Cow::Borrowed)
.unwrap_or_else(|| path.join("dist")); .unwrap_or_else(|| Cow::Owned(path.join("dist")));
let mut delete_paths = Vec::new(); let mut delete_paths = Vec::new();
@ -35,7 +35,7 @@ pub fn clean(verbose: bool, args: &CleanArgs) -> Result<()> {
if args.all { if args.all {
if args.force { if args.force {
delete_paths.push(dist_path.clone()); delete_paths.push(dist_path.clone().into_owned());
} else { } else {
print_error("You must use the --force flag to clean the whole output folder.") 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 { if verbose {
print_info(format!("Deleting {:?}, as it is empty", dist_path)); 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."); print_success("Project cleaned successfully.");

View File

@ -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 notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult};
use super::BuildArgs;
use crate::{ use crate::{
cli::Command, cli::Args,
error::Result, error::Result,
terminal_output::{print_error, print_info}, terminal_output::{print_error, print_info, print_warning},
}; };
#[derive(Debug, clap::Args, Clone)] #[derive(Debug, clap::Args, Clone)]
pub struct WatchArgs { 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. /// Do not run the command when starting, only after changes are detected.
#[clap(short, long)] #[clap(short, long)]
no_inital: bool, pub no_inital: bool,
/// The time to wait in ms before running the command after changes are detected. /// The time to wait in ms before running the command after changes are detected.
#[clap(short, long, default_value = "2000")] #[clap(short, long, default_value = "2000")]
debounce_time: u64, pub debounce_time: u64,
/// The command to run when changes are detected. /// The commands to run in the project directory when changes are detected.
#[command(subcommand)] #[clap(short = 'x', long, default_value = "build .")]
cmd: Option<WatchSubcommand>, pub execute: Vec<String>,
} }
#[derive(Debug, Clone, Subcommand)] #[derive(Debug, Clone)]
pub enum WatchSubcommand { enum WatchCommand {
/// Build the project. Internal(Args),
Build(BuildArgs), External(String),
} }
impl From<WatchSubcommand> for Command { pub fn watch(_verbose: bool, args: &WatchArgs) -> Result<()> {
fn from(value: WatchSubcommand) -> Self { print_info(format!("Watching project at {}", args.path.display()));
match value {
WatchSubcommand::Build(args) => Command::Build(args), 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::<Vec<_>>();
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)] #[allow(clippy::collapsible_if)]
if !args.no_inital { if !args.no_inital {
if cmd.run(verbose).is_err() { run_cmds(&commands, true);
print_error("Command failed to run initially");
}
} }
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
print_info("Stopping watcher..."); print_info("Stopping watcher...");
std::process::exit(0); process::exit(0);
}) })
.expect("Error setting Ctrl-C handler"); .expect("Error setting Ctrl-C handler");
@ -66,11 +77,9 @@ pub fn watch(verbose: bool, args: &WatchArgs) -> Result<()> {
Duration::from_millis(args.debounce_time), Duration::from_millis(args.debounce_time),
move |res: DebounceEventResult| { move |res: DebounceEventResult| {
if res.is_ok() { if res.is_ok() {
if cmd.run(verbose).is_err() { run_cmds(&commands, false)
print_error("Command failed to run");
}
} else { } else {
std::process::exit(1); process::exit(1);
} }
}, },
) )
@ -78,17 +87,17 @@ pub fn watch(verbose: bool, args: &WatchArgs) -> Result<()> {
let watcher = debouncer.watcher(); let watcher = debouncer.watcher();
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"); .expect("Failed to watch project src");
watcher watcher
.watch( .watch(
project_path.join("pack.png").as_path(), args.path.join("pack.png").as_path(),
RecursiveMode::NonRecursive, RecursiveMode::NonRecursive,
) )
.expect("Failed to watch project pack.png"); .expect("Failed to watch project pack.png");
watcher watcher
.watch( .watch(
project_path.join("pack.toml").as_path(), args.path.join("pack.toml").as_path(),
RecursiveMode::NonRecursive, RecursiveMode::NonRecursive,
) )
.expect("Failed to watch project pack.toml"); .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)); 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<ExitStatus> {
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()
}