Refactor into cargo-project
Rather than have this logic in the aiken binary, this provides a generic mechanism to do "something" on file change events. KtorZ is going to handle wiring it up to the CLI in the best way for the project. I tried to write some tests for this, but it's hard to isolate the watcher logic without wrestling with the borrow checker, or overly neutering this utility.
This commit is contained in:
parent
771f6d1601
commit
5068da3a17
|
@ -26,6 +26,7 @@ indexmap = "1.9.2"
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
miette = { version = "5.9.0", features = ["fancy"] }
|
miette = { version = "5.9.0", features = ["fancy"] }
|
||||||
minicbor = "0.19.1"
|
minicbor = "0.19.1"
|
||||||
|
notify = "6.1.1"
|
||||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||||
pallas = "0.18.0"
|
pallas = "0.18.0"
|
||||||
pallas-traverse = "0.18.0"
|
pallas-traverse = "0.18.0"
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub mod script;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
pub mod watch;
|
||||||
|
|
||||||
use crate::blueprint::{
|
use crate::blueprint::{
|
||||||
definitions::Definitions,
|
definitions::Definitions,
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
use miette::IntoDiagnostic;
|
||||||
|
use notify::{Event, RecursiveMode, Watcher};
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
env,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{telemetry, Project};
|
||||||
|
|
||||||
|
/// A default filter for file events that catches the most relevant "source" changes
|
||||||
|
pub fn default_filter(evt: &Event) -> bool {
|
||||||
|
// Only watch for changes to .ak and aiken.toml files, and ignore the build directory
|
||||||
|
let source_file = evt
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.ends_with(".ak") || p.ends_with("aiken.toml"));
|
||||||
|
let build_dir = evt
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.all(|p| p.ancestors().any(|a| a.ends_with("build")));
|
||||||
|
match evt.kind {
|
||||||
|
notify::EventKind::Create(_)
|
||||||
|
| notify::EventKind::Modify(_)
|
||||||
|
| notify::EventKind::Remove(_) => source_file && !build_dir,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a function each time a file in the project changes
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use aiken_project::watch::{watch_project, default_filter};
|
||||||
|
/// use aiken_project::Project;
|
||||||
|
/// watch_project(None, Terminal, default_filter, 500, |project| {
|
||||||
|
/// println!("Project changed!");
|
||||||
|
/// Ok(())
|
||||||
|
/// })
|
||||||
|
/// ```
|
||||||
|
pub fn watch_project<T, F, A>(
|
||||||
|
directory: Option<PathBuf>,
|
||||||
|
events: T,
|
||||||
|
filter: F,
|
||||||
|
debounce: u32,
|
||||||
|
mut action: A,
|
||||||
|
) -> miette::Result<()>
|
||||||
|
where
|
||||||
|
T: Copy + telemetry::EventListener,
|
||||||
|
F: Fn(&Event) -> bool,
|
||||||
|
A: FnMut(&mut Project<T>) -> Result<(), Vec<crate::error::Error>>,
|
||||||
|
{
|
||||||
|
let project_path = directory.unwrap_or(env::current_dir().into_diagnostic()?);
|
||||||
|
|
||||||
|
// Set up a queue for events, primarily so we can debounce on related events
|
||||||
|
let queue = Arc::new(Mutex::new(VecDeque::new()));
|
||||||
|
|
||||||
|
// pre-seed that queue with a single event, so it builds once at the start
|
||||||
|
queue.lock().unwrap().push_back(Event::default());
|
||||||
|
|
||||||
|
// Spawn a file-watcher that will put each change event on the queue
|
||||||
|
let queue_write = queue.clone();
|
||||||
|
let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
|
||||||
|
match res {
|
||||||
|
Ok(event) => queue_write
|
||||||
|
.lock()
|
||||||
|
.expect("lock queue")
|
||||||
|
.push_back(event.clone()),
|
||||||
|
Err(e) => {
|
||||||
|
// TODO: miette diagnostic?
|
||||||
|
println!(
|
||||||
|
"Encountered an error while monitoring for file changes: {:?}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
// Start watching for any changes in the project directory
|
||||||
|
let _ = watcher.watch(project_path.as_path(), RecursiveMode::Recursive);
|
||||||
|
|
||||||
|
// And then start reading from the queue
|
||||||
|
let queue_read = queue.clone();
|
||||||
|
loop {
|
||||||
|
// We sleep for the debounce interval, because notify will dump 12 related events into the queue all at once
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(debounce.into()));
|
||||||
|
|
||||||
|
// Grab the lock, and pop all events except the last one off the queue
|
||||||
|
let mut queue = queue_read.lock().expect("lock queue");
|
||||||
|
let mut latest = None;
|
||||||
|
// debounce the events, and ignore build/lock changes, because they come in in large batches
|
||||||
|
while let Some(evt) = queue.pop_back() {
|
||||||
|
// check if this event is meaningful to the caller
|
||||||
|
if !filter(&evt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
latest = Some(evt);
|
||||||
|
}
|
||||||
|
// release the lock here, in case other events come in
|
||||||
|
drop(queue);
|
||||||
|
|
||||||
|
// If we have an event that survived the filter, then we can construct the project and invoke the action
|
||||||
|
if latest.is_some() {
|
||||||
|
let mut project = match Project::new(project_path.clone(), events) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
// TODO: what should we actually do here?
|
||||||
|
e.report();
|
||||||
|
return Err(miette::Report::msg("??"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Invoke the action, and abort on an error
|
||||||
|
// TODO: what should we actually do with the error here?
|
||||||
|
action(&mut project).or(Err(miette::Report::msg("??")))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,6 @@ hex = "0.4.3"
|
||||||
ignore = "0.4.20"
|
ignore = "0.4.20"
|
||||||
indoc = "2.0"
|
indoc = "2.0"
|
||||||
miette = { version = "5.5.0", features = ["fancy"] }
|
miette = { version = "5.5.0", features = ["fancy"] }
|
||||||
notify = "6.1.1"
|
|
||||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||||
pallas-addresses = "0.18.0"
|
pallas-addresses = "0.18.0"
|
||||||
pallas-codec = "0.18.0"
|
pallas-codec = "0.18.0"
|
||||||
|
|
|
@ -12,7 +12,6 @@ pub mod new;
|
||||||
pub mod packages;
|
pub mod packages;
|
||||||
pub mod tx;
|
pub mod tx;
|
||||||
pub mod uplc;
|
pub mod uplc;
|
||||||
pub mod watch;
|
|
||||||
|
|
||||||
/// Aiken: a smart-contract language and toolchain for Cardano
|
/// Aiken: a smart-contract language and toolchain for Cardano
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -24,7 +23,6 @@ pub enum Cmd {
|
||||||
Build(build::Args),
|
Build(build::Args),
|
||||||
Address(blueprint::address::Args),
|
Address(blueprint::address::Args),
|
||||||
Check(check::Args),
|
Check(check::Args),
|
||||||
Watch(watch::Args),
|
|
||||||
Docs(docs::Args),
|
Docs(docs::Args),
|
||||||
Add(packages::add::Args),
|
Add(packages::add::Args),
|
||||||
|
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
use notify::{event::EventAttributes, Event, RecursiveMode, Watcher};
|
|
||||||
use std::{
|
|
||||||
collections::VecDeque,
|
|
||||||
path::Path,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
#[derive(clap::Args)]
|
|
||||||
/// Type-check an Aiken project
|
|
||||||
pub struct Args {
|
|
||||||
/// Clear the screen between each run
|
|
||||||
#[clap(long)]
|
|
||||||
clear: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exec(Args { clear }: Args) -> miette::Result<()> {
|
|
||||||
let project = Path::new("../sundae-contracts/aiken")
|
|
||||||
.to_path_buf()
|
|
||||||
.canonicalize()
|
|
||||||
.expect("");
|
|
||||||
let build = project.join("build").canonicalize().expect("");
|
|
||||||
let lock = project.join("aiken.lock");
|
|
||||||
let queue = Arc::new(Mutex::new(VecDeque::new()));
|
|
||||||
queue.lock().unwrap().push_back(Event {
|
|
||||||
kind: notify::EventKind::Any,
|
|
||||||
paths: vec![],
|
|
||||||
attrs: EventAttributes::new(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let queue_write = queue.clone();
|
|
||||||
let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
|
|
||||||
match res {
|
|
||||||
Ok(event) => match event.kind {
|
|
||||||
notify::EventKind::Create(_)
|
|
||||||
| notify::EventKind::Modify(_)
|
|
||||||
| notify::EventKind::Remove(_) => {
|
|
||||||
let mut queue = queue_write.lock().expect("lock queue");
|
|
||||||
queue.push_back(event.clone());
|
|
||||||
drop(queue);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("watch error: {:?}", e)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.expect("watcher");
|
|
||||||
let _ = watcher.watch(
|
|
||||||
Path::new("../sundae-contracts/aiken"),
|
|
||||||
RecursiveMode::Recursive,
|
|
||||||
);
|
|
||||||
let queue_read = queue.clone();
|
|
||||||
loop {
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
|
||||||
|
|
||||||
let mut queue = queue_read.lock().expect("lock queue");
|
|
||||||
let mut latest = None;
|
|
||||||
// debounce the events, and ignore build/lock changes, because they come in in large batches
|
|
||||||
while let Some(evt) = queue.pop_back() {
|
|
||||||
if evt.paths.iter().any(|p| {
|
|
||||||
let p = p.canonicalize().expect("");
|
|
||||||
p.starts_with(&build) || p.starts_with(&lock)
|
|
||||||
}) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
latest = Some(evt);
|
|
||||||
}
|
|
||||||
drop(queue);
|
|
||||||
if latest.is_some() {
|
|
||||||
if clear {
|
|
||||||
println!("{esc}c", esc = 27 as char);
|
|
||||||
}
|
|
||||||
let _ = crate::with_project_ok(Some(project.clone()), false, |p| {
|
|
||||||
p.check(false, None, true, false, false.into())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ use aiken::cmd::{
|
||||||
blueprint::{self, address},
|
blueprint::{self, address},
|
||||||
build, check, completion, docs, fmt, lsp, new,
|
build, check, completion, docs, fmt, lsp, new,
|
||||||
packages::{self, add},
|
packages::{self, add},
|
||||||
tx, uplc, watch, Cmd,
|
tx, uplc, Cmd,
|
||||||
};
|
};
|
||||||
use aiken_project::{config, pretty};
|
use aiken_project::{config, pretty};
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ fn main() -> miette::Result<()> {
|
||||||
Cmd::Build(args) => build::exec(args),
|
Cmd::Build(args) => build::exec(args),
|
||||||
Cmd::Address(args) => address::exec(args),
|
Cmd::Address(args) => address::exec(args),
|
||||||
Cmd::Check(args) => check::exec(args),
|
Cmd::Check(args) => check::exec(args),
|
||||||
Cmd::Watch(args) => watch::exec(args),
|
|
||||||
Cmd::Docs(args) => docs::exec(args),
|
Cmd::Docs(args) => docs::exec(args),
|
||||||
Cmd::Add(args) => add::exec(args),
|
Cmd::Add(args) => add::exec(args),
|
||||||
Cmd::Blueprint(args) => blueprint::exec(args),
|
Cmd::Blueprint(args) => blueprint::exec(args),
|
||||||
|
|
Loading…
Reference in New Issue