use anyhow::{Context, Result, bail};
use console::style;
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Deserialize, Default)]
#[serde(default)]
struct Pom {
properties: Properties,
build: Build,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct Properties {
#[serde(rename = "maven.compiler.release")]
compiler_release: Option<String>,
#[serde(rename = "maven.compiler.source")]
compiler_source: Option<String>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct Build {
plugins: Plugins,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct Plugins {
plugin: Vec<Plugin>,
}
#[derive(Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
struct Plugin {
artifact_id: Option<String>,
configuration: Option<CompilerConfiguration>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct CompilerConfiguration {
release: Option<String>,
source: Option<String>,
}
pub fn detect_java_version(pom: &Path) -> Result<u32> {
let content =
std::fs::read_to_string(pom).with_context(|| format!("cannot read {}", pom.display()))?;
let parsed: Pom = quick_xml::de::from_str(&content)
.with_context(|| format!("failed to parse {}", pom.display()))?;
let plugin_cfg = parsed
.build
.plugins
.plugin
.iter()
.find(|p| p.artifact_id.as_deref() == Some("maven-compiler-plugin"))
.and_then(|p| p.configuration.as_ref());
let ver = parsed.properties.compiler_release
.or(parsed.properties.compiler_source)
.or_else(|| plugin_cfg.and_then(|c| c.release.clone()))
.or_else(|| plugin_cfg.and_then(|c| c.source.clone()))
.ok_or_else(|| anyhow::anyhow!(
"{}",
style("Could not detect Java version from pom.xml").red().bold()
))?;
parse_java_version(&ver)
}
pub fn parse_java_version(v: &str) -> Result<u32> {
let v = v.trim();
if let Some(rest) = v.strip_prefix("1.") {
return Ok(rest.parse::<u32>().with_context(|| format!("bad java version: {v}"))?);
}
Ok(v.parse::<u32>().with_context(|| format!("bad java version: {v}"))?)
}
pub fn find_jdk_home(major: u32) -> Result<PathBuf> {
if let Some(path) = try_find_jdk_home(major) {
return Ok(path);
}
bail!(
"{} JDK {} not found — install it or set JAVA_HOME",
style("✗").red().bold(),
style(major).yellow()
);
}
fn try_find_jdk_home(major: u32) -> Option<PathBuf> {
#[cfg(target_os = "macos")]
if let Some(p) = find_jdk_macos(major) {
return Some(p);
}
#[cfg(target_os = "linux")]
if let Some(p) = find_jdk_linux(major) {
return Some(p);
}
find_jdk_env(major)
}
#[cfg(target_os = "macos")]
fn find_jdk_macos(major: u32) -> Option<PathBuf> {
let out = duct::cmd!("/usr/libexec/java_home", "-v", major.to_string())
.stderr_null()
.read()
.ok()?;
let path = PathBuf::from(out.trim());
path.exists().then_some(path)
}
#[cfg(target_os = "linux")]
fn find_jdk_linux(major: u32) -> Option<PathBuf> {
if let Some(p) = scan_dir("/usr/lib/jvm", &format!("java-{major}-")) {
return Some(p);
}
if let Some(home) = std::env::var("HOME").ok() {
let sdkman = PathBuf::from(home).join(".sdkman/candidates/java");
if let Some(p) = scan_dir(&sdkman, &format!("{major}.")) {
return Some(p);
}
}
if let Some(p) = scan_dir("/opt/jdk", &format!("jdk-{major}")) {
return Some(p);
}
None
}
fn scan_dir(dir: impl AsRef<Path>, prefix: &str) -> Option<PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_dir())
.find(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with(prefix))
.unwrap_or(false)
})
}
fn find_jdk_env(major: u32) -> Option<PathBuf> {
let home = std::env::var("JAVA_HOME").ok()?;
let path = PathBuf::from(home);
if !path.exists() {
return None;
}
let java_bin = path.join("bin/java");
let out = duct::cmd!(&java_bin, "-version")
.stderr_capture()
.run()
.ok()?;
let version_output = String::from_utf8_lossy(&out.stderr);
let version_str = version_output.lines().next()?;
let quoted = version_str.find('"')?;
let rest = &version_str[quoted + 1..];
let end = rest.find('"')?;
let ver = parse_java_version(&rest[..end]).ok()?;
(ver == major).then_some(path)
}
pub fn find_pom(start: &Path) -> Option<PathBuf> {
let mut dir = start.to_path_buf();
for _ in 0..3 {
let candidate = dir.join("pom.xml");
if candidate.exists() {
return Some(candidate);
}
if !dir.pop() {
break;
}
}
None
}