diff --git a/README.ja.md b/README.ja.md index 9621a207..4c288e89 100644 --- a/README.ja.md +++ b/README.ja.md @@ -184,6 +184,31 @@ smoke_obj_array = "NYASH_LLVM_OBJ_OUT={root}/nyash_llvm_temp.o ./target/release/ --- +## 🧰 一発ビルド(MVP): `nyash --build` + +`nyash.toml` を読み、プラグイン → コア → AOT → リンクまでを一発実行する最小ビルド機能です。 + +基本(Cranelift AOT) +```bash +./target/release/nyash --build nyash.toml \ + --app apps/egui-hello-plugin/main.nyash \ + --out app_egui +``` + +主なオプション(最小) +- `--build `: nyash.toml の場所 +- `--app `: エントリ `.nyash` +- `--out `: 出力EXE名(既定: `app`/`app.exe`) +- `--build-aot cranelift|llvm`(既定: cranelift) +- `--profile release|debug`(既定: release) +- `--target `(必要時のみ) + +注意 +- LLVM AOT には LLVM 18 が必要(`LLVM_SYS_180_PREFIX` を設定)。 +- GUIを含む場合、AOTのオブジェクト出力時にウィンドウが一度開きます(閉じて続行)。 +- WSL で表示されない場合は `docs/guides/cranelift_aot_egui_hello.md` のWSL Tips(Wayland→X11切替)を参照。 + + ## 📊 **パフォーマンスベンチマーク** 実世界ベンチマーク結果 (ny_bench.nyash): diff --git a/README.md b/README.md index ce56141f..6256bb85 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,31 @@ cargo build --release --features wasm-backend --- +## 🧰 One‑Command Build (MVP): `nyash --build` + +Reads `nyash.toml`, builds plugins → core → emits AOT object → links an executable in one shot. + +Basic (Cranelift AOT) +```bash +./target/release/nyash --build nyash.toml \ + --app apps/egui-hello-plugin/main.nyash \ + --out app_egui +``` + +Key options (minimal) +- `--build `: path to nyash.toml +- `--app `: entry `.nyash` +- `--out `: output executable (default: `app`/`app.exe`) +- `--build-aot cranelift|llvm` (default: cranelift) +- `--profile release|debug` (default: release) +- `--target ` (only when needed) + +Notes +- LLVM AOT requires LLVM 18 (`LLVM_SYS_180_PREFIX`). +- Apps that open a GUI may show a window during AOT emission; close it to continue. +- On WSL if the window doesn’t show, see `docs/guides/cranelift_aot_egui_hello.md` (Wayland→X11). + + ## 📊 **Performance Benchmarks** Real-world benchmark results (ny_bench.nyash): diff --git a/src/cli.rs b/src/cli.rs index f100f0fe..8d3fe09e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -57,6 +57,13 @@ pub struct CliConfig { // Phase-15: JSON IR v0 bridge pub ny_parser_pipe: bool, pub json_file: Option, + // Build system (MVP) + pub build_path: Option, + pub build_app: Option, + pub build_out: Option, + pub build_aot: Option, + pub build_profile: Option, + pub build_target: Option, } impl CliConfig { @@ -317,6 +324,43 @@ impl CliConfig { .help("Opt-in: read [ny_plugins] from nyash.toml and load scripts in order") .action(clap::ArgAction::SetTrue) ) + // Build system (MVP) + .arg( + Arg::new("build") + .long("build") + .value_name("PATH") + .help("Build AOT executable using nyash.toml at PATH (MVP)") + ) + .arg( + Arg::new("build-app") + .long("app") + .value_name("FILE") + .help("Entry Nyash script for --build (e.g., apps/hello/main.nyash)") + ) + .arg( + Arg::new("build-out") + .long("out") + .value_name("FILE") + .help("Output executable name for --build (default: app/app.exe)") + ) + .arg( + Arg::new("build-aot") + .long("build-aot") + .value_name("{cranelift|llvm}") + .help("AOT backend for --build (default: cranelift)") + ) + .arg( + Arg::new("build-profile") + .long("profile") + .value_name("{release|debug}") + .help("Cargo profile for --build (default: release)") + ) + .arg( + Arg::new("build-target") + .long("target") + .value_name("TRIPLE") + .help("Target triple for --build (e.g., x86_64-pc-windows-msvc)") + ) } /// Convert ArgMatches to CliConfig @@ -361,6 +405,12 @@ impl CliConfig { parser_ny: matches.get_one::("parser").map(|s| s == "ny").unwrap_or(false), ny_parser_pipe: matches.get_flag("ny-parser-pipe"), json_file: matches.get_one::("json-file").cloned(), + build_path: matches.get_one::("build").cloned(), + build_app: matches.get_one::("build-app").cloned(), + build_out: matches.get_one::("build-out").cloned(), + build_aot: matches.get_one::("build-aot").cloned(), + build_profile: matches.get_one::("build-profile").cloned(), + build_target: matches.get_one::("build-target").cloned(), } } } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 325e5718..37f3ed76 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -75,6 +75,14 @@ impl NyashRunner { /// Run Nyash based on the configuration pub fn run(&self) { + // Build system (MVP): nyash --build + if let Some(cfg_path) = self.config.build_path.clone() { + if let Err(e) = self.run_build_mvp(&cfg_path) { + eprintln!("❌ build error: {}", e); + std::process::exit(1); + } + return; + } // Phase-15: JSON IR v0 bridge (stdin/file) if self.config.ny_parser_pipe || self.config.json_file.is_some() { let json = if let Some(path) = &self.config.json_file { @@ -808,6 +816,145 @@ impl NyashRunner { } } } + + /// Minimal AOT build pipeline driven by nyash.toml (MVP, single-platform, best-effort) + fn run_build_mvp(&self, cfg_path: &str) -> Result<(), String> { + use std::path::{Path, PathBuf}; + let cwd = std::env::current_dir().unwrap_or(PathBuf::from(".")); + let cfg_abspath = if Path::new(cfg_path).is_absolute() { PathBuf::from(cfg_path) } else { cwd.join(cfg_path) }; + // 1) Load nyash.toml + let text = std::fs::read_to_string(&cfg_abspath).map_err(|e| format!("read {}: {}", cfg_abspath.display(), e))?; + let doc = toml::from_str::(&text).map_err(|e| format!("parse {}: {}", cfg_abspath.display(), e))?; + // 2) Apply [env] + if let Some(env_tbl) = doc.get("env").and_then(|v| v.as_table()) { + for (k, v) in env_tbl.iter() { if let Some(s) = v.as_str() { std::env::set_var(k, s); } } + } + // Derive options + let profile = self.config.build_profile.clone().unwrap_or_else(|| "release".into()); + let aot = self.config.build_aot.clone().unwrap_or_else(|| "cranelift".into()); + let out = self.config.build_out.clone(); + let target = self.config.build_target.clone(); + // 3) Build plugins: read [plugins] values as paths and build each + if let Some(pl_tbl) = doc.get("plugins").and_then(|v| v.as_table()) { + for (name, v) in pl_tbl.iter() { + if let Some(path) = v.as_str() { + let p = if Path::new(path).is_absolute() { PathBuf::from(path) } else { cwd.join(path) }; + let mut cmd = std::process::Command::new("cargo"); + cmd.arg("build"); + if profile == "release" { cmd.arg("--release"); } + if let Some(t) = &target { cmd.args(["--target", t]); } + cmd.current_dir(&p); + println!("[build] plugin {} at {}", name, p.display()); + let status = cmd.status().map_err(|e| format!("spawn cargo (plugin {}): {}", name, e))?; + if !status.success() { + return Err(format!("plugin build failed: {} (dir={})", name, p.display())); + } + } + } + } + // 4) Build nyash core (features) + { + let mut cmd = std::process::Command::new("cargo"); + cmd.arg("build"); + if profile == "release" { cmd.arg("--release"); } + match aot.as_str() { "llvm" => { cmd.args(["--features","llvm"]); }, _ => { cmd.args(["--features","cranelift-jit"]); } } + if let Some(t) = &target { cmd.args(["--target", t]); } + println!("[build] nyash core ({}, features={})", profile, if aot=="llvm" {"llvm"} else {"cranelift-jit"}); + let status = cmd.status().map_err(|e| format!("spawn cargo (core): {}", e))?; + if !status.success() { return Err("nyash core build failed".into()); } + } + // 5) Determine app entry + let app = if let Some(a) = self.config.build_app.clone() { a } else { + // try [build].app, else suggest + if let Some(tbl) = doc.get("build").and_then(|v| v.as_table()) { + if let Some(s) = tbl.get("app").and_then(|v| v.as_str()) { s.to_string() } else { String::new() } + } else { String::new() } + }; + let app = if !app.is_empty() { app } else { + // collect candidates under apps/**/main.nyash + let mut cand: Vec = Vec::new(); + fn walk(dir: &Path, acc: &mut Vec) { + if let Ok(rd) = std::fs::read_dir(dir) { + for e in rd.flatten() { + let p = e.path(); + if p.is_dir() { walk(&p, acc); } + else if p.file_name().map(|n| n=="main.nyash").unwrap_or(false) { + acc.push(p.display().to_string()); + } + } + } + } + walk(&cwd.join("apps"), &mut cand); + let msg = if cand.is_empty() { + "no app specified (--app) and no apps/**/main.nyash found".to_string() + } else { + format!("no app specified (--app). Candidates:\n - {}", cand.join("\n - ")) + }; + return Err(msg); + }; + // 6) Emit object + let obj_dir = cwd.join("target").join("aot_objects"); + let _ = std::fs::create_dir_all(&obj_dir); + let obj_path = obj_dir.join("main.o"); + if aot == "llvm" { + if std::env::var("LLVM_SYS_180_PREFIX").ok().is_none() && std::env::var("LLVM_SYS_181_PREFIX").ok().is_none() { + return Err("LLVM 18 not configured. Set LLVM_SYS_180_PREFIX or install LLVM 18 (llvm-config)".into()); + } + std::env::set_var("NYASH_LLVM_OBJ_OUT", &obj_path); + println!("[emit] LLVM object → {}", obj_path.display()); + let status = std::process::Command::new(cwd.join("target").join(profile.clone()).join(if cfg!(windows) {"nyash.exe"} else {"nyash"})) + .args(["--backend","llvm", &app]) + .status().map_err(|e| format!("spawn nyash llvm: {}", e))?; + if !status.success() { return Err("LLVM emit failed".into()); } + } else { + std::env::set_var("NYASH_AOT_OBJECT_OUT", &obj_dir); + println!("[emit] Cranelift object → {} (directory)", obj_dir.display()); + let status = std::process::Command::new(cwd.join("target").join(profile.clone()).join(if cfg!(windows) {"nyash.exe"} else {"nyash"})) + .args(["--backend","vm", &app]) + .status().map_err(|e| format!("spawn nyash jit-aot: {}", e))?; + if !status.success() { return Err("Cranelift emit failed".into()); } + } + if !obj_path.exists() { + // In Cranelift path we produce target/aot_objects/.o; fall back to main.o default + if !obj_dir.join("main.o").exists() { return Err(format!("object not generated under {}", obj_dir.display())); } + } + let out_path = if let Some(o) = out { PathBuf::from(o) } else { if cfg!(windows) { cwd.join("app.exe") } else { cwd.join("app") } }; + // 7) Link + println!("[link] → {}", out_path.display()); + #[cfg(windows)] + { + // Prefer MSVC link.exe, then clang fallback + if let Ok(link) = which::which("link") { + let status = std::process::Command::new(&link).args(["/NOLOGO", &format!("/OUT:{}", out_path.display().to_string())]) + .arg(&obj_path) + .arg(cwd.join("target").join("release").join("nyrt.lib")) + .status().map_err(|e| format!("spawn link.exe: {}", e))?; + if status.success() { println!("OK"); return Ok(()); } + } + if let Ok(clang) = which::which("clang") { + let status = std::process::Command::new(&clang) + .args(["-o", &out_path.display().to_string(), &obj_path.display().to_string()]) + .arg(cwd.join("target").join("release").join("nyrt.lib").display().to_string()) + .arg("-lntdll") + .status().map_err(|e| format!("spawn clang: {}", e))?; + if status.success() { println!("OK"); return Ok(()); } + return Err("link failed on Windows (tried link.exe and clang)".into()); + } + return Err("no linker found (need Visual Studio link.exe or LLVM clang)".into()); + } + #[cfg(not(windows))] + { + let status = std::process::Command::new("cc") + .arg(&obj_path) + .args(["-L", &cwd.join("target").join("release").display().to_string()]) + .args(["-Wl,--whole-archive", "-lnyrt", "-Wl,--no-whole-archive", "-lpthread", "-ldl", "-lm"]) + .args(["-o", &out_path.display().to_string()]) + .status().map_err(|e| format!("spawn cc: {}", e))?; + if !status.success() { return Err("link failed (cc)".into()); } + } + println!("✅ Success: {}", out_path.display()); + Ok(()) + } } impl NyashRunner {