use crate::compile::pkg_prefix_for_src_root;
use crate::jar::classpath_string;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
fn fqcn_from_source(src_roots: &[PathBuf], source: &Path, stem_suffix: &str) -> Option<String> {
for src_root in src_roots {
if let Ok(rel) = source.strip_prefix(src_root) {
let without_ext = rel.with_extension("");
let mut parts: Vec<String> = without_ext
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
if parts.is_empty() {
continue;
}
if !stem_suffix.is_empty() {
let last = parts.len() - 1;
parts[last].push_str(stem_suffix);
}
let rel_fqcn = parts.join(".");
let pkg_prefix = pkg_prefix_for_src_root(src_root);
let fqcn = if pkg_prefix.is_empty() {
rel_fqcn
} else {
format!("{}.{}", pkg_prefix, rel_fqcn)
};
return Some(fqcn);
}
}
None
}
fn fqcn_from_java_source(src_roots: &[PathBuf], source: &Path) -> Option<String> {
fqcn_from_source(src_roots, source, "")
}
fn fqcn_from_kotlin_source(src_roots: &[PathBuf], source: &Path) -> Option<String> {
fqcn_from_source(src_roots, source, "Kt")
}
fn is_compact_source(text: &str) -> bool {
let no_line: String = text
.lines()
.map(|l| if let Some(i) = l.find("//") { &l[..i] } else { l })
.collect::<Vec<_>>()
.join("\n");
let mut stripped = String::with_capacity(no_line.len());
let mut chars = no_line.chars().peekable();
let mut in_block = false;
while let Some(ch) = chars.next() {
if in_block {
if ch == '*' && chars.peek() == Some(&'/') { chars.next(); in_block = false; }
} else if ch == '/' && chars.peek() == Some(&'*') {
chars.next(); in_block = true;
} else {
stripped.push(ch);
}
}
for kw in ["class", "interface", "enum", "record"] {
let mut s = stripped.as_str();
while let Some(idx) = s.find(kw) {
let before = if idx == 0 { ' ' } else { s.as_bytes()[idx - 1] as char };
let after_idx = idx + kw.len();
let after = if after_idx >= s.len() { ' ' } else { s.as_bytes()[after_idx] as char };
if !before.is_alphanumeric() && before != '_' && !after.is_alphanumeric() && after != '_' {
return false;
}
s = &s[idx + kw.len()..];
}
}
true
}
fn java_source_has_main(text: &str) -> bool {
if is_compact_source(text) && text.contains("void main") {
return true;
}
let flat = text.replace(['\n', '\r'], " ");
if flat.contains("static") && flat.contains("void main") {
return true;
}
if flat.contains("void main") {
return true;
}
false
}
fn kotlin_source_has_main(text: &str) -> bool {
let mut depth: i32 = 0;
for raw_line in text.lines() {
let line = if let Some(i) = raw_line.find("//") {
&raw_line[..i]
} else {
raw_line
};
let trimmed = line.trim_start();
if depth == 0
&& (trimmed.starts_with("fun main(") || trimmed.starts_with("suspend fun main("))
{
return true;
}
for ch in line.chars() {
match ch {
'{' => depth += 1,
'}' => { depth -= 1; if depth < 0 { depth = 0; } }
_ => {}
}
}
}
false
}
fn javap_output_has_main(javap_out: &str) -> bool {
for line in javap_out.lines() {
let l = line.trim();
if l.contains("static") && l.contains("void main(") {
return true;
}
if l.contains("void main(") && !l.contains("private") {
return true;
}
}
false
}
pub fn validate_main_class(
class_name: &str,
classes_dir: &Path,
dep_jars: &[PathBuf],
) -> Result<()> {
let mut cp_entries = vec![classes_dir.to_path_buf()];
cp_entries.extend_from_slice(dep_jars);
let cp = classpath_string(&cp_entries);
let output = Command::new("javap")
.arg("-p")
.arg("-classpath")
.arg(&cp)
.arg(class_name)
.output()
.context("failed to invoke javap — is a JDK installed?")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"mainClass `{}` was not found in compiled output\n {}",
class_name,
stderr.trim()
);
}
let javap_out = String::from_utf8_lossy(&output.stdout);
if !javap_output_has_main(&javap_out) {
anyhow::bail!(
"mainClass `{}` does not declare a launchable main method\n\
\n\
Expected one of:\n\
public static void main(String[] args)\n\
static void main()\n\
void main(String[] args) (instance, non-private)\n\
void main() (instance, non-private)",
class_name
);
}
Ok(())
}
pub fn detect_main_class(
src_roots: &[PathBuf],
sources: &[PathBuf],
classes_dir: &Path,
dep_jars: &[PathBuf],
) -> Result<String> {
let mut source_candidates: Vec<(String, PathBuf)> = Vec::new();
for source in sources {
let text = match std::fs::read_to_string(source) {
Ok(t) => t,
Err(_) => continue,
};
let ext = source.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"java" => {
if java_source_has_main(&text) {
if let Some(fqcn) = fqcn_from_java_source(src_roots, source) {
source_candidates.push((fqcn, source.clone()));
}
}
}
"kt" => {
if kotlin_source_has_main(&text) {
if let Some(fqcn) = fqcn_from_kotlin_source(src_roots, source) {
source_candidates.push((fqcn, source.clone()));
}
}
}
_ => {}
}
}
if source_candidates.is_empty() {
anyhow::bail!(
"no main method found in any production source file\n\
\n\
Add a main method to one of your classes, or declare it explicitly:\n\
\n\
# Curie.toml\n\
[application]\n\
mainClass = \"com.example.YourMainClass\""
);
}
let mut valid: Vec<String> = Vec::new();
for (fqcn, _) in &source_candidates {
if validate_main_class(fqcn, classes_dir, dep_jars).is_ok() {
valid.push(fqcn.clone());
}
}
match valid.len() {
0 => anyhow::bail!(
"no launchable main method found after bytecode inspection\n\
\n\
Source candidates that did not pass bytecode validation:\n\
{}\n\
\n\
Declare the main class explicitly in Curie.toml:\n\
\n\
[application]\n\
mainClass = \"com.example.YourMainClass\"",
source_candidates
.iter()
.map(|(n, _)| format!(" {}", n))
.collect::<Vec<_>>()
.join("\n")
),
1 => Ok(valid.remove(0)),
_ => anyhow::bail!(
"multiple classes with a main method found — declare one explicitly in Curie.toml:\n\
\n\
{}\n\
\n\
# Curie.toml\n\
[application]\n\
mainClass = \"com.example.YourChosenMainClass\"",
valid
.iter()
.map(|n| format!(" {}", n))
.collect::<Vec<_>>()
.join("\n")
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn kotlin_main_no_args() {
assert!(kotlin_source_has_main("fun main() {\n println(\"hi\")\n}\n"));
}
#[test]
fn kotlin_main_with_args() {
assert!(kotlin_source_has_main(
"fun main(args: Array<String>) {\n println(args[0])\n}\n"
));
}
#[test]
fn kotlin_suspend_main() {
assert!(kotlin_source_has_main("suspend fun main() {\n}\n"));
}
#[test]
fn kotlin_main_inside_class_not_matched() {
let src = "class Foo {\n fun main() {\n }\n}\n";
assert!(!kotlin_source_has_main(src));
}
#[test]
fn kotlin_no_main() {
assert!(!kotlin_source_has_main("fun helper() {}\n"));
}
#[test]
fn kotlin_main_in_comment_not_matched() {
let src = "// fun main() — example\nfun helper() {}\n";
assert!(!kotlin_source_has_main(src));
}
#[test]
fn kotlin_fqcn_maven_layout() {
let root = PathBuf::from("/proj/src/main/kotlin");
let src = PathBuf::from("/proj/src/main/kotlin/com/example/Hello.kt");
let fqcn = fqcn_from_kotlin_source(&[root], &src);
assert_eq!(fqcn, Some("com.example.HelloKt".to_string()));
}
#[test]
fn kotlin_fqcn_default_package() {
let root = PathBuf::from("/proj/src/main/kotlin");
let src = PathBuf::from("/proj/src/main/kotlin/App.kt");
let fqcn = fqcn_from_kotlin_source(&[root], &src);
assert_eq!(fqcn, Some("AppKt".to_string()));
}
#[test]
fn kotlin_fqcn_multiple_roots_picks_correct() {
let roots = vec![
PathBuf::from("/proj/src/main/java"),
PathBuf::from("/proj/src/main/kotlin"),
];
let src = PathBuf::from("/proj/src/main/kotlin/com/example/Main.kt");
let fqcn = fqcn_from_kotlin_source(&roots, &src);
assert_eq!(fqcn, Some("com.example.MainKt".to_string()));
}
#[test]
fn java_main_static() {
assert!(java_source_has_main(
"public class App { public static void main(String[] args) {} }"
));
}
#[test]
fn java_no_main() {
assert!(!java_source_has_main("public class Lib { public void run() {} }"));
}
#[test]
fn javap_static_main_detected() {
let out = "public static void main(java.lang.String[]);";
assert!(javap_output_has_main(out));
}
#[test]
fn javap_kotlin_compiled_main_detected() {
let out = "public static final void main(java.lang.String[]);";
assert!(javap_output_has_main(out));
}
#[test]
fn javap_no_main() {
let out = "public void helper();\npublic java.lang.String getName();";
assert!(!javap_output_has_main(out));
}
}