use anyhow::{anyhow, Context, Result};
use std::io::Read;
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 progress: crate::ProgressOptions,
}
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 progress = options.progress;
progress.phase(format!("resolving dependencies for {requested_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}"))?;
if let Some(main_class) = read_main_class_from_jar(primary_jar)? {
java.arg("-cp")
.arg(std::env::join_paths(&classpath)?)
.arg(main_class);
} else {
java.arg("-jar").arg(primary_jar);
}
}
java.args(options.args);
progress.phase(format!("running {coordinate}"));
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))
}
fn read_main_class_from_jar(jar: &std::path::Path) -> Result<Option<String>> {
let file = std::fs::File::open(jar)
.with_context(|| format!("failed to open primary JAR {}", jar.display()))?;
let mut archive = zip::ZipArchive::new(file)
.with_context(|| format!("failed to read primary JAR {}", jar.display()))?;
let Ok(mut manifest) = archive.by_name("META-INF/MANIFEST.MF") else {
return Ok(None);
};
let mut content = String::new();
manifest
.read_to_string(&mut content)
.with_context(|| format!("failed to read manifest from {}", jar.display()))?;
Ok(manifest_attribute(&content, "Main-Class"))
}
fn manifest_attribute(manifest: &str, key: &str) -> Option<String> {
let mut unfolded: Vec<String> = Vec::new();
for line in manifest.lines() {
if let Some(continuation) = line.strip_prefix(' ') {
if let Some(last) = unfolded.last_mut() {
last.push_str(continuation);
}
} else {
unfolded.push(line.to_string());
}
}
let prefix = format!("{key}:");
unfolded.into_iter().find_map(|line| {
line.strip_prefix(&prefix)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
})
}