use std::path::{Path, PathBuf};
use std::process::Command;
fn is_ling_source(name: &str) -> bool {
name.ends_with(".ling")
|| name.ends_with(".灵")
|| name.ends_with(".霊")
|| name.ends_with(".령")
|| name.ends_with(".ลิง")
}
fn main() {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(|s| s.as_str()) {
Some("visualize") => {
let file = args.get(2).map(|s| s.as_str()).unwrap_or_else(|| {
eprintln!("Usage: ling visualize <file.ling>");
std::process::exit(1);
});
let source = std::fs::read_to_string(file).unwrap_or_else(|e| {
eprintln!("error reading '{}': {}", file, e);
std::process::exit(1);
});
let program = ling::parser::parse(&source).unwrap_or_else(|e| {
eprintln!("parse error: {}", e);
std::process::exit(1);
});
print!("{}", ling::visualize::render(file, &program));
},
Some("run") => {
let wasm = args.iter().any(|a| a == "--wasm" || a == "--web");
let use_interp = args.iter().any(|a| a == "--interp");
let file = args[2..]
.iter()
.map(|s| s.as_str())
.find(|a| is_ling_source(a))
.unwrap_or_else(|| {
eprintln!("Usage: ling run [--wasm|--interp] <file.ling>");
std::process::exit(1);
});
if wasm {
run_wasm(file);
} else if use_interp {
run_file(file);
} else {
let is_server = std::fs::read_to_string(file)
.map(|s| s.contains("http_serve") || s.contains("เว็บเสิร์ฟ"))
.unwrap_or(false);
if is_server {
run_file(file);
} else {
run_file_jit(file);
}
}
},
Some("convert") => {
std::process::exit(ling::convert::run(&args[1..]));
},
Some("ast") => {
std::process::exit(run_ast(&args[2..]));
},
Some("build") => {
let target = args.get(2).map(|s| s.as_str()).unwrap_or(".");
let out = flag_value(&args, "--out").unwrap_or_else(|| "dist".into());
let platforms = collect_platforms(&args);
let icon = flag_value(&args, "--icon").map(PathBuf::from);
let pack = args.iter().any(|a| a == "--pack");
let aot = args.iter().any(|a| a == "--aot");
run_build(target, &out, &platforms, icon, pack, aot);
},
Some(file) if is_ling_source(file) => run_file_jit(file),
_ => {
println!("ling {} — The Omniglot Systems Language", ling::VERSION);
println!("Usage:");
println!(" ling run <file.ling> run using the Cranelift JIT backend (default)");
println!(
" ling run --interp <file.ling> run using the tree-walking interpreter"
);
println!(" ling run --wasm <file.ling> build to WebAssembly, serve, open in browser");
println!(" ling visualize <file.ling> emit SVG AST to stdout");
println!(" ling build <file.ling|dir> [opts] compile to distributable");
println!(" --out <dir> output folder (default: dist)");
println!(" --platform <targets> web win lin mac all (comma-sep)");
println!(" --icon <file.svg|png|ico> app icon (overrides manifest icon)");
println!(
" --pack embed [includes] resources into the exe"
);
println!(" --aot compile to native code via AOT");
println!(" ling ast [path] [--technical|--artwork|--ling|--all]");
println!(
" project-wide AST → SVG in ./AST/ (300 dpi)"
);
println!(" ling convert <asset> [opts] transcode an asset → importable .ling");
println!(" -o <out.ling> output path (default: <asset>.ling)");
println!(" --no-compression emit plain arrays instead of blobs");
println!(" (.gltf .glb .wav .ogg .flac .mid .svg .blend)");
},
}
}
fn run_file(path: &str) {
let resolved = std::path::Path::new(path);
if !resolved.exists() {
eprintln!("[ling] error: file does not exist: {}", resolved.display());
std::process::exit(1);
}
let source = std::fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("error reading '{}': {}", path, e);
std::process::exit(1);
});
let lang = ling::detect_language(&source);
if lang != "English" {
eprintln!("[detected language: {}]", lang);
}
let src_dir = resolved.parent().map(|p| p.to_path_buf());
if let Err(e) = ling::run_named(&source, src_dir, Some(path)) {
eprintln!("{e}");
std::process::exit(1);
}
}
fn run_file_jit(path: &str) {
let resolved = std::path::Path::new(path);
if !resolved.exists() {
eprintln!("[ling] error: file does not exist: {}", resolved.display());
std::process::exit(1);
}
let source = std::fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("error reading '{}': {}", path, e);
std::process::exit(1);
});
let lang = ling::detect_language(&source);
if lang != "English" {
eprintln!("[detected language: {}]", lang);
}
let has_entry = ling::parser::parse(&source)
.map(|p| ling::entry::entry_name(&p.items).is_some())
.unwrap_or(true);
if !has_entry {
let src_dir = resolved.parent().map(|p| p.to_path_buf());
if let Err(e) = ling::run_named(&source, src_dir, Some(path)) {
eprintln!("{e}");
std::process::exit(1);
}
return;
}
use ling::CompilerConfig;
let config = CompilerConfig::default();
let compiler = ling::LingCompiler::new(config);
if let Err(e) = compiler.compile_and_run_jit(path) {
use ling::core::LingError;
match e {
LingError::Parse(m) => {
let out_lang = ling::diag::OutputLang::from_env();
eprintln!(
"{}",
ling::diag::render_parse(&m, &source, Some(path), out_lang)
);
std::process::exit(1);
},
LingError::Mir(m) => {
eprintln!("{m}");
std::process::exit(1);
},
_ => {
let src_dir = resolved.parent().map(|p| p.to_path_buf());
if let Err(e) = ling::run_named(&source, src_dir, Some(path)) {
eprintln!("{e}");
std::process::exit(1);
}
},
}
}
}
fn run_ast(args: &[String]) -> i32 {
use ling::astviz::AstStyle;
let out_dir = flag_value(&Vec::from(args), "--out").unwrap_or_else(|| "AST".into());
let mut styles: Vec<AstStyle> = Vec::new();
let all = args.iter().any(|a| a == "--all");
if all || args.iter().any(|a| a == "--technical") {
styles.push(AstStyle::Technical);
}
if all || args.iter().any(|a| a == "--artwork") {
styles.push(AstStyle::Artwork);
}
if all || args.iter().any(|a| a == "--ling") {
styles.push(AstStyle::Ling);
}
if styles.is_empty() {
styles = vec![AstStyle::Technical, AstStyle::Artwork, AstStyle::Ling];
}
let path = pick_ast_path(args).unwrap_or_else(|| ".".into());
let (proj_name, files) = gather_project(&path);
if files.is_empty() {
eprintln!("ling ast: no parseable .ling files found in '{path}'");
return 1;
}
if let Err(e) = std::fs::create_dir_all(&out_dir) {
eprintln!("ling ast: cannot create '{out_dir}': {e}");
return 1;
}
let n_fns: usize = files
.iter()
.map(|(_, p)| {
p.items
.iter()
.filter(|i| matches!(i, ling::parser::ast::Item::Fn(_)))
.count()
})
.sum();
println!(
"ling ast: '{proj_name}' — {} file(s), {n_fns} fn(s)",
files.len()
);
for style in &styles {
let svg = ling::astviz::render(*style, &proj_name, &files);
let dst = std::path::Path::new(&out_dir).join(format!("{proj_name}.{}.svg", style.slug()));
match std::fs::write(&dst, svg.as_bytes()) {
Ok(()) => println!(" ✓ {}", dst.display()),
Err(e) => {
eprintln!(" ✗ {}: {e}", dst.display());
return 1;
},
}
}
0
}
fn pick_ast_path(args: &[String]) -> Option<String> {
let mut i = 0;
while i < args.len() {
let a = &args[i];
if a == "--out" {
i += 2;
continue;
}
if a.starts_with('-') {
i += 1;
continue;
}
return Some(a.clone());
}
None
}
fn gather_project(path: &str) -> (String, Vec<(String, ling::parser::ast::Program)>) {
let p = Path::new(path);
let mut files = Vec::new();
if p.is_file() {
let name = p
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "program".into());
if let Some(prog) = parse_one(p) {
files.push((
p.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
prog,
));
}
return (sanitise_name(&name), files);
}
let mut paths = Vec::new();
collect_ling_files(p, &mut paths);
paths.sort();
let base = p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
for fp in &paths {
if let Some(prog) = parse_one(fp) {
let label = fp
.strip_prefix(&base)
.unwrap_or(fp)
.to_string_lossy()
.into_owned();
files.push((label, prog));
}
}
let proj = p
.canonicalize()
.ok()
.and_then(|c| c.file_name().map(|n| n.to_string_lossy().into_owned()))
.or_else(|| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "project".into());
(sanitise_name(&proj), files)
}
fn parse_one(path: &Path) -> Option<ling::parser::ast::Program> {
let src = std::fs::read_to_string(path).ok()?;
match ling::parser::parse(&src) {
Ok(prog) => Some(prog),
Err(e) => {
eprintln!(" [skip] {}: parse error: {e}", path.display());
None
},
}
}
fn collect_ling_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else { return };
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if path.is_dir() {
if matches!(
name.as_ref(),
".ling-build" | "灵碑" | "target" | "dist" | ".git" | "node_modules" | "AST"
) {
continue;
}
collect_ling_files(&path, out);
} else if matches!(
path.extension().and_then(|e| e.to_str()),
Some("ling" | "灵" | "霊" | "령" | "ลิง")
) {
out.push(path);
}
}
}
fn flag_value(args: &[String], flag: &str) -> Option<String> {
args.windows(2).find(|w| w[0] == flag).map(|w| w[1].clone())
}
fn collect_platforms(args: &[String]) -> Vec<String> {
let mut platforms: Vec<String> = args
.windows(2)
.filter(|w| w[0] == "--platform")
.flat_map(|w| {
w[1].split(',')
.map(|s| s.trim().to_lowercase())
.collect::<Vec<_>>()
})
.collect();
if platforms.iter().any(|p| p == "all") {
return vec!["win".into(), "lin".into(), "mac".into(), "web".into()];
}
if platforms.is_empty() {
platforms.push(native_platform().into());
platforms.push("web".into());
}
platforms
}
fn native_platform() -> &'static str {
if cfg!(target_os = "windows") {
"win"
} else if cfg!(target_os = "macos") {
"mac"
} else {
"lin"
}
}
#[derive(Debug, Clone, PartialEq)]
enum ProjKind {
Bin,
Web,
Game,
Ui,
Ai,
Crypto,
Lib,
Polyglot,
Kernel,
}
impl ProjKind {
#[allow(dead_code)]
fn default_platforms(&self) -> Vec<&'static str> {
match self {
ProjKind::Web => vec!["web"],
_ => vec![native_platform(), "web"],
}
}
}
struct LingProject {
name: String,
version: String,
kind: ProjKind,
entry: PathBuf, source_dir: PathBuf, build_dir: PathBuf, icon: Option<PathBuf>, includes: Vec<String>, graphics: bool,
}
fn discover_project(target: &str) -> LingProject {
let raw = Path::new(target);
let path = raw.canonicalize().unwrap_or_else(|_| raw.to_path_buf());
if path.is_file() {
let name = sanitise_name(&path.file_stem().unwrap_or_default().to_string_lossy());
let source_dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
let build_dir = source_dir.join(".ling-build").join(&name);
LingProject {
name,
version: "0.1.0".into(),
kind: ProjKind::Bin,
entry: path,
source_dir,
build_dir,
icon: None,
includes: Vec::new(),
graphics: false,
}
} else if path.is_dir() {
let lf = path.join("灵符.toml");
if lf.exists() {
return parse_lingfu_toml(&lf, &path);
}
let lt = path.join("Ling.toml");
if lt.exists() {
return parse_ling_toml(<, &path);
}
let entry = auto_entry(&path).unwrap_or_else(|| {
eprintln!("error: no .ling file found in '{}'", path.display());
std::process::exit(1);
});
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "app".into());
let build_dir = path.join(".ling-build").join(&name);
LingProject {
name,
version: "0.1.0".into(),
kind: ProjKind::Bin,
entry,
source_dir: path,
build_dir,
icon: None,
includes: Vec::new(),
graphics: false,
}
} else {
eprintln!("error: '{}' is not a .ling file or directory", target);
std::process::exit(1);
}
}
fn parse_lingfu_toml(toml: &Path, base: &Path) -> LingProject {
let text = std::fs::read_to_string(toml).unwrap_or_else(|e| {
eprintln!("read 灵符.toml: {e}");
std::process::exit(1);
});
let mut name = base
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "app".into());
let mut version = "0.1.0".into();
let mut kind = ProjKind::Bin;
let mut icon: Option<PathBuf> = None;
for line in text.lines() {
let line = line.trim();
if let Some(v) = toml_kv(line, "名") {
name = sanitise_name(&v);
}
if let Some(v) = toml_kv(line, "版") {
version = v;
}
if let Some(v) = toml_kv(line, "型") {
kind = parse_kind(&v);
}
if let Some(v) = toml_kv(line, "图标").or_else(|| toml_kv(line, "icon")) {
icon = Some(base.join(v));
}
}
let entry = ["灵源/启.灵", "灵源/main.ling", "main.ling"]
.iter()
.map(|p| base.join(p))
.find(|p| p.exists())
.unwrap_or_else(|| {
auto_entry(base).unwrap_or_else(|| {
eprintln!("error: cannot find entry point for project '{}'", name);
std::process::exit(1);
})
});
let source_dir = base.to_path_buf();
let build_dir = base.join("灵碑").join(&name);
let includes = parse_includes(&text);
LingProject {
name,
version,
kind,
entry,
source_dir,
build_dir,
icon,
includes,
graphics: false,
}
}
fn parse_ling_toml(toml: &Path, base: &Path) -> LingProject {
let text = std::fs::read_to_string(toml).unwrap_or_else(|e| {
eprintln!("read Ling.toml: {e}");
std::process::exit(1);
});
let mut name = base
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "app".into());
let mut version = "0.1.0".to_string();
let mut entry_name = "main.ling".to_string();
let mut kind = ProjKind::Bin;
let mut icon: Option<PathBuf> = None;
let mut graphics = false;
for line in text.lines() {
let line = line.trim();
if let Some(v) = toml_kv(line, "name") {
name = sanitise_name(&v);
}
if let Some(v) = toml_kv(line, "version") {
version = v;
}
if let Some(v) = toml_kv(line, "entry") {
entry_name = v;
}
if let Some(v) = toml_kv(line, "kind") {
kind = parse_kind(&v);
}
if let Some(v) = toml_kv(line, "icon") {
icon = Some(base.join(v));
}
if let Some(v) = toml_kv(line, "graphics") {
graphics = v == "true";
}
}
let entry = base.join(&entry_name);
if !entry.exists() {
eprintln!("error: entry '{}' not found", entry.display());
std::process::exit(1);
}
let build_dir = base.join(".ling-build").join(&name);
let includes = parse_includes(&text);
LingProject {
name,
version,
kind,
entry,
source_dir: base.to_path_buf(),
build_dir,
icon,
includes,
graphics,
}
}
fn auto_entry(dir: &Path) -> Option<PathBuf> {
for name in &["main.ling", "start.ling", "启.灵"] {
let p = dir.join(name);
if p.exists() {
return Some(p);
}
}
for subdir in &["灵源", "src"] {
let sub = dir.join(subdir);
if let Some(e) = auto_entry(&sub) {
return Some(e);
}
}
std::fs::read_dir(dir)
.ok()?
.flatten()
.find(|e| e.path().extension().is_some_and(|x| x == "ling"))
.map(|e| e.path())
}
fn parse_kind(s: &str) -> ProjKind {
match s.to_lowercase().as_str() {
"web" | "网灵" => ProjKind::Web,
"game" | "游灵" => ProjKind::Game,
"ui" | "显灵" => ProjKind::Ui,
"ai" | "智灵" => ProjKind::Ai,
"crypto" | "密灵" => ProjKind::Crypto,
"lib" | "共修" => ProjKind::Lib,
"polyglot" | "万言" => ProjKind::Polyglot,
"kernel" | "内核" => ProjKind::Kernel,
_ => ProjKind::Bin,
}
}
fn toml_kv(line: &str, key: &str) -> Option<String> {
let pat = format!("{key} =");
if !line.starts_with(&pat) {
return None;
}
let v = line[pat.len()..].trim().trim_matches('"').to_string();
if v.is_empty() {
None
} else {
Some(v)
}
}
fn sanitise_name(s: &str) -> String {
let out: String = s
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect();
let out = out.trim_matches('-').to_string();
let out = if out.is_empty() { "app".into() } else { out };
if out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
format!("r{}", out)
} else {
out
}
}
fn run_build(
target: &str,
out: &str,
platforms: &[String],
icon_override: Option<PathBuf>,
pack: bool,
aot: bool,
) {
let project = discover_project(target);
println!(
"Building '{}' v{} ({:?})",
project.name, project.version, project.kind
);
std::fs::create_dir_all(out).unwrap_or_else(|e| {
eprintln!("create dist dir '{}': {e}", out);
std::process::exit(1);
});
let icon = icon_override.as_deref();
for platform in platforms {
match platform.as_str() {
"web" => build_web(&project, out),
"win" | "windows" => {
build_native(&project, out, NativePlatform::Windows, icon, pack, aot)
},
"lin" | "linux" => build_native(&project, out, NativePlatform::Linux, icon, pack, aot),
"mac" | "macos" | "darwin" => {
build_native(&project, out, NativePlatform::Mac, icon, pack, aot)
},
"kernel" | "bare" => {
build_native(&project, out, NativePlatform::BareMetal, icon, pack, aot)
},
"rpi" | "raspberrypi" => {
build_native(&project, out, NativePlatform::Rpi, icon, pack, aot)
},
other => {
eprintln!("unknown platform '{}' — use web|win|lin|mac|kernel|rpi|all", other);
std::process::exit(1);
},
}
}
println!("\nOutputs written to: {out}/");
}
fn build_web(project: &LingProject, out: &str) {
println!(" [web] building WebGL bundle…");
let lingc = sibling_binary("lingc");
let web_out = Path::new(out).join("web");
let status = Command::new(&lingc)
.arg("webgl")
.arg(&project.entry)
.arg("--out")
.arg(&web_out)
.status()
.unwrap_or_else(|e| {
eprintln!(" lingc not found ({e}). Build lingc first: cargo build --bin lingc");
std::process::exit(1);
});
if !status.success() {
eprintln!(" [web] build failed");
std::process::exit(1);
}
println!(" [web] → {}/web/", out);
}
fn run_wasm(file: &str) {
if !Path::new(file).exists() {
eprintln!("[ling] error: file does not exist: {file}");
std::process::exit(1);
}
let out = std::env::temp_dir().join(format!("ling-wasm-{}", std::process::id()));
let _ = std::fs::create_dir_all(&out);
let lingc = sibling_binary("lingc");
println!("[ling] building WebAssembly bundle (first run compiles the runtime, ~1 min)…");
let status = Command::new(&lingc)
.arg("webgl")
.arg(file)
.arg("--out")
.arg(&out)
.status()
.unwrap_or_else(|e| {
eprintln!("[ling] lingc not found ({e}); build it first: cargo build --bin lingc");
std::process::exit(1);
});
if !status.success() {
eprintln!("[ling] wasm build failed");
std::process::exit(1);
}
serve_and_open(&out);
}
fn serve_and_open(root: &Path) {
use std::io::{Read, Write};
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:8080")
.or_else(|_| TcpListener::bind("127.0.0.1:0"))
.unwrap_or_else(|e| {
eprintln!("[ling] could not bind a local port: {e}");
std::process::exit(1);
});
let port = listener.local_addr().map(|a| a.port()).unwrap_or(8080);
let url = format!("http://localhost:{port}/index.html");
println!("[ling] serving {} ", root.display());
println!("[ling] ▶ {url}");
println!("[ling] press Ctrl+C to stop.");
open_browser(&url);
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let root = root.to_path_buf();
std::thread::spawn(move || {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).unwrap_or(0);
let req = String::from_utf8_lossy(&buf[..n]);
let raw = req
.lines()
.next()
.and_then(|l| l.split_whitespace().nth(1))
.unwrap_or("/");
let path = raw.split('?').next().unwrap_or("/");
let rel = if path == "/" {
"index.html"
} else {
path.trim_start_matches('/')
};
let target = root.join(rel);
let safe = target.canonicalize().ok().filter(|p| {
root.canonicalize()
.map(|r| p.starts_with(r))
.unwrap_or(false)
});
let (status_line, body, ctype) = match safe.and_then(|p| std::fs::read(p).ok()) {
Some(bytes) => ("200 OK", bytes, mime_for(rel)),
None => ("404 Not Found", b"404 not found".to_vec(), "text/plain"),
};
let header = format!(
"HTTP/1.1 {status_line}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nCross-Origin-Opener-Policy: same-origin\r\nCross-Origin-Embedder-Policy: require-corp\r\nCross-Origin-Resource-Policy: cross-origin\r\nConnection: close\r\n\r\n",
body.len()
);
let _ = stream.write_all(header.as_bytes());
let _ = stream.write_all(&body);
});
}
}
fn mime_for(path: &str) -> &'static str {
let ext = path.rsplit('.').next().unwrap_or("");
match ext {
"html" => "text/html; charset=utf-8",
"js" | "mjs" => "text/javascript; charset=utf-8",
"wasm" => "application/wasm",
"json" => "application/json",
"css" => "text/css; charset=utf-8",
"ling" => "text/plain; charset=utf-8",
"png" => "image/png",
"svg" => "image/svg+xml",
"wav" => "audio/wav",
"ogg" => "audio/ogg",
_ => "application/octet-stream",
}
}
fn open_browser(url: &str) {
#[cfg(target_os = "windows")]
let _ = Command::new("cmd").args(["/C", "start", "", url]).spawn();
#[cfg(target_os = "macos")]
let _ = Command::new("open").arg(url).spawn();
#[cfg(all(unix, not(target_os = "macos")))]
let _ = Command::new("xdg-open").arg(url).spawn();
}
#[derive(Debug, Clone, Copy)]
enum NativePlatform {
Windows,
Linux,
Mac,
BareMetal,
Rpi,
}
impl NativePlatform {
fn dir_name(self) -> &'static str {
match self {
Self::Windows => "windows",
Self::Linux => "linux",
Self::Mac => "macos",
Self::BareMetal => "kernel",
Self::Rpi => "rpi",
}
}
fn triple(self) -> &'static str {
match self {
Self::Windows => {
if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc"
} else {
"x86_64-pc-windows-gnu"
}
},
Self::Linux => {
if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu"
} else {
"x86_64-unknown-linux-musl"
}
},
Self::Mac => {
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
"aarch64-apple-darwin"
} else {
"x86_64-apple-darwin"
}
},
Self::BareMetal => "x86_64-unknown-none",
Self::Rpi => "aarch64-unknown-none",
}
}
fn exe_suffix(self) -> &'static str {
match self {
Self::Windows => ".exe",
Self::BareMetal | Self::Rpi => "",
_ => "",
}
}
fn is_current_host(self) -> bool {
match self {
Self::Windows => cfg!(target_os = "windows"),
Self::Linux => cfg!(target_os = "linux"),
Self::Mac => cfg!(target_os = "macos"),
Self::BareMetal | Self::Rpi => false,
}
}
fn cranelift_arch(self) -> &'static str {
match self {
Self::BareMetal => "x86_64",
Self::Rpi => "aarch64",
_ => unreachable!("cranelift_arch only used for kernel targets"),
}
}
}
fn build_native(
project: &LingProject,
out: &str,
platform: NativePlatform,
icon: Option<&Path>,
pack: bool,
aot: bool,
) {
let triple = platform.triple();
let is_kernel = matches!(platform, NativePlatform::BareMetal | NativePlatform::Rpi);
let is_rpi = matches!(platform, NativePlatform::Rpi);
println!(
" [{}] building {} ({triple}){}…",
platform.dir_name(),
platform.dir_name(),
if aot { " [AOT]" } else { "" }
);
let ling_root = find_ling_root().unwrap_or_else(|| {
eprintln!(
" cannot find ling-lang source.\n \
Run from the repository or set LING_HOME=<path-to-ling-repo>."
);
std::process::exit(1);
});
let build_dir = &project.build_dir;
std::fs::create_dir_all(build_dir.join("src")).unwrap_or_else(|e| {
eprintln!(" create build dir: {e}");
std::process::exit(1);
});
let use_aot = aot || is_kernel;
if !use_aot {
copy_ling_sources(&project.source_dir, build_dir);
}
let entry_filename = project
.entry
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let resources = expand_includes(project);
let do_pack = pack && !resources.is_empty();
if is_kernel {
println!(" AOT-compiling {} for kernel…", entry_filename);
let mir = ling::mir::compile_path(&project.entry, ling::core::OptimizationLevel::O3)
.unwrap_or_else(|e| {
eprintln!(" MIR compilation failed: {e}");
std::process::exit(1);
});
let mir_prog = ling_codegen::MirProgram::new(mir, entry_filename.clone());
println!(
" compiling {} functions to native code…",
mir_prog.mir.functions.len()
);
let mut backend =
ling_codegen::CraneliftBackend::new_for_arch(platform.cranelift_arch())
.with_progress(true);
let obj_path = build_dir.join("entry.o");
use ling_codegen::CodegenBackend;
backend.emit(&mir_prog, &obj_path).unwrap_or_else(|e| {
eprintln!(" AOT codegen failed: {e}");
std::process::exit(1);
});
println!(" AOT object written to {}", obj_path.display());
std::fs::write(
build_dir.join("Cargo.toml"),
gen_kernel_cargo_toml(&project.name, &project.version, &ling_root, project.graphics),
)
.expect("write Cargo.toml");
let idle_font_mod = if !is_rpi {
find_font_asset(&project.source_dir).and_then(|font_path| {
let bytes = std::fs::read(&font_path).ok()?;
let cjk_fallback = std::fs::read(ling_root.join("assets/fonts/NotoSansSC.ttf")).ok();
let atlas = rasterize_vga_font(&bytes, cjk_fallback.as_deref());
std::fs::write(build_dir.join("src/idle_font.rs"), gen_idle_font_rs(&atlas))
.ok()?;
println!(" idle font: {} → src/idle_font.rs", font_path.display());
Some("idle_font")
})
} else {
None
};
let main_rs = if is_rpi {
gen_rpi_kernel_main_rs()
} else {
gen_kernel_main_rs(idle_font_mod)
};
std::fs::write(build_dir.join("src/main.rs"), main_rs).expect("write src/main.rs");
std::fs::write(build_dir.join("build.rs"), gen_kernel_build_rs())
.expect("write build.rs");
let linker_script = if is_rpi { RPI_LINKER_SCRIPT } else { KERNEL_LINKER_SCRIPT };
std::fs::write(build_dir.join("linker.ld"), linker_script).expect("write linker.ld");
println!(" kernel build files written to {}", build_dir.display());
} else if use_aot {
println!(" AOT-compiling {}…", entry_filename);
let mir = ling::mir::compile_path(&project.entry, ling::core::OptimizationLevel::O3)
.unwrap_or_else(|e| {
eprintln!(" MIR compilation failed: {e}");
std::process::exit(1);
});
let mir_prog = ling_codegen::MirProgram::new(mir, entry_filename.clone());
println!(
" compiling {} functions to native code…",
mir_prog.mir.functions.len()
);
let mut backend = ling_codegen::CraneliftBackend::new().with_progress(true);
let obj_path = build_dir.join("entry.o");
use ling_codegen::CodegenBackend;
backend.emit(&mir_prog, &obj_path).unwrap_or_else(|e| {
eprintln!(" AOT codegen failed: {e}");
std::process::exit(1);
});
println!(" AOT object written to {}", obj_path.display());
std::fs::write(build_dir.join("src/main.rs"), gen_aot_main_rs(do_pack))
.expect("write src/main.rs");
std::fs::write(build_dir.join("build.rs"), gen_aot_build_rs()).expect("write build.rs");
} else {
std::fs::write(
build_dir.join("src/main.rs"),
gen_main_rs(&entry_filename, do_pack),
)
.expect("write src/main.rs");
std::fs::write(build_dir.join("build.rs"), gen_build_rs()).expect("write build.rs");
}
if do_pack {
write_packed_resources(build_dir, &resources);
println!(
" packing {} resource(s) into the executable",
resources.len()
);
} else if pack {
println!(" --pack: no [includes] resources to pack");
}
if matches!(platform, NativePlatform::Windows) {
match resolve_icon(icon, project, &ling_root) {
Some(src) => {
let out_ico = build_dir.join("app.ico");
match ling_icon::write_ico(&src, &out_ico, ling_icon::DEFAULT_SIZES) {
Ok(()) => println!(" [windows] icon: {}", src.display()),
Err(e) => eprintln!(" [windows] icon skipped: {e}"),
}
},
None => println!(" [windows] no icon found; building without one"),
}
}
ensure_rustup_target(triple);
let build_cmd = choose_build_tool(platform);
let mut cargo_args: Vec<&str> = vec!["build", "--release", "--target", triple];
if is_kernel {
let cargo_dir = build_dir.join(".cargo");
std::fs::create_dir_all(&cargo_dir).expect("create .cargo dir");
std::fs::write(
cargo_dir.join("config.toml"),
format!(
r#"[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]
[target.{triple}]
rustflags = ["-C", "relocation-model=static", "--cfg", "curve25519_dalek_backend=\"serial\""]
"#
),
)
.ok();
let nightly = "cargo";
cargo_args = vec!["+nightly", "build", "--release", "--target", triple, "-Z", "build-std=core,compiler_builtins", "-Z", "build-std-features=compiler-builtins-mem"];
let status = Command::new(nightly)
.args(&cargo_args)
.current_dir(build_dir)
.status()
.unwrap_or_else(|e| {
eprintln!(" {nightly}: {e}");
std::process::exit(1);
});
if !status.success() {
eprintln!(" [{}] build failed.", platform.dir_name());
eprintln!(" Tip: ensure nightly Rust is installed: rustup toolchain install nightly");
eprintln!(" Then install: rustup target add x86_64-unknown-none --toolchain nightly");
eprintln!(" And: rustup component add rust-src --toolchain nightly");
std::process::exit(1);
}
} else {
let status = Command::new(build_cmd)
.args(&cargo_args)
.current_dir(build_dir)
.status()
.unwrap_or_else(|e| {
eprintln!(" {build_cmd}: {e}");
if build_cmd == "cross" {
eprintln!(" install cross: cargo install cross (requires Docker)");
}
std::process::exit(1);
});
if !status.success() {
eprintln!(" [{}] build failed.", platform.dir_name());
if !platform.is_current_host() && build_cmd == "cargo" && !has_cross() {
eprintln!(" Tip: install `cross` for cross-compilation (needs Docker):");
eprintln!(" cargo install cross");
}
std::process::exit(1);
}
}
let exe = format!("{}{}", project.name, platform.exe_suffix());
let src_bin = build_dir
.join("target")
.join(triple)
.join("release")
.join(&exe);
let platform_dir = Path::new(out).join(platform.dir_name());
std::fs::create_dir_all(&platform_dir).expect("create platform dir");
let dst = platform_dir.join(&exe);
std::fs::copy(&src_bin, &dst).unwrap_or_else(|e| {
eprintln!(" copy binary {}: {e}", src_bin.display());
std::process::exit(1);
});
println!(" [{}] → {}", platform.dir_name(), dst.display());
if is_rpi {
let img_path = platform_dir.join("kernel8.img");
objcopy_to_raw_binary(&dst, &img_path);
println!(" [{}] → {}", platform.dir_name(), img_path.display());
}
if !do_pack && !resources.is_empty() {
for (rel, abs) in &resources {
let dst = platform_dir.join(rel);
if let Some(parent) = dst.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::copy(abs, &dst) {
eprintln!(" resource copy {}: {e}", rel);
}
}
println!(
" [{}] bundled {} resource(s)",
platform.dir_name(),
resources.len()
);
}
}
fn objcopy_to_raw_binary(elf: &Path, out_img: &Path) {
for tool in ["llvm-objcopy", "objcopy"] {
let status = Command::new(tool)
.args(["-O", "binary"])
.arg(elf)
.arg(out_img)
.status();
match status {
Ok(s) if s.success() => return,
Ok(s) => {
eprintln!(" {tool} exited with {s}");
std::process::exit(1);
},
Err(_) => continue, }
}
eprintln!(
" error: neither llvm-objcopy nor objcopy found on PATH; can't produce {}",
out_img.display()
);
eprintln!(" install LLVM (provides llvm-objcopy) or GNU binutils (objcopy)");
std::process::exit(1);
}
fn find_font_asset(dir: &Path) -> Option<PathBuf> {
let mut cur = Some(dir);
for _ in 0..6 {
let d = cur?;
let font_dir = d.join("font");
if font_dir.is_dir() {
let mut candidates: Vec<PathBuf> = std::fs::read_dir(&font_dir)
.ok()?
.flatten()
.map(|e| e.path())
.filter(|p| {
matches!(
p.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()),
Some(ref e) if e == "otf" || e == "ttf"
)
})
.collect();
candidates.sort();
if let Some(square) = candidates
.iter()
.find(|p| p.file_stem().and_then(|s| s.to_str()) == Some("square"))
{
return Some(square.clone());
}
if let Some(first) = candidates.into_iter().next() {
return Some(first);
}
}
cur = d.parent();
}
None
}
const SPECIAL_GLYPHS: &[(u8, char)] = &[
(0x01, '灵'), (0x02, '内'), (0x03, '核'), ];
fn rasterize_vga_font(primary: &[u8], cjk_fallback: Option<&[u8]>) -> [u8; 4096] {
let mut out = [0u8; 4096];
let Ok(primary_font) = fontdue::Font::from_bytes(primary, fontdue::FontSettings::default())
else {
return out;
};
let cjk_font = cjk_fallback
.and_then(|b| fontdue::Font::from_bytes(b, fontdue::FontSettings::default()).ok());
for code in 0u32..256 {
if let Some(&(_, ch)) = SPECIAL_GLYPHS.iter().find(|(b, _)| *b as u32 == code) {
let font = cjk_font.as_ref().unwrap_or(&primary_font);
blit_glyph(&mut out, code as usize, font, ch);
} else if (0x20..=0x7E).contains(&code) {
let ch = char::from_u32(code).unwrap();
blit_glyph(&mut out, code as usize, &primary_font, ch);
}
}
out
}
fn blit_glyph(out: &mut [u8; 4096], slot: usize, font: &fontdue::Font, ch: char) {
let (metrics, bitmap) = font.rasterize(ch, 15.0);
if metrics.width == 0 || metrics.height == 0 {
return; }
let y_off = ((16i32 - metrics.height as i32) / 2).max(0) as usize;
let x_off = ((8i32 - metrics.width as i32) / 2).max(0) as usize;
for row in 0..metrics.height.min(16) {
let mut byte = 0u8;
for col in 0..metrics.width.min(8) {
if bitmap[row * metrics.width + col] > 128 {
byte |= 1 << (7usize.saturating_sub(x_off + col));
}
}
let out_row = y_off + row;
if out_row < 16 {
out[slot * 16 + out_row] |= byte;
}
}
}
fn gen_idle_font_rs(atlas: &[u8; 4096]) -> String {
let mut out = String::with_capacity(4096 * 4 + 64);
out.push_str("pub static IDLE_FONT: [u8; 4096] = [\n");
for chunk in atlas.chunks(16) {
out.push_str(" ");
for b in chunk {
out.push_str(&format!("{b},"));
}
out.push('\n');
}
out.push_str("];\n");
out
}
fn copy_ling_sources(src: &Path, dst: &Path) {
let Ok(entries) = std::fs::read_dir(src) else { return };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let dname = path.file_name().unwrap_or_default().to_string_lossy();
if dname == "灵源" || dname == "src" {
copy_ling_sources(&path, dst);
}
} else if path.extension().is_some_and(|e| e == "ling") {
if let Some(name) = path.file_name() {
let _ = std::fs::copy(&path, dst.join(name));
}
}
}
}
fn gen_build_rs() -> String {
r#"// Generated by `ling build` — embeds the app icon on Windows.
use std::path::Path;
fn main() {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
if !Path::new("app.ico").exists() {
return;
}
println!("cargo:rerun-if-changed=app.ico");
let mut res = winresource::WindowsResource::new();
res.set_icon("app.ico");
if let Err(e) = res.compile() {
println!("cargo:warning=icon embed skipped ({e})");
}
}
"#
.to_string()
}
fn gen_kernel_cargo_toml(name: &str, version: &str, ling_root: &Path, graphics: bool) -> String {
let root_str = ling_root.display().to_string().replace('\\', "/");
let ling_kernel_dep = if graphics {
format!(r#"{{ path = "{root_str}/crates/ling-kernel", features = ["request_framebuffer"] }}"#)
} else {
format!(r#"{{ path = "{root_str}/crates/ling-kernel" }}"#)
};
format!(
r#"[workspace]
[package]
name = "{name}"
version = "{version}"
edition = "2021"
build = "build.rs"
[[bin]]
name = "{name}"
path = "src/main.rs"
[dependencies]
ling-kernel = {ling_kernel_dep}
[build-dependencies]
cc = "1.0"
[profile.release]
lto = "fat"
codegen-units = 1
opt-level = 3
panic = "abort"
strip = true
"#
)
}
fn gen_kernel_main_rs(idle_font: Option<&str>) -> String {
let idle_font_mod = idle_font.map(|m| format!("mod {m};\n")).unwrap_or_default();
let idle_font_reg = idle_font
.map(|m| format!(" ling_kernel::vga::set_idle_font(&{m}::IDLE_FONT);\n"))
.unwrap_or_default();
let template = r#"#![no_std]
#![no_main]
// Nothing below references `ling_kernel` by path — only `entry.o`'s compiled
// code calls its `#[no_mangle] extern "C"` functions, which is invisible to
// rustc's own "is this dependency used" tracking. Without this, rustc never
// links ling-kernel's rlib in at all, and the link fails with "undefined
// symbol: ling_kernel_vga_clear" (or whichever intrinsic is first referenced).
extern crate ling_kernel;
/*IDLE_FONT_MOD*/
extern "C" {
fn __main__() -> u64;
}
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn kernel_entry() -> ! {
unsafe {
/*IDLE_FONT_REG*/
__main__();
}
loop {
unsafe { core::arch::asm!("hlt"); }
}
}
"#;
template
.replace("/*IDLE_FONT_MOD*/", &idle_font_mod)
.replace("/*IDLE_FONT_REG*/", &idle_font_reg)
}
fn gen_kernel_build_rs() -> String {
r#"fn main() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
// NOTE: no `-nostartfiles` here — that's a cc-frontend flag, not a raw
// ld/lld one, and rustc links `*-unknown-none` targets by invoking the
// linker directly (no cc frontend in between).
println!("cargo:rustc-link-arg-bins=-static");
println!("cargo:rustc-link-arg-bins=-n");
println!("cargo:rustc-link-arg-bins=-T{}/linker.ld", manifest_dir);
println!("cargo:rustc-link-arg-bins={}/entry.o", manifest_dir);
println!("cargo:rerun-if-changed=entry.o");
println!("cargo:rerun-if-changed=linker.ld");
}
"#
.to_string()
}
fn gen_rpi_kernel_main_rs() -> String {
r#"#![no_std]
#![no_main]
// See the equivalent comment in `gen_kernel_main_rs` — forces ling-kernel's
// rlib to actually be linked (the `zero_bss` call below would do this too,
// but keep both: it's the correct idiom and doesn't depend on this function
// body never changing).
extern crate ling_kernel;
extern "C" {
fn __main__() -> u64;
}
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn kernel_entry() -> ! {
unsafe {
ling_kernel::boot::zero_bss();
__main__();
}
loop {
unsafe { core::arch::asm!("wfi"); }
}
}
"#
.to_string()
}
const KERNEL_LINKER_SCRIPT: &str = r#"OUTPUT_FORMAT(elf64-x86-64)
ENTRY(_start)
SECTIONS {
. = 1M;
.multiboot : ALIGN(8) {
KEEP(*(.ling_multiboot))
}
.text : ALIGN(4096) {
*(.text .text.*)
}
.rodata : ALIGN(4096) {
*(.rodata .rodata.*)
}
.data : ALIGN(4096) {
*(.data .data.*)
}
.bss : ALIGN(4096) {
*(.bss .bss.*)
*(COMMON)
}
/DISCARD/ : {
*(.eh_frame)
*(.comment)
*(.note.*)
}
}
"#;
const RPI_LINKER_SCRIPT: &str = r#"OUTPUT_FORMAT(elf64-littleaarch64)
ENTRY(_start)
SECTIONS {
. = 0x80000;
.text : ALIGN(4096) {
KEEP(*(.text.boot))
*(.text .text.*)
}
.rodata : ALIGN(4096) {
*(.rodata .rodata.*)
}
.data : ALIGN(4096) {
*(.data .data.*)
}
.bss (NOLOAD) : ALIGN(16) {
__bss_start = .;
*(.bss .bss.*)
*(COMMON)
__bss_end = .;
}
. = ALIGN(16);
. += 0x4000; /* 16KB boot stack */
_stack_top = .;
/DISCARD/ : {
*(.eh_frame)
*(.comment)
*(.note.*)
}
}
"#;
fn resolve_icon(cli: Option<&Path>, project: &LingProject, ling_root: &Path) -> Option<PathBuf> {
for candidate in [cli.map(Path::to_path_buf), project.icon.clone()]
.into_iter()
.flatten()
{
if candidate.exists() {
return Some(candidate);
}
eprintln!(
" [icon] '{}' not found; using default",
candidate.display()
);
}
let default = ling_root.join("ling-lang.org/images/logo.svg");
default.exists().then_some(default)
}
fn gen_main_rs(entry_file: &str, packed: bool) -> String {
let res_mod = if packed { "mod resources;\n" } else { "" };
let unpack = if packed {
" ling::unpack_resources(env!(\"CARGO_PKG_NAME\"), resources::RESOURCES);\n"
} else {
""
};
format!(
r#"// Built by ling build — no console window on Windows.
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
{res_mod}
fn main() {{
const SOURCE: &str = include_str!("../{entry_file}");
{unpack} let lang = ling::detect_language(SOURCE);
if lang != "English" {{
eprintln!("[language: {{}}]", lang);
}}
if let Err(e) = ling::run(SOURCE) {{
eprintln!("{{e}}");
std::process::exit(1);
}}
}}
"#
)
}
fn gen_aot_main_rs(packed: bool) -> String {
let res_mod = if packed { "mod resources;\n" } else { "" };
let unpack = if packed {
" ling::unpack_resources(env!(\"CARGO_PKG_NAME\"), resources::RESOURCES);\n"
} else {
""
};
format!(
r#"// Built by ling build --aot — no console window on Windows.
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
{res_mod}
fn main() {{
ling::runtime::init_aot_runtime();
{unpack}
extern "C" {{
fn __main__() -> u64;
}}
unsafe {{
__main__();
}}
}}
"#
)
}
fn gen_aot_build_rs() -> String {
r#"// Generated by `ling build --aot`. Links the AOT-compiled object file.
use std::path::Path;
fn main() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let obj = Path::new(&manifest_dir).join("entry.o");
println!("cargo:rustc-link-arg={}", obj.display());
println!("cargo:rerun-if-changed=entry.o");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
&& Path::new("app.ico").exists()
{
println!("cargo:rerun-if-changed=app.ico");
let mut res = winresource::WindowsResource::new();
res.set_icon("app.ico");
if let Err(e) = res.compile() {
println!("cargo:warning=icon embed skipped ({e})");
}
}
}
"#
.to_string()
}
fn parse_includes(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut in_section = false;
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') {
in_section = line == "[includes]" || line == "[包含]";
continue;
}
if let Some(rest) = line
.strip_prefix("includes")
.or_else(|| line.strip_prefix("包含"))
.and_then(|r| r.trim_start().strip_prefix('='))
{
collect_quoted(rest, &mut out);
continue;
}
if in_section {
collect_quoted(line, &mut out);
}
}
out
}
fn collect_quoted(s: &str, out: &mut Vec<String>) {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'"' {
if let Some(end) = s[i + 1..].find('"') {
let val = &s[i + 1..i + 1 + end];
if !val.is_empty() {
out.push(val.to_string());
}
i += end + 2;
continue;
}
}
i += 1;
}
}
fn load_ignore_patterns(root: &Path) -> Vec<String> {
let mut pats = Vec::new();
for name in ["ling.ignore", ".lingignore"] {
if let Ok(text) = std::fs::read_to_string(root.join(name)) {
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let line = line
.trim_start_matches("./")
.trim_start_matches('/')
.trim_end_matches('/');
if !line.is_empty() {
pats.push(line.replace('\\', "/"));
}
}
}
}
pats
}
fn is_ignored(pats: &[String], rel: &str) -> bool {
pats.iter().any(|p| {
if p.contains('*') || p.contains('?') {
glob_match(p, rel)
} else {
rel == p || rel.starts_with(&format!("{p}/"))
}
})
}
fn expand_includes(project: &LingProject) -> Vec<(String, PathBuf)> {
let root = &project.source_dir;
let mut all: Vec<(String, PathBuf)> = Vec::new();
let mut seen = std::collections::HashSet::new();
let files = walk_files(root); let ignore = load_ignore_patterns(root);
let mut skipped = 0usize;
for pat in &project.includes {
let pat = pat.trim().trim_start_matches("./").trim_start_matches('/');
if pat.is_empty() {
continue;
}
let has_glob = pat.contains('*') || pat.contains('?');
if !has_glob {
let abs = root.join(pat);
if abs.is_dir() {
let prefix = format!("{}/", pat.replace('\\', "/"));
for (rel, abs) in &files {
if rel.starts_with(&prefix) {
if is_ignored(&ignore, rel) {
skipped += 1;
continue;
}
if seen.insert(rel.clone()) {
all.push((rel.clone(), abs.clone()));
}
}
}
} else if abs.is_file() {
let rel = pat.replace('\\', "/");
if is_ignored(&ignore, &rel) {
skipped += 1;
} else if seen.insert(rel.clone()) {
all.push((rel, abs));
}
} else {
eprintln!(" [includes] no match for '{pat}'");
}
continue;
}
let mut matched = false;
for (rel, abs) in &files {
if glob_match(pat, rel) {
if is_ignored(&ignore, rel) {
skipped += 1;
matched = true; continue;
}
if seen.insert(rel.clone()) {
all.push((rel.clone(), abs.clone()));
matched = true;
}
}
}
if !matched {
eprintln!(" [includes] no match for '{pat}'");
}
}
if skipped > 0 {
println!(" ling.ignore: excluded {skipped} file(s)");
}
all
}
fn walk_files(root: &Path) -> Vec<(String, PathBuf)> {
fn rec(base: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) {
let Ok(entries) = std::fs::read_dir(dir) else { return };
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if path.is_dir() {
if matches!(
name.as_ref(),
".ling-build" | "灵碑" | "target" | "dist" | ".git"
) {
continue;
}
rec(base, &path, out);
} else if let Ok(rel) = path.strip_prefix(base) {
out.push((rel.to_string_lossy().replace('\\', "/"), path.clone()));
}
}
}
let mut out = Vec::new();
rec(root, root, &mut out);
out
}
fn glob_match(pattern: &str, path: &str) -> bool {
let pat: Vec<&str> = pattern.split('/').collect();
let seg: Vec<&str> = path.split('/').collect();
seg_match(&pat, &seg)
}
fn seg_match(pat: &[&str], seg: &[&str]) -> bool {
match pat.first() {
None => seg.is_empty(),
Some(&"**") => {
(0..=seg.len()).any(|i| seg_match(&pat[1..], &seg[i..]))
},
Some(p) => match seg.first() {
Some(s) if wildcard(p.as_bytes(), s.as_bytes()) => seg_match(&pat[1..], &seg[1..]),
_ => false,
},
}
}
fn wildcard(pat: &[u8], s: &[u8]) -> bool {
let (mut pi, mut si) = (0, 0);
let (mut star, mut mark) = (usize::MAX, 0);
while si < s.len() {
if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == s[si]) {
pi += 1;
si += 1;
} else if pi < pat.len() && pat[pi] == b'*' {
star = pi;
mark = si;
pi += 1;
} else if star != usize::MAX {
pi = star + 1;
mark += 1;
si = mark;
} else {
return false;
}
}
while pi < pat.len() && pat[pi] == b'*' {
pi += 1;
}
pi == pat.len()
}
fn write_packed_resources(build_dir: &Path, resources: &[(String, PathBuf)]) {
let res_root = build_dir.join("res");
let mut entries = String::new();
for (rel, abs) in resources {
let dst = res_root.join(rel);
if let Some(parent) = dst.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::copy(abs, &dst) {
eprintln!(" pack copy {rel}: {e}");
continue;
}
entries.push_str(&format!(
" ({rel:?}, include_bytes!(\"../res/{rel}\")),\n"
));
}
let module = format!(
"// Generated by `ling build --pack`. Embedded resource table.\n\
pub static RESOURCES: &[(&str, &[u8])] = &[\n{entries}];\n"
);
let _ = std::fs::write(build_dir.join("src/resources.rs"), module);
}
fn ensure_rustup_target(triple: &str) {
let Ok(out) = Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
else {
return;
};
let installed = String::from_utf8_lossy(&out.stdout);
if !installed.contains(triple) {
println!(" installing target {triple}…");
let _ = Command::new("rustup")
.args(["target", "add", triple])
.status();
}
}
fn choose_build_tool(platform: NativePlatform) -> &'static str {
if !platform.is_current_host() && has_cross() {
"cross"
} else {
"cargo"
}
}
fn has_cross() -> bool {
Command::new("cross").arg("--version").output().is_ok()
}
fn sibling_binary(name: &str) -> PathBuf {
if let Ok(exe) = std::env::current_exe() {
let candidate =
exe.parent()
.unwrap_or(Path::new("."))
.join(if cfg!(target_os = "windows") {
format!("{name}.exe")
} else {
name.to_string()
});
if candidate.exists() {
return candidate;
}
}
PathBuf::from(name)
}
fn find_ling_root() -> Option<PathBuf> {
if let Ok(home) = std::env::var("LING_HOME") {
let p = PathBuf::from(home);
if p.join("Cargo.toml").exists() {
return Some(p);
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(repo) = exe
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
{
if repo.join("Cargo.toml").exists() {
return Some(repo.to_path_buf());
}
}
}
let cwd = std::env::current_dir().ok()?;
if cwd.join("Cargo.toml").exists() {
return Some(cwd);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_includes_section_form() {
let toml = "\
[project]
name = \"game\"
[includes]
\"/music/*.wav\",
\"/font/*.otf\",
\"data.bin\"
";
let inc = parse_includes(toml);
assert_eq!(inc, vec!["/music/*.wav", "/font/*.otf", "data.bin"]);
}
#[test]
fn parses_includes_inline_array() {
let inc = parse_includes("includes = [\"a/*.png\", \"b.txt\"]\n");
assert_eq!(inc, vec!["a/*.png", "b.txt"]);
}
#[test]
fn includes_section_stops_at_next_header() {
let toml = "[includes]\n\"a.txt\"\n[other]\n\"b.txt\"\n";
assert_eq!(parse_includes(toml), vec!["a.txt"]);
}
#[test]
fn glob_matches_within_and_across_segments() {
assert!(glob_match("music/*.wav", "music/song.wav"));
assert!(!glob_match("music/*.wav", "music/sub/song.wav")); assert!(glob_match("music/**/*.wav", "music/a/b/song.wav")); assert!(glob_match("**/*.otf", "font/deep/x.otf"));
assert!(glob_match("data?.bin", "data7.bin"));
assert!(!glob_match("*.wav", "song.mp3"));
assert!(glob_match("font/x.otf", "font/x.otf")); }
}