diff --git a/Cargo.lock b/Cargo.lock index 2c5b7ab..e29326e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,7 +125,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bacon" -version = "3.2.0" +version = "3.2.1-no-cargo" dependencies = [ "anyhow", "cargo_metadata", diff --git a/Cargo.toml b/Cargo.toml index cbdedbd..e1a0d1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bacon" -version = "3.2.0" +version = "3.2.1-no-cargo" authors = ["dystroy "] repository = "https://github.com/Canop/bacon" description = "background rust compiler" diff --git a/src/app.rs b/src/app.rs index 45593bd..1dba21b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -38,7 +38,7 @@ pub fn run( w: &mut W, mut settings: Settings, args: &Args, - location: MissionLocation, + location: Context, ) -> Result<()> { let event_source = EventSource::with_options(EventSourceOptions { combine_keys: false, @@ -54,7 +54,7 @@ pub fn run( break; } }; - let mission = Mission::new(&location, concrete_job_ref, job, &settings)?; + let mission = location.mission(concrete_job_ref, job, &settings)?; let do_after = app::run_mission(w, mission, &event_source, message.take())?; match do_after { DoAfterMission::NextJob(job_ref) => { @@ -88,14 +88,10 @@ fn run_mission( // build the watcher detecting and transmitting mission file changes let ignorer = time!(Info, mission.ignorer()); - let mission_watcher = Watcher::new( - &mission.files_to_watch, - &mission.directories_to_watch, - ignorer, - )?; + let mission_watcher = Watcher::new(&mission.paths_to_watch, ignorer)?; // create the watcher for config file changes - let config_watcher = Watcher::new(&mission.settings.config_files, &[], None)?; + let config_watcher = Watcher::new(&mission.settings.config_files, None)?; // create the executor, mission, and state let mut executor = MissionExecutor::new(&mission)?; diff --git a/src/cli.rs b/src/cli.rs index 77bd5a6..3a70ff2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -58,11 +58,11 @@ pub fn run() -> anyhow::Result<()> { return Ok(()); } - let location = MissionLocation::new(&args)?; - info!("mission location: {:#?}", &location); + let context = Context::new(&args)?; + info!("mission context: {:#?}", &context); if args.init { - let package_config_path = location.package_config_path(); + let package_config_path = context.package_config_path(); if !package_config_path.exists() { fs::write(&package_config_path, DEFAULT_PACKAGE_CONFIG.trim_start())?; eprintln!("bacon project configuration file written."); @@ -73,7 +73,7 @@ pub fn run() -> anyhow::Result<()> { return Ok(()); } - let settings = Settings::read(&args, &location)?; + let settings = Settings::read(&args, &context)?; if args.list_jobs { print_jobs(&settings); @@ -86,7 +86,7 @@ pub fn run() -> anyhow::Result<()> { #[cfg(windows)] w.queue(EnableMouseCapture)?; w.flush()?; - let result = app::run(&mut w, settings, &args, location); + let result = app::run(&mut w, settings, &args, context); #[cfg(windows)] w.queue(DisableMouseCapture)?; w.queue(cursor::Show)?; diff --git a/src/conf/keybindings.rs b/src/conf/keybindings.rs index 281be70..518fee1 100644 --- a/src/conf/keybindings.rs +++ b/src/conf/keybindings.rs @@ -2,16 +2,19 @@ use { crate::*, crokey::*, serde::Deserialize, - std::collections::{ - HashMap, - hash_map, + std::{ + collections::{ + HashMap, + hash_map, + }, + fmt, }, }; /// A mapping from key combinations to actions. /// /// Several key combinations can go to the same action. -#[derive(Debug, Clone, Deserialize)] +#[derive(Clone, Deserialize)] pub struct KeyBindings { #[serde(flatten)] map: HashMap, @@ -131,6 +134,19 @@ impl<'a> IntoIterator for &'a KeyBindings { } } +impl fmt::Debug for KeyBindings { + fn fmt( + &self, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + let mut ds = f.debug_struct("KeyBindings"); + for (kc, action) in &self.map { + ds.field(&kc.to_string(), &action.to_string()); + } + ds.finish() + } +} + #[test] fn test_deserialize_keybindings() { #[derive(Deserialize)] diff --git a/src/conf/settings.rs b/src/conf/settings.rs index e4a1af3..0c8f740 100644 --- a/src/conf/settings.rs +++ b/src/conf/settings.rs @@ -1,6 +1,9 @@ use { crate::*, - anyhow::*, + anyhow::{ + Result, + bail, + }, std::{ collections::HashMap, path::PathBuf, @@ -84,7 +87,7 @@ impl Settings { /// * args given as arguments, coming from the cli call pub fn read( args: &Args, - location: &MissionLocation, + context: &Context, ) -> Result { let mut settings = Settings::default(); @@ -107,10 +110,10 @@ impl Settings { settings.apply_config(&config); } - let workspace_config_path = location.workspace_config_path(); - let package_config_path = location.package_config_path(); + let workspace_config_path = context.workspace_config_path(); + let package_config_path = context.package_config_path(); - if package_config_path != workspace_config_path { + if let Some(workspace_config_path) = workspace_config_path { if workspace_config_path.exists() { info!("loading workspace level bacon.toml"); let workspace_config = Config::from_path(&workspace_config_path)?; diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..c048f13 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,253 @@ +use { + crate::*, + anyhow::{ + Result, + bail, + }, + cargo_metadata::MetadataCommand, + std::{ + env, + fmt, + fs, + path::{ + Path, + PathBuf, + }, + }, +}; + +static DEFAULT_WATCHES: &[&str] = &["src", "tests", "benches", "examples", "build.rs"]; + +/// information on the paths which are relevant for a mission +#[derive(Debug)] +pub struct Context { + pub name: String, + pub intended_dir: PathBuf, + /// The current package + pub package_directory: PathBuf, + /// The root of the workspace, only defined when it makes sense + /// and it's different from the package directory. + /// + /// Today it's only obtained from cargo metadata but in the future + /// it could be obtained from other kind of sources. + pub workspace_root: Option, + cargo_mission_location: Option, + /// When intended, the path given at launch, isn't a package, it means + /// we don't want to watch the whole workspace but only the given path + pub intended_is_package: bool, +} + +/// Specific data for a cargo related mission +struct CargoContext { + pub cargo_toml_file: PathBuf, + pub packages: Vec, +} + +impl fmt::Debug for CargoContext { + fn fmt( + &self, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + f.debug_struct("CargoContext") + .field("cargo_toml_file", &self.cargo_toml_file) + .finish_non_exhaustive() + } +} + +impl Context { + pub fn new(args: &Args) -> Result { + let intended_dir = args + .path + .as_ref() + .map_or_else(|| env::current_dir().unwrap(), PathBuf::from); + let metadata = if args.offline { + MetadataCommand::new() + .current_dir(&intended_dir) + .no_deps() + .other_options(["--frozen".to_string(), "--offline".to_string()]) + .exec() + } else { + MetadataCommand::new().current_dir(&intended_dir).exec() + }; + let need_cargo = false; + let metadata = match metadata { + Ok(m) => Some(m), + Err(cargo_metadata::Error::CargoMetadata { stderr }) + if cargo_manifest_not_found(&stderr) => + { + if need_cargo { + bail!( + "Cargo.toml file not found.\n\ + bacon must be launched \n\ + * in a rust project directory\n\ + * or with a rust project directory given in argument\n\ + (a rust project directory contains a Cargo.toml file or has such parent)\n\ + " + ); + } else { + None + } + } + Err(other) => bail!(other), + }; + let package_directory; + let workspace_root; + let cargo_mission_location; + let intended_is_package; + if let Some(metadata) = metadata { + // Cargo/Rust project + let cargo_toml_file; + if let Some(resolved_root) = metadata.resolve.and_then(|resolve| resolve.root) { + // resolved to a single package + cargo_toml_file = metadata + .packages + .iter() + .find(|p| p.id == resolved_root) + .map(|p| p.manifest_path.as_std_path().to_path_buf()) + .expect("resolved manifest was not in package list"); + package_directory = cargo_toml_file + .parent() + .expect("file has no parent") + .to_path_buf(); + workspace_root = if metadata.workspace_root.as_std_path() == package_directory { + None + } else { + Some(metadata.workspace_root.as_std_path().to_path_buf()) + }; + } else { + // resolved to a virtual manifest (of a workspace) + package_directory = metadata.workspace_root.as_std_path().to_path_buf(); + cargo_toml_file = package_directory.join("Cargo.toml"); + workspace_root = None; + } + intended_is_package = + fs::canonicalize(&intended_dir)? == fs::canonicalize(&package_directory)?; + cargo_mission_location = Some(CargoContext { + cargo_toml_file, + packages: metadata.packages, + }); + } else { + // Non cargo project + workspace_root = None; + cargo_mission_location = None; + package_directory = intended_dir.clone(); + intended_is_package = true; + }; + let name = package_directory + .file_name() + .unwrap_or(package_directory.as_os_str()) + .to_string_lossy() + .to_string(); + Ok(Self { + name, + intended_dir, + package_directory, + workspace_root, + intended_is_package, + cargo_mission_location, + }) + } + pub fn mission<'s>( + &self, + concrete_job_ref: ConcreteJobRef, + job: Job, + settings: &'s Settings, + ) -> Result> { + let location_name = self.name.clone(); + // We don't need to make the difference between a file and a dir, this can + // be determined by the watcher + + // "watches", at this point, aren't full path, they still must be joined + // with the right path which may depend on the + let mut watches: Vec<&str> = job + .watch + .as_ref() + .unwrap_or(&settings.watch) + .iter() + .map(|s| s.as_str()) + .collect(); + let add_default = job.default_watch.unwrap_or(settings.default_watch); + if add_default { + for watch in DEFAULT_WATCHES { + if !watches.contains(watch) { + watches.push(watch); + } + } + } + debug!("watches: {watches:?}"); + + let mut paths_to_watch: Vec = Vec::new(); + + // when bacon is given a path at launch, and when this path isn't a + // path to a package, the goal is to specify that this path should + // be watched, wich allows not watching everything in the workspace + info!("intended_is_package: {0}", self.intended_is_package); + if self.intended_is_package { + // automatically watch all kinds of source files + add_to_paths_to_watch(&watches, &self.intended_dir, &mut paths_to_watch); + if let Some(workspace_root) = &self.workspace_root { + add_to_paths_to_watch(&watches, workspace_root, &mut paths_to_watch); + } + if let Some(location) = &self.cargo_mission_location { + for item in &location.packages { + if item.source.is_none() { + // FIXME why this check + let item_path = item + .manifest_path + .parent() + .expect("parent of a target folder is a root folder"); + add_to_paths_to_watch( + &watches, + item_path.as_std_path(), + &mut paths_to_watch, + ); + if item.manifest_path.exists() { + paths_to_watch.push(item.manifest_path.clone().into()); + } else { + warn!("missing manifest file: {:?}", item.manifest_path); + } + } + } + } + } else { + // we only watch the given "intended" path + paths_to_watch.push(self.intended_dir.clone()); + } + + let execution_directory = self.package_directory.clone(); + Ok(Mission { + location_name, + concrete_job_ref, + execution_directory, + package_directory: self.package_directory.clone(), + job, + paths_to_watch, + settings, + }) + } + pub fn package_config_path(&self) -> PathBuf { + self.package_directory.join("bacon.toml") + } + /// return the location of the workspace level bacon.toml file + /// (it may be the same path than the package config) + pub fn workspace_config_path(&self) -> Option { + self.workspace_root.as_ref().map(|p| p.join("bacon.toml")) + } +} + +fn cargo_manifest_not_found(err: &str) -> bool { + err.starts_with("error: could not find `Cargo.toml`") +} + +fn add_to_paths_to_watch( + watches: &[&str], + base_path: &Path, + paths_to_watch: &mut Vec, +) { + for watch in watches { + let full_path = base_path.join(watch); + if !paths_to_watch.contains(&full_path) && full_path.exists() { + paths_to_watch.push(full_path); + } + } +} diff --git a/src/export/export_settings.rs b/src/export/export_settings.rs index 158ed49..0d639d2 100644 --- a/src/export/export_settings.rs +++ b/src/export/export_settings.rs @@ -20,7 +20,7 @@ impl ExportSettings { state: &AppState<'_>, ) -> anyhow::Result<()> { let path = if self.path.is_relative() { - state.mission.workspace_root.join(&self.path) + state.mission.package_directory.join(&self.path) } else { self.path.to_path_buf() }; diff --git a/src/lib.rs b/src/lib.rs index 227878e..ac4563d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod app; mod auto_refresh; mod cli; mod conf; +mod context; mod drawing; mod exec; mod export; @@ -12,7 +13,6 @@ mod internal; mod jobs; mod messages; mod mission; -mod mission_location; mod result; mod scroll; mod state; @@ -25,6 +25,7 @@ pub use { auto_refresh::*, cli::*, conf::*, + context::*, drawing::*, exec::*, export::*, @@ -34,7 +35,6 @@ pub use { jobs::*, messages::*, mission::*, - mission_location::*, result::*, scroll::*, state::*, diff --git a/src/mission.rs b/src/mission.rs index e6ef2f2..2234854 100644 --- a/src/mission.rs +++ b/src/mission.rs @@ -1,98 +1,24 @@ use { crate::*, - anyhow::Result, lazy_regex::regex_replace_all, rustc_hash::FxHashSet, std::path::PathBuf, }; -static DEFAULT_WATCHES: &[&str] = &["src", "tests", "benches", "examples", "build.rs"]; - /// the description of the mission of bacon /// after analysis of the args, env, and surroundings #[derive(Debug)] pub struct Mission<'s> { pub location_name: String, pub concrete_job_ref: ConcreteJobRef, - pub cargo_execution_directory: PathBuf, - pub workspace_root: PathBuf, + pub execution_directory: PathBuf, + pub package_directory: PathBuf, pub job: Job, - pub files_to_watch: Vec, - pub directories_to_watch: Vec, + pub paths_to_watch: Vec, pub settings: &'s Settings, } impl<'s> Mission<'s> { - pub fn new( - location: &MissionLocation, - concrete_job_ref: ConcreteJobRef, - job: Job, - settings: &'s Settings, - ) -> Result { - let location_name = location.name(); - let add_all_src = location.intended_is_package; - let mut files_to_watch: Vec = Vec::new(); - let mut directories_to_watch = Vec::new(); - if !location.intended_is_package { - directories_to_watch.push(location.intended_dir.clone()); - } - for item in &location.packages { - if item.source.is_none() { - let item_path = item - .manifest_path - .parent() - .expect("parent of a target folder is a root folder"); - if add_all_src { - let mut watches: Vec<&str> = job - .watch - .as_ref() - .unwrap_or(&settings.watch) - .iter() - .map(|s| s.as_str()) - .collect(); - let add_default = job.default_watch.unwrap_or(settings.default_watch); - if add_default { - for watch in DEFAULT_WATCHES { - if !watches.contains(watch) { - watches.push(watch); - } - } - } - debug!("watches: {watches:?}"); - for dir in &watches { - let full_path = item_path.join(dir); - if full_path.exists() { - if full_path.is_dir() { - directories_to_watch.push(full_path.into()); - } else { - files_to_watch.push(full_path.into()); - } - } else { - debug!("missing {} : {:?}", dir, full_path); - } - } - } - if item.manifest_path.exists() { - files_to_watch.push(item.manifest_path.clone().into()); - } else { - warn!("missing manifest file: {:?}", item.manifest_path); - } - } - } - - let cargo_execution_directory = location.package_directory.clone(); - Ok(Mission { - location_name, - concrete_job_ref, - cargo_execution_directory, - workspace_root: location.workspace_root.clone(), - job, - files_to_watch, - directories_to_watch, - settings, - }) - } - /// Return an Ignorer if required by the job's settings /// and if the mission takes place in a git repository pub fn ignorer(&self) -> Option { @@ -103,7 +29,7 @@ impl<'s> Mission<'s> { } _ => { // by default we apply gitignore rules - match Ignorer::new(&self.workspace_root) { + match Ignorer::new(&self.package_directory) { Ok(ignorer) => Some(ignorer), Err(e) => { // might be normal, eg not in a git repo @@ -172,7 +98,7 @@ impl<'s> Mission<'s> { if !self.job.extraneous_args { command.args(tokens); - command.current_dir(&self.cargo_execution_directory); + command.current_dir(&self.execution_directory); command.envs(&self.job.env); debug!("command: {:#?}", &command); return command; @@ -251,7 +177,7 @@ impl<'s> Mission<'s> { command.arg(arg); } } - command.current_dir(&self.cargo_execution_directory); + command.current_dir(&self.execution_directory); command.envs(&self.job.env); debug!("command builder: {:#?}", &command); command diff --git a/src/mission_location.rs b/src/mission_location.rs deleted file mode 100644 index 483c403..0000000 --- a/src/mission_location.rs +++ /dev/null @@ -1,121 +0,0 @@ -use { - crate::*, - anyhow::{ - Result, - bail, - }, - cargo_metadata::MetadataCommand, - std::{ - env, - fmt, - fs, - path::PathBuf, - }, -}; - -/// information on the paths which are relevant for a mission -pub struct MissionLocation { - pub intended_dir: PathBuf, - pub workspace_root: PathBuf, - pub package_directory: PathBuf, - pub cargo_toml_file: PathBuf, - pub intended_is_package: bool, - pub packages: Vec, -} - -impl fmt::Debug for MissionLocation { - fn fmt( - &self, - f: &mut fmt::Formatter<'_>, - ) -> fmt::Result { - f.debug_struct("MissionLocation") - .field("intended_dir", &self.intended_dir) - .field("package_directory", &self.package_directory) - .field("cargo_toml_file", &self.cargo_toml_file) - .field("intended_is_package", &self.intended_is_package) - .finish() - } -} - -impl MissionLocation { - pub fn new(args: &Args) -> Result { - let intended_dir = args - .path - .as_ref() - .map_or_else(|| env::current_dir().unwrap(), PathBuf::from); - let metadata = if args.offline { - MetadataCommand::new() - .current_dir(&intended_dir) - .no_deps() - .other_options(["--frozen".to_string(), "--offline".to_string()]) - .exec() - } else { - MetadataCommand::new().current_dir(&intended_dir).exec() - }; - let metadata = match metadata { - Ok(m) => m, - Err(cargo_metadata::Error::CargoMetadata { stderr }) - if cargo_manifest_not_found(&stderr) => - { - bail!( - "Cargo.toml file not found.\n\ - bacon must be launched \n\ - * in a rust project directory\n\ - * or with a rust project directory given in argument\n\ - (a rust project directory contains a Cargo.toml file or has such parent)\n\ - " - ); - } - Err(other) => bail!(other), - }; - let workspace_root = metadata.workspace_root.clone().into(); - let cargo_toml_file; - let package_directory; - if let Some(resolved_root) = metadata.resolve.and_then(|resolve| resolve.root) { - // resolved to a single package - cargo_toml_file = metadata - .packages - .iter() - .find(|p| p.id == resolved_root) - .map(|p| p.manifest_path.as_std_path().to_path_buf()) - .expect("resolved manifest was not in package list"); - package_directory = cargo_toml_file - .parent() - .expect("file has no parent") - .to_path_buf(); - } else { - // resolved to a virtual manifest (of a workspace) - package_directory = metadata.workspace_root.as_std_path().to_path_buf(); - cargo_toml_file = package_directory.join("Cargo.toml"); - } - let intended_is_package = - fs::canonicalize(&intended_dir)? == fs::canonicalize(&package_directory)?; - Ok(Self { - intended_dir, - package_directory, - workspace_root, - cargo_toml_file, - intended_is_package, - packages: metadata.packages, - }) - } - pub fn name(&self) -> String { - self.package_directory - .file_name() - .unwrap_or(self.package_directory.as_os_str()) - .to_string_lossy() - .to_string() - } - pub fn package_config_path(&self) -> PathBuf { - self.package_directory.join("bacon.toml") - } - /// return the location of the workspace level bacon.toml file - /// (it may be the same path than the package config) - pub fn workspace_config_path(&self) -> PathBuf { - self.workspace_root.join("bacon.toml") - } -} - -fn cargo_manifest_not_found(err: &str) -> bool { - err.starts_with("error: could not find `Cargo.toml`") -} diff --git a/src/result/line.rs b/src/result/line.rs index 020ff2e..f40e532 100644 --- a/src/result/line.rs +++ b/src/result/line.rs @@ -59,7 +59,7 @@ impl Line { let location_path = self.location()?; let mut location_path = PathBuf::from(location_path); if !location_path.is_absolute() { - location_path = mission.workspace_root.join(location_path); + location_path = mission.package_directory.join(location_path); } Some(location_path) } diff --git a/src/result/report.rs b/src/result/report.rs index d27a2ff..a4b7141 100644 --- a/src/result/report.rs +++ b/src/result/report.rs @@ -94,7 +94,7 @@ impl Report { let path_string; if path_buf.is_relative() { path_string = mission - .workspace_root + .package_directory .join(path) .to_string_lossy() .to_string(); diff --git a/src/watcher.rs b/src/watcher.rs index 86f07ae..e1c0a83 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -28,8 +28,7 @@ pub struct Watcher { impl Watcher { pub fn new( - files_to_watch: &[PathBuf], - directories_to_watch: &[PathBuf], + paths_to_watch: &[PathBuf], mut ignorer: Option, ) -> Result { let (sender, receiver) = bounded(0); @@ -76,13 +75,18 @@ impl Watcher { } Err(e) => warn!("watch error: {:?}", e), })?; - for file in files_to_watch { - debug!("add watch file {:?}", file); - notify_watcher.watch(file, RecursiveMode::NonRecursive)?; - } - for dir in directories_to_watch { - debug!("add watch dir {:?}", dir); - notify_watcher.watch(dir, RecursiveMode::Recursive)?; + for path in paths_to_watch { + if !path.exists() { + warn!("watch path doesn't exist: {:?}", path); + continue; + } + if path.is_dir() { + debug!("add watch dir {:?}", path); + notify_watcher.watch(path, RecursiveMode::Recursive)?; + } else if path.is_file() { + debug!("add watch file {:?}", path); + notify_watcher.watch(path, RecursiveMode::NonRecursive)?; + } } Ok(Self { _notify_watcher: notify_watcher, diff --git a/website/docs/css/extra.css b/website/docs/css/extra.css index 0b6fb1b..b033e2b 100644 --- a/website/docs/css/extra.css +++ b/website/docs/css/extra.css @@ -22,7 +22,7 @@ body::before { body { background-image: - linear-gradient(to right, rgba(255,255,255,0.8) 0%, rgba(255,255,255,1) 25%, rgba(255,255,255,1) 75%, rgba(255,255,255,0.8) 100%), + linear-gradient(to right, rgba(255,255,255,0.95) 0%, rgba(255,255,255,1) 25%, rgba(255,255,255,1) 75%, rgba(255,255,255,0.95) 100%), url("../img/cochon.jpg"); background-position: center; background-size: cover;