jbx 0.6.1

jbx: one-stop Java toolbox for scripts, tools, and agents
Documentation
use anyhow::{anyhow, Context, Result};
use std::path::PathBuf;
use std::process::Command as ProcessCommand;

#[derive(Debug, Clone)]
pub struct MavenToolOptions {
    pub coordinate: String,
    pub repos: Vec<String>,
    pub cache_dir: Option<PathBuf>,
    pub main_class: Option<String>,
    pub args: Vec<String>,
}

pub fn maven_repositories(repo_args: &[String]) -> Vec<crate::resolver::Repository> {
    let mut repos = vec![crate::resolver::Repository::central()];
    for repo in repo_args {
        if repo == "central" || repo == "mavenCentral" {
            continue;
        }
        if let Some((id, url)) = repo.split_once('=') {
            repos.push(crate::resolver::Repository {
                id: id.to_string(),
                url: url.to_string(),
            });
        } else {
            repos.push(crate::resolver::Repository {
                id: repo.clone(),
                url: repo.clone(),
            });
        }
    }
    repos
}

fn primary_jar_name(coordinate: &str) -> Result<String> {
    let dep = crate::resolver::parse_coordinate(coordinate)?;
    Ok(match dep.classifier {
        Some(classifier) => format!("{}-{}-{classifier}.jar", dep.module.name, dep.version),
        None => format!("{}-{}.jar", dep.module.name, dep.version),
    })
}

fn resolve_maven_tool_coordinate(
    coordinate: &str,
    repos: &[crate::resolver::Repository],
) -> Result<String> {
    let parts: Vec<&str> = coordinate.split(':').collect();
    if parts.len() != 2 {
        crate::resolver::parse_coordinate(coordinate)?;
        return Ok(coordinate.to_string());
    }
    let module = crate::resolver::Module {
        org: parts[0].to_string(),
        name: parts[1].to_string(),
    };
    let version = crate::resolver::resolve_latest_version(&module, repos)?;
    Ok(format!("{}:{}:{version}", module.org, module.name))
}

pub fn run(options: MavenToolOptions) -> Result<i32> {
    let cache_dir = match options.cache_dir {
        Some(path) => path,
        None => crate::default_cache_dir()?.join("deps"),
    };
    let repos = maven_repositories(&options.repos);
    let requested_coordinate = options.coordinate;
    let coordinate = resolve_maven_tool_coordinate(&requested_coordinate, &repos)?;
    let coordinates = vec![coordinate.clone()];
    let classpath = crate::resolver::resolve_classpath(&coordinates, &repos, &cache_dir)?;
    if classpath.is_empty() {
        return Err(anyhow!("no JARs resolved for {coordinate}"));
    }

    let mut java = ProcessCommand::new("java");
    if let Some(main_class) = options.main_class {
        java.arg("-cp")
            .arg(std::env::join_paths(&classpath)?)
            .arg(main_class);
    } else {
        let jar_name = primary_jar_name(&coordinate)?;
        let primary_jar = classpath
            .iter()
            .find(|path| {
                path.file_name()
                    .is_some_and(|name| name == jar_name.as_str())
            })
            .ok_or_else(|| anyhow!("resolved classpath did not contain primary JAR {jar_name}"))?;
        java.arg("-jar").arg(primary_jar);
    }
    java.args(options.args);
    let status = java.status().context("failed to launch java")?;
    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        if let Some(signal) = status.signal() {
            return Ok(128 + signal);
        }
    }
    Ok(status.code().unwrap_or(1))
}