use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
#[derive(Debug, Clone)]
pub struct GradleSettings {
pub root_project_name: String,
pub subprojects: Vec<SubprojectConfig>,
pub plugin_management: Option<PluginManagement>,
pub dependency_resolution_management: Option<DependencyResolutionManagement>,
pub build_cache: Option<BuildCacheConfig>,
}
#[derive(Debug, Clone)]
pub struct SubprojectConfig {
pub path: String,
pub project_dir: Option<PathBuf>,
pub build_file_name: Option<String>,
}
impl SubprojectConfig {
pub fn new(path: impl Into<String>) -> Self {
Self {
path: path.into(),
project_dir: None,
build_file_name: None,
}
}
pub fn with_project_dir(mut self, dir: PathBuf) -> Self {
self.project_dir = Some(dir);
self
}
pub fn name(&self) -> &str {
self.path.rsplit(':').next().unwrap_or(&self.path)
}
pub fn directory(&self, root_dir: &Path) -> PathBuf {
if let Some(ref dir) = self.project_dir {
root_dir.join(dir)
} else {
let segments: Vec<&str> = self.path.split(':').filter(|s| !s.is_empty()).collect();
let mut path = root_dir.to_path_buf();
for segment in segments {
path = path.join(segment);
}
path
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PluginManagement {
pub repositories: Vec<String>,
pub plugins: Vec<PluginSpec>,
}
#[derive(Debug, Clone)]
pub struct PluginSpec {
pub id: String,
pub version: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct DependencyResolutionManagement {
pub repositories_mode: Option<String>,
pub repositories: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct BuildCacheConfig {
pub local_enabled: bool,
pub remote_enabled: bool,
pub remote_url: Option<String>,
pub push: bool,
}
impl GradleSettings {
pub fn new(root_project_name: impl Into<String>) -> Self {
Self {
root_project_name: root_project_name.into(),
subprojects: Vec::new(),
plugin_management: None,
dependency_resolution_management: None,
build_cache: None,
}
}
pub fn include(&mut self, path: impl Into<String>) {
self.subprojects.push(SubprojectConfig::new(path));
}
pub fn is_multi_project(&self) -> bool {
!self.subprojects.is_empty()
}
pub fn all_project_paths(&self) -> Vec<String> {
let mut paths = vec![":".to_string()];
for subproject in &self.subprojects {
paths.push(subproject.path.clone());
}
paths
}
}
pub fn parse_settings_file(settings_file: &Path, root_dir: &Path) -> Result<GradleSettings> {
let content = std::fs::read_to_string(settings_file)
.with_context(|| format!("Failed to read settings file: {settings_file:?}"))?;
parse_settings(&content, root_dir)
}
pub fn parse_settings(content: &str, root_dir: &Path) -> Result<GradleSettings> {
let root_name = root_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("project")
.to_string();
let mut settings = GradleSettings::new(root_name);
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") || line.starts_with("/*") {
continue;
}
if let Some(name) = parse_root_project_name(line) {
settings.root_project_name = name;
continue;
}
if let Some(includes) = parse_include(line) {
for include in includes {
settings.include(include);
}
continue;
}
if let Some(includes) = parse_include_flat(line) {
for include in includes {
let mut config = SubprojectConfig::new(format!(":{include}"));
config.project_dir = Some(PathBuf::from(format!("../{include}")));
settings.subprojects.push(config);
}
}
}
Ok(settings)
}
fn parse_root_project_name(line: &str) -> Option<String> {
let patterns = [
"rootProject.name",
"rootProject.name=",
];
for pattern in patterns {
if line.starts_with(pattern) {
let rest = line[pattern.len()..].trim();
let rest = rest.trim_start_matches('=').trim();
return extract_string_value(rest);
}
}
None
}
fn parse_include(line: &str) -> Option<Vec<String>> {
if !line.starts_with("include") {
return None;
}
let rest = line["include".len()..].trim();
let rest = rest.trim_start_matches('(').trim_end_matches(')');
let mut includes = Vec::new();
for part in rest.split(',') {
let part = part.trim();
if let Some(value) = extract_string_value(part) {
includes.push(value);
}
}
if includes.is_empty() {
None
} else {
Some(includes)
}
}
fn parse_include_flat(line: &str) -> Option<Vec<String>> {
if !line.starts_with("includeFlat") {
return None;
}
let rest = line["includeFlat".len()..].trim();
let rest = rest.trim_start_matches('(').trim_end_matches(')');
let mut includes = Vec::new();
for part in rest.split(',') {
let part = part.trim();
if let Some(value) = extract_string_value(part) {
includes.push(value);
}
}
if includes.is_empty() {
None
} else {
Some(includes)
}
}
fn extract_string_value(s: &str) -> Option<String> {
let s = s.trim();
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
return Some(s[1..s.len() - 1].to_string());
}
if s.starts_with('\'') && s.ends_with('\'') && s.len() >= 2 {
return Some(s[1..s.len() - 1].to_string());
}
None
}
pub fn find_settings_file(dir: &Path) -> Option<PathBuf> {
let settings_gradle = dir.join("settings.gradle");
if settings_gradle.exists() {
return Some(settings_gradle);
}
let settings_kts = dir.join("settings.gradle.kts");
if settings_kts.exists() {
return Some(settings_kts);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_root_project_name() {
assert_eq!(
parse_root_project_name("rootProject.name = \"my-project\""),
Some("my-project".to_string())
);
assert_eq!(
parse_root_project_name("rootProject.name = 'my-project'"),
Some("my-project".to_string())
);
assert_eq!(
parse_root_project_name("rootProject.name=\"my-project\""),
Some("my-project".to_string())
);
}
#[test]
fn test_parse_include() {
assert_eq!(
parse_include("include ':app'"),
Some(vec![":app".to_string()])
);
assert_eq!(
parse_include("include ':app', ':lib'"),
Some(vec![":app".to_string(), ":lib".to_string()])
);
assert_eq!(
parse_include("include(\":app\", \":lib\")"),
Some(vec![":app".to_string(), ":lib".to_string()])
);
}
#[test]
fn test_parse_include_flat() {
assert_eq!(
parse_include_flat("includeFlat 'shared'"),
Some(vec!["shared".to_string()])
);
}
#[test]
fn test_parse_settings() {
let content = r#"
rootProject.name = "my-multi-project"
include ':app'
include ':lib:core', ':lib:utils'
"#;
let settings = parse_settings(content, Path::new("/project")).unwrap();
assert_eq!(settings.root_project_name, "my-multi-project");
assert_eq!(settings.subprojects.len(), 3);
assert!(settings.is_multi_project());
}
#[test]
fn test_subproject_config() {
let config = SubprojectConfig::new(":lib:core");
assert_eq!(config.name(), "core");
let dir = config.directory(Path::new("/project"));
assert_eq!(dir, PathBuf::from("/project/lib/core"));
}
#[test]
fn test_subproject_with_custom_dir() {
let config = SubprojectConfig::new(":app")
.with_project_dir(PathBuf::from("application"));
let dir = config.directory(Path::new("/project"));
assert_eq!(dir, PathBuf::from("/project/application"));
}
#[test]
fn test_all_project_paths() {
let mut settings = GradleSettings::new("root");
settings.include(":app");
settings.include(":lib");
let paths = settings.all_project_paths();
assert_eq!(paths, vec![":".to_string(), ":app".to_string(), ":lib".to_string()]);
}
}