use anyhow::{bail, Result};
use console::style;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::commands::build;
use crate::config;
pub fn execute(docker: bool, out: Option<String>, platform: Option<String>, install: bool) -> Result<()> {
if platform.is_some() && !docker {
bail!("--platform requires --docker (GraalVM native-image does not support cross-compilation)");
}
let (config_path, cfg) = config::load_or_find_config()?;
let project = config::project_dir(&config_path);
if cfg.main.is_none() {
bail!("Native compilation requires a main class. Add 'main' to package.toml.");
}
build::ensure_jdk_for_config(&cfg)?;
if cfg.workspaces.is_some() {
bail!("Native compilation is not supported for workspace root. Run in a specific module.");
}
let compile_jars = build::resolve_deps_with_scopes(&project, &cfg, &["compile", "provided"])?;
build::compile_project(&project, &cfg, &compile_jars)?;
let src_dir = config::source_dir(&project);
let out_dir = config::output_classes_dir(&project);
let custom_res_ext = cfg.compiler.as_ref().and_then(|c| c.resource_extensions.as_ref());
let res_exclude = cfg.compiler.as_ref().and_then(|c| c.resource_exclude.as_ref());
crate::resources::copy_resources_with_extensions(&src_dir, &out_dir, custom_res_ext.map(|v| v.as_slice()), res_exclude.map(|v| v.as_slice()))?;
let resources_dir = project.join("src").join("main").join("resources");
if resources_dir.exists() {
crate::resources::copy_resources_with_extensions(&resources_dir, &out_dir, custom_res_ext.map(|v| v.as_slice()), res_exclude.map(|v| v.as_slice()))?;
}
let runtime_jars = build::resolve_deps_with_scopes(&project, &cfg, &["compile", "runtime"])?;
build::build_release_jar(&project, &cfg, &runtime_jars, None, None)?;
let version = cfg.version.as_deref().unwrap_or("0.0.0");
let jar_name = format!("{}-{}.jar", cfg.name, version);
let release_dir = project.join("out").join("release");
let jar_path = release_dir.join(&jar_name);
if !jar_path.exists() {
bail!("Release JAR not found: {}", jar_path.display());
}
let output_name = out.as_deref().unwrap_or(&cfg.name);
let output_path = release_dir.join(output_name);
let native_config = cfg.native.as_ref();
let extra_args: Vec<String> = native_config
.and_then(|n| n.args.clone())
.unwrap_or_default();
if docker {
run_docker_native(&project, &jar_path, &output_path, &cfg, &extra_args, platform.as_deref())?;
} else {
run_local_native(&jar_path, &output_path, &extra_args)?;
}
let size = std::fs::metadata(&output_path)
.map(|m| m.len())
.unwrap_or(0);
let size_mb = size as f64 / 1_048_576.0;
println!(
"\n {} native binary: {} ({:.1} MB)",
style("✓").green(),
output_path.display(),
size_mb
);
if install {
install_binary(&output_path, output_name)?;
}
Ok(())
}
fn install_binary(binary_path: &Path, name: &str) -> Result<()> {
let bin_dir = crate::home_dir().join(".ym").join("bin");
std::fs::create_dir_all(&bin_dir)?;
let dest_name = if cfg!(windows) && !name.ends_with(".exe") {
format!("{}.exe", name)
} else {
name.to_string()
};
let dest = bin_dir.join(&dest_name);
std::fs::copy(binary_path, &dest)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&dest)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&dest, perms)?;
}
println!(
" {} installed to {}",
style("✓").green(),
dest.display()
);
if let Ok(path_var) = std::env::var("PATH") {
let bin_str = bin_dir.display().to_string();
if !path_var.split(if cfg!(windows) { ';' } else { ':' }).any(|p| p == bin_str) {
println!(
"\n {} add to PATH: export PATH=\"{}:$PATH\"",
style("hint").yellow(),
bin_dir.display()
);
}
}
Ok(())
}
fn find_native_image() -> Option<PathBuf> {
let cmd_name = if cfg!(windows) { "native-image.cmd" } else { "native-image" };
if let Ok(graalvm) = std::env::var("GRAALVM_HOME") {
let p = PathBuf::from(&graalvm).join("bin").join(cmd_name);
if p.exists() {
return Some(p);
}
}
if let Ok(java_home) = std::env::var("JAVA_HOME") {
let p = PathBuf::from(&java_home).join("bin").join(cmd_name);
if p.exists() {
return Some(p);
}
}
if let Ok(output) = Command::new(if cfg!(windows) { "where" } else { "which" })
.arg("native-image")
.output()
{
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path.lines().next().unwrap_or(&path)));
}
}
}
{
let jdks_dir = crate::home_dir().join(".ym").join("jdks");
if jdks_dir.exists() {
if let Ok(entries) = std::fs::read_dir(&jdks_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("graalvm") {
let p = entry.path().join("bin").join(cmd_name);
if p.exists() {
return Some(p);
}
}
}
}
}
}
None
}
fn run_local_native(jar_path: &Path, output_path: &Path, extra_args: &[String]) -> Result<()> {
let native_image = find_native_image().ok_or_else(|| {
anyhow::anyhow!(
"GraalVM native-image not found.\n\n\
1. Set GRAALVM_HOME environment variable\n\
2. Or use: ymc native --docker\n\
3. Or install GraalVM and ensure native-image is on PATH"
)
})?;
println!(
" {} native-image: {}",
style("➜").green(),
native_image.display()
);
let mut cmd = Command::new(&native_image);
cmd.arg("-jar").arg(jar_path)
.arg("-o").arg(output_path);
for arg in extra_args {
cmd.arg(arg);
}
let status = cmd
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()?;
if !status.success() {
bail!("native-image failed with exit code {}", status.code().unwrap_or(-1));
}
Ok(())
}
fn run_docker_native(
project: &Path,
jar_path: &Path,
output_path: &Path,
cfg: &config::schema::YmConfig,
extra_args: &[String],
platform: Option<&str>,
) -> Result<()> {
let target = cfg.target.as_deref().unwrap_or("21");
let image = cfg.native.as_ref()
.and_then(|n| n.docker_image.clone())
.unwrap_or_else(|| format!("ghcr.io/graalvm/native-image-community:{}", target));
println!(
" {} docker image: {}",
style("➜").green(),
image
);
let jar_rel = jar_path.strip_prefix(project)
.unwrap_or(jar_path);
let output_rel = output_path.strip_prefix(project)
.unwrap_or(output_path);
let mut cmd = Command::new("docker");
cmd.arg("run").arg("--rm");
if let Some(p) = platform {
cmd.arg("--platform").arg(p);
}
cmd.arg("-v").arg(format!("{}:/app", project.display()))
.arg("-w").arg("/app")
.arg(&image)
.arg("-jar").arg(format!("/app/{}", jar_rel.display()))
.arg("-o").arg(format!("/app/{}", output_rel.display()));
for arg in extra_args {
cmd.arg(arg);
}
let status = cmd
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()?;
if !status.success() {
bail!("docker native-image failed with exit code {}", status.code().unwrap_or(-1));
}
Ok(())
}