diff --git a/Cargo.lock b/Cargo.lock index b015354..e89c015 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -423,6 +433,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "colored", "hex", "home", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 38d18ea..dcd138b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" toml = "0.8" anyhow = "1.0" strong-xml = "0.6" +colored = "2.0" reqwest = {version = "0.11", features = ["blocking"]} tokio = { version = "1", features = ["full"] } bytes = "1.5" diff --git a/README.md b/README.md index ec82ce9..a68fa46 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ **Jargo** -Exploring the need/possibility to move from maven/gradle to cargo. +An experimental build tool for Java taking inspiration from Cargo. -That is, build tool for java taking inspiration from Cargo - -And it's called Jargo. I do not wish to put a J in front of anything, as is the java tradition, +And it's called *Jargo*. I do not wish to put a J in front of anything, as is the java tradition, but 'jargo' actually sounds kinda nice and it conveys pretty much what it is. It is NOT a new maven (not yet at least). That's the reason it's not called 'raven.' @@ -12,8 +10,8 @@ It is NOT a new maven (not yet at least). That's the reason it's not called 'rav Basic premisses: 1. written in Rust 2. does NOT copy anything from the maven philosophy (phases, goals etc). Instead find out on the go what would be -a good design -3. uses TOML +a good design. _That said, some things are just good to keep using, such as the default project structure._ +3. configured in TOML. ie. no XML, **yay!**, AND no Turing-completeness (groovy/kotlin in gradle), **yay2!!** see [tests/sample_project/Jargo.toml](https://github.com/shautvast/jargo/blob/main/tests/sample_project/Jargo.toml) to get an impression of what that looks like. @@ -38,7 +36,8 @@ _Every tool is currently being rewritten in rust._ And for good reason! 2. Why not create a drop-in replacement for maven written in rust? -_While that would make migration a no-brainer, it seems too ambitious based on what I've seen of the maven -codebase_ +_While that would (in theory) make migration a no-brainer, it seems too ambitious based on what I've seen of the maven +codebase. Other than that you will most likely run into onforeseen issues while migrating this way, because this or +that is subtly different here and there. Better avoid the promise of easy migration altogether._ diff --git a/src/compile/mod.rs b/src/compile/mod.rs new file mode 100644 index 0000000..9a8a08f --- /dev/null +++ b/src/compile/mod.rs @@ -0,0 +1,107 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::Error; +use colored::Colorize; +use crate::compile::PathNode::*; + +use crate::Project; + +const SOURCES: &str = "src/main/java"; +const TESTSOURCES: &str = "src/test/java"; +const RESOURCES: &str = "src/main/resources"; +const TESTRESOURCES: &str = "src/main/java"; + +const TARGET_MAIN: &str = "target/classes"; +const TARGET_TEST: &str = "target/test-classes"; + + +#[derive(Debug)] +enum PathNode { + DirNode(PathBuf, Vec, Vec), + FileNode(PathBuf), +} + +pub fn run(project: &Project) -> Result<(), Error> { + println!("{} {}.{}-{}", "Compiling".green(), project.group, project.name, project.version); + + let root = PathBuf::from(&project.project_root).join(SOURCES); + + let mut src_tree = DirNode(root.clone(), Vec::new(), Vec::new()); + determine_src_tree(root, &mut src_tree)?; + println!("{:?}", src_tree); + + compile_sourcedir(project, &mut src_tree)?; + + Ok(()) +} + +fn compile_sourcedir(project: &Project, src_tree: &mut PathNode) -> Result<(), Error> { + if let DirNode(dir_name, subdirs, contents) = src_tree { + if !contents.is_empty() { + let mut javac = if cfg!(target_os = "windows") { + vec!["/C"] + } else { + vec![] + }; + let classes = PathBuf::from(&project.project_root).join(TARGET_MAIN); + let classes = classes.to_str().unwrap(); + + javac.append(&mut vec!["javac", "-d", classes, "-sourcepath"]); + javac.push(dir_name.to_str().unwrap().into()); + for source in contents { + if let FileNode(source_name) = source { + let name = source_name.to_str().unwrap(); + javac.push(name); + } + } + + let output = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(javac) + .output() + .expect("failed to execute process") + } else { + Command::new("sh") + .arg("-c") + .arg(javac.join(" ")) + .output() + .expect("failed to execute process") + }; + if !output.stderr.is_empty() { + println!("{}", String::from_utf8(output.stderr)?.red()); + } + println!("{}", String::from_utf8(output.stdout)?); + } + for subdir in subdirs { + compile_sourcedir(project, subdir)?; + } + } + Ok(()) +} + +fn determine_src_tree(parent: PathBuf, parent_node: &mut PathNode) -> Result<(), Error> { + let paths = fs::read_dir(&parent)?; + + for path in paths { + let path = path?; + + if path.metadata()?.is_dir() { + let mut subdir = DirNode(path.path(), Vec::new(), Vec::new()); + determine_src_tree(path.path(), &mut subdir)?; + if let DirNode(_, subdirs, _) = parent_node { + subdirs.push(subdir); + } + } else { + let name = path.file_name(); + let name = name.to_str().unwrap().to_owned(); + if name.ends_with(".java") { + if let DirNode(_, _, contents) = parent_node { + contents.push(FileNode(path.path())); + } + } + } + } + Ok(()) +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 6f873a3..474fba2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,11 +9,11 @@ pub struct Config { pub static CONFIG: OnceLock = OnceLock::new(); -pub fn get_config() -> &'static Config { +pub fn config() -> &'static Config { CONFIG.get_or_init(|| { let user_home = home::home_dir().unwrap(); Config { - cache_location: format!("{}/jargo/repo", user_home.to_str().unwrap()).into(), + cache_location: format!("{}/jargo/repo", user_home.to_str().unwrap()).into(),//TODO make '.jargo' user_home, maven_central: "https://repo.maven.apache.org/maven2".into() } diff --git a/src/deploader.rs b/src/deploader.rs index 549a7b8..c5a620d 100644 --- a/src/deploader.rs +++ b/src/deploader.rs @@ -10,8 +10,9 @@ use sha1::{Digest, Sha1}; use strong_xml::XmlRead; use crate::Artifact; -use crate::config::get_config; +use crate::config::config; use crate::pom::model::Pom; +use colored::Colorize; pub fn load_artifacts(artifacts: &[Artifact]) -> Result<(), Error> { for art in artifacts { @@ -21,37 +22,22 @@ pub fn load_artifacts(artifacts: &[Artifact]) -> Result<(), Error> { } pub fn load_artifact(art: &Artifact) -> Result<(), Error> { - let artifact_path = format!("{}/{}/{}", - art.group.replace(".", "/"), - art.name, - art.version, - ); - let local_artifact_loc = format!("{}/{}", get_config().cache_location, artifact_path); + let artifact_path = format!("{}/{}/{}", art.group.replace(".", "/"), art.name, art.version); + + // check/create artifact directory + let local_artifact_loc = format!("{}/{}", config().cache_location, artifact_path); if !exists(&local_artifact_loc) { fs::create_dir_all(&local_artifact_loc)?; } + // download remote jar if not in cache and check its SHA-1 checksum let local_artifact_jar_path = format!("{}/{}-{}.jar", local_artifact_loc, art.name, art.version); if !exists(&local_artifact_jar_path) { - load_verify_artifact_jar(art, &artifact_path, &local_artifact_jar_path)?; + download_verify_artifact_jar(art, &artifact_path, &local_artifact_jar_path)?; } - let local_artifact_pom_path = format!("{}/{}-{}.pom", local_artifact_loc, art.name, art.version); - let pom_xml = if !exists(&local_artifact_pom_path) { - // download pom - let remote_artifact_pom_url = format!("{}/{}/{}-{}.pom", get_config().maven_central, artifact_path, art.name, art.version); - println!("Downloading {}", remote_artifact_pom_url); - let body = reqwest::blocking::get(&remote_artifact_pom_url)?.text().unwrap(); - println!("Downloaded {}", remote_artifact_pom_url); - write_text(&local_artifact_pom_path, &body)?; - body - } else { - // read local pom file - let mut file = File::open(local_artifact_pom_path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - contents - }; + // download remote pom if not in cache //TODO check its SHA-1 checksum + let pom_xml = download_verify_pom(art, artifact_path, local_artifact_loc)?; // parse pom file let pom = Pom::from_str(&pom_xml).unwrap(); @@ -61,50 +47,91 @@ pub fn load_artifact(art: &Artifact) -> Result<(), Error> { Ok(()) } -fn load_verify_artifact_jar(art: &Artifact, artifact_path: &str, local_artifact_jar_path: &str) -> Result<(), Error> { - let remote_artifact_jar_url = format!("{}/{}/{}-{}.jar", get_config().maven_central, artifact_path, art.name, art.version); +fn download_verify_pom(art: &Artifact, artifact_path: String, local_artifact_loc: String) -> Result { + let local_artifact_pom_path = &format!("{}/{}-{}.pom", local_artifact_loc, art.name, art.version); + let remote_artifact_pom_url = format!("{}/{}/{}-{}.pom", config().maven_central, artifact_path, art.name, art.version); + let pom_xml = if !exists(local_artifact_pom_path) { + println!("{} {}", "Downloading".green(), remote_artifact_pom_url); + let body = reqwest::blocking::get(&remote_artifact_pom_url)?.text().unwrap(); + println!("{} {}", "Downloaded".green(), remote_artifact_pom_url); + write_text(local_artifact_pom_path, &body)?; + body + } else { + read_file_to_string(local_artifact_pom_path)? + }; - println!("Downloading {}", remote_artifact_jar_url); + let local_artifact_pom_sha1_path = format!("{}.sha1", local_artifact_pom_path); + + // verify jarfile with SHA1 checksum (which is hex encoded) + let checksum = hex::decode( + if !exists(&local_artifact_pom_sha1_path) { + download_checksum(&remote_artifact_pom_url, &local_artifact_pom_sha1_path)? + } else { + read_file_to_bytes(local_artifact_pom_sha1_path)? + })?; + let validated = validate_checksum_text(&pom_xml, checksum); + if !validated { + Err(anyhow!("SHA1 checksum for {} is not valid", remote_artifact_pom_url)) + } else { Ok(pom_xml) } +} + + +/// Download jar from remote repo and check its signature +/// For now it's a blocking call, because async and recursion add unwanted complexity/I don't understand that +/// TODO add progress bar +fn download_verify_artifact_jar(art: &Artifact, artifact_path: &str, local_artifact_jar_path: &str) -> Result<(), Error> { + let remote_artifact_jar_url = format!("{}/{}/{}-{}.jar", config().maven_central, artifact_path, art.name, art.version); + + println!("{} {}", "Downloading".green(), remote_artifact_jar_url); let jar = reqwest::blocking::get(&remote_artifact_jar_url)?.bytes().unwrap(); - println!("Downloaded {}", remote_artifact_jar_url); - write_bytes(&local_artifact_jar_path, jar.clone())?; + println!("{} {}", "Downloaded".green(), remote_artifact_jar_url); + write_bytes_to_file(&local_artifact_jar_path, &jar)?; let local_artifact_jar_sha1_path = format!("{}.sha1", local_artifact_jar_path); - - // verify jarfile with SHA1 checksum + // verify jarfile with SHA1 checksum (which is hex encoded) let checksum = hex::decode( if !exists(&local_artifact_jar_sha1_path) { - let remote_artifact_jar_sha1_url = format!("{}.sha1", remote_artifact_jar_url); - println!("{}", remote_artifact_jar_sha1_url); - let jar_checksum = reqwest::blocking::get(&remote_artifact_jar_sha1_url)?.bytes().unwrap(); - write_bytes(&local_artifact_jar_sha1_path, jar_checksum.clone())?; - jar_checksum.to_vec() + download_checksum(&remote_artifact_jar_url, &local_artifact_jar_sha1_path)? } else { - let mut file = File::open(local_artifact_jar_sha1_path)?; - let mut contents = Vec::new(); - file.read_to_end(&mut contents)?; - contents + read_file_to_bytes(local_artifact_jar_sha1_path)? })?; - - let mut hasher = Sha1::new(); - hasher.update(&jar.to_vec()); - let result = hasher.finalize(); - let validated = result[..] == checksum; - + let validated = validate_checksum_bytes(&jar, checksum); if !validated { Err(anyhow!("SHA1 checksum for {} is not valid", remote_artifact_jar_url)) } else { Ok(()) } } + +fn download_checksum(remote_artifact_url: &String, local_artifact_jar_sha1_path: &String) -> Result, Error> { + let remote_artifact_jar_sha1_url = format!("{}.sha1", remote_artifact_url); + let jar_checksum = reqwest::blocking::get(&remote_artifact_jar_sha1_url)?.bytes().unwrap(); + write_bytes_to_file(&local_artifact_jar_sha1_path, &jar_checksum)?; + Ok(jar_checksum.to_vec()) +} + +fn validate_checksum_bytes(jar: &Bytes, checksum: Vec) -> bool { + let mut hasher = Sha1::new(); + hasher.update(&jar.to_vec()); + let result = hasher.finalize(); + result[..] == checksum +} + +fn validate_checksum_text(text: &str, checksum: Vec) -> bool { + let mut hasher = Sha1::new(); + hasher.update(text.as_bytes()); + let result = hasher.finalize(); + result[..] == checksum +} + fn exists(path: &str) -> bool { Path::new(path).exists() } -fn write_bytes(jar_path: &str, bytes: Bytes) -> Result<(), Error> { +fn write_bytes_to_file(jar_path: &str, bytes: &Bytes) -> Result<(), Error> { let mut file = File::create(jar_path)?; - file.write_all(&bytes)?; + file.write_all(bytes)?; Ok(()) } @@ -112,4 +139,18 @@ fn write_text(path: &str, contents: &String) -> Result<(), Error> { let mut file = File::create(path)?; file.write(contents.as_bytes())?; Ok(()) +} + +fn read_file_to_string(local_artifact_pom_path: &str) -> Result { + let mut file = File::open(local_artifact_pom_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(contents) +} + +fn read_file_to_bytes(local_artifact_jar_sha1_path: String) -> Result, Error> { + let mut file = File::open(local_artifact_jar_sha1_path)?; + let mut contents = Vec::new(); + file.read_to_end(&mut contents)?; + Ok(contents) } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 449d925..f32a71b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ use std::fs; +use std::path::Path; use anyhow::{anyhow, Error}; use toml::{Table, Value}; @@ -6,6 +7,7 @@ use toml::{Table, Value}; pub mod pom; mod config; pub mod deploader; +pub mod compile; #[derive(Debug)] pub struct Project { @@ -14,6 +16,7 @@ pub struct Project { pub version: String, pub dependencies: Vec, pub test_dependencies: Vec, + pub project_root: String, } #[derive(Debug)] @@ -51,19 +54,23 @@ impl Artifact { } pub fn load_project(jargo_file: Option<&str>) -> Result { - let project_table = fs::read_to_string(jargo_file.unwrap_or("./Jargo.toml"))?.parse::()?; + let jargo = Path::new(jargo_file.unwrap_or("./Jargo.toml")); + + let project_table = fs::read_to_string(jargo)?.parse::
()?; let package = project_table.get("package").expect("package info missing"); let dependencies = get_dependencies(project_table.get("dependencies"))?; let test_dependencies = get_dependencies(project_table.get("test-dependencies"))?; + Ok(Project { - group: package.get("group").unwrap().to_string(), - name: package.get("name").unwrap().to_string(), - version: package.get("version").unwrap().to_string(), + group: strip_first_last(package.get("group").unwrap().to_string()), + name: strip_first_last(package.get("name").unwrap().to_string()), + version: strip_first_last(package.get("version").unwrap().to_string()), dependencies, test_dependencies, + project_root: jargo.parent().map(Path::to_str).unwrap().expect(&format!("projectroot {:?} not usable", jargo)).into(), }) } @@ -80,4 +87,8 @@ fn get_dependencies(table: Option<&Value>) -> Result, Error> { } } Ok(dependencies) +} + +fn strip_first_last(text: String) -> String{ + text[1..text.len()-1].into() } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 6b7ab1b..4b57b4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,5 +3,6 @@ use anyhow::Error; fn main() -> anyhow::Result<(), Error> { let project = jargo::load_project(Some("tests/sample_project/Jargo.toml"))?; jargo::deploader::load_artifacts(&project.test_dependencies)?; + jargo::compile::run(&project)?; Ok(()) } diff --git a/tests/sample_project/src/main/java/nl/sander/Sample.java b/tests/sample_project/src/main/java/nl/sander/sample/Sample.java similarity index 100% rename from tests/sample_project/src/main/java/nl/sander/Sample.java rename to tests/sample_project/src/main/java/nl/sander/sample/Sample.java diff --git a/tests/sample_project/src/main/java/nl/sander/sample/SampleMain.java b/tests/sample_project/src/main/java/nl/sander/sample/SampleMain.java new file mode 100644 index 0000000..4f6649f --- /dev/null +++ b/tests/sample_project/src/main/java/nl/sander/sample/SampleMain.java @@ -0,0 +1,7 @@ +package nl.sander.sample; + +public class SampleMain { + public static void main(String[] args) { + System.out.println(Sample.getTheNumber()); + } +} diff --git a/tests/sample_project/src/test/java/nl/sander/SampleTest.java b/tests/sample_project/src/test/java/nl/sander/sample/SampleTest.java similarity index 100% rename from tests/sample_project/src/test/java/nl/sander/SampleTest.java rename to tests/sample_project/src/test/java/nl/sander/sample/SampleTest.java