use std::collections::HashMap; use std::fs::{OpenOptions, read_dir}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; use fs::expand_files; use java::{JAVA_EXT_CLASS, JAVA_EXT_SOURCE}; use serde::{Deserialize, Serialize}; use subprocess::Exec; use crate::Error; use crate::nest::{F_NEST_LOCK, F_NEST_TOML, Nest, NestLock}; use crate::package::PackageHandler; use crate::prey::{F_PREY_TOML, Prey}; #[derive(Debug)] pub struct WorkspaceHandler { nest: Nest, nest_lock: Option, project_root: PathBuf, packages: HashMap, } impl WorkspaceHandler { const DIR_SRC: &str = "src/"; const DIR_TARGET: &str = "target/"; pub fn new>(project_root: P) -> crate::Result { let project_root = project_root.as_ref().canonicalize()?; Ok(Self { nest: Nest::new( project_root .file_name() .ok_or(Error::MissingFileName)? .display(), ), nest_lock: None, packages: HashMap::new(), project_root, }) } pub fn load>(project_root: P) -> crate::Result { let project_root = project_root.as_ref().canonicalize()?; let mut workspace_manager = Self { nest: Nest::try_from(project_root.join(F_NEST_TOML))?, nest_lock: NestLock::try_from(project_root.join(F_NEST_LOCK)).ok(), packages: HashMap::new(), project_root, }; workspace_manager.discover_packages()?; Ok(workspace_manager) } pub fn write(&self) -> crate::Result<()> { self.write_nest()?; if let Option::Some(lock) = self.nest_lock.clone() { lock.write(self.project_root.join(F_NEST_LOCK))?; } Ok(()) } pub fn init(&mut self) -> crate::Result<&mut Self> { // ORDER MATTERS. let is_empty = read_dir(self.project_root.as_path()).is_ok_and(|tree| tree.count() == 0); // Make config file. self.write_nest()?; // Make .java-version self.write_java_version()?; if is_empty { self.write_example_project()?; self.discover_packages()?; Exec::cmd("git") .arg("init") .arg(".") .start()? .wait_timeout(Duration::from_secs(10))?; } // Append to .gitignore if let Result::Ok(mut f) = OpenOptions::new() .append(true) .create(true) .read(true) .open(".gitignore") { let mut buf = String::new(); f.read_to_string(&mut buf)?; for ignored in [ "# Automatically added by Raven".to_string(), Self::DIR_TARGET.to_string(), format!("*.{}", JAVA_EXT_CLASS), ] { if !buf.contains(&ignored) { f.write(format!("{}\n", ignored).as_bytes())?; } } } Ok(self) } // This is the naive build pub fn build(&mut self) -> crate::Result<&mut Self> { let mut targets = vec![]; for handler in self.packages.values_mut() { targets.append(&mut handler.get_update_targets()?); } let compiler = java::compiler::CompilerBuilder::new() .class_path(Self::DIR_TARGET) .destination(Self::DIR_TARGET) .build(); for target in targets.iter() { // Possibly come up with a source file handler for this? if let Ok(_) = compiler.clone().compile(target.path.as_path()) { // TODO: Prevent unnecessary recompile //target.update()?; } } Ok(self) } pub fn run>( &mut self, entry_point: Option

, assertions: bool, ) -> crate::Result<&mut Self> { let mut entry_point = if entry_point.is_none() { self.nest.default_package() } else { entry_point.unwrap().as_ref().to_path_buf() }; if !entry_point.is_file() { // Use is_file to skip messing with pathing for src/ // If target is not a file (explicit entry point), check if it's a known package // and use that's package's default entry point. entry_point = self .packages .get(&entry_point) .ok_or(Error::UnknownPackage)? .entry_point(); } java::runtime::JVMBuilder::new(Self::DIR_TARGET) .assertions(assertions) .monitor(true) .build() .run(entry_point)?; Ok(self) } pub fn clean(&mut self) -> crate::Result<&mut Self> { std::fs::remove_file(self.project_root.join(F_NEST_LOCK))?; std::fs::remove_dir_all(Self::DIR_TARGET)?; Ok(self) } /// Add any newly created packages. fn discover_packages(&mut self) -> crate::Result<()> { // Scan the src/ directory for entries, // filter out the files, // then construct PackageManagers for each package // Promote *not* using reverse domain name tree structures // by improving the speed of package discovery by using read_dir // and checking for an immediate Prey.toml before expanding the // whole subtree. // // Yes, I know this looks like shit. // That's because it is. for file in read_dir(self.project_root.join(Self::DIR_SRC))? // Get directories .filter_map(|entry| { if entry.as_ref().is_ok_and(|entry| entry.path().is_dir()) { Some(entry.unwrap().path()) } else { None } }) // Get Prey.toml files .filter_map(|dir| { Some(if dir.join(F_PREY_TOML).exists() { vec![dir.join(F_PREY_TOML)] } else { expand_files(dir) .ok()? .iter() .filter_map(|file| { if file.ends_with(PathBuf::from(F_PREY_TOML)) { Some(file.to_owned()) } else { None } }) .collect() }) }) .flatten() { let package_root = pathsub::sub_paths( file.as_path(), self.project_root.join(Self::DIR_SRC).as_path(), ) .unwrap() .parent() .unwrap() .to_path_buf(); self.packages.insert( package_root.to_path_buf(), PackageHandler::new( self.project_root.join(Self::DIR_SRC), package_root, self.project_root.join(Self::DIR_TARGET), )?, ); } Ok(()) } fn write_nest(&self) -> crate::Result<()> { Ok(self.nest.write(self.project_root.clone())?) } fn write_java_version(&self) -> crate::Result<()> { if let Result::Ok(mut f) = OpenOptions::new() .write(true) .create_new(true) .open(java::F_JAVA_VERSION) { f.write_all(format!("{}\n", java::get_javac_ver()?.major.to_string()).as_bytes())?; } Ok(()) } fn write_dir_tree(&self) -> std::io::Result<()> { for dir in [ format!("{}main/java", Self::DIR_SRC), format!("{}test/java", Self::DIR_SRC), Self::DIR_TARGET.to_string(), ] { std::fs::create_dir_all(std::env::current_dir()?.join(dir))?; } Ok(()) } fn write_example_project(&self) -> crate::Result<()> { let main: PathBuf = PathBuf::from(Self::DIR_SRC).join("main/"); let test: PathBuf = PathBuf::from(Self::DIR_SRC).join("test/"); // Make src/, target/, test/ self.write_dir_tree()?; // Make src/main/Prey.toml if let Result::Ok(mut f) = OpenOptions::new() .write(true) .create_new(true) .open(main.join(F_PREY_TOML)) { f.write_all( toml::to_string_pretty(&Prey::new("main").with_entry_point("Main"))?.as_bytes(), )?; } // Make src/main/Main.java if let Result::Ok(mut f) = OpenOptions::new() .write(true) .create_new(true) .open(main.join("java/Main").with_extension(JAVA_EXT_SOURCE)) { f.write_all(include_bytes!("../assets/Main.java"))?; } // Make src/test/Prey.toml if let Result::Ok(mut f) = OpenOptions::new() .write(true) .create_new(true) .open(test.join(F_PREY_TOML)) { f.write_all( toml::to_string_pretty(&Prey::new("test").with_entry_point("MainTest"))?.as_bytes(), )?; } // Make src/test/MainTest.java if let Result::Ok(mut f) = OpenOptions::new() .write(true) .create_new(true) .open(test.join("java/MainTest").with_extension(JAVA_EXT_SOURCE)) { f.write_all(include_bytes!("../assets/MainTest.java"))?; } Ok(()) } } /// Data struct #[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Workspace { pub default_package: PathBuf, } impl Default for Workspace { fn default() -> Self { Workspace { default_package: PathBuf::from("main"), } } }