pub use curie_meta::workspace::{
discover, list, load, Member, Workspace, WorkspaceContext,
};
use crate::audit::{self, AuditOptions};
use crate::descriptor;
use crate::update::{self, UpdateOptions};
use crate::{build, compile, fmt, jar, run, test};
use anyhow::{bail, Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
struct MemberArtifact {
classes_dir: PathBuf,
classpath_contribution: Vec<PathBuf>,
}
fn collect_dep_classpath(
deps: &[usize],
artifacts: &std::collections::HashMap<usize, MemberArtifact>,
) -> Vec<PathBuf> {
let mut cp: Vec<PathBuf> = Vec::new();
let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
for &i in deps {
let a = artifacts
.get(&i)
.expect("subset must include all transitive workspace_deps of every member it builds");
if seen.insert(a.classes_dir.clone()) {
cp.push(a.classes_dir.clone());
}
for entry in &a.classpath_contribution {
if seen.insert(entry.clone()) {
cp.push(entry.clone());
}
}
}
cp
}
fn transitive_closure(ws: &Workspace, target: usize) -> Vec<usize> {
transitive_closure_multi(ws, &[target])
}
fn transitive_closure_multi(ws: &Workspace, targets: &[usize]) -> Vec<usize> {
let mut included: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut stack: Vec<usize> = targets.to_vec();
while let Some(i) = stack.pop() {
if included.insert(i) {
for &dep in &ws.members[i].workspace_deps {
stack.push(dep);
}
}
}
(0..ws.members.len()).filter(|i| included.contains(i)).collect()
}
fn fan_out<F>(ws: &Workspace, action: &str, subset: &[usize], mut run: F) -> Result<()>
where
F: FnMut(&Member, &[PathBuf]) -> Result<Vec<PathBuf>>,
{
let n = subset.len();
println!(
"Workspace {} {} ({} member{})",
ws.root.display(),
action,
n,
if n == 1 { "" } else { "s" },
);
println!();
let mut artifacts: std::collections::HashMap<usize, MemberArtifact> =
std::collections::HashMap::with_capacity(n);
for (pos, &idx) in subset.iter().enumerate() {
let m = &ws.members[idx];
println!("[{}/{}] {}", pos + 1, n, m.declared);
let extra_cp = collect_dep_classpath(&m.workspace_deps, &artifacts);
let own_dep_jars = run(m, &extra_cp)
.with_context(|| format!("workspace member \"{}\" failed", m.declared))?;
let classes_dir = m.path.join("target").join("classes");
let mut contribution = extra_cp; for j in own_dep_jars {
contribution.push(j);
}
artifacts.insert(idx, MemberArtifact { classes_dir, classpath_contribution: contribution });
println!();
}
Ok(())
}
pub fn build_all(workspace_root: &Path, opts: build::BuildOptions, jobs: usize) -> Result<()> {
let ws = load(workspace_root)?;
let subset: Vec<usize> = (0..ws.members.len()).collect();
if subset.len() > 1 {
return crate::parallel::run_jobs(&ws, &subset, "build", jobs, true, crate::parallel::TuiMode::Full, "Done", |m, extra_cp| {
build::build_with_desc(&m.path, &m.descriptor, opts, extra_cp).map(|o| o.dep_jars)
});
}
fan_out(&ws, "build", &subset, |m, extra_cp| {
build::build_with_desc(&m.path, &m.descriptor, opts, extra_cp).map(|o| o.dep_jars)
})
}
pub fn build_one(
workspace_root: &Path,
member_index: usize,
opts: build::BuildOptions,
jobs: usize,
) -> Result<()> {
let ws = load(workspace_root)?;
let subset = transitive_closure(&ws, member_index);
if subset.len() > 1 {
return crate::parallel::run_jobs(&ws, &subset, "build", jobs, true, crate::parallel::TuiMode::Full, "Done", |m, extra_cp| {
build::build_with_desc(&m.path, &m.descriptor, opts, extra_cp).map(|o| o.dep_jars)
});
}
fan_out(&ws, "build", &subset, |m, extra_cp| {
build::build_with_desc(&m.path, &m.descriptor, opts, extra_cp).map(|o| o.dep_jars)
})
}
pub fn build_subtree(
workspace_root: &Path,
member_indices: &[usize],
opts: build::BuildOptions,
jobs: usize,
) -> Result<()> {
let ws = load(workspace_root)?;
let subset = transitive_closure_multi(&ws, member_indices);
if subset.len() > 1 {
return crate::parallel::run_jobs(&ws, &subset, "build", jobs, true, crate::parallel::TuiMode::Full, "Done", |m, extra_cp| {
build::build_with_desc(&m.path, &m.descriptor, opts, extra_cp).map(|o| o.dep_jars)
});
}
fan_out(&ws, "build", &subset, |m, extra_cp| {
build::build_with_desc(&m.path, &m.descriptor, opts, extra_cp).map(|o| o.dep_jars)
})
}
pub fn test_all(workspace_root: &Path, filter: Option<&str>, offline: bool, jobs: usize) -> Result<()> {
let ws = load(workspace_root)?;
let subset: Vec<usize> = (0..ws.members.len()).collect();
if subset.len() > 1 {
return crate::parallel::run_jobs(&ws, &subset, "test", jobs, true, crate::parallel::TuiMode::Full, "Done", |m, extra_cp| {
test_one_member(m, filter, offline, extra_cp)
});
}
fan_out(&ws, "test", &subset, |m, extra_cp| {
test_one_member(m, filter, offline, extra_cp)
})
}
pub fn test_one(
workspace_root: &Path,
member_index: usize,
filter: Option<&str>,
offline: bool,
jobs: usize,
) -> Result<()> {
let ws = load(workspace_root)?;
let subset = transitive_closure(&ws, member_index);
if subset.len() > 1 {
return crate::parallel::run_jobs(&ws, &subset, "test", jobs, true, crate::parallel::TuiMode::Full, "Done", |m, extra_cp| {
test_one_member(m, filter, offline, extra_cp)
});
}
fan_out(&ws, "test", &subset, |m, extra_cp| {
test_one_member(m, filter, offline, extra_cp)
})
}
pub fn test_subtree(
workspace_root: &Path,
member_indices: &[usize],
filter: Option<&str>,
offline: bool,
jobs: usize,
) -> Result<()> {
let ws = load(workspace_root)?;
let subset = transitive_closure_multi(&ws, member_indices);
if subset.len() > 1 {
return crate::parallel::run_jobs(&ws, &subset, "test", jobs, true, crate::parallel::TuiMode::Full, "Done", |m, extra_cp| {
test_one_member(m, filter, offline, extra_cp)
});
}
fan_out(&ws, "test", &subset, |m, extra_cp| {
test_one_member(m, filter, offline, extra_cp)
})
}
fn test_one_member(
m: &Member,
filter: Option<&str>,
offline: bool,
extra_cp: &[PathBuf],
) -> Result<Vec<PathBuf>> {
crate::parallel::emit(&crate::style::headline(
"Testing", m.descriptor.buildable_name(), m.descriptor.buildable_version(),
));
let compiled = compile::compile(&m.path, &m.descriptor, offline, extra_cp)?;
test::run_tests(
&m.path,
&m.descriptor,
&compiled.classes_dir,
&compiled.dep_jars,
&compiled.kotlin_stdlib_jars,
&compiled.groovy_jars,
compiled.resources_dir.as_deref(),
compiled.test_resources_dir.as_deref(),
filter,
offline,
extra_cp,
)?;
Ok(compiled.dep_jars)
}
pub fn run_one(
workspace_root: &Path,
member_index: usize,
opts: run::RunOptions,
args: &[String],
) -> Result<()> {
let ws = load(workspace_root)?;
let target = &ws.members[member_index];
if target.descriptor.is_library() {
bail!("`curie run` is not supported for library projects");
}
if !opts.no_docker && descriptor::docker_enabled(&target.path, &target.descriptor) {
bail!(
"Docker support for `curie run` on a workspace member with \
[workspace-dependencies] is not yet implemented. Re-run \
with --no-docker, or remove [workspace-dependencies] and \
use the standalone path."
);
}
let subset = transitive_closure(&ws, member_index);
let build_opts = build::BuildOptions { no_docker: opts.no_docker, no_native: false, offline: opts.offline };
let n = subset.len();
println!(
"Workspace {} run ({} member{} to build)",
ws.root.display(),
n,
if n == 1 { "" } else { "s" },
);
println!();
let mut artifacts: std::collections::HashMap<usize, MemberArtifact> =
std::collections::HashMap::with_capacity(n);
let mut outputs: std::collections::HashMap<usize, build::BuildOutput> =
std::collections::HashMap::with_capacity(n);
for (pos, &idx) in subset.iter().enumerate() {
let m = &ws.members[idx];
println!("[{}/{}] {}", pos + 1, n, m.declared);
let extra_cp = collect_dep_classpath(&m.workspace_deps, &artifacts);
let output = build::build_with_desc(&m.path, &m.descriptor, build_opts, &extra_cp)
.with_context(|| format!("workspace member \"{}\" failed", m.declared))?;
let classes_dir = m.path.join("target").join("classes");
let mut contribution = extra_cp;
for j in output.dep_jars.iter().cloned() {
contribution.push(j);
}
artifacts.insert(idx, MemberArtifact { classes_dir, classpath_contribution: contribution });
outputs.insert(idx, output);
println!();
}
let target_output = &outputs[&member_index];
let main_class = target_output
.main_class
.as_deref()
.expect("application member should have resolved main_class after build");
println!("{}", crate::style::run_step(
target.descriptor.buildable_name(),
target.descriptor.buildable_version(),
));
println!();
let mut runtime_cp: Vec<PathBuf> = Vec::new();
let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
let push = |cp: &mut Vec<PathBuf>, seen: &mut std::collections::HashSet<PathBuf>, p: PathBuf| {
if seen.insert(p.clone()) {
cp.push(p);
}
};
push(&mut runtime_cp, &mut seen, target_output.jar.clone());
if let Some(rd) = &target_output.resources_dir {
if rd.exists() {
push(&mut runtime_cp, &mut seen, rd.clone());
}
}
for j in &target_output.dep_jars {
push(&mut runtime_cp, &mut seen, j.clone());
}
for &idx in &subset {
if idx == member_index {
continue;
}
let out = &outputs[&idx];
push(&mut runtime_cp, &mut seen, out.jar.clone());
for j in &out.dep_jars {
push(&mut runtime_cp, &mut seen, j.clone());
}
}
let mut java = Command::new("java");
java.arg("-cp").arg(jar::classpath_string(&runtime_cp));
java.arg(main_class);
for a in args {
java.arg(a);
}
let status = java
.status()
.context("failed to invoke java — is a JRE installed?")?;
if !status.success() {
let code = status.code().unwrap_or(1);
std::process::exit(code);
}
Ok(())
}
pub fn clean_all(workspace_root: &Path, jobs: usize) -> Result<()> {
let ws = load(workspace_root)?;
let subset: Vec<usize> = (0..ws.members.len()).collect();
if subset.len() > 1 {
return crate::parallel::run_jobs(&ws, &subset, "clean", jobs, false, crate::parallel::TuiMode::StatusOnly, "Cleaned", |m, _extra_cp| {
build::clean(&m.path).map(|_| Vec::new())
});
}
fan_out(&ws, "clean", &subset, |m, _extra_cp| {
build::clean(&m.path).map(|_| Vec::new())
})
}
pub fn clean_subtree(workspace_root: &Path, member_indices: &[usize], jobs: usize) -> Result<()> {
let ws = load(workspace_root)?;
if member_indices.len() > 1 {
return crate::parallel::run_jobs(&ws, member_indices, "clean", jobs, false, crate::parallel::TuiMode::StatusOnly, "Cleaned", |m, _: &[PathBuf]| {
build::clean(&m.path).map(|_| Vec::<PathBuf>::new())
});
}
fan_out(&ws, "clean", member_indices, |m, _extra_cp| {
build::clean(&m.path).map(|_| Vec::new())
})
}
pub fn audit_all(workspace_root: &Path, opts: &AuditOptions) -> Result<bool> {
let ws = load(workspace_root)?;
let n = ws.members.len();
println!(
"Workspace {} audit ({} member{})",
ws.root.display(),
n,
if n == 1 { "" } else { "s" },
);
println!();
let mut exit_nonzero = false;
for (pos, m) in ws.members.iter().enumerate() {
println!("[{}/{}] {}", pos + 1, n, m.declared);
let member_opts = override_output(opts, &m.path);
let report = audit::run_audit_with_desc(&m.path, &m.descriptor, &member_opts)
.with_context(|| format!("audit failed for workspace member \"{}\"", m.declared))?;
if audit::should_exit_nonzero(&report, &member_opts) {
exit_nonzero = true;
}
println!();
}
Ok(exit_nonzero)
}
pub fn audit_one(
workspace_root: &Path,
member_index: usize,
opts: &AuditOptions,
) -> Result<bool> {
let ws = load(workspace_root)?;
let m = &ws.members[member_index];
let member_opts = override_output(opts, &m.path);
let report = audit::run_audit_with_desc(&m.path, &m.descriptor, &member_opts)?;
Ok(audit::should_exit_nonzero(&report, &member_opts))
}
pub fn audit_subtree(
workspace_root: &Path,
member_indices: &[usize],
opts: &AuditOptions,
) -> Result<bool> {
let ws = load(workspace_root)?;
let n = member_indices.len();
println!(
"Workspace {} audit ({} member{})",
ws.root.display(),
n,
if n == 1 { "" } else { "s" },
);
println!();
let mut exit_nonzero = false;
for (pos, &idx) in member_indices.iter().enumerate() {
let m = &ws.members[idx];
println!("[{}/{}] {}", pos + 1, n, m.declared);
let member_opts = override_output(opts, &m.path);
let report = audit::run_audit_with_desc(&m.path, &m.descriptor, &member_opts)
.with_context(|| format!("audit failed for workspace member \"{}\"", m.declared))?;
if audit::should_exit_nonzero(&report, &member_opts) {
exit_nonzero = true;
}
println!();
}
Ok(exit_nonzero)
}
pub fn update_all(workspace_root: &Path, opts: &UpdateOptions) -> Result<bool> {
let ws = load(workspace_root)?;
let n = ws.members.len();
println!(
"Workspace {} update ({} member{})",
ws.root.display(),
n,
if n == 1 { "" } else { "s" },
);
println!();
let mut any_updates = false;
for (pos, m) in ws.members.iter().enumerate() {
println!("[{}/{}] {}", pos + 1, n, m.declared);
let report = update::run_update_with_desc(&m.path, &m.descriptor, opts)
.with_context(|| format!("update failed for workspace member \"{}\"", m.declared))?;
if report.has_updates() {
any_updates = true;
}
println!();
}
Ok(any_updates)
}
pub fn update_one(
workspace_root: &Path,
member_index: usize,
opts: &UpdateOptions,
) -> Result<bool> {
let ws = load(workspace_root)?;
let m = &ws.members[member_index];
let report = update::run_update_with_desc(&m.path, &m.descriptor, opts)?;
Ok(report.has_updates())
}
pub fn update_subtree(
workspace_root: &Path,
member_indices: &[usize],
opts: &UpdateOptions,
) -> Result<bool> {
let ws = load(workspace_root)?;
let n = member_indices.len();
println!(
"Workspace {} update ({} member{})",
ws.root.display(),
n,
if n == 1 { "" } else { "s" },
);
println!();
let mut any_updates = false;
for (pos, &idx) in member_indices.iter().enumerate() {
let m = &ws.members[idx];
println!("[{}/{}] {}", pos + 1, n, m.declared);
let report = update::run_update_with_desc(&m.path, &m.descriptor, opts)
.with_context(|| format!("update failed for workspace member \"{}\"", m.declared))?;
if report.has_updates() {
any_updates = true;
}
println!();
}
Ok(any_updates)
}
fn override_output(opts: &AuditOptions, _member_path: &Path) -> AuditOptions {
opts.clone()
}
pub fn fmt_all(workspace_root: &Path, check_only: bool, offline: bool, jobs: usize) -> Result<()> {
let ws = load(workspace_root)?;
let subset: Vec<usize> = (0..ws.members.len()).collect();
fmt_members(&ws, &subset, check_only, offline, jobs)
}
pub fn fmt_subtree(
workspace_root: &Path,
member_indices: &[usize],
check_only: bool,
offline: bool,
jobs: usize,
) -> Result<()> {
let ws = load(workspace_root)?;
fmt_members(&ws, member_indices, check_only, offline, jobs)
}
fn fmt_members(
ws: &Workspace,
subset: &[usize],
check_only: bool,
offline: bool,
jobs: usize,
) -> Result<()> {
let pjf_jars = fmt::resolve_pjf(offline)?;
let kt_in_workspace = subset
.iter()
.any(|&i| fmt::has_kotlin_sources(&ws.members[i].path));
let ktfmt_jars = if kt_in_workspace {
fmt::resolve_ktfmt(offline)?
} else {
Vec::new()
};
if subset.len() > 1 {
return crate::parallel::run_jobs(ws, subset, "fmt", jobs, false, crate::parallel::TuiMode::Full, "Formatted", |m, _| {
fmt::run_fmt_with_jars(&m.path, check_only, &pjf_jars, &ktfmt_jars)
.map(|_| Vec::<PathBuf>::new())
});
}
for &i in subset {
let m = &ws.members[i];
fmt::run_fmt_with_jars(&m.path, check_only, &pjf_jars, &ktfmt_jars)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_workspace(members: &[&str]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let members_toml = members
.iter()
.map(|m| format!("\"{}\"", m))
.collect::<Vec<_>>()
.join(", ");
std::fs::write(
dir.path().join("Curie.toml"),
format!("[workspace]\nmembers = [{members_toml}]\n"),
)
.unwrap();
for m in members {
let mpath = dir.path().join(m);
std::fs::create_dir_all(&mpath).unwrap();
std::fs::write(
mpath.join("Curie.toml"),
format!("[application]\nname = \"{m}\"\nversion = \"0.1.0\"\nmainClass = \"X\"\n"),
)
.unwrap();
}
dir
}
#[test]
fn fmt_all_no_java_files_succeeds() {
let dir = make_workspace(&["alpha", "beta", "gamma"]);
fmt_all(dir.path(), false, false, 4).expect("fmt_all should succeed on empty members");
}
#[test]
fn fmt_all_reports_all_member_errors() {
let dir = make_workspace(&["m1", "m2"]);
let result = fmt_all(dir.path(), true, false, 4);
assert!(result.is_ok(), "unexpected error: {:?}", result);
}
}