diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..67dc089 --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,18 @@ +// Project-local debug tasks +// +// For more documentation on how to configure debug tasks, +// see: https://zed.dev/docs/debugger +[ + { + "label": "Build & Debug raven new", + "build": { + "command": "cargo", + "args": ["run", "--", "new", "JavaProjectTest"], + }, + "program": "$ZED_WORKTREE_ROOT/target/debug/binary", + // sourceLanguages is required for CodeLLDB (not GDB) when using Rust + "sourceLanguages": ["rust"], + "request": "launch", + "adapter": "CodeLLDB", + }, +] diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..b1eee26 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,9 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "file_types": { + "TOML": ["lock"], + }, +} diff --git a/Cargo.lock b/Cargo.lock index abb7af2..fdc1e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "async-trait" @@ -66,17 +66,14 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -89,9 +86,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytesize" @@ -107,9 +104,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.54" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -117,9 +114,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -129,21 +126,28 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "cli" +version = "0.2.1" +dependencies = [ + "clap", +] [[package]] name = "colorchoice" @@ -151,6 +155,31 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core" +version = "0.1.4" +dependencies = [ + "anyhow", + "derive_more", + "fs", + "java", + "pathsub", + "semver", + "serde", + "sha256", + "subprocess", + "toml", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -170,6 +199,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.116", + "unicode-xid", +] + [[package]] name = "digest" version = "0.10.7" @@ -186,6 +238,13 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fs" +version = "0.1.0" +dependencies = [ + "derive_more", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -196,6 +255,31 @@ dependencies = [ "version_check", ] +[[package]] +name = "hard-xml" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b07b8ba970e18a03dbb79f6786b6e4d6f198a0ac839aa5182017001bb8dee17" +dependencies = [ + "hard-xml-derive", + "jetscii", + "lazy_static", + "memchr", + "xmlparser", +] + +[[package]] +name = "hard-xml-derive" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c43e7c3212bd992c11b6b9796563388170950521ae8487f5cdf6f6e792f1c8" +dependencies = [ + "bitflags", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -231,16 +315,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +name = "java" +version = "0.2.2" +dependencies = [ + "bytesize", + "derive_more", + "fs", + "semver", + "subprocess", +] [[package]] -name = "once_cell" -version = "1.21.3" +name = "jetscii" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "once_cell_polyfill" @@ -248,12 +355,29 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "pathsub" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dadd38133bcbe43264410412c48614bab4ef899f0792ffc4530dc19ec000a970" + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pom" +version = "0.1.0" +dependencies = [ + "derive_more", + "hard-xml", + "semver", + "serde", + "strum", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -265,40 +389,33 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "raven" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "bytesize", - "clap", - "ron", - "semver", - "serde", - "sha256", - "subprocess", + "cli", + "core", + "fs", + "java", "toml", ] [[package]] -name = "ron" -version = "0.12.0" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", + "semver", ] [[package]] @@ -338,7 +455,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -381,10 +498,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "subprocess" -version = "0.2.13" +name = "strum" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75238edb5be30a9ea3035b945eb9c319dde80e879411cdc9a8978e1ac822960" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "subprocess" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98e067360c7f5a302e35c9f9cd24cb583c378fbedc73d0237284dbdd0650fcc5" dependencies = [ "libc", "winapi", @@ -392,9 +530,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -413,9 +562,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "bbe30f93627849fa362d4a602212d41bb237dc2bd0f8ba0b2ce785012e124220" dependencies = [ "indexmap", "serde_core", @@ -428,18 +577,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" dependencies = [ "winnow", ] @@ -450,12 +599,6 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.19.0" @@ -464,9 +607,21 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utf8parse" @@ -522,3 +677,9 @@ name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" diff --git a/Cargo.toml b/Cargo.toml index fa363ba..b224154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,48 @@ -[package] -name = "raven" -version = "0.1.0" +[workspace] +resolver = "3" +members = ["crates/*"] +default-members = ["crates/*"] + +[workspace.package] edition = "2024" -authors = ["Olivia Brooks", "Adrian Long"] -repository = "https://gitea.cutieguwu.ca/Cutieguwu/raven" license = "MIT" + +repository = "https://gitea.cutieguwu.ca/Cutieguwu/raven" + publish = false -[dependencies] +[workspace.dependencies] +# +# Workspace member crates +# +cli = { path = "crates/cli" } +fs = { path = "crates/fs" } +java = { path = "crates/java" } +core = { path = "crates/core" } +pom = { path = "crates/pom" } +raven = { path = "crates/raven" } + +# +# External crates +# anyhow = "1.0" bytesize = "2.3" -ron = "0.12" +const_format = "0.2.35" +hard-xml = "1.41" +lenient_semver = "0.4.2" +pathsub = "0.1.1" +ron = "0.12.0" sha256 = "1.6" -subprocess = "0.2" -toml = "0.9" +subprocess = "1.0" +toml = "1.0" -[dependencies.clap] -version = "4.5" -features = ["cargo", "derive"] +derive_more = { version = "2.1", features = ["display", "from"] } +semver = { version = "1.0",features = ["serde"] } +serde = { version = "1.0",features = ["derive"] } +strum = { version = "0.27.2", features = ["derive"] } -[dependencies.semver] -version = "1.0" -features = ["serde"] +[workspace.lints.rust] +unsafe_code = "forbid" -[dependencies.serde] -version = "1.0" -features =["derive"] +[profile.dev] +incremental = true diff --git a/README.md b/README.md index 4a94467..1de7495 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,12 @@ WSL *may* affect the behaviour of raven. Quirks to be worked out. ```bash raven new demo cd demo -raven run main.Main +raven run main ``` ## Raven commands + +```bash Usage: raven Commands: @@ -47,6 +49,7 @@ Commands: Options: -h, --help Print help -V, --version Print version +``` ## Future plans diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000..26cfc41 --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cli" +version = "0.2.1" +edition.workspace = true +license.workspace = true + +description = "Raven's CLI" +repository.workspace = true + +publish.workspace = true + +[dependencies.clap] +version = "4.5" +features = ["cargo", "derive"] diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000..6931437 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,152 @@ +use std::sync::LazyLock; +use std::{fmt::Display, path::PathBuf}; + +use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; + +pub static CLI_ARGS: LazyLock = LazyLock::new(|| CliArgs::parse()); + +#[derive(Debug, Parser)] +#[clap(name = "Raven", version = "0.2.0")] +pub struct CliArgs { + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum Command { + /// Create a new raven project + New { + #[clap(flatten)] + type_: ProjectFlag, + project_name: String, + }, + /// Create a new raven project in an existing directory + Init, + /// Compile the current project + Build, + /// Run the current project + Run { + #[clap(value_hint = clap::ValueHint::DirPath)] + entry_point: Option, + + #[clap(flatten)] + assertions: Assertions, + }, + /// !!! BORKED !!! Run the tests + Test { + #[clap(flatten)] + assertions: Assertions, + }, + /// Remove the target directory and caching + Clean, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Args)] +pub struct Assertions { + /// Disable assertions. + #[arg(short, long = "no-assert", action = ArgAction::SetFalse)] + assertions: bool, +} + +impl Into for &Assertions { + fn into(self) -> bool { + self.assertions + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Args)] +#[group(multiple = false)] +pub struct ProjectFlag { + #[arg(long, action = ArgAction::SetTrue)] + _nest: (), + #[arg(long, action = ArgAction::SetTrue)] + _package: (), + + #[arg( + hide = true, + required = false, + short, + long, + default_value_ifs = [ + ("_nest", "true", "nest"), + ("_package", "true", "package"), + ("_nest", "false", "nest"), + ], + )] + pub project_type: ProjectType, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum ProjectType { + #[default] + Nest, + Package, +} + +impl Display for ProjectType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Self::to_string(&self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn args_new_w_arg() { + let args: CliArgs = Parser::try_parse_from(["raven", "new", "demo"]).unwrap(); + let type_ = match args.command { + Command::New { type_, .. } => type_, + _ => unreachable!(), + }; + + assert!(type_.project_type == ProjectType::Nest); + } + + #[test] + fn args_new_nest() { + let args: CliArgs = Parser::try_parse_from(["raven", "new", "--nest", "demo"]).unwrap(); + let type_ = match args.command { + Command::New { type_, .. } => type_, + _ => unreachable!(), + }; + + assert!(type_.project_type == ProjectType::Nest); + } + + #[test] + fn args_new_package() { + let args: CliArgs = Parser::try_parse_from(["raven", "new", "--package", "demo"]).unwrap(); + let type_ = match args.command { + Command::New { type_, .. } => type_, + _ => unreachable!(), + }; + + assert!(type_.project_type == ProjectType::Package); + } + + #[test] + fn args_run_assert() { + let args: CliArgs = Parser::try_parse_from(["raven", "run", "Main"]).unwrap(); + let assertions: bool = match args.command { + Command::Run { assertions, .. } => assertions.assertions, + _ => unreachable!(), + }; + + assert!(assertions == true) + } + + #[test] + fn args_run_no_assert() { + let args: CliArgs = + Parser::try_parse_from(["raven", "run", "--no-assert", "Main"]).unwrap(); + let assertions: bool = match args.command { + Command::Run { assertions, .. } => assertions.assertions, + _ => unreachable!(), + }; + + assert!(assertions == false) + } +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 0000000..0fdd923 --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "core" +version = "0.1.4" +edition.workspace = true +license.workspace = true + +description = "Raven's core, including metadata tooling and resources" +repository.workspace = true + +publish.workspace = true + +[dependencies] +derive_more.workspace = true +fs.workspace = true +java.workspace = true +pathsub.workspace = true +semver.workspace = true +serde.workspace = true +sha256.workspace = true +subprocess.workspace = true +toml.workspace = true + +[dependencies.anyhow] +workspace = true +optional = true + +[features] +into_anyhow = ["dep:anyhow"] diff --git a/assets/src/main/Main.java b/crates/core/assets/Main.java similarity index 100% rename from assets/src/main/Main.java rename to crates/core/assets/Main.java diff --git a/assets/src/test/MainTest.java b/crates/core/assets/MainTest.java similarity index 100% rename from assets/src/test/MainTest.java rename to crates/core/assets/MainTest.java diff --git a/crates/core/src/class.rs b/crates/core/src/class.rs new file mode 100644 index 0000000..accec4e --- /dev/null +++ b/crates/core/src/class.rs @@ -0,0 +1,74 @@ +use std::path::{Path, PathBuf}; + +use java::{JAVA_EXT_CLASS, JAVA_EXT_SOURCE}; +use serde::{Deserialize, Serialize}; + +use crate::{Error, Result}; + +/// [`Class`] represents a source file. It is a handler. +#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Class { + /// Path relative to PACKAGE/java, without file extension. + pub path: PathBuf, + pub checksum: String, +} + +impl Class { + /// Creates a new [`Class`]. + /// + /// # Error + /// + /// * [`Error::MismatchedPackage`] if the provided `file_path` is absolute, and cannot be + /// subtracted to just its relative path within its parent package. + /// * [`Error::Io`] if the source file cannot be loaded and digested. + pub fn new>(package_src_root: P, file_path: P) -> Result { + let mut file_path = file_path.as_ref().to_path_buf(); + if file_path.is_absolute() { + file_path = pathsub::sub_paths(file_path.as_path(), package_src_root.as_ref()) + .ok_or(Error::MismatchedPackage)?; + } + + Ok(Self { + path: dbg!(file_path.with_extension("")), + checksum: sha256::try_digest(package_src_root.as_ref().join(file_path))?, + }) + } + + /// Returns a boolean representing if the class is up to date. + /// + /// # Criteria + /// + /// * The class path is local. + /// * The class has been compiled to `target/` + /// * The class' source file has not been updated. + /// + /// # Errors + /// + /// * [`Error::Io`] + pub fn is_updated>(&self, class_path: P) -> Result { + // If the path is local and that file has not been updated. + Ok(class_path + .as_ref() + .join(self.path.as_path()) + .with_extension(JAVA_EXT_CLASS) + .exists() + && self.checksum == sha256::try_digest(self.path.clone())?) + } + + /// Updates the class' checksum. + /// + /// Only call this if you know that the class has been compiled successfully. + /// + /// # Errors + /// + /// * [`Error::Io`] if the source file cannot be loaded and digested. + pub fn update>(&mut self, package_src_root: P) -> Result<()> { + self.checksum = sha256::try_digest(dbg!( + package_src_root + .as_ref() + .join(self.path.as_path()) + .with_extension(JAVA_EXT_SOURCE), + ))?; + Ok(()) + } +} diff --git a/crates/core/src/dependency.rs b/crates/core/src/dependency.rs new file mode 100644 index 0000000..ce45527 --- /dev/null +++ b/crates/core/src/dependency.rs @@ -0,0 +1,51 @@ +use semver::Version; +use serde::{Deserialize, Serialize}; + +use crate::Result; +use crate::package::PackageHandler; + +/// Data struct +#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Dependency { + name: String, + #[serde(skip_serializing_if = ">::is_none")] + version: Option, + pub checksum: String, + #[serde(skip_serializing_if = ">::is_none")] + source: Option, // Path / URL +} + +impl Dependency { + /// Returns a path to the dependency in local storage + /// if there is one. + pub fn local_path(&self) -> String { + if self.source.as_ref().is_some_and(|path| !is_url(path)) { + return self.source.clone().unwrap(); + } + + // TODO: Convert from reverse domain name to path. + return self.name.clone(); + } + + pub fn is_updated(&self) -> Result { + // If the path is local and that file has not been updated. + Ok(self.source.as_ref().is_some_and(|path| is_url(path)) + && self.checksum == sha256::try_digest(self.source.as_ref().unwrap())?) + } +} + +impl From for Dependency { + fn from(value: PackageHandler) -> Self { + Dependency { + name: value.name(), + version: Some(value.version()), + checksum: String::new(), + source: None, + } + } +} + +/// TODO: This is just a placeholder at present. +fn is_url(_path: S) -> bool { + return false; +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs new file mode 100644 index 0000000..5a030ab --- /dev/null +++ b/crates/core/src/error.rs @@ -0,0 +1,30 @@ +use derive_more::{Display, From}; + +pub type Result = std::result::Result; + +#[derive(Debug, From, Display)] +pub enum Error { + /// Attempted to replace a value in a hash collection, + /// but there was no prime present when one was expected. + AbsentPrimeHashingError, + + #[from] + Io(std::io::Error), + + #[from] + Java(java::Error), + + MissingFileName, + + MismatchedPackage, + + #[from] + TomlDeserialize(toml::de::Error), + + #[from] + TomlSerialize(toml::ser::Error), + + UndefinedEntryPoint, + + UnknownPackage, +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 0000000..88f608f --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,11 @@ +pub mod class; +pub mod dependency; +pub mod error; +pub mod meta; +pub mod nest; +pub mod package; +pub mod prelude; +pub mod prey; +pub mod workspace; + +pub use error::{Error, Result}; diff --git a/crates/core/src/meta.rs b/crates/core/src/meta.rs new file mode 100644 index 0000000..3095992 --- /dev/null +++ b/crates/core/src/meta.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +use semver::Version; +use serde::{Deserialize, Serialize}; + +/// Data struct +#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Meta { + pub name: String, + pub version: Version, + #[serde(skip_serializing_if = ">::is_none")] + pub authors: Option>, + #[serde(skip_serializing_if = ">::is_none")] + pub repository: Option, + #[serde(skip_serializing_if = ">::is_none")] + pub license: Option, + #[serde(skip_serializing_if = ">::is_none")] + pub license_file: Option, +} + +impl Meta { + pub fn new(name: S) -> Self { + let mut meta = Self::default(); + meta.name = name.to_string(); + meta + } +} + +impl Default for Meta { + fn default() -> Self { + Meta { + name: String::from("Main"), + version: Version::new(0, 1, 0), + authors: None, + repository: None, + license: None, + license_file: None, + } + } +} diff --git a/crates/core/src/nest.rs b/crates/core/src/nest.rs new file mode 100644 index 0000000..c13c8c2 --- /dev/null +++ b/crates/core/src/nest.rs @@ -0,0 +1,110 @@ +use std::collections::HashSet; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::dependency::Dependency; +use crate::meta::Meta; +use crate::workspace::Workspace; + +pub const F_NEST_TOML: &str = "Nest.toml"; +pub const F_NEST_LOCK: &str = "Nest.lock"; + +/// Data struct +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct Nest { + workspace: Workspace, + meta: Meta, + dependencies: HashSet, +} + +impl Nest { + pub fn new(name: S) -> Self { + Self { + workspace: Workspace::default(), + meta: Meta::new(name), + dependencies: Default::default(), + } + } + + pub fn write>(&self, project_root: P) -> crate::Result<()> { + let mut project_root = project_root.as_ref().to_path_buf(); + if project_root.is_dir() { + project_root = project_root.join(F_NEST_TOML); + } + + Ok(OpenOptions::new() + .write(true) + .create(true) + .open(project_root)? + .write_all(toml::to_string_pretty(&self)?.as_bytes())?) + } + + pub fn default_package(&self) -> PathBuf { + self.workspace.default_package.clone() + } + + pub fn name(&self) -> String { + self.meta.name.clone() + } + + pub fn set_default_package>(&mut self, package: P) { + self.workspace.default_package = package.as_ref().to_path_buf(); + } +} + +impl TryFrom for Nest { + type Error = crate::Error; + + fn try_from(value: PathBuf) -> Result { + let f = OpenOptions::new().read(true).open(value)?; + Self::try_from(f) + } +} + +impl TryFrom for Nest { + type Error = crate::Error; + + fn try_from(mut value: File) -> Result { + let mut buf = String::new(); + value.read_to_string(&mut buf)?; + Ok(toml::from_str(buf.as_str())?) + } +} + +/// Data struct +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct NestLock { + pub dependencies: Vec, +} + +impl NestLock { + pub fn write>(&self, path: P) -> crate::Result<()> { + Ok(OpenOptions::new() + .write(true) + .create(true) + .open(path)? + .write_all(toml::to_string_pretty(&self)?.as_bytes())?) + } +} + +impl TryFrom for NestLock { + type Error = crate::Error; + + fn try_from(value: PathBuf) -> Result { + let f = OpenOptions::new().read(true).open(value)?; + Self::try_from(f) + } +} + +impl TryFrom for NestLock { + type Error = crate::Error; + + fn try_from(mut value: File) -> Result { + let mut buf = String::new(); + value.read_to_string(&mut buf)?; + Ok(toml::from_str(buf.as_str())?) + } +} diff --git a/crates/core/src/package.rs b/crates/core/src/package.rs new file mode 100644 index 0000000..4991dc4 --- /dev/null +++ b/crates/core/src/package.rs @@ -0,0 +1,85 @@ +use std::hash::Hash; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::class::Class; +use crate::prey::{F_PREY_LOCK, F_PREY_TOML, Prey, PreyLock}; + +pub const DIR_JAVA: &str = "java/"; + +/// Hashing is only based off the Prey. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackageHandler { + prey: Prey, + prey_lock: PreyLock, + /// Path relative to WORKSPACE/src + package_root: PathBuf, + target_dir: PathBuf, +} + +impl PackageHandler { + pub fn new>(src_dir: P, package_root: P, target_dir: P) -> crate::Result { + let package_root = src_dir.as_ref().join(package_root.as_ref()); + let prey_lock = if let Ok(prey_lock) = PreyLock::try_from(package_root.join(F_PREY_LOCK)) { + prey_lock + } else { + PreyLock::new(package_root.clone(), package_root.join(DIR_JAVA))? + }; + + Ok(Self { + prey: Prey::try_from(package_root.join(F_PREY_TOML))?, + prey_lock, + package_root, + target_dir: target_dir.as_ref().to_path_buf(), + }) + } + + pub fn entry_point(&self) -> PathBuf { + self.prey.entry_point() + } + + pub fn name(&self) -> String { + self.prey.name() + } + + pub fn version(&self) -> semver::Version { + self.prey.version() + } + + pub fn prey_lock(&mut self) -> &mut PreyLock { + &mut self.prey_lock + } + + pub fn get_outdated>(&self, class_path: P) -> Vec { + self.prey_lock + .clone() + .classes() + .iter() + .filter(|class| { + class + .is_updated(class_path.as_ref()) + .is_ok_and(|b| b == false) + }) + .cloned() + .collect() + } + + pub fn write_lock(&self) -> crate::Result<()> { + self.prey_lock.write(self.package_root.as_path()) + } +} + +impl Hash for PackageHandler { + fn hash(&self, state: &mut H) { + self.prey.hash(state); + } +} + +/// Data struct +#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Package { + pub entry_point: PathBuf, +} + +//impl Into for Package {} diff --git a/crates/core/src/prelude.rs b/crates/core/src/prelude.rs new file mode 100644 index 0000000..2b6b762 --- /dev/null +++ b/crates/core/src/prelude.rs @@ -0,0 +1,9 @@ +#![allow(unused_imports)] + +pub use crate::class::Class; +pub use crate::dependency::Dependency; +pub use crate::meta::Meta; +pub use crate::nest::{F_NEST_LOCK, F_NEST_TOML, Nest, NestLock}; +pub use crate::package::{Package, PackageHandler}; +pub use crate::prey::{F_PREY_LOCK, F_PREY_TOML, Prey, PreyLock}; +pub use crate::workspace::{Workspace, WorkspaceHandler}; diff --git a/crates/core/src/prey.rs b/crates/core/src/prey.rs new file mode 100644 index 0000000..e4dd707 --- /dev/null +++ b/crates/core/src/prey.rs @@ -0,0 +1,141 @@ +use std::collections::HashSet; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use fs::expand_files; +use serde::{Deserialize, Serialize}; + +use crate::class::Class; +use crate::meta::Meta; +use crate::package::Package; + +pub const F_PREY_TOML: &str = "Prey.toml"; +pub const F_PREY_LOCK: &str = "Prey.lock"; + +/// Data struct +#[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq)] +pub struct Prey { + package: Package, + meta: Meta, +} + +impl Prey { + pub fn new(name: S) -> Self { + Self { + package: Package { + entry_point: PathBuf::from(""), + }, + meta: Meta::new(name), + } + } + + pub fn with_entry_point>(&mut self, entry_point: P) -> &mut Self { + self.package.entry_point = entry_point.as_ref().to_path_buf(); + self + } + + pub fn entry_point(&self) -> PathBuf { + self.package.entry_point.clone() + } + + pub fn name(&self) -> String { + self.meta.name.clone() + } + + pub fn version(&self) -> semver::Version { + self.meta.version.clone() + } +} + +impl TryFrom for Prey { + type Error = crate::Error; + + fn try_from(value: PathBuf) -> Result { + let f = OpenOptions::new().read(true).open(value)?; + Self::try_from(f) + } +} + +impl TryFrom for Prey { + type Error = crate::Error; + + fn try_from(mut value: File) -> Result { + let mut buf = String::new(); + value.read_to_string(&mut buf)?; + Ok(toml::from_str(buf.as_str())?) + } +} + +/// Data struct +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct PreyLock { + classes: HashSet, +} + +impl PreyLock { + pub fn new>(package_root: P, package_src_root: P) -> crate::Result { + Ok(Self::from_paths( + expand_files(package_root)?, + package_src_root, + )) + } + + pub fn from_paths>(paths: Vec, package_src_root: P) -> Self { + let mut lock = Self::default(); + lock.classes = paths + .iter() + .filter_map(|f| { + let dep = Class::new(package_src_root.as_ref().to_path_buf(), f.to_owned()); + if dep.is_ok() { + Some(dep.unwrap()) + } else { + None + } + }) + .collect(); + lock + } + + pub fn write>(&self, package_root: P) -> crate::Result<()> { + let mut package_root = package_root.as_ref().to_path_buf(); + if package_root.is_dir() { + package_root = package_root.join(F_PREY_LOCK); + } + + Ok(OpenOptions::new() + .write(true) + .create(true) + .open(package_root)? + .write_all(toml::to_string_pretty(&self)?.as_bytes())?) + } + + pub fn with_class(&mut self, class: Class) -> &mut Self { + self.classes.insert(class); + self + } + + pub fn classes(&mut self) -> &mut HashSet { + &mut self.classes + } +} + +/// Load the PreyLock from Prey.lock file. +impl TryFrom for PreyLock { + type Error = crate::Error; + + fn try_from(value: PathBuf) -> Result { + let f = OpenOptions::new().read(true).open(value)?; + Self::try_from(f) + } +} + +impl TryFrom for PreyLock { + type Error = crate::Error; + + fn try_from(mut value: File) -> Result { + let mut buf = String::new(); + value.read_to_string(&mut buf)?; + Ok(toml::from_str(buf.as_str())?) + } +} diff --git a/crates/core/src/workspace.rs b/crates/core/src/workspace.rs new file mode 100644 index 0000000..8ad18d6 --- /dev/null +++ b/crates/core/src/workspace.rs @@ -0,0 +1,461 @@ +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::nest::{F_NEST_LOCK, F_NEST_TOML, Nest, NestLock}; +use crate::package::PackageHandler; +use crate::prey::{F_PREY_LOCK, F_PREY_TOML, Prey}; +use crate::{Error, package}; + +/// Represents a raven workspace. +/// +/// [`WorkspaceHandler`] manages project compilation, cache writing, +/// and tracking of all packages that make up the workspace. +#[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/"; + + /// Creates a new [`WorkspaceHandler`] instance at the provided path. + /// + /// # Errors + /// + /// * [`Error::Io`] if the `project_root` cannot be canonicalized. + /// * [`Error::MissingFileName`] if a name for the workspace cannot be derived + /// from the `project_root` + 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, + }) + } + + /// Loads an existing workspace from the provided path. + /// + /// # Errors + /// + /// * [`Error::Io`] for any number of IO-related errors. + /// Unwrap the [`std::io::Error`] for further information. + /// * [`Error::MissingFileName`] if a name for the workspace cannot be derived + /// from the `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) + } + + /// Writes all lock files (Nest.toml, src/**/Prey.toml) to the disk. + pub fn write_locks(&self) -> crate::Result<()> { + if let Option::Some(lock) = self.nest_lock.clone() { + lock.write(self.project_root.join(F_NEST_LOCK))?; + } + + for handler in self.packages.values() { + handler.write_lock()?; + } + + Ok(()) + } + + /// Initializes a new workspace. + /// + /// # Behaviour + /// + /// Init will avoid overwriting or adding an example project if raven is being integrated into + /// an existing workspace codebase. + /// + /// Init will always write: + /// + /// * Nest.toml + /// * .java-version + /// * Attempt to append to .gitignore + /// + /// If this is an empty workspace, init will include: + /// + /// * Calling `git init` + /// * Writing an example project. + /// * Automatic package discovery. + /// + /// # Errors + 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 a naive build; it attempts to build every source file individually. + /// + /// It calls [`PackageHandler::get_outdated()`] on all known packages to determine + /// compile targets. + /// + /// It will then attempt to write all locks to update the caches and avoid attempting to + /// needlessly recompile targets. + /// + /// # Errors + /// + /// * [`Error::AbsentPrimeError`] + /// * [`Error::Io`] + pub fn build(&mut self) -> crate::Result<&mut Self> { + let compiler = java::compiler::CompilerBuilder::new() + .class_path(Self::DIR_TARGET) + .destination(Self::DIR_TARGET) + .build(); + + // No unintentional deep copying anywhere, right? + // Probably not. We'll see. + for handler in self.packages.values_mut() { + let package_src_root = self + .project_root + .join(Self::DIR_SRC) + .join(handler.name()) + .join(package::DIR_JAVA); + let targets = handler.get_outdated(self.project_root.join(Self::DIR_TARGET)); + + for mut target in targets { + dbg!(&target); + if let Ok(true) = compiler + .clone() + .compile(package_src_root.join(target.path.as_path())) + { + target.update(package_src_root.as_path())?; + handler + .prey_lock() + .classes() + .replace(target) + .ok_or(Error::AbsentPrimeHashingError)?; + } + } + } + + self.write_locks()?; + + Ok(self) + } + + /// Attempts to call the JVM to run, in order of precedence: + /// + /// 1. Provided entry point which includes a specific source file. + /// 2. Provided entry point to a package, using that package's default entry point + /// defined in it's `Prey.toml` + /// 3. The workspace's default package defined in `Nest.toml`, and that package's default + /// entry point as defined in it's `Prey.toml`. + /// + /// # Errors + /// + /// * [`Error::UnknownPackage`] if the package cannot be found. + /// * [`Error::UndefinedEntryPoint`] if no fallback entry point can be found. + /// * [`Error::Io`] + /// * [`Error::Java`] + 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(); + } + + if entry_point.file_name().is_none() { + return Err(Error::UndefinedEntryPoint); + } + + java::runtime::JVMBuilder::new(Self::DIR_TARGET) + .assertions(assertions) + .monitor(true) + .build() + .run(entry_point)?; + + Ok(self) + } + + /// Cleans the workspace of it's caches: + /// + /// * `target/` + /// * Nest.lock + /// * Prey.lock + pub fn clean(&mut self) -> crate::Result<&mut Self> { + // Clear Nest.lock + if let Err(err) = std::fs::remove_file(self.project_root.join(F_NEST_LOCK)) { + if err.kind() != std::io::ErrorKind::NotFound { + return Err(Error::from(err)); + } + } + + // Clear src/**/Prey.lock + for handler in self.packages.values() { + if let Err(err) = std::fs::remove_file( + self.project_root + .join(Self::DIR_SRC) + .join(handler.name()) + .join(F_PREY_LOCK), + ) { + if err.kind() != std::io::ErrorKind::NotFound { + return Err(Error::from(err)); + } + } + } + + // Clear target/ + let _ = std::fs::remove_dir_all(Self::DIR_TARGET); + + Ok(self) + } + + /// Scan for newly created packages, wrapping them in [`PackageHandler`]s + /// and adding them to the [`WorkspaceHandler`]'s package map. + /// + /// Packages are identified by their `Prey.toml` manifests. + /// + /// # Errors + /// + /// * [`Error::Io`] + 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(()) + } + + /// Writes the [`WorkspaceHandler`]'s `Nest.toml` to disk. + fn write_nest(&self) -> crate::Result<()> { + Ok(self.nest.write(self.project_root.clone())?) + } + + /// Writes a `.java-version` file for `jenv` to the disk with an automatically parsed + /// java version from [`java::get_javac_version()`]. + /// + /// # Errors + /// + /// * [`Error::Io`] + /// * [`Error::Java`] + 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_version()?.major.to_string()).as_bytes())?; + } + + Ok(()) + } + + /// Writes an example project tree to disk. + 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(()) + } + + /// Writes an example project to disk. + 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"), + } + } +} diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml new file mode 100644 index 0000000..c9c8c8e --- /dev/null +++ b/crates/fs/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fs" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +description = "Raven's FS utilities" +repository.workspace = true + +publish.workspace = true + +[dependencies] +derive_more.workspace = true diff --git a/src/fs.rs b/crates/fs/src/lib.rs similarity index 74% rename from src/fs.rs rename to crates/fs/src/lib.rs index 34d55a8..c15cd3d 100644 --- a/src/fs.rs +++ b/crates/fs/src/lib.rs @@ -1,6 +1,9 @@ use std::path::{Path, PathBuf}; -pub fn expand_files>(path: P) -> anyhow::Result> { +pub const EXT_TOML: &str = ".toml"; +pub const EXT_LOCK: &str = ".lock"; + +pub fn expand_files>(path: P) -> std::io::Result> { let path = path.as_ref(); if path.is_file() { diff --git a/crates/java/Cargo.toml b/crates/java/Cargo.toml new file mode 100644 index 0000000..56d05c5 --- /dev/null +++ b/crates/java/Cargo.toml @@ -0,0 +1,20 @@ +# May want to find a better name, more reflective of the JDK part +# than the entire Java language. + +[package] +name = "java" +version = "0.2.2" +edition.workspace = true +license.workspace = true + +description = "Tools for interfacing with the Java Development Kit" +repository.workspace = true + +publish.workspace = true + +[dependencies] +bytesize.workspace = true +derive_more.workspace = true +fs.workspace = true +semver.workspace = true +subprocess.workspace = true diff --git a/crates/java/src/compiler.rs b/crates/java/src/compiler.rs new file mode 100644 index 0000000..9f6d46c --- /dev/null +++ b/crates/java/src/compiler.rs @@ -0,0 +1,96 @@ +use std::fmt::Display; +use std::path::{Path, PathBuf}; + +use subprocess::Exec; +use subprocess::Redirection; + +use crate::JAVA_BIN_COMPILER; +use crate::JAVA_EXT_SOURCE; +use crate::Result; + +#[derive(Debug, Default, Clone)] +pub struct CompilerBuilder { + class_path: Option, + destination: Option, +} + +impl CompilerBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn class_path>(&mut self, class_path: S) -> &mut Self { + self.class_path = Some(class_path.as_ref().to_path_buf()); + self + } + + pub fn destination>(&mut self, destination: S) -> &mut Self { + self.destination = Some(destination.as_ref().to_path_buf()); + self + } + + pub fn build(&self) -> Compiler { + let mut flags = vec![]; + + if let Option::Some(path) = self.destination.to_owned() { + flags.push(CompilerFlag::Destination { path }); + } + + if let Option::Some(path) = self.class_path.to_owned() { + flags.push(CompilerFlag::Classpath { path }); + } + + Compiler { flags } + } +} + +#[derive(Debug, Clone)] +pub struct Compiler { + flags: Vec, +} + +impl Compiler { + pub fn compile>(self, path: P) -> Result { + Ok(Exec::cmd(JAVA_BIN_COMPILER) + .args( + self.flags + .clone() + .into_iter() + .flat_map(|f| Into::>::into(f)), + ) + .arg(path.as_ref().with_extension(JAVA_EXT_SOURCE)) + .stdout(Redirection::Pipe) + .detached() + .start()? + .wait()? + .success()) + } +} + +#[derive(Debug, Clone)] +pub enum CompilerFlag { + Classpath { path: PathBuf }, + Destination { path: PathBuf }, + Version, +} + +impl Into> for CompilerFlag { + fn into(self) -> Vec { + match self { + Self::Classpath { path } => vec!["-classpath".to_string(), path.display().to_string()], + Self::Destination { path } => vec!["-d".to_string(), path.display().to_string()], + Self::Version => vec!["--version".to_string()], + } + } +} + +// TODO: Clean this up a bit? +impl Display for CompilerFlag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Classpath { path } => write!(f, "-classpath {}", path.display()), + Self::Destination { path } => write!(f, "-d {}", path.display()), + Self::Version => write!(f, "--version"), + } + } +} diff --git a/crates/java/src/error.rs b/crates/java/src/error.rs new file mode 100644 index 0000000..7d9b7d6 --- /dev/null +++ b/crates/java/src/error.rs @@ -0,0 +1,16 @@ +use derive_more::{Display, From}; + +pub type Result = std::result::Result; + +#[derive(Debug, From, Display)] +pub enum Error { + EmptyStdout, + + #[from] + Io(std::io::Error), + + NthOutOfBounds, + + #[from] + Semver(semver::Error), +} diff --git a/crates/java/src/lib.rs b/crates/java/src/lib.rs new file mode 100644 index 0000000..ab1391c --- /dev/null +++ b/crates/java/src/lib.rs @@ -0,0 +1,55 @@ +pub mod compiler; +pub mod error; +pub mod runtime; + +use std::str::FromStr; + +pub use error::{Error, Result}; +use runtime::VMFlag; +use subprocess::Exec; + +use crate::compiler::CompilerFlag; + +pub const JAVA_BIN_VM: &str = "java"; +pub const JAVA_BIN_COMPILER: &str = "javac"; + +pub const JAVA_EXT_SOURCE: &str = "java"; +pub const JAVA_EXT_CLASS: &str = "class"; + +pub const F_JAVA_VERSION: &str = ".java-version"; + +/// Uses the javac binary to get information about the install compiler. +pub fn get_javac_version() -> Result { + // Try to pull from javac first. + if let Ok(version) = semver::Version::from_str( + Exec::cmd(JAVA_BIN_COMPILER) + .arg(CompilerFlag::Version.to_string()) + .capture()? + .stdout_str() + .replace("javac", "") + .trim(), + ) { + return Ok(version); + } + + Ok(semver::Version::from_str( + get_java_version_info()? + .split_ascii_whitespace() + .nth(1) + .ok_or(Error::NthOutOfBounds)?, + )?) +} + +/// Calls the java binary, returning the complete stdout version information. +fn get_java_version_info() -> Result { + /* + * $ java --version + * openjdk 21.0.9 2025-10-21 + * OpenJDK Runtime Environment (build 21.0.9+10) + * OpenJDK 64-Bit Server VM (build 21.0.9+10, mixed mode, sharing) + */ + Ok(Exec::cmd(JAVA_BIN_VM) + .arg(VMFlag::Version.to_string()) + .capture()? + .stdout_str()) +} diff --git a/crates/java/src/runtime.rs b/crates/java/src/runtime.rs new file mode 100644 index 0000000..7b18bc1 --- /dev/null +++ b/crates/java/src/runtime.rs @@ -0,0 +1,141 @@ +use std::fmt; +use std::path::{Path, PathBuf}; + +use crate::JAVA_BIN_VM; +use crate::Result; + +use bytesize::ByteSize; +use subprocess::Exec; + +#[derive(Debug, Default, Clone)] +pub struct JVMBuilder { + assertions: bool, + monitor: bool, + ram_min: Option, + ram_max: Option, + class_path: PathBuf, +} + +impl JVMBuilder { + pub fn new>(class_path: P) -> Self { + Self { + class_path: class_path.as_ref().to_path_buf(), + ..Default::default() + } + } + + pub fn assertions(&mut self, assertions: bool) -> &mut Self { + self.assertions = assertions; + self + } + + pub fn ram_min(&mut self, ram_min: ByteSize) -> &mut Self { + self.ram_min = Some(ram_min); + self + } + + pub fn ram_max(&mut self, ram_max: ByteSize) -> &mut Self { + self.ram_max = Some(ram_max); + self + } + + /// Monitor stdout and stderr in raven's process. + pub fn monitor(&mut self, monitor: bool) -> &mut Self { + self.monitor = monitor; + self + } + + pub fn build(&self) -> JVM { + let mut flags = vec![VMFlag::Classpath { + path: self.class_path.to_owned(), + }]; + + if self.assertions { + flags.push(VMFlag::EnableAssert); + } + + if let Option::Some(size) = self.ram_min { + flags.push(VMFlag::HeapMin { size }); + } + + if let Option::Some(size) = self.ram_max { + flags.push(VMFlag::HeapMax { size }); + } + + JVM { + monitor: self.monitor, + flags, + } + } +} + +pub struct JVM { + monitor: bool, + flags: Vec, +} + +impl JVM { + pub fn run>(self, entry_point: P) -> Result<()> { + let capture = Exec::cmd(JAVA_BIN_VM) + .args( + self.flags + .clone() + .into_iter() + .flat_map(|f| Into::>::into(f)), + ) + .arg(entry_point.as_ref()) + .capture()?; + + if !self.monitor { + return Ok(()); + } + + let stdout = capture.stdout_str(); + if stdout.len() > 0 { + println!("{stdout}"); + } + + let stderr = capture.stderr_str(); + if stderr.len() > 0 { + eprintln!("{stderr}"); + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub enum VMFlag { + Classpath { path: PathBuf }, + EnableAssert, + HeapMax { size: ByteSize }, + HeapMin { size: ByteSize }, + Version, +} + +impl Into> for VMFlag { + fn into(self) -> Vec { + self.to_string() + .split_ascii_whitespace() + .map(|s| s.to_string()) + .collect() + } +} + +// Currently being kept around because it's fine for the current branches, +// and is currently serving a to_string() method for the Java version fetch. +impl fmt::Display for VMFlag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "-{}", + match self { + Self::Classpath { path } => format!("classpath {}", path.display()), + Self::EnableAssert => String::from("ea"), + Self::HeapMax { size } => format!("Xmx{}", size.as_u64()), + Self::HeapMin { size } => format!("Xms{}", size.as_u64()), + Self::Version => String::from("-version"), // --version + } + ) + } +} diff --git a/crates/pom/Cargo.toml b/crates/pom/Cargo.toml new file mode 100644 index 0000000..2715308 --- /dev/null +++ b/crates/pom/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pom" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +description = "Library for serializing and deserializing Maven's POM" +repository.workspace = true + +publish.workspace = true + +[dependencies] +derive_more.workspace = true +hard-xml.workspace = true +serde.workspace = true +semver.workspace = true +strum.workspace = true diff --git a/crates/pom/src/error.rs b/crates/pom/src/error.rs new file mode 100644 index 0000000..4ec9dde --- /dev/null +++ b/crates/pom/src/error.rs @@ -0,0 +1,12 @@ +use derive_more::{Display, From}; + +pub type Result = std::result::Result; + +#[derive(Debug, From, Display)] +pub enum Error { + #[from] + Io(std::io::Error), + + #[from] + Xml(hard_xml::XmlError), +} diff --git a/crates/pom/src/lib.rs b/crates/pom/src/lib.rs new file mode 100644 index 0000000..d717f8d --- /dev/null +++ b/crates/pom/src/lib.rs @@ -0,0 +1,25 @@ +pub mod error; +pub mod xml; + +pub use error::{Error, Result}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct Pom { + model_version: semver::Version, +} + +impl Pom { + pub fn is_empty(&self) -> bool { + self == &Self::default() + } +} + +impl Default for Pom { + fn default() -> Self { + Self { + model_version: semver::Version::new(4, 0, 0), + } + } +} diff --git a/crates/pom/src/xml.rs b/crates/pom/src/xml.rs new file mode 100644 index 0000000..0a49766 --- /dev/null +++ b/crates/pom/src/xml.rs @@ -0,0 +1,118 @@ +use std::{fs::File, io::Read}; + +use crate::Error; + +use hard_xml::{XmlRead, XmlWrite}; +use serde::{Deserialize, Serialize}; +use strum::EnumString; + +// start with the super POM idiot. + +#[derive(Debug, Clone, Deserialize, Serialize, XmlRead, XmlWrite)] +#[xml(tag = "project")] +pub struct Project { + #[xml(attr = "xmlns")] + xmlns: String, + #[xml(attr = "xlmns:xsi")] + xmlns_xsi: String, + #[xml(attr = "xsi:schemaLocation")] + xsi_schema_location: String, + + #[xml(flatten_text = "modelVersion")] + model_version: semver::Version, + + #[xml(flatten_text = "groupId")] + group_id: String, + + #[xml(flatten_text = "artifactId")] + artifact_id: String, + + #[xml(flatten_text = "version")] + version: String, + + #[xml(flatten_text = "packaging")] + packaging: String, + + #[xml(child = "properties")] + properties: Properties, + + #[xml(child = "dependencyManagement")] + dependency_management: DependencyManagement, + + #[xml(child = "dependencies")] + dependencies: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, XmlRead, XmlWrite)] +#[xml(tag = "properties")] +pub struct Properties { + #[xml(flatten_text = "maven.compiler.source")] + source: String, + + #[xml(flatten_text = "maven.compiler.target")] + target: String, + // lwjgl.version + // lwjgl.natives +} + +#[derive(Debug, Clone, Deserialize, Serialize, XmlRead, XmlWrite)] +#[xml(tag = "dependencyManagement")] +pub struct DependencyManagement { + #[xml(child = "dependencies")] + dependencies: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, XmlRead, XmlWrite)] +#[xml(tag = "dependency")] +pub struct Dependency { + #[xml(flatten_text = "groupId")] + group_id: String, + + #[xml(flatten_text = "artifactId")] + artifact_id: String, + + #[xml(flatten_text = "version")] + version: semver::Version, + + #[xml(flatten_text = "scope")] + scope: String, + + #[xml(flatten_text = "type")] + type_: String, + + #[xml(flatten_text = "optional")] + optional: bool, + + #[xml(child = "exclusions")] + exclusions: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, XmlRead, XmlWrite)] +#[xml(tag = "exclusion")] +pub struct Exclusion { + #[xml(flatten_text = "groupId")] + group_id: String, + + #[xml(flatten_text = "artifactId")] + artifact_id: String, +} + +#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, EnumString)] +#[strum(serialize_all = "camelCase")] +pub enum Packaging { + #[default] + Pom, + Jar, +} + +impl TryFrom for Project { + type Error = Error; + + fn try_from(value: File) -> Result { + let mut value = value; + let mut buf = String::new(); + value.read_to_string(&mut buf)?; + + Ok(Project::from_str(&buf)?) + } +} diff --git a/crates/raven/Cargo.lock b/crates/raven/Cargo.lock new file mode 100644 index 0000000..dd47a29 --- /dev/null +++ b/crates/raven/Cargo.lock @@ -0,0 +1,671 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hard-xml" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b07b8ba970e18a03dbb79f6786b6e4d6f198a0ac839aa5182017001bb8dee17" +dependencies = [ + "hard-xml-derive", + "jetscii", + "lazy_static", + "memchr", + "xmlparser", +] + +[[package]] +name = "hard-xml-derive" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c43e7c3212bd992c11b6b9796563388170950521ae8487f5cdf6f6e792f1c8" +dependencies = [ + "bitflags", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jetscii" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lenient_semver" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de8de3f4f3754c280ce1c8c42ed8dd26a9c8385c2e5ad4ec5a77e774cea9c1ec" +dependencies = [ + "lenient_semver_parser", + "semver", +] + +[[package]] +name = "lenient_semver_parser" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f650c1d024ddc26b4bb79c3076b30030f2cf2b18292af698c81f7337a64d7d6" +dependencies = [ + "lenient_semver_version_builder", + "semver", +] + +[[package]] +name = "lenient_semver_version_builder" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9049f8ff49f75b946f95557148e70230499c8a642bf2d6528246afc7d0282d17" +dependencies = [ + "semver", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pathsub" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dadd38133bcbe43264410412c48614bab4ef899f0792ffc4530dc19ec000a970" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raven" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytesize", + "clap", + "const_format", + "hard-xml", + "lenient_semver", + "pathsub", + "ron", + "semver", + "serde", + "sha256", + "strum", + "subprocess", + "toml", +] + +[[package]] +name = "ron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha256" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "subprocess" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75238edb5be30a9ea3035b945eb9c319dde80e879411cdc9a8978e1ac822960" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "pin-project-lite", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" diff --git a/crates/raven/Cargo.toml b/crates/raven/Cargo.toml new file mode 100644 index 0000000..e9492b1 --- /dev/null +++ b/crates/raven/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "raven" +version = "0.2.0" +edition.workspace = true +license.workspace = true + +description = "A simple build tool for Java" +keywords = ["java", "tool"] +categories = ["development-tools::build-utils"] +repository.workspace = true + +publish.workspace = true + +[dependencies] +anyhow.workspace = true +bytesize.workspace = true +cli.workspace = true +core.workspace = true +fs.workspace = true +java.workspace = true +toml.workspace = true diff --git a/crates/raven/src/main.rs b/crates/raven/src/main.rs new file mode 100644 index 0000000..3f815be --- /dev/null +++ b/crates/raven/src/main.rs @@ -0,0 +1,53 @@ +use core::prelude::WorkspaceHandler; + +use anyhow::anyhow; +use cli::{CLI_ARGS, Command}; + +fn main() -> anyhow::Result<()> { + // type_ is yet unused. + + // `raven new` requires special behaviour. + // This will call the "new" part of the command, + // changing the cwd, before calling WorkspaceHandler.init() + // on the actual workspace directory. + if let Command::New { project_name, .. } = &CLI_ARGS.command { + new(project_name.to_owned())?; + } + + let project_root = std::env::current_dir()?; + + let mut wh: WorkspaceHandler = match &CLI_ARGS.command { + Command::New { .. } | Command::Init => WorkspaceHandler::new(project_root), + _ => WorkspaceHandler::load(project_root), + } + .map_err(|err| anyhow!(err))?; + + match &CLI_ARGS.command { + Command::New { .. } | Command::Init => wh.init(), + Command::Build => wh.build(), + Command::Run { + entry_point, + assertions, + } => wh + .build() + .map_err(|err| anyhow!(err))? + .run(entry_point.to_owned(), assertions.into()), + Command::Clean => wh.clean(), + Command::Test { .. } => unimplemented!(), + } + .map_err(|err| anyhow!(err))?; + + Ok(()) +} + +/// Creates a new project directory and enters it. +fn new(project_name: String) -> anyhow::Result<()> { + // Because this messes around with the CWD, this should live external to any + // instance of a `WorkspaceHandler`. + let cwd = std::env::current_dir()?.join(project_name); + + std::fs::create_dir(&cwd)?; + std::env::set_current_dir(&cwd)?; + + Ok(()) +} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index fcb1cdc..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::sync::LazyLock; - -use clap::{ArgAction, Parser, Subcommand}; - -pub static CONFIG: LazyLock = LazyLock::new(|| Args::parse()); - -#[derive(Debug, Parser)] -#[clap(author, version)] -#[command(help_template = "{author-section}\n{usage-heading} {usage}\n\n{all-args}")] -pub struct Args { - #[command(subcommand)] - pub command: Command, -} - -#[derive(Debug, Subcommand)] -pub enum Command { - /// Create a new raven project - New { project_name: String }, - /// Create a new raven project in an existing directory - Init, - /// Compile the current project - Build, - /// Run the current project - #[command(arg_required_else_help = true)] - Run { - #[arg(required = true)] - entry_point: String, - - #[clap(flatten)] - assertions: Assertions, - }, - /// !!! BORKED !!! Run the tests - Test { - #[clap(flatten)] - assertions: Assertions, - }, - /// Remove the target directory and caching - Clean, -} - -impl Command { - pub fn depends_on_nest(&self) -> bool { - match self { - Self::Init | Self::New { .. } => false, - _ => true, - } - } -} - -#[derive(Debug, clap::Args)] -pub struct Assertions { - /// Disable assertions. - #[arg(short, long = "no-assert", action = ArgAction::SetFalse)] - disable_assert: bool, -} - -impl Into for &Assertions { - fn into(self) -> bool { - !self.disable_assert - } -} diff --git a/src/env.rs b/src/env.rs deleted file mode 100644 index 42de9e6..0000000 --- a/src/env.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::io; -use std::path::Path; -use std::str::FromStr; -use std::sync::LazyLock; - -use crate::java::{JAVA_VM, VMFlag}; - -use anyhow::Context; - -static JAVA_VERSION: LazyLock = LazyLock::new(|| { - crate::io::run_process(&[JAVA_VM, VMFlag::Version.to_string().as_str()]) - .expect("Failed to call java") - .0 - .expect("Failed to read java version from stdout") -}); - -pub fn get_javac_ver() -> anyhow::Result { - /* - * $ java --version - * openjdk 21.0.9 2025-10-21 - * OpenJDK Runtime Environment (build 21.0.9+10) - * OpenJDK 64-Bit Server VM (build 21.0.9+10, mixed mode, sharing) - */ - - semver::Version::from_str( - JAVA_VERSION - .lines() - .nth(0) - .context("stdout from Java is all f-cked up")? - .split_ascii_whitespace() - .nth(1) - .context("Java version position is all f-cked up")?, - ) - .context("Failed to produce a version struct from string") -} - -pub fn set_cwd>(path: P) -> io::Result<()> { - let mut cwd = crate::PROJECT_ROOT.clone(); - cwd.push(path.as_ref()); - std::env::set_current_dir(cwd.as_path()) -} diff --git a/src/io.rs b/src/io.rs deleted file mode 100644 index b329ec3..0000000 --- a/src/io.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::ffi; - -use anyhow::Context; - -pub fn run_process(argv: &[S]) -> anyhow::Result<(Option, Option)> -where - S: AsRef, -{ - let mut process = subprocess::Popen::create( - argv, - subprocess::PopenConfig { - stdout: subprocess::Redirection::Pipe, - ..Default::default() - }, - )?; - - let result = process - .communicate(None) - .context("Failed to communicate with subprocess"); - - if process - .wait_timeout(std::time::Duration::from_secs(5)) - .is_err() - || process.exit_status().is_none() - { - process.terminate()?; - } - - result -} diff --git a/src/java.rs b/src/java.rs deleted file mode 100644 index 8b4285e..0000000 --- a/src/java.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::fmt; -use std::path::{Path, PathBuf}; - -use anyhow::Context; -use bytesize::ByteSize; - -pub const JAVA_VM: &str = "java"; -pub const JAVA_COMPILER: &str = "javac"; - -pub const JAVA_EXT_SOURCE: &str = "java"; -pub const JAVA_EXT_CLASS: &str = "class"; - -// TODO: Builder-pattern JVM wrapper. -#[derive(Debug, Default, Clone)] -pub struct JVMBuilder { - assertions: bool, - monitor: bool, - ram_min: Option, - ram_max: Option, - class_path: PathBuf, -} - -impl JVMBuilder { - pub fn new>(class_path: P) -> Self { - Self { - class_path: class_path.as_ref().to_path_buf(), - ..Default::default() - } - } - - pub fn assertions(&mut self, assertions: bool) -> &mut Self { - self.assertions = assertions; - self - } - - pub fn ram_min(&mut self, ram_min: ByteSize) -> &mut Self { - self.ram_min = Some(ram_min); - self - } - - pub fn ram_max(&mut self, ram_max: ByteSize) -> &mut Self { - self.ram_max = Some(ram_max); - self - } - - /// Monitor stdout and stderr in raven's process. - pub fn monitor(&mut self, monitor: bool) -> &mut Self { - self.monitor = monitor; - self - } - - pub fn build(&self) -> JVM { - let mut flags = vec![VMFlag::Classpath { - path: self.class_path.to_owned(), - }]; - - if self.assertions { - flags.push(VMFlag::EnableAssert); - } - - if let Option::Some(size) = self.ram_min { - flags.push(VMFlag::HeapMin { size }); - } - - if let Option::Some(size) = self.ram_max { - flags.push(VMFlag::HeapMax { size }); - } - - JVM { - monitor: self.monitor, - flags, - } - } -} - -pub struct JVM { - monitor: bool, - flags: Vec, -} - -impl JVM { - pub fn run>( - self, - entry_point: P, - ) -> anyhow::Result<(Option, Option)> { - let mut cmd = vec![JAVA_VM.to_string()]; - - cmd.extend( - self.flags - .clone() - .into_iter() - .flat_map(|f| Into::>::into(f)), - ); - - cmd.push(entry_point.as_ref().to_path_buf().display().to_string()); - - let result = crate::io::run_process(cmd.as_slice()); - - if self.monitor - && let Result::Ok((stdout, stderr)) = &result - { - if let Option::Some(stdout) = stdout - && stdout.len() > 0 - { - print!("{stdout}"); - } - if let Option::Some(stderr) = stderr - && stderr.len() > 0 - { - eprintln!("{stderr}"); - } - } - - result - } -} - -#[derive(Debug, Clone)] -pub enum VMFlag { - Classpath { path: PathBuf }, - EnableAssert, - HeapMax { size: ByteSize }, - HeapMin { size: ByteSize }, - Version, -} - -impl Into> for VMFlag { - fn into(self) -> Vec { - self.to_string() - .split_ascii_whitespace() - .map(|s| s.to_string()) - .collect() - } -} - -// Currently being kept around because it's fine for the current branches, -// and is currently serving a to_string() method for the Java version fetch. -impl fmt::Display for VMFlag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "-{}", - match self { - Self::Classpath { path } => format!("classpath {}", path.display()), - Self::EnableAssert => String::from("ea"), - Self::HeapMax { size } => format!("Xmx{}", size.as_u64()), - Self::HeapMin { size } => format!("Xms{}", size.as_u64()), - Self::Version => String::from("-version"), // --version - } - ) - } -} - -#[derive(Debug, Default, Clone)] -pub struct CompilerBuilder { - class_path: Option, - destination: Option, -} - -impl CompilerBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn class_path>(&mut self, class_path: S) -> &mut Self { - self.class_path = Some(class_path.as_ref().to_path_buf()); - self - } - - pub fn destination>(&mut self, destination: S) -> &mut Self { - self.destination = Some(destination.as_ref().to_path_buf()); - self - } - - pub fn build(&self) -> Compiler { - let mut flags = vec![]; - - if let Option::Some(path) = self.destination.to_owned() { - flags.push(CompilerFlag::Destination { path }); - } - - if let Option::Some(path) = self.class_path.to_owned() { - flags.push(CompilerFlag::Classpath { path }); - } - - Compiler { flags } - } -} - -#[derive(Debug, Clone)] -pub struct Compiler { - flags: Vec, -} - -impl Compiler { - pub fn compile>( - self, - path: P, - ) -> anyhow::Result<(Option, Option)> { - let mut cmd: Vec = vec![JAVA_COMPILER.to_string()]; - - cmd.extend( - self.flags - .clone() - .into_iter() - .flat_map(|f| Into::>::into(f)), - ); - cmd.extend(crate::fs::expand_files(path)?.into_iter().filter_map(|f| { - Some( - f.to_str() - .context("Failed to cast PathBuf to &str") - .ok()? - .to_string(), - ) - })); - - crate::io::run_process(cmd.as_slice()) - } -} - -#[derive(Debug, Clone)] -pub enum CompilerFlag { - Classpath { path: PathBuf }, - Destination { path: PathBuf }, -} - -impl Into> for CompilerFlag { - fn into(self) -> Vec { - match self { - Self::Classpath { path } => vec!["-classpath".to_string(), path.display().to_string()], - Self::Destination { path } => vec!["-d".to_string(), path.display().to_string()], - } - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index ed58804..0000000 --- a/src/main.rs +++ /dev/null @@ -1,272 +0,0 @@ -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/MainTest.java - if let Result::Ok(mut f) = OpenOptions::new().write(true).create_new(is_empty).open( - DIR_TEST - .clone() - .join("MainTest") - .with_extension(JAVA_EXT_SOURCE), - ) { - f.write_all(include_bytes!("../assets/src/test/MainTest.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 [ - format!("{}/", DIR_TARGET.file_name().unwrap().display()), - 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(DIR_TARGET.as_path()) - .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(DIR_TARGET.as_path()) - .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()); -} diff --git a/src/nest.rs b/src/nest.rs deleted file mode 100644 index 1a87251..0000000 --- a/src/nest.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::collections::HashMap; -use std::fs::{File, OpenOptions}; -use std::io::Read; -use std::path::PathBuf; -use std::sync::LazyLock; - -use crate::java::{JAVA_EXT_CLASS, JAVA_EXT_SOURCE}; -use crate::{DIR_SRC, DIR_TARGET, DIR_TEST, F_NEST_LOCK, F_NEST_TOML, PROJECT_ROOT}; - -use anyhow::Context; -use semver::Version; -use serde::{Deserialize, Serialize}; - -pub static NEST: LazyLock> = - LazyLock::new(|| Nest::try_from(PROJECT_ROOT.join(F_NEST_TOML.as_path()))); - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Nest { - pub package: Package, -} - -impl TryFrom for Nest { - type Error = anyhow::Error; - - fn try_from(value: File) -> Result { - let mut value = value; - let mut buf = String::new(); - value - .read_to_string(&mut buf) - .context("Failed to read Nest")?; - - toml::from_str(buf.as_str()).context("Failed to deserialize Nest") - } -} - -impl TryFrom for Nest { - type Error = anyhow::Error; - - fn try_from(value: PathBuf) -> Result { - Nest::try_from( - OpenOptions::new() - .read(true) - .open(value) - .expect("Failed to load Nest.toml"), - ) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Package { - pub name: String, - pub version: semver::Version, -} - -impl Default for Package { - fn default() -> Self { - Self { - name: String::from("MyPackage"), - version: Version::new(0, 1, 0), - } - } -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct NestLock(pub HashMap); - -impl NestLock { - pub fn load() -> anyhow::Result { - NestLock::try_from(F_NEST_LOCK.clone()) - } - - /// Update, retaining all classes that still exist and whose paths are still files, rather than - /// being shifted into a package. - pub fn update(&mut self) { - self.0 = self - .0 - .clone() - .into_iter() - .filter_map(|(path, class)| { - if path.exists() && path.is_file() { - return Some((path, class)); - } - - None - }) - .collect(); - } -} - -impl TryFrom for NestLock { - type Error = anyhow::Error; - - fn try_from(value: File) -> Result { - let mut value = value; - let mut buf = String::new(); - - value.read_to_string(&mut buf)?; - toml::from_str(buf.as_str()).context("Failed to deserialize NestLock") - } -} - -impl TryFrom for NestLock { - type Error = anyhow::Error; - - fn try_from(value: PathBuf) -> Result { - NestLock::try_from( - OpenOptions::new() - .read(true) - .open(&value) - .with_context(|| format!("Failed to open {}", value.display()))?, - ) - .context("") - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Class { - pub name: PathBuf, - pub checksum: String, - pub test: bool, -} - -impl Class { - /// Returns true if the class needs updating. - /// This may also cautionarily return true if it cannot digest the file. - pub fn is_updated(&self) -> bool { - // If the source file exists, hasn't been moved to a package (path is a file) and - // the associated compiled class file exists in DIR_TARGET - PROJECT_ROOT - .join(DIR_TARGET.as_path()) - .join(self.name.as_path()) - .with_extension(JAVA_EXT_CLASS) - .exists() - && sha256::try_digest( - PROJECT_ROOT - .join(if self.test { - DIR_TEST.clone() - } else { - DIR_SRC.clone() - }) - .join(self.name.as_path()) - .with_extension(JAVA_EXT_SOURCE), - ) - .is_ok_and(|hash| self.checksum == hash) - } -} - -impl TryFrom for Class { - type Error = anyhow::Error; - - fn try_from(value: PathBuf) -> Result { - Ok(Self { - name: PathBuf::from( - value - .file_name() - .context("Failed to get file name from PathBuf for class")?, - ) - .with_extension(""), - checksum: sha256::try_digest(&value)?, - test: value.is_relative() && value.starts_with(DIR_TEST.as_path()), - }) - } -} diff --git a/templates/demo/Nest.toml b/templates/demo/Nest.toml new file mode 100644 index 0000000..f52da64 --- /dev/null +++ b/templates/demo/Nest.toml @@ -0,0 +1,26 @@ +[workspace] +default_package = "main" # PathBuf || String ? + +[meta] +name = "demo" # String +version = "0.1.0" # semver::Version +authors = ["Olivia Brooks", "Adrian Long"] # Option> +repository = "https://gitea.cutieguwu.ca/Cutieguwu/raven" # Option struct? +license = "MIT" # Option +license-file = "LICENSE" + +[dependencies] +anyhow = "1.0" # semver::VersionReq +bytesize = "2.3" +pathsub = "0.1.1" +ron = "0.12" +sha256 = "1.6" +subprocess = "0.2" +toml = "0.9" + +[dependencies.clap] +version = "4.5" +#features = ["cargo", "derive"] # Is this a concept in POM? + +[pom] # Only POM-specific data +model_version = "4.0.0" # semver::Version diff --git a/templates/demo/src/main/Prey.lock b/templates/demo/src/main/Prey.lock new file mode 100644 index 0000000..25de180 --- /dev/null +++ b/templates/demo/src/main/Prey.lock @@ -0,0 +1,7 @@ +[[classes]] +path = "Main" # PathBuf to .class +checksum = "24dffb40073ff21878cf879bf8c67d189ad600115f9a8ecead11a3ca6c086767" + +[[classes]] +path = "Cli" +checksum = "24dffb40073ff21878cf879bf8c67d189ad600115f9a8ecead11a3ca6c086767" diff --git a/templates/demo/src/main/Prey.toml b/templates/demo/src/main/Prey.toml new file mode 100644 index 0000000..8911e5f --- /dev/null +++ b/templates/demo/src/main/Prey.toml @@ -0,0 +1,6 @@ +[package] +entry_point = "Main" # PathBuf + +[meta] +name = "main" +version = "0.1.0" diff --git a/templates/imports.rs b/templates/imports.rs deleted file mode 100644 index 4e9db5f..0000000 --- a/templates/imports.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Acknowledge sister/child -mod module; - -// std -use std::*; - -// sister/child -use module1::*; - -// parent -use super::*; - -// ancestor of parent -use crate::*; - -// external -use external::*; diff --git a/templates/pom.xml b/templates/pom.xml new file mode 100644 index 0000000..7fb1e6b --- /dev/null +++ b/templates/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + com.viffx + GameEngine + 1.0-SNAPSHOT + + + 21 + 21 + 3.4.0 + natives-windows + + + + + + org.lwjgl + lwjgl-bom + ${lwjgl.version} + import + pom + + + + + + + org.lwjgl + lwjgl + + + org.lwjgl + lwjgl-assimp + + + org.lwjgl + lwjgl-glfw + + + org.lwjgl + lwjgl-openal + + + org.lwjgl + lwjgl-stb + + + org.lwjgl + lwjgl-vulkan + + + org.lwjgl + lwjgl + ${lwjgl.natives} + + + org.lwjgl + lwjgl-assimp + ${lwjgl.natives} + + + org.lwjgl + lwjgl-glfw + ${lwjgl.natives} + + + org.lwjgl + lwjgl-openal + ${lwjgl.natives} + + + org.lwjgl + lwjgl-stb + ${lwjgl.natives} + + +