use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JbuildWorkspace {
#[serde(flatten)]
pub workspace: WorkspaceConfig,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkspaceConfig {
pub members: Vec<String>,
#[serde(default)]
pub default_members: Vec<String>,
#[serde(default)]
pub resolver: ResolverConfig,
#[serde(default)]
pub package: WorkspacePackage,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct WorkspacePackage {
pub name: Option<String>,
pub version: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
pub description: Option<String>,
pub documentation: Option<String>,
pub repository: Option<String>,
pub homepage: Option<String>,
pub license: Option<String>,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default)]
pub categories: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ResolverConfig {
#[serde(default)]
pub version_resolution: VersionResolution,
#[serde(default)]
pub conflict_resolution: ConflictResolution,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum VersionResolution {
#[default]
Highest,
Lowest,
Fail,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum ConflictResolution {
#[default]
Highest,
Lowest,
Fail,
}
#[derive(Debug, Clone)]
pub struct Workspace {
pub root: PathBuf,
pub config: JbuildWorkspace,
pub members: Vec<WorkspaceMember>,
pub dependency_graph: Vec<WorkspaceDependency>,
}
#[derive(Debug, Clone)]
pub struct WorkspaceMember {
pub name: String,
pub path: PathBuf,
pub relative_path: String,
pub build_system: Option<crate::build::BuildSystem>,
pub workspace_dependencies: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct WorkspaceDependency {
pub from: String,
pub to: String,
pub dependency_type: WorkspaceDependencyType,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WorkspaceDependencyType {
Direct,
Transitive,
}
impl Default for JbuildWorkspace {
fn default() -> Self {
Self {
workspace: WorkspaceConfig {
members: vec![],
default_members: vec![],
resolver: ResolverConfig::default(),
package: WorkspacePackage::default(),
},
}
}
}
impl JbuildWorkspace {
pub fn from_file(path: &Path) -> anyhow::Result<Self> {
let contents = std::fs::read_to_string(path)?;
let config: JbuildWorkspace = toml::from_str(&contents)?;
Ok(config)
}
pub fn save_to_file(&self, path: &Path) -> anyhow::Result<()> {
let contents = toml::to_string_pretty(self)?;
std::fs::write(path, contents)?;
Ok(())
}
pub fn new() -> Self {
Self::default()
}
pub fn add_member(&mut self, member_path: String) {
if !self.workspace.members.contains(&member_path) {
self.workspace.members.push(member_path);
}
}
pub fn remove_member(&mut self, member_path: &str) {
self.workspace.members.retain(|m| m != member_path);
self.workspace.default_members.retain(|m| m != member_path);
}
pub fn set_default_members(&mut self, members: Vec<String>) {
self.workspace.default_members = members;
}
}
impl Workspace {
pub fn from_directory(root: &Path) -> anyhow::Result<Self> {
let workspace_file = root.join("jbuild-workspace.toml");
if !workspace_file.exists() {
return Err(anyhow::anyhow!(
"No jbuild-workspace.toml found in {}",
root.display()
));
}
let config = JbuildWorkspace::from_file(&workspace_file)?;
let members = Self::resolve_members(root, &config)?;
let dependency_graph = Self::build_dependency_graph(&members)?;
Ok(Workspace {
root: root.to_path_buf(),
config,
members,
dependency_graph,
})
}
fn resolve_members(root: &Path, config: &JbuildWorkspace) -> anyhow::Result<Vec<WorkspaceMember>> {
let mut members = Vec::new();
for member_path in &config.workspace.members {
let member_dir = root.join(member_path);
if !member_dir.exists() {
return Err(anyhow::anyhow!(
"Workspace member directory does not exist: {}",
member_dir.display()
));
}
let build_system = crate::build::BuildSystem::detect(&member_dir);
let name = Self::extract_project_name(&member_dir)?;
let workspace_dependencies = Self::analyze_workspace_dependencies(&member_dir, &members)?;
members.push(WorkspaceMember {
name,
path: member_dir,
relative_path: member_path.clone(),
build_system,
workspace_dependencies,
});
}
Ok(members)
}
fn extract_project_name(member_dir: &Path) -> anyhow::Result<String> {
let pom_path = member_dir.join("pom.xml");
if pom_path.exists() {
if let Ok(content) = std::fs::read_to_string(&pom_path) {
if let Some(name) = Self::extract_name_from_pom(&content) {
return Ok(name);
}
}
}
let gradle_path = member_dir.join("build.gradle");
if gradle_path.exists() {
if let Ok(content) = std::fs::read_to_string(&gradle_path) {
if let Some(name) = Self::extract_name_from_gradle(&content) {
return Ok(name);
}
}
}
Ok(member_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string())
}
fn extract_name_from_pom(content: &str) -> Option<String> {
let re = regex::Regex::new(r"<artifactId>([^<]+)</artifactId>").ok()?;
for cap in re.captures_iter(content) {
if let Some(name) = cap.get(1) {
return Some(name.as_str().to_string());
}
}
None
}
fn extract_name_from_gradle(content: &str) -> Option<String> {
let re = regex::Regex::new(r#"rootProject\.name\s*=\s*['"]([^'"]+)['"]"#).ok()?;
for cap in re.captures_iter(content) {
if let Some(name) = cap.get(1) {
return Some(name.as_str().to_string());
}
}
None
}
fn analyze_workspace_dependencies(member_dir: &Path, existing_members: &[WorkspaceMember]) -> anyhow::Result<Vec<String>> {
let mut deps = Vec::new();
let pom_path = member_dir.join("pom.xml");
if pom_path.exists() {
if let Ok(content) = std::fs::read_to_string(&pom_path) {
deps.extend(Self::extract_workspace_deps_from_pom(&content, existing_members));
}
}
let gradle_path = member_dir.join("build.gradle");
if gradle_path.exists() {
if let Ok(content) = std::fs::read_to_string(&gradle_path) {
deps.extend(Self::extract_workspace_deps_from_gradle(&content, existing_members));
}
}
Ok(deps)
}
fn extract_workspace_deps_from_pom(content: &str, members: &[WorkspaceMember]) -> Vec<String> {
let mut deps = Vec::new();
let re = regex::Regex::new(r"<artifactId>([^<]+)</artifactId>").unwrap();
for cap in re.captures_iter(content) {
if let Some(artifact_id) = cap.get(1) {
let artifact_name = artifact_id.as_str();
if members.iter().any(|m| m.name == artifact_name) {
deps.push(artifact_name.to_string());
}
}
}
deps
}
fn extract_workspace_deps_from_gradle(content: &str, members: &[WorkspaceMember]) -> Vec<String> {
let mut deps = Vec::new();
let re = regex::Regex::new(r#"project\s*\(\s*['"]:([^'"]+)['"]\s*\)"#).unwrap();
for cap in re.captures_iter(content) {
if let Some(dep_path) = cap.get(1) {
let dep_name = dep_path.as_str().split(':').next_back().unwrap_or(dep_path.as_str());
if members.iter().any(|m| m.name == dep_name) {
deps.push(dep_name.to_string());
}
}
}
deps
}
fn build_dependency_graph(members: &[WorkspaceMember]) -> anyhow::Result<Vec<WorkspaceDependency>> {
let mut graph = Vec::new();
for member in members {
for dep_name in &member.workspace_dependencies {
if members.iter().any(|m| &m.name == dep_name) {
graph.push(WorkspaceDependency {
from: member.name.clone(),
to: dep_name.clone(),
dependency_type: WorkspaceDependencyType::Direct,
});
}
}
}
let mut transitive_deps = Vec::new();
for dep in &graph {
for target_dep in &graph {
if target_dep.from == dep.to {
transitive_deps.push(WorkspaceDependency {
from: dep.from.clone(),
to: target_dep.to.clone(),
dependency_type: WorkspaceDependencyType::Transitive,
});
}
}
}
graph.extend(transitive_deps);
Ok(graph)
}
pub fn get_build_order(&self) -> Vec<&WorkspaceMember> {
let mut result = Vec::new();
let mut visited = std::collections::HashSet::new();
for member in &self.members {
if !visited.contains(&member.name) {
self.visit_member(member, &mut visited, &mut result);
}
}
result
}
fn visit_member<'a>(
&'a self,
member: &'a WorkspaceMember,
visited: &mut std::collections::HashSet<String>,
result: &mut Vec<&'a WorkspaceMember>,
) {
visited.insert(member.name.clone());
for dep_name in &member.workspace_dependencies {
if let Some(dep_member) = self.members.iter().find(|m| &m.name == dep_name) {
if !visited.contains(&dep_member.name) {
self.visit_member(dep_member, visited, result);
}
}
}
result.push(member);
}
pub fn is_workspace_root(dir: &Path) -> bool {
dir.join("jbuild-workspace.toml").exists()
}
}