From ef07526d5742ab8cbe4c5751e2018541432187e0 Mon Sep 17 00:00:00 2001 From: Olivia Brooks <109807080+Cutieguwu@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:20:03 -0500 Subject: [PATCH] Initial Commit. --- .gitignore | 1 + CODE_OF_CONDUCT.md | 128 +++++++++++ Cargo.lock | 524 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 24 ++ LICENSE | 21 ++ README.md | 30 +++ assets/Main.java | 6 + src/cli.rs | 59 +++++ src/env.rs | 44 ++++ src/fs.rs | 16 ++ src/io.rs | 30 +++ src/java.rs | 227 +++++++++++++++++++ src/main.rs | 261 +++++++++++++++++++++ src/nest.rs | 173 ++++++++++++++ templates/imports.rs | 17 ++ 15 files changed, 1561 insertions(+) create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/Main.java create mode 100644 src/cli.rs create mode 100644 src/env.rs create mode 100644 src/fs.rs create mode 100644 src/io.rs create mode 100644 src/java.rs create mode 100644 src/main.rs create mode 100644 src/nest.rs create mode 100644 templates/imports.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4fc2199 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, +available at +. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/inclusion). + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..abb7af2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,524 @@ +# 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", +] + +[[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", +] + +[[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 = "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 = "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 = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[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 = "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", + "ron", + "semver", + "serde", + "sha256", + "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", +] + +[[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 = "subprocess" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75238edb5be30a9ea3035b945eb9c319dde80e879411cdc9a8978e1ac822960" +dependencies = [ + "libc", + "winapi", +] + +[[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 = "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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ad2eaff --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "raven" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0" +bytesize = "2.3" +ron = "0.12" +sha256 = "1.6.0" +subprocess = "0.2" +toml = "0.9" + +[dependencies.serde] +version = "1.0" +features =["derive"] + +[dependencies.clap] +version = "4.5" +features = ["derive"] + +[dependencies.semver] +version = "1.0" +features = ["serde"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bbd8a11 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Olivia Brooks + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaaad4c --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Raven + +First off, the name is not a play on `Rust + maven`, but `maven` and my uni's +mascot. + +Second, this is meant to be a *simple* tool to manage *simple* java projects +from the command line with sanity. + +It takes inspiration from Cargo, and essentially serves as a sanity-preserving +wrapper for `javac` and `java`. + +## Installing + +Installing with `cargo`: + +```bash +cargo install --git https://gitea.cutieguwu.ca/Cutieguwu/raven.git --branch master +``` + +`raven` will likely fall back to panicking in various scenarios. This will be +worked out over time. + +`raven` is only tested under linux. NT and Darwin support is **not** guaranteed. + +## Future plans + +- [ ] Make `nest run` fall back to an entry point specified in Nest.toml, when + none provided. +- [ ] Possibly add support for pulling remote packages via `nest add`. This + will be implemented should there arise a need. diff --git a/assets/Main.java b/assets/Main.java new file mode 100644 index 0000000..0e49608 --- /dev/null +++ b/assets/Main.java @@ -0,0 +1,6 @@ +public class Main { + + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..dc7f144 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,59 @@ +use std::sync::LazyLock; + +use clap::{ArgAction, Parser, Subcommand}; + +pub static CONFIG: LazyLock = LazyLock::new(|| Args::parse()); + +#[derive(Debug, Parser)] +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, + }, + /// Run the tests + Test { + #[clap(flatten)] + assertions: Assertions, + }, + /// Remove the target directory + 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 new file mode 100644 index 0000000..3dcfcbb --- /dev/null +++ b/src/env.rs @@ -0,0 +1,44 @@ +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 + .lock() + .expect("Failed to lock Mutex") + .to_owned(); + cwd.push(path.as_ref()); + std::env::set_current_dir(cwd) +} diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..83d0142 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,16 @@ +use std::path::{Path, PathBuf}; + +pub fn expand_files>(path: P) -> anyhow::Result> { + let path = path.as_ref(); + + if path.is_file() { + return Ok(vec![path.to_path_buf()]); + } + + Ok(std::fs::read_dir(path)? + .filter_map(|entry| { + let path = entry.ok()?.path(); + if path.is_file() { Some(path) } else { None } + }) + .collect()) +} diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..b329ec3 --- /dev/null +++ b/src/io.rs @@ -0,0 +1,30 @@ +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 new file mode 100644 index 0000000..e9d6f7c --- /dev/null +++ b/src/java.rs @@ -0,0 +1,227 @@ +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, +} + +impl JVMBuilder { + pub fn new() -> Self { + Self::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![]; + + 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().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 { + EnableAssert, + HeapMax { size: ByteSize }, + HeapMin { size: ByteSize }, + Version, +} + +impl Into> for VMFlag { + fn into(self) -> Vec { + match self { + Self::EnableAssert => vec![String::from("-ea")], + Self::HeapMax { size } => vec![format!("Xmx{}", size)], + Self::HeapMin { size } => vec![format!("Xms{}", size)], + Self::Version => vec![String::from("--version")], + } + } +} + +// 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::EnableAssert => String::from("ea"), + Self::HeapMax { size } => format!("Xmx{}", size), + Self::HeapMin { size } => format!("Xms{}", size), + 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 { + Destination { path: PathBuf }, + Classpath { path: PathBuf }, +} + +impl Into> for CompilerFlag { + fn into(self) -> Vec { + match self { + Self::Classpath { path } => vec!["-cp".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 new file mode 100644 index 0000000..797447a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,261 @@ +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, Mutex}; + +use cli::{CONFIG, Command}; +use java::{JAVA_EXT_CLASS, JAVA_EXT_SOURCE}; +use nest::{Class, NEST, NestLock}; + +use anyhow::Context; +use bytesize::ByteSize; + +const DIR_TARGET: LazyLock = LazyLock::new(|| PathBuf::from("target/")); +const DIR_TEST: LazyLock = LazyLock::new(|| PathBuf::from("test/")); +const DIR_SRC: LazyLock = LazyLock::new(|| PathBuf::from("src/")); +const F_NEST_TOML: LazyLock = LazyLock::new(|| PathBuf::from("Nest.toml")); +const F_NEST_LOCK: LazyLock = LazyLock::new(|| PathBuf::from("Nest.lock")); + +/// This may not actually be the project root. +pub static PROJECT_ROOT: LazyLock> = LazyLock::new(|| { + Mutex::new(std::env::current_dir().expect("Failed to get current working directory")) +}); + +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 is_empty = std::fs::read_dir(PROJECT_ROOT.lock().expect("Failed to lock mutex").as_path()) + .is_ok_and(|tree| tree.count() == 0); + + let project_name = PROJECT_ROOT + .lock() + .expect("Failed to lock mutex") + .file_name() + .context("Invalid directory name")? + .to_str() + .context("Unable to convert OsStr to str")? + .to_owned(); + + // Make src, target, tests + for dir in [DIR_SRC, DIR_TARGET, DIR_TEST] { + std::fs::create_dir(dir.clone())?; + } + + // 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 Main.java + if let Result::Ok(mut f) = OpenOptions::new() + .write(true) + .create_new(is_empty) + .open(DIR_SRC.clone().join("Main").with_extension(JAVA_EXT_SOURCE)) + { + f.write_all(include_bytes!("../assets/Main.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") + { + dbg!(); + let mut buf = String::new(); + f.read_to_string(&mut buf)?; + + for ignored in [ + DIR_TARGET.as_path().display().to_string(), + format!("*.{}", JAVA_EXT_CLASS), + ] { + if dbg!(!buf.contains(&ignored)) { + f.write(format!("{}\n", ignored).as_bytes())?; + } + } + } + + Ok(()) +} + +fn new(project_name: String) -> anyhow::Result<()> { + let cwd = PROJECT_ROOT + .lock() + .expect("Failed to lock mutex") + .join(project_name); + + std::fs::create_dir(&cwd)?; + std::env::set_current_dir(&cwd)?; + + // Replace PathBuf within mutex + let mut state = PROJECT_ROOT.lock().expect("Failed to lock mutex"); + *state = std::env::current_dir().expect("Failed to change current working directory"); + + Ok(()) +} + +fn build() -> anyhow::Result<()> { + let mut targets = crate::fs::expand_files(DIR_SRC.as_path())?; + 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(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)> { + crate::env::set_cwd(DIR_TARGET.as_path())?; + + java::JVMBuilder::new() + .assertions(assertions) + .monitor(true) + .build() + .run(entry_point) +} + +fn test(assertions: bool) -> anyhow::Result<(Option, Option)> { + java::CompilerBuilder::new() + .class_path(DIR_TARGET.as_path()) + .destination(DIR_TARGET.as_path()) + .build() + .compile(DIR_TEST.as_path())?; + + // Change cwd to avoid Java pathing issues. + crate::env::set_cwd( + PROJECT_ROOT + .lock() + .expect("Failed to lock mutex") + .join(DIR_TARGET.as_path()), + )?; + + java::JVMBuilder::new() + .assertions(assertions) + .ram_min(ByteSize::mib(128)) + .ram_max(ByteSize::mib(512)) + .monitor(true) + .build() + .run(DIR_TEST.as_path()) +} + +fn clean() { + let _ = std::fs::remove_file( + PROJECT_ROOT + .lock() + .expect("Failed to lock mutex") + .join(F_NEST_LOCK.as_path()), + ); + let _ = std::fs::remove_dir_all( + PROJECT_ROOT + .lock() + .expect("Failed to lock mutex") + .join(DIR_TARGET.as_path()), + ); +} diff --git a/src/nest.rs b/src/nest.rs new file mode 100644 index 0000000..f97010b --- /dev/null +++ b/src/nest.rs @@ -0,0 +1,173 @@ +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 + .lock() + .expect("Failed to lock mutex") + .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 + .lock() + .expect("Failed to lock mutex") + .join(DIR_TARGET.as_path()) + .join(self.name.as_path()) + .with_extension(JAVA_EXT_CLASS) + .exists() + && sha256::try_digest( + PROJECT_ROOT + .lock() + .expect("Failed to lock mutex") + .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/imports.rs b/templates/imports.rs new file mode 100644 index 0000000..4e9db5f --- /dev/null +++ b/templates/imports.rs @@ -0,0 +1,17 @@ +// Acknowledge sister/child +mod module; + +// std +use std::*; + +// sister/child +use module1::*; + +// parent +use super::*; + +// ancestor of parent +use crate::*; + +// external +use external::*;