use crate::compile::compile;
use crate::config;
use crate::descriptor;
use crate::docker;
use crate::git;
use crate::incremental::needs_repackage;
use crate::jar::{populate_libs_dir, write_deterministic_jar};
use crate::main_class::{detect_main_class, validate_main_class};
use crate::native;
use crate::test;
use anyhow::{Context, Result};
use curie_deps::repo::Repository;
use std::path::{Path, PathBuf};
#[derive(Copy, Clone)]
pub struct BuildOptions {
pub no_docker: bool,
pub no_native: bool,
pub offline: bool,
}
pub struct BuildOutput {
pub jar: PathBuf,
pub dep_jars: Vec<PathBuf>,
pub main_class: Option<String>,
pub resources_dir: Option<PathBuf>,
}
pub fn build(project_root: &Path, opts: BuildOptions) -> Result<()> {
let desc = descriptor::load(project_root)?;
build_with_desc(project_root, &desc, opts, &[]).map(|_| ())
}
pub fn build_with_desc(
project_root: &Path,
desc: &descriptor::Descriptor,
opts: BuildOptions,
extra_cp: &[PathBuf],
) -> Result<BuildOutput> {
crate::parallel::emit(&crate::style::headline(
"Building", desc.buildable_name(), desc.buildable_version(),
));
if desc.is_bom() {
return build_bom(project_root, desc);
}
if desc.is_library() && project_root.join("Dockerfile").exists() {
anyhow::bail!(
"library projects do not support Docker: remove the Dockerfile from the project root"
);
}
let output = do_build(project_root, desc, opts.offline, extra_cp)?;
crate::parallel::emit(&crate::style::done(
&output.jar
.strip_prefix(project_root)
.unwrap_or(&output.jar)
.display()
.to_string(),
));
if !desc.is_library() && !opts.no_docker && descriptor::docker_enabled(project_root, desc) {
docker::docker_build(project_root, desc, &output.jar, &output.dep_jars)?;
}
if !desc.is_library() && !opts.no_native && descriptor::native_image_enabled(desc) {
native::build_native(project_root, desc, &output.jar, &output.dep_jars)?;
}
Ok(output)
}
pub fn central_repos() -> Vec<Repository> {
let cfg = config::load_config().unwrap_or_default();
config::apply_mirrors(curie_deps::repo::default_repositories(), &cfg.mirrors)
}
pub fn extra_repos(desc: &descriptor::Descriptor) -> Vec<Repository> {
let cfg = config::load_config().unwrap_or_default();
let repos = desc
.repositories
.iter()
.map(|r| Repository {
id: r.id.clone(),
name: r.display_name().to_string(),
url: r.url.clone(),
})
.collect();
config::apply_mirrors(repos, &cfg.mirrors)
}
fn build_bom(project_root: &Path, desc: &descriptor::Descriptor) -> Result<BuildOutput> {
let target = project_root.join("target");
std::fs::create_dir_all(&target)
.with_context(|| format!("failed to create {}", target.display()))?;
let name = desc.buildable_name();
let version = desc.buildable_version();
let pom_path = target.join(format!("{}-{}.pom", name, version));
crate::pom_writer::write_bom_pom(desc, &pom_path)?;
crate::parallel::emit(&crate::style::done(
&pom_path
.strip_prefix(project_root)
.unwrap_or(&pom_path)
.display()
.to_string(),
));
Ok(BuildOutput {
jar: pom_path,
dep_jars: vec![],
main_class: None,
resources_dir: None,
})
}
pub fn do_build(
project_root: &Path,
desc: &descriptor::Descriptor,
offline: bool,
extra_cp: &[PathBuf],
) -> Result<BuildOutput> {
let compiled = compile(project_root, desc, offline, extra_cp)?;
test::run_tests(
project_root,
desc,
&compiled.classes_dir,
&compiled.dep_jars,
&compiled.kotlin_stdlib_jars,
&compiled.groovy_jars,
compiled.resources_dir.as_deref(),
compiled.test_resources_dir.as_deref(),
None,
offline,
extra_cp,
)?;
let resources_dir = compiled.resources_dir.as_deref();
let toml_path = project_root.join("Curie.toml");
let build_info_content: Option<String> = if desc.build_info.enabled {
git::detect(project_root).map(|info| {
format!("git.commit.id={}\n", info.commit_id)
})
} else {
None
};
let resolved_main_class: Option<String> = if needs_repackage(&compiled.jar_path, &compiled.classes_dir, resources_dir, &toml_path) {
let main_class = if let Some(app) = desc.application() {
let mc = match &app.main_class {
Some(declared) => {
validate_main_class(declared, &compiled.classes_dir, &compiled.dep_jars)?;
declared.clone()
}
None => {
let detected = detect_main_class(
&compiled.src_roots,
&compiled.sources,
&compiled.classes_dir,
&compiled.dep_jars,
)?;
crate::parallel::emit(&crate::style::info("Detected", &format!("mainClass = {}", detected)));
detected
}
};
Some(mc)
} else {
None };
crate::parallel::emit(&crate::style::active("Package", &compiled.jar_name));
let mut effective_dep_jars = compiled.dep_jars.clone();
effective_dep_jars.extend_from_slice(&compiled.groovy_jars);
write_deterministic_jar(
&compiled.jar_path,
&compiled.classes_dir,
resources_dir,
main_class.as_deref(),
&effective_dep_jars,
build_info_content.as_deref(),
)
.context("failed to write JAR")?;
main_class
} else {
crate::parallel::emit(&crate::style::up_to_date("Package"));
if let Some(declared) = desc.application().and_then(|a| a.main_class.clone()) {
Some(declared)
} else if desc.application().is_some() {
read_main_class_from_jar(&compiled.jar_path)
} else {
None
}
};
let effective_dep_jars: Vec<std::path::PathBuf> = {
let mut v = compiled.dep_jars;
v.extend(compiled.groovy_jars);
v
};
if !effective_dep_jars.is_empty() && desc.application().is_some() {
let libs_dir = project_root.join("target").join("libs");
populate_libs_dir(&libs_dir, &effective_dep_jars)
.context("failed to populate target/libs/")?;
}
Ok(BuildOutput {
jar: compiled.jar_path,
dep_jars: effective_dep_jars,
main_class: resolved_main_class,
resources_dir: compiled.resources_dir,
})
}
fn read_main_class_from_jar(jar_path: &Path) -> Option<String> {
let file = std::fs::File::open(jar_path).ok()?;
let mut zip = zip::ZipArchive::new(file).ok()?;
let mut entry = zip.by_name("META-INF/MANIFEST.MF").ok()?;
let mut contents = String::new();
std::io::Read::read_to_string(&mut entry, &mut contents).ok()?;
for line in contents.lines() {
if let Some(rest) = line.strip_prefix("Main-Class:") {
let mc = rest.trim().to_string();
if !mc.is_empty() {
return Some(mc);
}
}
}
None
}
pub fn clean(project_root: &Path) -> Result<()> {
let target_dir = project_root.join("target");
match std::fs::remove_dir_all(&target_dir) {
Ok(()) => {
crate::parallel::emit(&crate::style::clean_step("Target dir", "removed"));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
crate::parallel::emit(&crate::style::neutral("Target dir", "nothing to clean"));
}
Err(e) => {
return Err(e)
.with_context(|| format!("failed to remove {}", target_dir.display()));
}
}
Ok(())
}
#[cfg(test)]
mod clean_tests {
use super::*;
fn minimal_app_toml() -> &'static str {
"[application]\nname = \"test\"\nversion = \"0.1.0\"\nmainClass = \"Main\"\n\
[java]\nsourceCompatibility = \"21\"\n"
}
#[test]
fn clean_removes_target_dir() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("Curie.toml"), minimal_app_toml()).unwrap();
let target = root.join("target");
std::fs::create_dir_all(target.join("classes")).unwrap();
std::fs::write(target.join("app.jar"), b"jar").unwrap();
clean(root).unwrap();
assert!(!root.join("target").exists());
}
#[test]
fn clean_no_target_dir_is_ok() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("Curie.toml"), minimal_app_toml()).unwrap();
clean(root).unwrap();
}
}