change watch command to work with arbitrary commands
This commit is contained in:
parent
0284eccfcd
commit
eaeb4166da
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
));
|
));
|
||||||
|
|
|
@ -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.");
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue