mod cli; mod env; mod fs; mod io; mod java; mod nest; use std::fs::OpenOptions; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use cli::{CONFIG, Command}; use java::{JAVA_EXT_CLASS, JAVA_EXT_SOURCE}; use nest::{Class, NEST, NestLock}; use anyhow::Context; use bytesize::ByteSize; pub static PROJECT_ROOT: LazyLock = LazyLock::new(|| { // Start from CWD let cwd = std::env::current_dir().expect("Failed to get current working directory"); let mut probe = cwd.clone(); while !probe.join(F_NEST_TOML.as_path()).exists() { if !probe.pop() { // This is easier than having a Result everywhere. For now. panic!( "No {} found in current directory or any ancestor. (cwd: {})", F_NEST_TOML.display(), cwd.display() ) } } probe }); const DIR_TARGET: LazyLock = LazyLock::new(|| PROJECT_ROOT.join("target/")); const DIR_SRC: LazyLock = LazyLock::new(|| PROJECT_ROOT.join("src/")); const DIR_MAIN: LazyLock = LazyLock::new(|| DIR_SRC.join("main/")); const DIR_TEST: LazyLock = LazyLock::new(|| DIR_SRC.join("test/")); const F_NEST_TOML: LazyLock = LazyLock::new(|| PathBuf::from("Nest.toml")); const F_NEST_LOCK: LazyLock = LazyLock::new(|| PathBuf::from("Nest.lock")); fn main() -> anyhow::Result<()> { // Ensure that Nest.toml exists, if the requested command depends upon it. if CONFIG.command.depends_on_nest() && NEST.is_err() { println!("No Nest.toml found in project directory"); println!("Aborting..."); return Ok(()); } match &CONFIG.command { Command::Init => init()?, Command::New { project_name } => { new(project_name.to_owned())?; init()?; } Command::Build => { build()?; } Command::Run { entry_point, assertions, } => { build()?; run(entry_point, assertions.into())?; } Command::Test { assertions } => { test(assertions.into())?; } Command::Clean => clean(), } println!("Done."); Ok(()) } fn init() -> anyhow::Result<()> { let cwd = std::env::current_dir()?; let is_empty = std::fs::read_dir(cwd.as_path()).is_ok_and(|tree| tree.count() == 0); let project_name = cwd .file_name() .context("Invalid directory name")? .to_str() .context("Unable to convert OsStr to str")? .to_owned(); // ORDER MATTERS. THIS MUST COME FIRST. // Make config file. if let Result::Ok(mut f) = OpenOptions::new() .write(true) .create_new(true) .open(F_NEST_TOML.as_path()) { f.write_all( toml::to_string_pretty(&nest::Nest { package: nest::Package { name: project_name.to_owned(), ..Default::default() }, })? .as_bytes(), )?; } // Make .java-version if let Result::Ok(mut f) = OpenOptions::new() .write(true) .create_new(true) .open(PathBuf::from(".java-version")) { f.write_all( { let mut version = crate::env::get_javac_ver()?.major.to_string(); version.push('\n'); version } .as_bytes(), )?; } // Make src, target, tests for dir in [DIR_SRC, DIR_MAIN, DIR_TARGET, DIR_TEST] { std::fs::create_dir_all(dir.clone())?; } // Make src/main/Main.java if let Result::Ok(mut f) = OpenOptions::new().write(true).create_new(is_empty).open( DIR_MAIN .clone() .join("Main") .with_extension(JAVA_EXT_SOURCE), ) { f.write_all(include_bytes!("../assets/src/main/Main.java"))?; } // Make src/test/Test.java if let Result::Ok(mut f) = OpenOptions::new().write(true).create_new(is_empty).open( DIR_TEST .clone() .join("Test") .with_extension(JAVA_EXT_SOURCE), ) { f.write_all(include_bytes!("../assets/src/test/Test.java"))?; } // git init . crate::io::run_process(&["git", "init", "."])?; // 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 [ DIR_TARGET.as_path().display().to_string(), format!("*.{}", JAVA_EXT_CLASS), ] { if !buf.contains(&ignored) { f.write(format!("{}\n", ignored).as_bytes())?; } } } Ok(()) } fn new(project_name: String) -> anyhow::Result<()> { let cwd = std::env::current_dir()?.join(project_name); std::fs::create_dir(&cwd)?; std::env::set_current_dir(&cwd)?; Ok(()) } fn build() -> anyhow::Result<()> { let mut targets: Vec = crate::fs::expand_files(DIR_SRC.as_path())? .into_iter() .filter(|f| { f.extension() .is_some_and(|ext| ext.to_str().is_some_and(|ext| ext == JAVA_EXT_SOURCE)) }) .collect(); let mut nest_lock = NestLock::load().unwrap_or_default(); nest_lock.update(); let mut retained_targets = vec![]; for path in targets.into_iter() { if let Option::Some((_path, class)) = nest_lock.0.get_key_value(&path) && class.is_updated() { continue; } retained_targets.push(path); } targets = retained_targets; let javac = java::CompilerBuilder::new() .class_path(DIR_TARGET.as_path()) .destination(DIR_TARGET.as_path()) .build(); for target in targets { println!("Compiling {}", target.display()); if javac.clone().compile(dbg!(target.as_path())).is_ok() && let Result::Ok(class) = Class::try_from(target.clone()) { nest_lock.0.insert(target, class); } } OpenOptions::new() .create(true) .write(true) .truncate(true) .open(F_NEST_LOCK.as_path())? .write_all(toml::to_string_pretty(&nest_lock)?.as_bytes()) .with_context(|| format!("Failed to write {}", F_NEST_LOCK.display())) } fn run>( entry_point: P, assertions: bool, ) -> anyhow::Result<(Option, Option)> { // JRE pathing will be messed up without this. crate::env::set_cwd(DIR_TARGET.as_path())?; java::JVMBuilder::new() .assertions(assertions) .monitor(true) .build() .run(entry_point) } fn test(assertions: bool) -> anyhow::Result<(Option, Option)> { java::CompilerBuilder::new() .class_path(DIR_TARGET.as_path()) .destination(DIR_TARGET.as_path()) .build() .compile(DIR_TEST.as_path())?; // Change cwd to avoid Java pathing issues. crate::env::set_cwd(DIR_TARGET.as_path())?; java::JVMBuilder::new() .assertions(assertions) .ram_min(ByteSize::mib(128)) .ram_max(ByteSize::mib(512)) .monitor(true) .build() .run(DIR_TARGET.as_path()) } fn clean() { let _ = std::fs::remove_file(PROJECT_ROOT.join(F_NEST_LOCK.as_path())); let _ = std::fs::remove_dir_all(DIR_TARGET.join("/*").as_path()); }