use crate::build::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, 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 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 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 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());
}
src_roots.extend(flat_src_dirs);
if src_roots.is_empty() {
bail!(
"no source directory found: expected src/main/java/, src/main/kotlin/, \
or at least one dot-named directory under src/ \
(e.g. src/com.example.myapp/)"
);
}
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<(&str, &str)> = desc
.dependencies
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let jars = resolve(
&pairs,
&ResolveOptions {
extra_repos: extra_repos(desc),
progress: true,
bom_imports: bom_gavs.clone(),
offline,
},
)
.context("dependency resolution failed")?;
println!(" Resolve deps {} 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 jars = resolve(
&ap_pairs,
&ResolveOptions {
extra_repos: extra_repos(desc),
progress: true,
bom_imports: bom_gavs.clone(),
offline,
},
)
.context("annotation-processor resolution failed")?;
println!(" Resolve APs {} 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(
&[(coord, version)],
&ResolveOptions {
extra_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();
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);
}
java_sources.sort();
java_sources.dedup();
kotlin_sources.sort();
kotlin_sources.dedup();
let mut sources: Vec<PathBuf> = Vec::new();
sources.extend(java_sources.iter().cloned());
sources.extend(kotlin_sources.iter().cloned());
sources.sort();
sources.dedup();
let has_kotlin = !kotlin_sources.is_empty();
let has_java = !java_sources.is_empty();
if sources.is_empty() {
bail!(
"no Java or Kotlin 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(
&[
(KOTLIN_COMPILER_COORD, kver),
(KOTLIN_STDLIB_COORD, kver),
],
&ResolveOptions {
extra_repos: extra_repos(desc),
progress: true,
bom_imports: bom_gavs.clone(),
offline,
},
)
.context("Kotlin compiler/stdlib resolution failed")?;
println!(" Resolve Kotlin {} 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 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() {
println!(
" Compile {} 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 = kotlinc
.status()
.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 {
println!(
" Stale (Kotlin) removed {} orphan class file{}",
kotlin_orphans,
if kotlin_orphans == 1 { "" } else { "s" },
);
}
} else if !wiped_kotlin_classes.is_empty() {
println!(
" Stale (Kotlin) removed {} orphan class file{}",
wiped_kotlin_classes.len(),
if wiped_kotlin_classes.len() == 1 { "" } else { "s" },
);
}
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())
.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 = javac
.status()
.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 {
println!(
" Stale removed {} orphaned class file{}",
n,
if n == 1 { "" } else { "s" },
);
}
}
}
} else if has_kotlin {
}
if let Ok(version) = javac_version() {
write_javac_version_stamp(&output_dir, &version)?;
}
kt_stale::write_kt_sources(&output_dir, &kt_set)?;
} else {
println!(" Compile up to date");
}
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,
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");
}
}