use super::EcosystemAdapter;
use crate::graph::GraphNode;
use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct GradleAdapter;
impl EcosystemAdapter for GradleAdapter {
fn name(&self) -> &'static str {
"gradle"
}
fn detect_tool(&self) -> Result<String> {
let gradlew = if cfg!(windows) {
"gradlew.bat"
} else {
"./gradlew"
};
let output = Command::new(gradlew)
.arg("--version")
.output()
.or_else(|_| Command::new("gradle").arg("--version").output())
.context("Failed to execute `gradle --version`. Ensure Gradle or gradlew wrapper is installed.")?;
if !output.status.success() {
bail!(
"`gradle --version` failed with exit status: {}",
output.status
);
}
let version_str = String::from_utf8_lossy(&output.stdout)
.lines()
.find(|line| line.starts_with("Gradle "))
.unwrap_or("Gradle 8.x")
.to_string();
Ok(version_str)
}
fn resolve(&self, workspace_path: &Path, frozen: bool) -> Result<PathBuf> {
let lockfile_path = workspace_path.join("gradle.lockfile");
if frozen && lockfile_path.exists() {
return Ok(lockfile_path);
}
let gradlew = if cfg!(windows) {
"gradlew.bat"
} else {
"./gradlew"
};
let mut cmd = if workspace_path.join(gradlew).exists() {
Command::new(workspace_path.join(gradlew))
} else {
Command::new("gradle")
};
cmd.arg("dependencies")
.arg("--write-locks")
.current_dir(workspace_path);
let output = cmd
.output()
.context("Failed to run `gradle dependencies --write-locks` in workspace")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::warn!("Gradle write-locks warning: {}", stderr);
}
if lockfile_path.exists() {
return Ok(lockfile_path);
}
let build_gradle = workspace_path.join("build.gradle");
let build_gradle_kts = workspace_path.join("build.gradle.kts");
if build_gradle.exists() {
Ok(build_gradle)
} else if build_gradle_kts.exists() {
Ok(build_gradle_kts)
} else {
bail!(
"Neither `gradle.lockfile` nor `build.gradle` found at {}",
workspace_path.display()
);
}
}
fn parse_lockfile(&self, lockfile_path: &Path) -> Result<Vec<GraphNode>> {
let content = fs::read_to_string(lockfile_path).with_context(|| {
format!("Failed to read Gradle file at {}", lockfile_path.display())
})?;
let mut nodes = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("#") || line.starts_with("//") {
continue;
}
if let Some((dep_spec, _config)) = line.split_once('=') {
let parts: Vec<&str> = dep_spec.split(':').collect();
if parts.len() >= 3 {
let group = parts[0];
let artifact = parts[1];
let version = parts[2];
let pkg_name = format!("{}:{}", group, artifact);
let content_hash = format!(
"sha256:{}",
hex::encode(Sha256::digest(
format!("gradle:{}:{}:{}", group, artifact, version).as_bytes()
))
);
nodes.push(GraphNode {
ecosystem: "gradle".to_string(),
package_name: pkg_name,
resolved_version: version.to_string(),
content_hash,
platform_markers: None,
lock_ref: lockfile_path.to_string_lossy().to_string(),
});
}
} else if line.contains("implementation") || line.contains("api") || line.contains("id")
{
let cleaned = line
.replace(['"', '\''], "")
.replace("implementation", "")
.replace("api", "")
.trim()
.to_string();
let parts: Vec<&str> = cleaned.split(':').collect();
if parts.len() >= 3 {
let group = parts[0].trim();
let artifact = parts[1].trim();
let version = parts[2].trim();
let pkg_name = format!("{}:{}", group, artifact);
let content_hash = format!(
"sha256:{}",
hex::encode(Sha256::digest(
format!("gradle:{}:{}:{}", group, artifact, version).as_bytes()
))
);
nodes.push(GraphNode {
ecosystem: "gradle".to_string(),
package_name: pkg_name,
resolved_version: version.to_string(),
content_hash,
platform_markers: None,
lock_ref: lockfile_path.to_string_lossy().to_string(),
});
}
}
}
nodes.sort_by(|a, b| {
a.package_name
.cmp(&b.package_name)
.then(a.resolved_version.cmp(&b.resolved_version))
});
nodes.dedup_by(|a, b| {
a.package_name == b.package_name && a.resolved_version == b.resolved_version
});
Ok(nodes)
}
}