use crate::build::{central_repos, extra_repos};
use crate::descriptor;
use crate::incremental::{
javac_version, needs_recompile, walk_files, write_javac_version_stamp, CompileStatus,
};
use crate::jar::classpath_string;
use crate::kt_stale;
use anyhow::{bail, Context, Result};
use curie_deps::resolver::{resolve, DepEntry, ResolveOptions};
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(test)]
pub use crate::descriptor::DEFAULT_KOTLIN_VERSION as KOTLIN_VERSION;
pub const KOTLIN_COMPILER_COORD: &str = "org.jetbrains.kotlin:kotlin-compiler-embeddable";
pub const KOTLIN_STDLIB_COORD: &str = "org.jetbrains.kotlin:kotlin-stdlib";
pub const GROOVY_COORD: &str = "org.apache.groovy:groovy";
pub struct CompileOutput {
pub jar_path: PathBuf,
pub jar_name: String,
pub classes_dir: PathBuf,
pub src_roots: Vec<PathBuf>,
pub sources: Vec<PathBuf>,
pub dep_jars: Vec<PathBuf>,
pub kotlin_stdlib_jars: Vec<PathBuf>,
pub groovy_jars: Vec<PathBuf>,
pub resources_dir: Option<PathBuf>,
pub test_resources_dir: Option<PathBuf>,
}
pub fn flat_package_src_dirs(project_root: &Path) -> Vec<PathBuf> {
flat_package_dirs_under(&project_root.join("src"))
}
pub fn flat_package_test_dirs(project_root: &Path) -> Vec<PathBuf> {
flat_package_dirs_under(&project_root.join("tests"))
}
fn flat_package_dirs_under(parent: &Path) -> Vec<PathBuf> {
if !parent.exists() {
return vec![];
}
let mut dirs: Vec<PathBuf> = std::fs::read_dir(parent)
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let s = name.to_string_lossy();
s.contains('.') && e.path().is_dir()
})
.map(|e| e.path())
.collect();
dirs.sort();
dirs
}
pub fn pkg_prefix_for_src_root(src_root: &Path) -> String {
src_root
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.filter(|n| n.contains('.'))
.unwrap_or_default()
}
pub fn compile(
project_root: &Path,
desc: &descriptor::Descriptor,
offline: bool,
extra_cp: &[PathBuf],
) -> Result<CompileOutput> {
let maven_java_src = project_root.join("src").join("main").join("java");
let maven_kotlin_src = project_root.join("src").join("main").join("kotlin");
let maven_groovy_src = project_root.join("src").join("main").join("groovy");
let flat_src_dirs = flat_package_src_dirs(project_root);
let mut src_roots: Vec<PathBuf> = Vec::new();
if maven_java_src.exists() { src_roots.push(maven_java_src.clone()); }
if maven_kotlin_src.exists() { src_roots.push(maven_kotlin_src.clone()); }
if maven_groovy_src.exists() { src_roots.push(maven_groovy_src.clone()); }
src_roots.extend(flat_src_dirs);
let bare_src = project_root.join("src");
if bare_src.exists() && !src_roots.contains(&bare_src) {
let has_direct_sources = std::fs::read_dir(&bare_src)
.ok()
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| {
e.file_type().map(|t| t.is_file()).unwrap_or(false)
&& matches!(
e.path().extension().and_then(|s| s.to_str()),
Some("java") | Some("kt") | Some("groovy")
)
})
})
.unwrap_or(false);
if has_direct_sources {
src_roots.push(bare_src);
}
}
if src_roots.is_empty() {
bail!(
"no source directory found: expected src/main/java/, src/main/kotlin/, \
src/main/groovy/, src/<dot-named>/ (flat-package), or source files \
directly in src/ (unnamed classes)"
);
}
let classes_dir = project_root.join("target").join("classes");
let output_dir = project_root.join("target");
std::fs::create_dir_all(&classes_dir)
.context("failed to create target/classes")?;
let bom_gavs = desc.prod_bom_gavs()?;
let dep_jars = if desc.dependencies.is_empty() {
vec![]
} else {
let pairs: Vec<DepEntry> = desc
.dependencies
.iter()
.map(|(k, v)| DepEntry { key: k, version: v.version(), repo_id: v.repository() })
.collect();
let jars = resolve(
&pairs,
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos(desc),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: bom_gavs.clone(),
offline,
},
)
.context("dependency resolution failed")?;
crate::parallel::emit(&crate::style::resolve("Resolve deps", &format!("{} JAR(s)", jars.len())));
jars
};
let ap_pairs = desc.ap_pairs();
let (ap_jars, ap_on_compile_classpath_jars) = if ap_pairs.is_empty() {
(Vec::new(), Vec::new())
} else {
let ap_entries: Vec<DepEntry> = ap_pairs
.iter()
.map(|(k, v)| DepEntry { key: k, version: v, repo_id: None })
.collect();
let jars = resolve(
&ap_entries,
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos(desc),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: bom_gavs.clone(),
offline,
},
)
.context("annotation-processor resolution failed")?;
crate::parallel::emit(&crate::style::resolve("Resolve APs", &format!("{} JAR(s)", jars.len())));
let on_cp_coords = desc.ap_on_compile_classpath_coords();
let mut on_cp_jars: Vec<PathBuf> = Vec::new();
for coord in on_cp_coords {
let version = ap_pairs
.iter()
.find(|(k, _)| *k == coord)
.map(|(_, v)| *v)
.expect("on-cp coord must be in ap_pairs");
let single = resolve(
&[DepEntry { key: coord, version, repo_id: None }],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos(desc),
progress: false,
bom_imports: bom_gavs.clone(),
offline,
},
)
.with_context(|| format!("annotation-processor classpath resolution failed for {}", coord))?;
on_cp_jars.extend(single);
}
(jars, on_cp_jars)
};
let mut java_sources: Vec<PathBuf> = Vec::new();
let mut kotlin_sources: Vec<PathBuf> = Vec::new();
let mut groovy_sources: Vec<PathBuf> = Vec::new();
for src_root in &src_roots {
let root_java: Vec<_> = walk_files(src_root)
.filter(|e| {
let name = e.file_name().to_string_lossy();
name.ends_with(".java")
&& !name.ends_with("Test.java")
&& !name.ends_with("Tests.java")
&& !name.ends_with("Spec.java")
})
.map(|e| e.into_path())
.collect();
java_sources.extend(root_java);
let root_kotlin: Vec<_> = walk_files(src_root)
.filter(|e| {
let name = e.file_name().to_string_lossy();
name.ends_with(".kt")
&& !name.ends_with("Test.kt")
&& !name.ends_with("Tests.kt")
&& !name.ends_with("Spec.kt")
})
.map(|e| e.into_path())
.collect();
kotlin_sources.extend(root_kotlin);
let root_groovy: Vec<_> = walk_files(src_root)
.filter(|e| {
let name = e.file_name().to_string_lossy();
name.ends_with(".groovy")
&& !name.ends_with("Test.groovy")
&& !name.ends_with("Tests.groovy")
&& !name.ends_with("Spec.groovy")
})
.map(|e| e.into_path())
.collect();
groovy_sources.extend(root_groovy);
}
java_sources.sort(); java_sources.dedup();
kotlin_sources.sort(); kotlin_sources.dedup();
groovy_sources.sort(); groovy_sources.dedup();
let has_kotlin = !kotlin_sources.is_empty();
let has_java = !java_sources.is_empty();
let has_groovy = !groovy_sources.is_empty();
if has_groovy && has_kotlin {
bail!(
"mixing Groovy and Kotlin sources in the same module is not supported; \
use separate modules for each language"
);
}
let mut sources: Vec<PathBuf> = Vec::new();
sources.extend(java_sources.iter().cloned());
sources.extend(kotlin_sources.iter().cloned());
sources.extend(groovy_sources.iter().cloned());
sources.sort();
sources.dedup();
if sources.is_empty() {
bail!(
"no Java, Kotlin, or Groovy source files found under {}",
src_roots.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", ")
);
}
let resources_dir = {
let maven = project_root.join("src").join("main").join("resources");
let flat = project_root.join("resources");
if maven.exists() { Some(maven) } else if flat.exists() { Some(flat) } else { None }
};
let test_resources_dir = {
let maven = project_root.join("src").join("test").join("resources");
let flat = project_root.join("test-resources");
if maven.exists() { Some(maven) } else if flat.exists() { Some(flat) } else { None }
};
let kotlin_stdlib_jars: Vec<PathBuf>;
let kotlin_compiler_jars: Vec<PathBuf>;
if has_kotlin {
let kver = desc.kotlin.version();
let kotlin_jars = resolve(
&[
DepEntry { key: KOTLIN_COMPILER_COORD, version: kver, repo_id: None },
DepEntry { key: KOTLIN_STDLIB_COORD, version: kver, repo_id: None },
],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos(desc),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: bom_gavs.clone(),
offline,
},
)
.context("Kotlin compiler/stdlib resolution failed")?;
crate::parallel::emit(&crate::style::resolve("Resolve Kotlin", &format!("{} JAR(s)", kotlin_jars.len())));
let stdlib: Vec<PathBuf> = kotlin_jars
.iter()
.filter(|p| {
p.file_name()
.map(|f| !f.to_string_lossy().starts_with("kotlin-compiler-embeddable"))
.unwrap_or(true)
})
.cloned()
.collect();
kotlin_compiler_jars = kotlin_jars;
kotlin_stdlib_jars = stdlib;
} else {
kotlin_compiler_jars = Vec::new();
kotlin_stdlib_jars = Vec::new();
}
let groovy_jars: Vec<PathBuf>;
if has_groovy {
let gver = desc.groovy.version();
let jars = resolve(
&[DepEntry { key: GROOVY_COORD, version: gver, repo_id: None }],
&ResolveOptions {
default_repos: central_repos(),
named_repos: extra_repos(desc),
progress: crate::parallel::try_get_sink().is_none(),
bom_imports: bom_gavs.clone(),
offline,
},
)
.context("Groovy compiler/runtime resolution failed")?;
crate::parallel::emit(&crate::style::resolve("Resolve Groovy", &format!("{} JAR(s)", jars.len())));
groovy_jars = jars;
} else {
groovy_jars = Vec::new();
}
let toml_path = project_root.join("Curie.toml");
let manifest_path = output_dir.join(".classes.toml");
let old_manifest = crate::class_manifest::load(&manifest_path)?;
let current_sources_set: std::collections::HashSet<String> = sources
.iter()
.filter_map(|p| p.canonicalize().ok())
.map(|p| p.to_string_lossy().into_owned())
.collect();
let canonical_target = output_dir
.canonicalize()
.ok()
.and_then(|p| p.to_str().map(String::from));
let pre_pruned: usize = match &old_manifest {
Some(old) => {
let stale = crate::class_manifest::stale_classes(
old,
None,
¤t_sources_set,
canonical_target.as_deref(),
);
crate::class_manifest::delete_classes(&classes_dir, &stale)?
}
None => 0,
};
let kt_set = kt_stale::canonical_kt_set(&kotlin_sources);
let kt_prev = kt_stale::load_kt_sources(&output_dir);
let kt_set_changed = kt_prev.as_ref().map(|p| p != &kt_set).unwrap_or(false);
let compile_status = if pre_pruned > 0 || kt_set_changed {
CompileStatus::StaleClasses
} else {
needs_recompile(&sources, &classes_dir, &toml_path, &output_dir)
};
if compile_status.needs_recompile() {
crate::parallel::emit(&crate::style::active(
"Compile",
&format!("{} source file(s) [{}]", sources.len(), compile_status.reason()),
));
let wiped_kotlin_classes: Vec<PathBuf> = if has_kotlin || kt_set_changed {
kt_stale::wipe_kotlin_derived_classes(&classes_dir)?
} else {
Vec::new()
};
let mut shared_cp: Vec<PathBuf> = Vec::new();
if let Some(ref rd) = resources_dir {
shared_cp.push(rd.clone());
}
shared_cp.extend_from_slice(&dep_jars);
shared_cp.extend_from_slice(extra_cp);
shared_cp.extend_from_slice(&ap_on_compile_classpath_jars);
shared_cp.extend_from_slice(&kotlin_stdlib_jars);
if has_kotlin {
let mut kotlinc = Command::new("java");
kotlinc.arg("--enable-native-access=ALL-UNNAMED");
kotlinc.arg("-cp").arg(classpath_string(&kotlin_compiler_jars));
kotlinc.arg("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler");
kotlinc.arg("-no-stdlib");
kotlinc.arg("-no-reflect");
kotlinc.arg("-d").arg(&classes_dir);
if !shared_cp.is_empty() {
kotlinc.arg("-cp").arg(classpath_string(&shared_cp));
}
for src in &kotlin_sources {
kotlinc.arg(src);
}
for src in &java_sources {
kotlinc.arg(src);
}
let status = crate::proc::spawn_cmd(&mut kotlinc)
.context("failed to invoke kotlinc — is a JRE installed?")?;
if !status.success() {
bail!("Kotlin compilation failed");
}
let kotlin_orphans = wiped_kotlin_classes.iter().filter(|p| !p.exists()).count();
if kotlin_orphans > 0 {
crate::parallel::emit(&crate::style::stale(
"Stale (Kotlin)",
&format!("removed {} orphan class file{}", kotlin_orphans, if kotlin_orphans == 1 { "" } else { "s" }),
));
}
} else if !wiped_kotlin_classes.is_empty() {
crate::parallel::emit(&crate::style::stale(
"Stale (Kotlin)",
&format!("removed {} orphan class file{}", wiped_kotlin_classes.len(), if wiped_kotlin_classes.len() == 1 { "" } else { "s" }),
));
}
if has_groovy {
let mut groovyc = Command::new("java");
groovyc.arg("-cp").arg(classpath_string(&groovy_jars));
groovyc.arg("org.codehaus.groovy.tools.FileSystemCompiler");
groovyc.arg("-d").arg(&classes_dir);
let mut gcp = shared_cp.clone();
gcp.extend_from_slice(&groovy_jars);
if !gcp.is_empty() {
groovyc.arg("--classpath").arg(classpath_string(&gcp));
}
if has_java {
groovyc.arg("--jointCompilation");
for src in &java_sources {
groovyc.arg(src);
}
}
for src in &groovy_sources {
groovyc.arg(src);
}
let status = crate::proc::spawn_cmd(&mut groovyc)
.context("failed to invoke groovyc — is a JRE installed?")?;
if !status.success() {
bail!("Groovy compilation failed");
}
} else if has_java {
let wrapper_jar = crate::wrapper::ensure()?;
let mut javac = Command::new("java");
javac.arg("-jar").arg(&wrapper_jar);
javac.arg("--curie-manifest-out").arg(&manifest_path);
javac
.arg("--release")
.arg(desc.java.effective());
if desc.java.preview_enabled() {
javac.arg("--enable-preview");
}
javac
.arg("-g")
.arg("-d")
.arg(&classes_dir);
let mut cp_entries: Vec<PathBuf> = Vec::new();
if has_kotlin {
cp_entries.push(classes_dir.clone());
}
cp_entries.extend_from_slice(&shared_cp);
if !cp_entries.is_empty() {
javac.arg("-cp").arg(classpath_string(&cp_entries));
}
if !ap_jars.is_empty() {
let gen_dir = output_dir.join("generated-sources").join("annotations");
std::fs::create_dir_all(&gen_dir).with_context(|| {
format!("failed to create {}", gen_dir.display())
})?;
javac.arg("-processorpath").arg(classpath_string(&ap_jars));
javac.arg("-s").arg(&gen_dir);
}
for (key, value) in desc.flat_ap_options() {
javac.arg(format!("-A{}={}", key, value));
}
for src in &java_sources {
javac.arg(src);
}
let status = crate::proc::spawn_cmd(&mut javac)
.context("failed to invoke java — is a JRE installed?")?;
if !status.success() {
bail!("compilation failed");
}
if let Some(old) = &old_manifest {
if let Some(new) = crate::class_manifest::load(&manifest_path)? {
let stale = crate::class_manifest::stale_classes(
old,
Some(&new),
¤t_sources_set,
None, );
let n = crate::class_manifest::delete_classes(&classes_dir, &stale)?;
if n > 0 {
crate::parallel::emit(&crate::style::stale(
"Stale",
&format!("removed {} orphaned class file{}", n, if n == 1 { "" } else { "s" }),
));
}
}
}
} else if has_kotlin || has_groovy {
}
if let Ok(version) = javac_version() {
write_javac_version_stamp(&output_dir, &version)?;
}
kt_stale::write_kt_sources(&output_dir, &kt_set)?;
} else {
crate::parallel::emit(&crate::style::up_to_date("Compile"));
}
let jar_name = format!(
"{}-{}.jar",
desc.buildable_name().replace(':', "-"), desc.buildable_version()
);
let jar_path = output_dir.join(&jar_name);
Ok(CompileOutput {
jar_path, jar_name, classes_dir, src_roots, sources, dep_jars,
kotlin_stdlib_jars, groovy_jars,
resources_dir, test_resources_dir,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pkg_prefix_maven_style_is_empty() {
let p = Path::new("/some/path/src/main/java");
assert_eq!(pkg_prefix_for_src_root(p), "");
}
#[test]
fn pkg_prefix_flat_package_is_dir_name() {
let p = Path::new("/some/path/src/com.example.myapp");
assert_eq!(pkg_prefix_for_src_root(p), "com.example.myapp");
}
#[test]
fn pkg_prefix_kotlin_maven_style_is_empty() {
let p = Path::new("/some/path/src/main/kotlin");
assert_eq!(pkg_prefix_for_src_root(p), "");
}
#[test]
fn flat_package_src_dirs_finds_dot_named_dirs() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(src.join("com.example.foo")).unwrap();
std::fs::create_dir_all(src.join("com.example.bar")).unwrap();
std::fs::create_dir_all(src.join("main")).unwrap();
let mut found = flat_package_src_dirs(dir.path());
found.sort();
assert_eq!(found.len(), 2);
assert!(found[0].ends_with("com.example.bar"));
assert!(found[1].ends_with("com.example.foo"));
}
#[test]
fn flat_package_src_dirs_empty_when_no_src_dir() {
let dir = tempfile::tempdir().unwrap();
assert!(flat_package_src_dirs(dir.path()).is_empty());
}
#[test]
fn kotlin_version_constant_is_set() {
assert!(!KOTLIN_VERSION.is_empty());
let parts: Vec<&str> = KOTLIN_VERSION.split('.').collect();
assert!(parts.len() >= 2, "KOTLIN_VERSION should be at least major.minor");
}
#[test]
fn groovy_sources_discovered_from_maven_layout() {
let dir = tempfile::tempdir().unwrap();
let groovy_src = dir.path().join("src").join("main").join("groovy")
.join("com").join("example");
std::fs::create_dir_all(&groovy_src).unwrap();
std::fs::write(groovy_src.join("Greeter.groovy"), b"package com.example; class Greeter {}").unwrap();
use crate::incremental::walk_files;
let root = dir.path().join("src").join("main").join("groovy");
let found: Vec<_> = walk_files(&root)
.filter(|e| {
let name = e.file_name().to_string_lossy();
name.ends_with(".groovy")
&& !name.ends_with("Test.groovy")
&& !name.ends_with("Tests.groovy")
&& !name.ends_with("Spec.groovy")
})
.collect();
assert_eq!(found.len(), 1, "should find exactly Greeter.groovy; got: {:?}", found);
assert!(found[0].file_name().to_string_lossy().ends_with("Greeter.groovy"));
}
#[test]
fn groovy_sources_discovered_from_flat_package() {
let dir = tempfile::tempdir().unwrap();
let flat_dir = dir.path().join("src").join("com.example");
std::fs::create_dir_all(&flat_dir).unwrap();
std::fs::write(flat_dir.join("Hello.groovy"), b"package com.example; class Hello {}").unwrap();
std::fs::write(flat_dir.join("Hello.java"), b"package com.example; class Hello {}").unwrap();
let flat_dirs = flat_package_src_dirs(dir.path());
assert!(!flat_dirs.is_empty(), "should find com.example dir");
use crate::incremental::walk_files;
let groovy_files: Vec<_> = flat_dirs.iter()
.flat_map(|d| walk_files(d)
.filter(|e| e.file_name().to_string_lossy().ends_with(".groovy"))
.collect::<Vec<_>>()
)
.collect();
assert_eq!(groovy_files.len(), 1, "should find Hello.groovy; got: {:?}", groovy_files);
}
}