From f255a7d17117be69f99b9f63d632a21eb05bf388 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 8 Mar 2024 10:58:54 +0530 Subject: [PATCH] feat: support for automatic update KCL dependencies on kcl.mod file save Signed-off-by: Abhishek Kumar --- kclvm/Cargo.lock | 22 ++++- kclvm/driver/Cargo.toml | 1 + kclvm/driver/src/kpm.rs | 197 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 207 insertions(+), 13 deletions(-) diff --git a/kclvm/Cargo.lock b/kclvm/Cargo.lock index 82655fa99..c384bc1fa 100644 --- a/kclvm/Cargo.lock +++ b/kclvm/Cargo.lock @@ -1661,6 +1661,7 @@ dependencies = [ "kclvm-parser", "kclvm-runtime", "kclvm-utils", + "notify 6.1.1", "serde", "serde_json", "walkdir", @@ -2194,6 +2195,25 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.3.3", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -2733,7 +2753,7 @@ checksum = "a680f2dbd796844ebeaa2a4d01ae209f412ddc2981f6512ab8bc9b471156e6cd" dependencies = [ "crossbeam-channel", "jod-thread", - "notify", + "notify 5.1.0", "ra_ap_paths", "ra_ap_vfs", "tracing", diff --git a/kclvm/driver/Cargo.toml b/kclvm/driver/Cargo.toml index 9d60f138f..dcd1943e6 100644 --- a/kclvm/driver/Cargo.toml +++ b/kclvm/driver/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] serde_json = "1.0.86" +notify = "6.1.1" kclvm-config ={ path = "../config"} kclvm-runtime ={ path = "../runtime"} diff --git a/kclvm/driver/src/kpm.rs b/kclvm/driver/src/kpm.rs index 1d0ec7a29..832ea38fb 100644 --- a/kclvm/driver/src/kpm.rs +++ b/kclvm/driver/src/kpm.rs @@ -1,24 +1,37 @@ -use crate::kcl; -use crate::lookup_the_nearest_file_dir; +use crate::{kcl, lookup_the_nearest_file_dir}; use anyhow::{bail, Result}; use kclvm_config::modfile::KCL_MOD_FILE; use kclvm_parser::LoadProgramOptions; +use notify::{RecursiveMode, Watcher}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, path::PathBuf, process::Command}; +use std::marker::Send; +use std::{ + collections::HashMap, + fs::File, + io::Write, + path::PathBuf, + process::Command, + sync::{mpsc::channel, Arc, Mutex}, +}; -/// [`fill_pkg_maps_for_k_file`] will call `kpm metadata` to obtain the metadata -/// of all dependent packages of the kcl package where the current file is located, -/// and fill the relevant information of the external packages into compilation option [`LoadProgramOptions`]. +/// Searches for the nearest kcl.mod directory containing the given file and fills the compilation options +/// with metadata of dependent packages. +/// +/// # Arguments +/// +/// * `k_file_path` - Path to the K file for which metadata is needed. +/// * `opts` - Mutable reference to the compilation options to fill. +/// +/// # Returns +/// +/// * `Result<()>` - Empty result if successful, error otherwise. pub(crate) fn fill_pkg_maps_for_k_file( k_file_path: PathBuf, opts: &mut LoadProgramOptions, ) -> Result<()> { - // 1. find the kcl.mod dir for the kcl package contains 'k_file_path'. match lookup_the_nearest_file_dir(k_file_path, KCL_MOD_FILE) { Some(mod_dir) => { - // 2. get the module metadata. let metadata = fetch_metadata(mod_dir.canonicalize()?)?; - // 3. fill the external packages local paths into compilation option [`LoadProgramOptions`]. let maps: HashMap = metadata .packages .into_iter() @@ -32,13 +45,149 @@ pub(crate) fn fill_pkg_maps_for_k_file( Ok(()) } +/// Trait for writing messages to a file. +pub trait Writer { + fn write_message(&mut self, message: &str) -> Result<()>; +} + +impl Writer for File { + /// Writes a message to the file followed by a newline. + /// + /// # Arguments + /// + /// * `message` - The message to write. + /// + /// # Returns + /// + /// * `Result<()>` - Empty result if successful, error otherwise. + fn write_message(&mut self, message: &str) -> Result<()> { + writeln!(self, "{}", message)?; + Ok(()) + } +} + +/// Watches for modifications in the kcl.mod file within the given directory and updates dependencies accordingly. +/// +/// # Arguments +/// +/// * `directory` - The directory containing the kcl.mod file to watch. +/// * `writer` - The writer for outputting log messages. +/// +/// # Returns +/// +/// * `Result<()>` - Empty result if successful, error otherwise. +pub fn watch_kcl_mod(directory: PathBuf, writer: W) -> Result<()> { + let writer = Arc::new(Mutex::new(writer)); // Wrap writer in Arc> for thread safety + let (sender, receiver) = channel(); + let writer_clone = Arc::clone(&writer); // Create a clone of writer for the closure + + let mut watcher = notify::recommended_watcher(move |res| { + if let Err(err) = sender.send(res) { + let mut writer = writer_clone.lock().unwrap(); // Lock the mutex before using writer + writer + .write_message(&format!("Error sending event to channel: {:?}", err)) + .ok(); + } + })?; + + watcher.watch(&directory, RecursiveMode::NonRecursive)?; + + loop { + match receiver.recv() { + Ok(event) => { + match event { + Ok(event) => match event.kind { + notify::event::EventKind::Modify(modify_kind) => { + if let notify::event::ModifyKind::Data(data_change) = modify_kind { + if data_change == notify::event::DataChange::Content { + let mut writer = writer.lock().unwrap(); // Lock the mutex before using writer + writer.write_message("kcl.mod file content modified. Updating dependencies...").ok(); + update_dependencies(directory.clone())?; + } + } + } + _ => {} + }, + Err(err) => { + let mut writer = writer.lock().unwrap(); // Lock the mutex before using writer + writer + .write_message(&format!("Watcher error: {:?}", err)) + .ok(); + } + } + } + Err(e) => { + let mut writer = writer.lock().unwrap(); // Lock the mutex before using writer + writer + .write_message(&format!("Receiver error: {:?}", e)) + .ok(); + } + } + } +} + +impl Writer for Arc> +where + W: Writer, +{ + /// Writes a message using the wrapped writer. + /// + /// # Arguments + /// + /// * `message` - The message to write. + /// + /// # Returns + /// + /// * `Result<()>` - Empty result if successful, error otherwise. + fn write_message(&mut self, message: &str) -> Result<()> { + self.lock().unwrap().write_message(message) + } +} + +/// Tracks changes in the kcl.mod file within the given working directory and watches for updates. +/// +/// # Arguments +/// +/// * `work_dir` - The working directory where the kcl.mod file is located. +/// +/// # Returns +/// +/// * `Result<()>` - Empty result if successful, error otherwise. +pub fn kcl_mod_file_track(work_dir: PathBuf, writer: W) -> Result<()> +where + W: Writer + Send + 'static, +{ + let writer = Arc::new(Mutex::new(writer)); // Wrap writer in Arc> for thread safety + + let directory = match lookup_the_nearest_file_dir(work_dir.clone(), KCL_MOD_FILE) { + Some(mod_dir) => mod_dir, + None => { + let mut writer = writer.lock().unwrap(); // Lock the writer + writer.write_message(&format!( + "Manifest file '{}' not found in directory hierarchy", + KCL_MOD_FILE + ))?; + return Ok(()); + } + }; + + if let Err(err) = watch_kcl_mod(directory, Arc::clone(&writer)) { + let mut writer = writer.lock().unwrap(); // Lock the writer + writer.write_message(&format!("Error watching kcl.mod file: {:?}", err))?; + } + Ok(()) +} + +/// Metadata structure representing package information. #[derive(Deserialize, Serialize, Default, Debug, Clone)] + /// [`Metadata`] is the metadata of the current KCL module, /// currently only the mapping between the name and path of the external dependent package is included. pub struct Metadata { pub packages: HashMap, } +/// Structure representing a package. #[derive(Clone, Debug, Serialize, Deserialize)] /// [`Package`] is a kcl package. pub struct Package { @@ -49,14 +198,30 @@ pub struct Package { } impl Metadata { - /// [`parse`] will parse the json string into [`Metadata`]. + /// Parses metadata from a string. + /// + /// # Arguments + /// + /// * `data` - The string containing metadata. + /// + /// # Returns + /// + /// * `Result` - Metadata if successful, error otherwise. fn parse(data: String) -> Result { let meta = serde_json::from_str(data.as_ref())?; Ok(meta) } } -/// [`fetch_metadata`] will call `kcl mod metadata` to obtain the metadata. +/// Fetches metadata of packages from the kcl.mod file within the given directory. +/// +/// # Arguments +/// +/// * `manifest_path` - The path to the directory containing the kcl.mod file. +/// +/// # Returns +/// +/// * `Result` - Metadata if successful, error otherwise. pub fn fetch_metadata(manifest_path: PathBuf) -> Result { match Command::new(kcl()) .arg("mod") @@ -79,7 +244,15 @@ pub fn fetch_metadata(manifest_path: PathBuf) -> Result { } } -/// [`update_dependencies`] will call `kcl mod update` to update the dependencies. +/// Updates dependencies for the kcl.mod file within the given directory. +/// +/// # Arguments +/// +/// * `work_dir` - The working directory containing the kcl.mod file. +/// +/// # Returns +/// +/// * `Result<()>` - Empty result if successful, error otherwise. pub fn update_dependencies(work_dir: PathBuf) -> Result<()> { match lookup_the_nearest_file_dir(work_dir.clone(), KCL_MOD_FILE) { Some(mod_dir) => {