diff --git a/.gitignore b/.gitignore index 6d6d89fd..74c142cb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ *.crate # Compiled crate files Cargo.lock +dist/ + # Build artifacts *.d # Dependency files generated by build tools *.o # Object files diff --git a/Cargo.toml b/Cargo.toml index 531f8d0c..aa813ef0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,12 @@ [package] name = "wavec" -version = "0.1.8-pre-beta" +version = "0.1.9-pre-beta" edition = "2021" [lib] name = "wavec" path = "src/lib.rs" -# [target.x86_64-apple-darwin] -# linker = "clang" -# [target.aarch64-apple-darwin] -# linker = "clang" - [dependencies] utils = { path = "utils" } lexer = { path = "front/lexer" } @@ -20,8 +15,11 @@ error = { path = "front/error" } llvm = { path = "./llvm", default-features = false } [features] -default = ["llvm-target-all"] +default = ["llvm-target-core64"] +llvm-target-core64 = ["llvm-target-x86", "llvm-target-aarch64", "llvm-target-riscv"] llvm-target-all = ["llvm/llvm-target-all"] +llvm-target-aarch64 = ["llvm/llvm-target-aarch64"] +llvm-target-riscv = ["llvm/llvm-target-riscv"] llvm-target-x86 = ["llvm/llvm-target-x86"] [workspace] diff --git a/llvm/Cargo.toml b/llvm/Cargo.toml index db4da564..7796fda9 100644 --- a/llvm/Cargo.toml +++ b/llvm/Cargo.toml @@ -10,7 +10,13 @@ error = { path = "../front/error" } inkwell = { version = "0.8.0", default-features = false, features = ["llvm21-1"] } llvm-sys = { version = "211.0.0", features = ["prefer-dynamic"] } +[target.'cfg(target_os = "windows")'.dependencies] +inkwell = { version = "0.8.0", default-features = false, features = ["no-libffi-linking"] } + [features] -default = ["llvm-target-all"] +default = ["llvm-target-core64"] +llvm-target-core64 = ["llvm-target-x86", "llvm-target-aarch64", "llvm-target-riscv"] llvm-target-all = ["inkwell/target-all"] +llvm-target-aarch64 = ["inkwell/target-aarch64"] +llvm-target-riscv = ["inkwell/target-riscv"] llvm-target-x86 = ["inkwell/target-x86"] diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index 9de75bb3..d4b97a6e 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -10,6 +10,8 @@ // SPDX-License-Identifier: MPL-2.0 // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. +use std::env; +use std::path::PathBuf; use std::process::Command; #[derive(Debug, Default, Clone)] @@ -34,7 +36,7 @@ fn is_windows_gnu_target(target: Option<&str>) -> bool { t.starts_with("x86_64-") && t.contains("windows") && !t.contains("msvc") } -fn normalize_clang_opt_flag(opt_flag: &str) -> &str { +fn normalize_llvm_opt_flag(opt_flag: &str) -> &str { match opt_flag { // LLVM pass pipeline currently has no dedicated Ofast preset, so keep // frontend/back-end optimization behavior aligned at O3. @@ -51,19 +53,28 @@ pub fn compile_ir_to_object( ) -> String { let object_path = format!("{}.o", file_stem); - let normalized_opt = normalize_clang_opt_flag(opt_flag); - let mut cmd = Command::new("clang"); + let normalized_opt = normalize_llvm_opt_flag(opt_flag); + let llc = resolve_bundled_tool("llc"); + let mut cmd = Command::new(&llc); + configure_bundled_llvm_tool_env(&mut cmd, &llc); if let Some(target) = &backend.target { - cmd.arg(format!("--target={}", target)); + cmd.arg(format!("--mtriple={}", target)); } - - if let Some(sysroot) = &backend.sysroot { - cmd.arg(format!("--sysroot={}", sysroot)); + if let Some(cpu) = &backend.cpu { + cmd.arg(format!("--mcpu={}", cpu)); + } + if let Some(features) = &backend.features { + cmd.arg(format!("--mattr={}", features)); + } + if let Some(model) = &backend.code_model { + cmd.arg(format!("--code-model={}", model)); + } + if let Some(model) = &backend.relocation_model { + cmd.arg(format!("--relocation-model={}", model)); } - if let Some(abi) = &backend.abi { - cmd.arg("-target-abi").arg(abi); + cmd.arg(format!("--target-abi={}", abi)); } if !normalized_opt.is_empty() { @@ -71,16 +82,13 @@ pub fn compile_ir_to_object( } let mut child = cmd - .arg("-c") - .arg("-x") - .arg("ir") + .arg("--filetype=obj") .arg("-") .arg("-o") .arg(&object_path) - .arg("-Wno-override-module") .stdin(std::process::Stdio::piped()) .spawn() - .expect("Failed to execute clang"); + .expect("Failed to execute llc"); use std::io::Write; child @@ -92,7 +100,7 @@ pub fn compile_ir_to_object( let output = child.wait_with_output().unwrap(); if !output.status.success() { - eprintln!("clang failed: {}", String::from_utf8_lossy(&output.stderr)); + eprintln!("llc failed: {}", String::from_utf8_lossy(&output.stderr)); return String::new(); } @@ -106,19 +114,16 @@ pub fn link_objects( lib_paths: &[String], backend: &BackendOptions, ) { - let linker_bin = backend.linker.as_deref().unwrap_or("clang"); - let mut cmd = Command::new(linker_bin); + let target = backend.target.as_deref().unwrap_or(""); + let linker_bin = backend + .linker + .clone() + .unwrap_or_else(|| default_lld_for_target(target)); + let mut cmd = Command::new(&linker_bin); + configure_bundled_llvm_tool_env(&mut cmd, &linker_bin); if backend.linker.is_none() { - if let Some(target) = &backend.target { - cmd.arg(format!("--target={}", target)); - } - if let Some(sysroot) = &backend.sysroot { - cmd.arg(format!("--sysroot={}", sysroot)); - } - if let Some(abi) = &backend.abi { - cmd.arg("-target-abi").arg(abi); - } + append_lld_target_args(&mut cmd, target, backend); } for obj in objects { @@ -133,14 +138,18 @@ pub fn link_objects( cmd.arg(format!("-l{}", lib)); } - for arg in &backend.link_args { + for arg in expand_lld_link_args(&backend.link_args) { cmd.arg(arg); } cmd.arg("-o").arg(output); - if !backend.no_default_libs && !is_windows_gnu_target(backend.target.as_deref()) { - cmd.arg("-lc").arg("-lm"); + if !backend.no_default_libs { + if is_darwin_target(target) { + cmd.arg("-lSystem"); + } else if !is_windows_gnu_target(backend.target.as_deref()) { + cmd.arg("-lc").arg("-lm"); + } } let output = cmd.output().expect("Failed to link"); @@ -148,3 +157,158 @@ pub fn link_objects( eprintln!("link failed: {}", String::from_utf8_lossy(&output.stderr)); } } + +fn default_lld_for_target(target: &str) -> String { + if is_darwin_target(target) { + resolve_bundled_tool("ld64.lld") + } else { + resolve_bundled_tool("ld.lld") + } +} + +fn append_lld_target_args(cmd: &mut Command, target: &str, backend: &BackendOptions) { + if is_darwin_target(target) { + cmd.arg("-arch") + .arg(if target.starts_with("x86_64-") { + "x86_64" + } else { + "arm64" + }) + .arg("-platform_version") + .arg("macos") + .arg("11.0") + .arg("11.0"); + + if let Some(sysroot) = &backend.sysroot { + cmd.arg("-syslibroot").arg(sysroot); + } + return; + } + + if is_windows_gnu_target(Some(target)) { + cmd.arg("-m").arg("i386pep"); + return; + } + + if let Some(emulation) = elf_lld_emulation(target) { + cmd.arg("-m").arg(emulation); + } + if let Some(sysroot) = &backend.sysroot { + cmd.arg(format!("--sysroot={}", sysroot)); + } +} + +fn expand_lld_link_args(link_args: &[String]) -> Vec { + let mut out = Vec::new(); + for arg in link_args { + if arg == "-nostartfiles" { + continue; + } + if let Some(rest) = arg.strip_prefix("-Wl,") { + out.extend( + rest.split(',') + .filter(|part| !part.is_empty()) + .map(|part| part.to_string()), + ); + } else { + out.push(arg.clone()); + } + } + out +} + +fn is_darwin_target(target: &str) -> bool { + target.contains("apple-darwin") +} + +fn elf_lld_emulation(target: &str) -> Option<&'static str> { + match target.split('-').next().unwrap_or(target) { + "x86_64" => Some("elf_x86_64"), + "aarch64" => Some("aarch64elf"), + "riscv64" => Some("elf64lriscv"), + _ => None, + } +} + +fn resolve_bundled_tool(tool: &str) -> String { + for dir in llvm_tool_search_dirs() { + let candidate = dir.join(executable_tool_name(tool)); + if candidate.is_file() { + return candidate.to_string_lossy().to_string(); + } + } + executable_tool_name(tool) +} + +fn configure_bundled_llvm_tool_env(cmd: &mut Command, bin: &str) { + let Some(lib_dir) = bundled_llvm_lib_dir(bin) else { + return; + }; + + if cfg!(target_os = "linux") { + prepend_env_path(cmd, "LD_LIBRARY_PATH", lib_dir); + } +} + +fn bundled_llvm_lib_dir(bin: &str) -> Option { + let bin_path = std::path::Path::new(bin); + let bin_dir = bin_path.parent()?; + if bin_dir.file_name().and_then(|name| name.to_str()) != Some("bin") { + return None; + } + + let llvm_dir = bin_dir.parent()?; + if llvm_dir.file_name().and_then(|name| name.to_str()) != Some("llvm") { + return None; + } + + let lib_dir = llvm_dir.join("lib"); + lib_dir.is_dir().then_some(lib_dir) +} + +fn prepend_env_path(cmd: &mut Command, name: &str, first: PathBuf) { + let mut paths = vec![first]; + if let Some(current) = env::var_os(name) { + paths.extend(env::split_paths(¤t)); + } + if let Ok(joined) = env::join_paths(paths) { + cmd.env(name, joined); + } +} + +fn executable_tool_name(tool: &str) -> String { + if cfg!(windows) && !tool.to_ascii_lowercase().ends_with(".exe") { + format!("{}.exe", tool) + } else { + tool.to_string() + } +} + +fn llvm_tool_search_dirs() -> Vec { + let mut dirs = Vec::new(); + + if let Ok(path) = env::var("WAVE_LLVM_BIN") { + if !path.trim().is_empty() { + dirs.push(PathBuf::from(path)); + } + } + for env_name in ["WAVE_LLVM_HOME", "LLVM_SYS_211_PREFIX"] { + if let Ok(path) = env::var(env_name) { + if !path.trim().is_empty() { + dirs.push(PathBuf::from(path).join("bin")); + } + } + } + if let Ok(exe) = env::current_exe() { + if let Some(dir) = exe.parent() { + dirs.push(dir.to_path_buf()); + dirs.push(dir.join("llvm").join("bin")); + if let Some(root) = dir.parent() { + dirs.push(root.join("llvm").join("bin")); + dirs.push(root.join("lib").join("wave").join("llvm").join("bin")); + } + } + } + + dirs +} diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index 256ee9d6..2b65cd3a 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -11,13 +11,15 @@ // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. use inkwell::context::Context; +use inkwell::module::Module; use inkwell::passes::PassBuilderOptions; use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; use inkwell::values::{BasicValue, BasicValueEnum, FunctionValue}; use inkwell::OptimizationLevel; use inkwell::targets::{ - CodeModel, InitializationConfig, RelocMode, Target, TargetData, TargetMachine, TargetTriple, + CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetData, TargetMachine, + TargetTriple, }; use parser::ast::{ ASTNode, EnumNode, ExternFunctionNode, FunctionNode, Mutability, ParameterNode, ProtoImplNode, @@ -34,6 +36,18 @@ use super::types::{wave_type_to_llvm_type, TypeFlavor, VariableInfo}; use crate::codegen::abi_c::{apply_extern_c_attrs, lower_extern_c, ExternCInfo}; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CodegenFileKind { + Bitcode, + Assembly, + Object, +} + +struct GeneratedModule { + module: &'static Module<'static>, + target_machine: TargetMachine, +} + fn is_implicit_i32_main(name: &str, return_type: &Option) -> bool { name == "main" && matches!(return_type, None | Some(WaveType::Void)) } @@ -44,7 +58,7 @@ fn is_supported_extern_abi(abi: &str) -> bool { fn normalize_opt_flag_for_passes(opt_flag: &str) -> &str { match opt_flag { - // Keep consistent with backend clang optimization mapping. + // LLVM's pass pipeline has no dedicated Ofast preset; keep it aligned with codegen tools. "-Ofast" => "-O3", other => other, } @@ -72,6 +86,16 @@ fn initialize_llvm_targets() { { Target::initialize_x86(&config); } + + #[cfg(all(not(feature = "llvm-target-all"), feature = "llvm-target-aarch64"))] + { + Target::initialize_aarch64(&config); + } + + #[cfg(all(not(feature = "llvm-target-all"), feature = "llvm-target-riscv"))] + { + Target::initialize_riscv(&config); + } } pub unsafe fn generate_ir( @@ -79,6 +103,53 @@ pub unsafe fn generate_ir( opt_flag: &str, backend: &BackendOptions, ) -> String { + let generated = build_module(ast_nodes, opt_flag, backend); + generated.module.print_to_string().to_string() +} + +pub unsafe fn emit_codegen_file( + ast_nodes: &[ASTNode], + opt_flag: &str, + backend: &BackendOptions, + output: &std::path::Path, + kind: CodegenFileKind, +) { + let generated = build_module(ast_nodes, opt_flag, backend); + + match kind { + CodegenFileKind::Bitcode => { + if !generated.module.write_bitcode_to_path(output) { + panic!("failed to write LLVM bitcode to '{}'", output.display()); + } + } + CodegenFileKind::Assembly => generated + .target_machine + .write_to_file(generated.module, FileType::Assembly, output) + .unwrap_or_else(|e| { + panic!( + "failed to emit LLVM assembly to '{}': {}", + output.display(), + e.to_string() + ) + }), + CodegenFileKind::Object => generated + .target_machine + .write_to_file(generated.module, FileType::Object, output) + .unwrap_or_else(|e| { + panic!( + "failed to emit object file to '{}': {}", + output.display(), + e.to_string() + ) + }), + } +} + +fn build_module( + ast_nodes: &[ASTNode], + opt_flag: &str, + backend: &BackendOptions, +) -> GeneratedModule { let context: &'static Context = Box::leak(Box::new(Context::create())); let module: &'static _ = Box::leak(Box::new(context.create_module("main"))); let builder: &'static _ = Box::leak(Box::new(context.create_builder())); @@ -431,7 +502,10 @@ pub unsafe fn generate_ir( .run_passes(pipeline, &tm, pbo) .expect("failed to run optimization passes"); - module.print_to_string().to_string() + GeneratedModule { + module, + target_machine: tm, + } } fn pipeline_from_opt_flag(opt_flag: &str) -> &'static str { diff --git a/llvm/src/codegen/mod.rs b/llvm/src/codegen/mod.rs index ecd4c5c6..b23ec842 100644 --- a/llvm/src/codegen/mod.rs +++ b/llvm/src/codegen/mod.rs @@ -22,7 +22,7 @@ pub mod types; pub use address::{generate_address_and_type_ir, generate_address_ir}; pub use format::{wave_format_to_c, wave_format_to_scanf}; -pub use ir::generate_ir; +pub use ir::{emit_codegen_file, generate_ir, CodegenFileKind}; pub use types::{wave_type_to_llvm_type, VariableInfo}; pub use legacy::{create_alloc, get_llvm_type}; diff --git a/llvm/src/expression/rvalue/literals.rs b/llvm/src/expression/rvalue/literals.rs index 397b4a35..5c340991 100644 --- a/llvm/src/expression/rvalue/literals.rs +++ b/llvm/src/expression/rvalue/literals.rs @@ -36,6 +36,15 @@ fn parse_int_radix(s: &str) -> (StringRadix, &str) { } } +fn is_zero_int_literal(s: &str) -> bool { + let s = s.trim(); + let s = s.strip_prefix('+').unwrap_or(s); + let (_neg, raw) = parse_signed_decimal(s); + let (_radix, digits) = parse_int_radix(raw); + + !digits.is_empty() && digits.chars().all(|c| c == '0') +} + pub(crate) fn gen_null<'ctx, 'a>( env: &mut ExprGenEnv<'ctx, 'a>, expected_type: Option>, @@ -82,10 +91,14 @@ pub(crate) fn gen<'ctx, 'a>( return gen(env, lit, Some(elem)); } - Some(BasicTypeEnum::PointerType(_)) => { - panic!( - "integer literals cannot initialize pointers; use `null` or an explicit cast", - ) + Some(BasicTypeEnum::PointerType(ptr_ty)) => { + if is_zero_int_literal(v) { + ptr_ty.const_null().as_basic_value_enum() + } else { + panic!( + "integer literals cannot initialize pointers; use `null` or an explicit cast", + ) + } } Some(BasicTypeEnum::FloatType(ft)) => { diff --git a/src/cli.rs b/src/cli.rs index 3e449641..a1c5e0f0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,7 +21,6 @@ use std::collections::BTreeSet; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::process::{self, Command as ProcessCommand, Stdio}; -use std::time::{SystemTime, UNIX_EPOCH}; use std::{env, fs}; use utils::colorex::*; @@ -306,6 +305,10 @@ fn dispatch_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> fn effective_global_for_build(global: &Global, build: &BuildRequest) -> Global { let mut out = global.clone(); + if out.llvm.target.is_none() { + out.llvm.target = Some(host_target_triple()); + } + if build.freestanding { out.llvm.no_default_libs = true; } @@ -346,7 +349,7 @@ fn dispatch_print(global: &Global, item: &str, target_arg: Option<&str>) -> Resu Ok(()) } "sysroot" => { - if let Some(s) = detect_clang_sysroot() { + if let Some(s) = detect_default_sysroot(&target) { println!("{}", s); } else { println!(); @@ -361,7 +364,7 @@ fn dispatch_print(global: &Global, item: &str, target_arg: Option<&str>) -> Resu Ok(()) } "default-linker" => { - println!("{}", global.llvm.linker.as_deref().unwrap_or("clang")); + println!("{}", default_linker_name(global)); Ok(()) } "supported-input-types" => { @@ -1746,20 +1749,18 @@ fn execute_explicit_emit_artifacts( _ => {} }, EmitKind::Bc => match input.kind { - InputKind::Wave => { - let text = unsafe { - runner::emit_wave_ir_text( - &input.path, - &global.opt, - &global.debug, - &global.dep, - &global.llvm, - ) - }; - emit_ir_text_via_clang(global, &text, &output, EmitKind::Bc)?; - } + InputKind::Wave => unsafe { + runner::emit_wave_bitcode_file( + &input.path, + &global.opt, + &global.debug, + &global.dep, + &global.llvm, + &output, + ); + }, InputKind::Ir => { - compile_lowering_with_clang( + compile_lowering_with_llvm_tools( global, &input.path, InputKind::Ir, @@ -1771,20 +1772,18 @@ fn execute_explicit_emit_artifacts( _ => {} }, EmitKind::Asm => match input.kind { - InputKind::Wave => { - let text = unsafe { - runner::emit_wave_ir_text( - &input.path, - &global.opt, - &global.debug, - &global.dep, - &global.llvm, - ) - }; - emit_ir_text_via_clang(global, &text, &output, EmitKind::Asm)?; - } + InputKind::Wave => unsafe { + runner::emit_wave_assembly_file( + &input.path, + &global.opt, + &global.debug, + &global.dep, + &global.llvm, + &output, + ); + }, InputKind::Ir | InputKind::Bc => { - compile_lowering_with_clang( + compile_lowering_with_llvm_tools( global, &input.path, input.kind, @@ -1805,144 +1804,143 @@ fn execute_explicit_emit_artifacts( fn compile_non_wave_to_object(global: &Global, job: &CompileJob) -> Result<(), CliError> { ensure_parent_dir(&job.output)?; - compile_lowering_with_clang(global, &job.input, job.kind, &job.output, EmitKind::Obj) + compile_lowering_with_llvm_tools(global, &job.input, job.kind, &job.output, EmitKind::Obj) } -fn compile_lowering_with_clang( +fn compile_lowering_with_llvm_tools( global: &Global, input: &Path, input_kind: InputKind, output: &Path, emit_kind: EmitKind, ) -> Result<(), CliError> { - let (bin, args) = build_clang_lowering_args(global, input, input_kind, output, emit_kind); - let output = ProcessCommand::new(&bin) - .args(&args) - .output() - .map_err(|e| { - if e.kind() == ErrorKind::NotFound && bin == "clang" { - CliError::ExternalToolMissing("clang") - } else { - CliError::Io(e) - } - })?; + let (bin, args) = build_llvm_lowering_args(global, input, input_kind, output, emit_kind); + let mut command = ProcessCommand::new(&bin); + configure_bundled_llvm_tool_env(&mut command, &bin); - if output.status.success() { + let process_output = command.args(&args).output().map_err(|e| { + if e.kind() == ErrorKind::NotFound { + CliError::ExternalToolMissing(linker_tool_name(&bin)) + } else { + CliError::Io(e) + } + })?; + + if process_output.status.success() { return Ok(()); } - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&process_output.stderr) + .trim() + .to_string(); + let stdout = String::from_utf8_lossy(&process_output.stdout) + .trim() + .to_string(); Err(CliError::CommandFailed(format!( "{} failed (status={})\nstdout: {}\nstderr: {}", emit_kind_name(emit_kind), - output.status, + process_output.status, stdout, stderr ))) } -fn build_clang_lowering_args( +fn build_llvm_lowering_args( global: &Global, input: &Path, input_kind: InputKind, output: &Path, emit_kind: EmitKind, +) -> (String, Vec) { + match (input_kind, emit_kind) { + (InputKind::Ir, EmitKind::Bc) => { + let args = vec![ + input.to_string_lossy().to_string(), + "-o".to_string(), + output.to_string_lossy().to_string(), + ]; + (resolve_bundled_tool("llvm-as"), args) + } + (InputKind::Ir | InputKind::Bc, EmitKind::Obj | EmitKind::Asm) => { + build_llc_lowering_args(global, input, output, emit_kind) + } + (InputKind::Asm, EmitKind::Obj) => build_llvm_mc_lowering_args(global, input, output), + _ => ( + resolve_bundled_tool("llvm-as"), + vec!["--version".to_string()], + ), + } +} + +fn build_llc_lowering_args( + global: &Global, + input: &Path, + output: &Path, + emit_kind: EmitKind, ) -> (String, Vec) { let mut args = Vec::new(); + args.push(format!( + "--filetype={}", + match emit_kind { + EmitKind::Asm => "asm", + _ => "obj", + } + )); + if let Some(target) = &global.llvm.target { - args.push(format!("--target={}", target)); + args.push(format!("--mtriple={}", target)); } - if let Some(sysroot) = &global.llvm.sysroot { - args.push(format!("--sysroot={}", sysroot)); + if let Some(cpu) = &global.llvm.cpu { + args.push(format!("--mcpu={}", cpu)); } - if let Some(abi) = &global.llvm.abi { - args.push("-target-abi".to_string()); - args.push(abi.to_string()); + if let Some(features) = &global.llvm.features { + args.push(format!("--mattr={}", features)); } - - if !global.opt.is_empty() { - args.push(normalize_opt_for_clang(&global.opt).to_string()); + if let Some(model) = &global.llvm.code_model { + args.push(format!("--code-model={}", model)); } - - if input_kind == InputKind::Ir { - args.push("-x".to_string()); - args.push("ir".to_string()); + if let Some(model) = &global.llvm.relocation_model { + args.push(format!("--relocation-model={}", model)); + } + if let Some(abi) = &global.llvm.abi { + args.push(format!("--target-abi={}", abi)); } - match emit_kind { - EmitKind::Obj => { - args.push("-c".to_string()); - } - EmitKind::Bc => { - args.push("-c".to_string()); - args.push("-emit-llvm".to_string()); - } - EmitKind::Asm => { - args.push("-S".to_string()); - } - _ => {} + if !global.opt.is_empty() { + args.push(normalize_opt_for_llvm_tool(&global.opt).to_string()); } args.push(input.to_string_lossy().to_string()); args.push("-o".to_string()); args.push(output.to_string_lossy().to_string()); - ("clang".to_string(), args) + (resolve_bundled_tool("llc"), args) } -fn emit_ir_text_via_clang( +fn build_llvm_mc_lowering_args( global: &Global, - ir_text: &str, + input: &Path, output: &Path, - emit_kind: EmitKind, -) -> Result<(), CliError> { - let temp_input = temp_ir_input_path(); - fs::write(&temp_input, ir_text)?; - let result = compile_lowering_with_clang(global, &temp_input, InputKind::Ir, output, emit_kind); - let _ = fs::remove_file(&temp_input); - result -} - -fn temp_ir_input_path() -> PathBuf { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - env::temp_dir().join(format!("wavec-{}-{}.ll", process::id(), now)) -} - -fn build_clang_compile_args(global: &Global, job: &CompileJob) -> (String, Vec) { +) -> (String, Vec) { let mut args = Vec::new(); if let Some(target) = &global.llvm.target { - args.push(format!("--target={}", target)); - } - if let Some(sysroot) = &global.llvm.sysroot { - args.push(format!("--sysroot={}", sysroot)); - } - if let Some(abi) = &global.llvm.abi { - args.push("-target-abi".to_string()); - args.push(abi.to_string()); + args.push(format!("--triple={}", target)); } - - if !global.opt.is_empty() { - args.push(normalize_opt_for_clang(&global.opt).to_string()); + if let Some(cpu) = &global.llvm.cpu { + args.push(format!("--mcpu={}", cpu)); } - - if job.kind == InputKind::Ir { - args.push("-x".to_string()); - args.push("ir".to_string()); + if let Some(features) = &global.llvm.features { + args.push(format!("--mattr={}", features)); } - - args.push("-c".to_string()); - args.push(job.input.to_string_lossy().to_string()); + args.push("--filetype=obj".to_string()); + args.push(input.to_string_lossy().to_string()); args.push("-o".to_string()); - args.push(job.output.to_string_lossy().to_string()); + args.push(output.to_string_lossy().to_string()); - ("clang".to_string(), args) + (resolve_bundled_tool("llvm-mc"), args) } fn link_objects( @@ -1954,16 +1952,16 @@ fn link_objects( ensure_parent_dir(output)?; let (bin, args) = build_linker_args(global, build, objects, output); - let out = ProcessCommand::new(&bin) - .args(&args) - .output() - .map_err(|e| { - if e.kind() == ErrorKind::NotFound && bin == "clang" { - CliError::ExternalToolMissing("clang") - } else { - CliError::Io(e) - } - })?; + let mut command = ProcessCommand::new(&bin); + configure_bundled_llvm_tool_env(&mut command, &bin); + + let out = command.args(&args).output().map_err(|e| { + if e.kind() == ErrorKind::NotFound { + CliError::ExternalToolMissing(linker_tool_name(&bin)) + } else { + CliError::Io(e) + } + })?; if out.status.success() { return Ok(()); @@ -1984,45 +1982,177 @@ fn build_linker_args( objects: &[String], output: &Path, ) -> (String, Vec) { - let linker_bin = global - .llvm - .linker - .clone() - .unwrap_or_else(|| "clang".to_string()); + if let Some(linker) = &global.llvm.linker { + return build_user_linker_args(linker, global, build, objects, output); + } + let target = target_triple_for_global(global); + if is_darwin_target(&target) { + build_darwin_lld_args(global, build, objects, output, &target) + } else if is_windows_gnu_target(&target) { + build_windows_gnu_lld_args(global, build, objects, output) + } else { + build_elf_lld_args(global, build, objects, output, &target) + } +} + +fn build_user_linker_args( + linker: &str, + global: &Global, + build: &BuildRequest, + objects: &[String], + output: &Path, +) -> (String, Vec) { let mut args = Vec::new(); - if global.llvm.linker.is_none() { - if let Some(target) = &global.llvm.target { - args.push(format!("--target={}", target)); - } - if let Some(sysroot) = &global.llvm.sysroot { - args.push(format!("--sysroot={}", sysroot)); - } - if let Some(abi) = &global.llvm.abi { - args.push("-target-abi".to_string()); - args.push(abi.to_string()); - } + for obj in objects { + args.push(obj.clone()); + } + append_link_search_and_libs(&mut args, global); + args.extend(global.llvm.link_args.iter().cloned()); + append_common_link_mode_args(&mut args, build, LinkerDialect::Gnu); + + args.push("-o".to_string()); + args.push(output.to_string_lossy().to_string()); + + if !global.llvm.no_default_libs && !is_windows_gnu_target_global(global) { + args.push("-lc".to_string()); + args.push("-lm".to_string()); + } + + (linker.to_string(), args) +} + +fn build_darwin_lld_args( + global: &Global, + build: &BuildRequest, + objects: &[String], + output: &Path, + target: &str, +) -> (String, Vec) { + let mut args = Vec::new(); + args.push("-arch".to_string()); + args.push(darwin_arch(target).to_string()); + + let macos_version = macos_deployment_version(); + args.push("-platform_version".to_string()); + args.push("macos".to_string()); + args.push(macos_version.clone()); + args.push(macos_version); + + let detected_sysroot = detect_macos_sysroot_owned(); + if let Some(sysroot) = global + .llvm + .sysroot + .as_deref() + .or(detected_sysroot.as_deref()) + { + args.push("-syslibroot".to_string()); + args.push(sysroot.to_string()); } for obj in objects { args.push(obj.clone()); } + append_link_search_and_libs(&mut args, global); + append_lld_link_args(&mut args, &global.llvm.link_args); + append_common_link_mode_args(&mut args, build, LinkerDialect::Darwin); - for path in &global.link.paths { - args.push(format!("-L{}", path)); + args.push("-o".to_string()); + args.push(output.to_string_lossy().to_string()); + + if !global.llvm.no_default_libs { + args.push("-lSystem".to_string()); } - for lib in &global.link.libs { - args.push(format!("-l{}", lib)); + (resolve_bundled_tool("ld64.lld"), args) +} + +fn build_windows_gnu_lld_args( + global: &Global, + build: &BuildRequest, + objects: &[String], + output: &Path, +) -> (String, Vec) { + let mut args = vec!["-m".to_string(), "i386pep".to_string()]; + + for obj in objects { + args.push(obj.clone()); + } + append_link_search_and_libs(&mut args, global); + append_lld_link_args(&mut args, &global.llvm.link_args); + append_common_link_mode_args(&mut args, build, LinkerDialect::Gnu); + + args.push("-o".to_string()); + args.push(output.to_string_lossy().to_string()); + + (resolve_bundled_tool("ld.lld"), args) +} + +fn build_elf_lld_args( + global: &Global, + build: &BuildRequest, + objects: &[String], + output: &Path, + target: &str, +) -> (String, Vec) { + let mut args = Vec::new(); + + if let Some(emulation) = elf_lld_emulation(target) { + args.push("-m".to_string()); + args.push(emulation.to_string()); + } + if let Some(sysroot) = &global.llvm.sysroot { + args.push(format!("--sysroot={}", sysroot)); + } + if !global.llvm.no_default_libs && is_linux_target(target) && !build.shared { + if let Some(dynamic_linker) = linux_dynamic_linker(target) { + args.push(format!("--dynamic-linker={}", dynamic_linker)); + } + append_elf_start_files(&mut args, target, global, build); + } + + for obj in objects { + args.push(obj.clone()); + } + if !global.llvm.no_default_libs && is_linux_target(target) { + append_elf_search_paths(&mut args, target, global); } + append_link_search_and_libs(&mut args, global); + append_lld_link_args(&mut args, &global.llvm.link_args); + append_common_link_mode_args(&mut args, build, LinkerDialect::Gnu); - for arg in &global.llvm.link_args { - args.push(arg.clone()); + if !global.llvm.no_default_libs && is_linux_target(target) { + args.push("-lc".to_string()); + args.push("-lm".to_string()); + append_elf_end_files(&mut args, target, global); } + args.push("-o".to_string()); + args.push(output.to_string_lossy().to_string()); + + (resolve_bundled_tool("ld.lld"), args) +} + +#[derive(Clone, Copy)] +enum LinkerDialect { + Gnu, + Darwin, +} + +fn append_common_link_mode_args( + args: &mut Vec, + build: &BuildRequest, + dialect: LinkerDialect, +) { if build.shared { - args.push("-shared".to_string()); + args.push( + match dialect { + LinkerDialect::Gnu => "-shared", + LinkerDialect::Darwin => "-dylib", + } + .to_string(), + ); } if build.static_link { args.push("-static".to_string()); @@ -2031,21 +2161,304 @@ fn build_linker_args( args.push("-pie".to_string()); } if build.pie == Some(false) { - args.push("-no-pie".to_string()); + args.push( + match dialect { + LinkerDialect::Gnu => "-no-pie", + LinkerDialect::Darwin => "-no_pie", + } + .to_string(), + ); } +} - args.push("-o".to_string()); - args.push(output.to_string_lossy().to_string()); +fn append_link_search_and_libs(args: &mut Vec, global: &Global) { + for path in &global.link.paths { + args.push(format!("-L{}", path)); + } + for lib in &global.link.libs { + args.push(format!("-l{}", lib)); + } +} - if !global.llvm.no_default_libs && !is_windows_gnu_target_global(global) { - args.push("-lc".to_string()); - args.push("-lm".to_string()); +fn append_lld_link_args(args: &mut Vec, link_args: &[String]) { + for arg in link_args { + if arg == "-nostartfiles" { + continue; + } + if let Some(rest) = arg.strip_prefix("-Wl,") { + args.extend( + rest.split(',') + .filter(|part| !part.is_empty()) + .map(|part| part.to_string()), + ); + } else { + args.push(arg.clone()); + } + } +} + +fn target_triple_for_global(global: &Global) -> String { + global + .llvm + .target + .clone() + .unwrap_or_else(host_target_triple) +} + +fn is_darwin_target(target: &str) -> bool { + target.contains("apple-darwin") +} + +fn is_linux_target(target: &str) -> bool { + target.contains("linux") +} + +fn target_arch(target: &str) -> &str { + target.split('-').next().unwrap_or(target) +} + +fn darwin_arch(target: &str) -> &'static str { + match target_arch(target) { + "aarch64" => "arm64", + "x86_64" => "x86_64", + _ => "arm64", + } +} + +fn elf_lld_emulation(target: &str) -> Option<&'static str> { + match target_arch(target) { + "x86_64" => Some("elf_x86_64"), + "aarch64" => Some("aarch64elf"), + "riscv64" => Some("elf64lriscv"), + _ => None, + } +} + +fn linux_dynamic_linker(target: &str) -> Option<&'static str> { + match target_arch(target) { + "x86_64" => Some("/lib64/ld-linux-x86-64.so.2"), + "aarch64" => Some("/lib/ld-linux-aarch64.so.1"), + "riscv64" => Some("/lib/ld-linux-riscv64-lp64d.so.1"), + _ => None, + } +} + +fn linux_multiarch(target: &str) -> Option<&'static str> { + match target_arch(target) { + "x86_64" => Some("x86_64-linux-gnu"), + "aarch64" => Some("aarch64-linux-gnu"), + "riscv64" => Some("riscv64-linux-gnu"), + _ => None, + } +} + +fn append_elf_start_files( + args: &mut Vec, + target: &str, + global: &Global, + build: &BuildRequest, +) { + if build.no_start_files { + return; + } + + let start_name = if build.pie == Some(true) { + "Scrt1.o" + } else { + "crt1.o" + }; + for file in [start_name, "crti.o"] { + if let Some(path) = find_elf_runtime_file(target, global, file) { + args.push(path); + } + } +} + +fn append_elf_end_files(args: &mut Vec, target: &str, global: &Global) { + if let Some(path) = find_elf_runtime_file(target, global, "crtn.o") { + args.push(path); + } +} + +fn append_elf_search_paths(args: &mut Vec, target: &str, global: &Global) { + for path in elf_runtime_dirs(target, global) { + if path.exists() { + args.push(format!("-L{}", path.display())); + } } +} + +fn find_elf_runtime_file(target: &str, global: &Global, name: &str) -> Option { + elf_runtime_dirs(target, global) + .into_iter() + .map(|dir| dir.join(name)) + .find(|path| path.exists()) + .map(|path| path.to_string_lossy().to_string()) +} + +fn elf_runtime_dirs(target: &str, global: &Global) -> Vec { + let sysroot = global.llvm.sysroot.as_deref().unwrap_or(""); + let mut dirs = Vec::new(); + + if let Some(multiarch) = linux_multiarch(target) { + dirs.push(sysroot_path(sysroot, &format!("usr/lib/{}", multiarch))); + dirs.push(sysroot_path(sysroot, &format!("lib/{}", multiarch))); + } + dirs.push(sysroot_path(sysroot, "usr/lib64")); + dirs.push(sysroot_path(sysroot, "lib64")); + dirs.push(sysroot_path(sysroot, "usr/lib")); + dirs.push(sysroot_path(sysroot, "lib")); + dirs +} + +fn sysroot_path(sysroot: &str, suffix: &str) -> PathBuf { + if sysroot.is_empty() { + PathBuf::from("/").join(suffix) + } else { + Path::new(sysroot).join(suffix) + } +} + +fn macos_deployment_version() -> String { + if let Ok(value) = env::var("MACOSX_DEPLOYMENT_TARGET") { + let value = value.trim(); + if !value.is_empty() { + return value.to_string(); + } + } + + ProcessCommand::new("sw_vers") + .arg("-productVersion") + .output() + .ok() + .filter(|out| out.status.success()) + .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "11.0".to_string()) +} + +fn detect_macos_sysroot_owned() -> Option { + if let Ok(value) = env::var("SDKROOT") { + let value = value.trim().to_string(); + if !value.is_empty() { + return Some(value); + } + } + + ProcessCommand::new("xcrun") + .args(["--sdk", "macosx", "--show-sdk-path"]) + .output() + .ok() + .filter(|out| out.status.success()) + .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) + .filter(|value| !value.is_empty()) +} - (linker_bin, args) +fn resolve_bundled_tool(tool: &str) -> String { + for dir in llvm_tool_search_dirs() { + let candidate = dir.join(executable_tool_name(tool)); + if candidate.is_file() { + return candidate.to_string_lossy().to_string(); + } + } + executable_tool_name(tool) } -fn normalize_opt_for_clang(flag: &str) -> &str { +fn configure_bundled_llvm_tool_env(cmd: &mut ProcessCommand, bin: &str) { + let Some(lib_dir) = bundled_llvm_lib_dir(bin) else { + return; + }; + + if cfg!(target_os = "linux") { + prepend_env_path(cmd, "LD_LIBRARY_PATH", lib_dir); + } +} + +fn bundled_llvm_lib_dir(bin: &str) -> Option { + let bin_path = Path::new(bin); + let bin_dir = bin_path.parent()?; + if bin_dir.file_name().and_then(|name| name.to_str()) != Some("bin") { + return None; + } + + let llvm_dir = bin_dir.parent()?; + if llvm_dir.file_name().and_then(|name| name.to_str()) != Some("llvm") { + return None; + } + + let lib_dir = llvm_dir.join("lib"); + lib_dir.is_dir().then_some(lib_dir) +} + +fn prepend_env_path(cmd: &mut ProcessCommand, name: &str, first: PathBuf) { + let mut paths = vec![first]; + if let Some(current) = env::var_os(name) { + paths.extend(env::split_paths(¤t)); + } + if let Ok(joined) = env::join_paths(paths) { + cmd.env(name, joined); + } +} + +fn executable_tool_name(tool: &str) -> String { + if cfg!(windows) && !tool.to_ascii_lowercase().ends_with(".exe") { + format!("{}.exe", tool) + } else { + tool.to_string() + } +} + +fn llvm_tool_search_dirs() -> Vec { + let mut dirs = Vec::new(); + + if let Ok(path) = env::var("WAVE_LLVM_BIN") { + if !path.trim().is_empty() { + dirs.push(PathBuf::from(path)); + } + } + for env_name in ["WAVE_LLVM_HOME", "LLVM_SYS_211_PREFIX"] { + if let Ok(path) = env::var(env_name) { + if !path.trim().is_empty() { + dirs.push(PathBuf::from(path).join("bin")); + } + } + } + if let Ok(exe) = env::current_exe() { + if let Some(dir) = exe.parent() { + dirs.push(dir.to_path_buf()); + dirs.push(dir.join("llvm").join("bin")); + if let Some(root) = dir.parent() { + dirs.push(root.join("llvm").join("bin")); + dirs.push(root.join("lib").join("wave").join("llvm").join("bin")); + } + } + } + + dirs +} + +fn linker_tool_name(bin: &str) -> String { + Path::new(bin) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(bin) + .to_string() +} + +fn default_linker_name(global: &Global) -> String { + if let Some(linker) = &global.llvm.linker { + return linker.clone(); + } + + let target = target_triple_for_global(global); + if is_darwin_target(&target) { + resolve_bundled_tool("ld64.lld") + } else { + resolve_bundled_tool("ld.lld") + } +} + +fn normalize_opt_for_llvm_tool(flag: &str) -> &str { match flag { "-Ofast" => "-O3", other => other, @@ -2102,7 +2515,7 @@ fn dry_run_explicit_emit_steps( } (EmitKind::Bc, InputKind::Wave) | (EmitKind::Asm, InputKind::Wave) => { format!( - "[wave frontend + clang] {} -> {} ({})", + "[wave frontend + LLVM] {} -> {} ({})", input.path.display(), output.display(), emit_kind_name(kind) @@ -2112,7 +2525,7 @@ fn dry_run_explicit_emit_steps( | (EmitKind::Asm, InputKind::Ir) | (EmitKind::Asm, InputKind::Bc) => { let (bin, args) = - build_clang_lowering_args(global, &input.path, input.kind, &output, kind); + build_llvm_lowering_args(global, &input.path, input.kind, &output, kind); shell_join(&bin, &args) } (EmitKind::Bc, InputKind::Bc) | (EmitKind::Asm, InputKind::Asm) => { @@ -2186,12 +2599,18 @@ fn print_dry_run_human( for job in &plan.compile_jobs { if job.kind == InputKind::Wave { println!( - " - [wave frontend] {} -> {}", + " - [wave frontend + LLVM] {} -> {}", job.input.display(), job.output.display() ); } else { - let (bin, args) = build_clang_compile_args(global, job); + let (bin, args) = build_llvm_lowering_args( + global, + &job.input, + job.kind, + &job.output, + EmitKind::Obj, + ); println!(" - {}", shell_join(&bin, &args)); } } @@ -2336,7 +2755,8 @@ fn print_dry_run_json( job.output.display() ) } else { - let (bin, args) = build_clang_compile_args(global, job); + let (bin, args) = + build_llvm_lowering_args(global, &job.input, job.kind, &job.output, EmitKind::Obj); shell_join(&bin, &args) }; @@ -2461,36 +2881,37 @@ fn host_target_triple() -> String { let os_part = match env::consts::OS { "linux" => "unknown-linux-gnu".to_string(), "macos" => "apple-darwin".to_string(), - "windows" => "pc-windows-msvc".to_string(), + "windows" => "pc-windows-gnu".to_string(), other => format!("unknown-{}", other), }; format!("{}-{}", arch, os_part) } -fn supported_targets() -> &'static [&'static str] { - #[cfg(all(feature = "llvm-target-x86", not(feature = "llvm-target-all")))] - { - return &[ - "x86_64-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-w64-windows-gnu", - "x86_64-pc-windows-gnu", - "x86_64-unknown-none-elf", - ]; - } +fn supported_targets() -> Vec<&'static str> { + let mut targets = Vec::new(); - #[cfg(not(all(feature = "llvm-target-x86", not(feature = "llvm-target-all"))))] - &[ + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] + targets.extend([ "x86_64-unknown-linux-gnu", - "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", - "aarch64-apple-darwin", "x86_64-w64-windows-gnu", "x86_64-pc-windows-gnu", "x86_64-unknown-none-elf", + ]); + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] + targets.extend([ + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", "aarch64-unknown-none-elf", - "riscv64-unknown-none-elf", - ] + ]); + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] + targets.extend(["riscv64-unknown-none-elf"]); + + targets.sort_unstable(); + targets.dedup(); + targets } fn is_windows_gnu_target(target: &str) -> bool { @@ -2541,18 +2962,12 @@ fn target_features_for_target(target: &str) -> Vec<&'static str> { } } -fn detect_clang_sysroot() -> Option { - let out = ProcessCommand::new("clang") - .arg("--print-sysroot") - .output() - .ok()?; - - if !out.status.success() { - return None; +fn detect_default_sysroot(target: &str) -> Option { + if is_darwin_target(target) { + detect_macos_sysroot_owned() + } else { + None } - - let text = String::from_utf8_lossy(&out.stdout).trim().to_string(); - Some(text) } pub fn print_usage() { @@ -2777,7 +3192,7 @@ pub fn print_help() { println!( " {:<24} {}", "-C linker=".color("38,139,235"), - "Override linker executable" + "Override linker executable (default: bundled LLD)" ); println!( " {:<24} {}", diff --git a/src/errors.rs b/src/errors.rs index cbc3faaf..8565dac2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,7 +19,7 @@ pub enum CliError { // std StdAlreadyInstalled { path: PathBuf }, - ExternalToolMissing(&'static str), + ExternalToolMissing(String), CommandFailed(String), HomeNotSet, diff --git a/src/runner.rs b/src/runner.rs index 233f88c2..9e4c8e4a 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -823,85 +823,6 @@ fn expand_imports_for_codegen( expand_from_dir(base_dir, ast, &mut already, import_config) } -fn materialize_output_path( - generated_path: &str, - output: Option<&Path>, - file_path: &Path, - source: &str, - stage: &str, -) -> String { - let Some(output) = output else { - return generated_path.to_string(); - }; - - if output.as_os_str().is_empty() { - WaveError::new( - WaveErrorKind::FileWriteError(file_path.display().to_string()), - "output path must not be empty", - file_path.display().to_string(), - 0, - 0, - ) - .with_code("E1005") - .with_source_code(source.to_string()) - .with_context(stage) - .with_help("pass a valid path to -o ") - .display(); - process::exit(1); - } - - if Path::new(generated_path) == output { - return generated_path.to_string(); - } - - if let Some(parent) = output.parent() { - if !parent.as_os_str().is_empty() && !parent.exists() { - if let Err(err) = fs::create_dir_all(parent) { - WaveError::new( - WaveErrorKind::FileWriteError(output.display().to_string()), - format!( - "failed to create output directory `{}`: {}", - parent.display(), - err - ), - file_path.display().to_string(), - 0, - 0, - ) - .with_code("E1005") - .with_source_code(source.to_string()) - .with_context(stage) - .with_help("check path permissions for the output directory") - .display(); - process::exit(1); - } - } - } - - if let Err(err) = fs::copy(generated_path, output) { - WaveError::new( - WaveErrorKind::FileWriteError(output.display().to_string()), - format!( - "failed to write output `{}` from `{}`: {}", - output.display(), - generated_path, - err - ), - file_path.display().to_string(), - 0, - 0, - ) - .with_code("E1005") - .with_source_code(source.to_string()) - .with_context(stage) - .with_help("check output path permissions and available disk space") - .display(); - process::exit(1); - } - - output.display().to_string() -} - #[allow(dead_code)] fn resolve_output_target( default_output: &str, @@ -1091,6 +1012,112 @@ pub(crate) unsafe fn emit_wave_ir_text( ir } +fn codegen_file_phase(kind: CodegenFileKind) -> &'static str { + match kind { + CodegenFileKind::Bitcode => "bitcode-emission", + CodegenFileKind::Assembly => "assembly-emission", + CodegenFileKind::Object => "object-emission", + } +} + +unsafe fn emit_wave_codegen_file( + file_path: &Path, + opt_flag: &str, + debug: &DebugFlags, + dep: &DepFlags, + llvm: &LlvmFlags, + output: &Path, + kind: CodegenFileKind, +) { + let (code, ast) = frontend_prepare_wave_ast(file_path, debug, dep, Some(llvm)); + emit_wave_codegen_file_from_ast(file_path, &code, &ast, opt_flag, debug, llvm, output, kind); +} + +unsafe fn emit_wave_codegen_file_from_ast( + file_path: &Path, + code: &str, + ast: &[ASTNode], + opt_flag: &str, + debug: &DebugFlags, + llvm: &LlvmFlags, + output: &Path, + kind: CodegenFileKind, +) { + let backend_opts = build_backend_options(llvm); + + if debug.ir { + let ir = match run_panic_guarded(|| unsafe { generate_ir(ast, opt_flag, &backend_opts) }) { + Ok(ir) => ir, + Err((msg, loc)) => { + emit_codegen_panic_and_exit(file_path, code, "llvm-ir-generation", msg, loc) + } + }; + println!("\n===== LLVM IR =====\n{}", ir); + } + + if let Some(parent) = output.parent() { + if !parent.as_os_str().is_empty() { + fs::create_dir_all(parent).unwrap_or_else(|e| { + emit_codegen_panic_and_exit( + file_path, + code, + codegen_file_phase(kind), + format!( + "failed to create output directory '{}': {}", + parent.display(), + e + ), + None, + ) + }); + } + } + + if let Err((msg, loc)) = run_panic_guarded(|| unsafe { + emit_codegen_file(ast, opt_flag, &backend_opts, output, kind); + }) { + emit_codegen_panic_and_exit(file_path, code, codegen_file_phase(kind), msg, loc); + } +} + +pub(crate) unsafe fn emit_wave_bitcode_file( + file_path: &Path, + opt_flag: &str, + debug: &DebugFlags, + dep: &DepFlags, + llvm: &LlvmFlags, + output: &Path, +) { + emit_wave_codegen_file( + file_path, + opt_flag, + debug, + dep, + llvm, + output, + CodegenFileKind::Bitcode, + ); +} + +pub(crate) unsafe fn emit_wave_assembly_file( + file_path: &Path, + opt_flag: &str, + debug: &DebugFlags, + dep: &DepFlags, + llvm: &LlvmFlags, + output: &Path, +) { + emit_wave_codegen_file( + file_path, + opt_flag, + debug, + dep, + llvm, + output, + CodegenFileKind::Assembly, + ); +} + #[allow(dead_code)] pub(crate) unsafe fn run_wave_file( file_path: &Path, @@ -1169,27 +1196,18 @@ pub(crate) unsafe fn run_wave_file( validate_wave_ast_or_exit(file_path, &code, &ast); - let backend_opts = build_backend_options(llvm); - - let ir = match run_panic_guarded(|| unsafe { generate_ir(&ast, opt_flag, &backend_opts) }) { - Ok(ir) => ir, - Err((msg, loc)) => { - emit_codegen_panic_and_exit(file_path, &code, "llvm-ir-generation", msg, loc) - } - }; - - if debug.ir { - println!("\n===== LLVM IR =====\n{}", ir); - } - let file_stem = file_path.file_stem().unwrap().to_str().unwrap(); - let object_patch = - match run_panic_guarded(|| compile_ir_to_object(&ir, file_stem, opt_flag, &backend_opts)) { - Ok(path) => path, - Err((msg, loc)) => { - emit_codegen_panic_and_exit(file_path, &code, "object-emission", msg, loc) - } - }; + let object_patch = format!("{}.o", file_stem); + emit_wave_codegen_file_from_ast( + file_path, + &code, + &ast, + opt_flag, + debug, + llvm, + Path::new(&object_patch), + CodegenFileKind::Object, + ); if debug.mc { println!("\n===== MACHINE CODE PATH ====="); @@ -1209,6 +1227,7 @@ pub(crate) unsafe fn run_wave_file( } let exe_patch = format!("target/{}", file_stem); + let backend_opts = build_backend_options(llvm); if let Err((msg, loc)) = run_panic_guarded(|| { link_objects( @@ -1304,34 +1323,20 @@ pub(crate) unsafe fn object_build_wave_file( validate_wave_ast_or_exit(file_path, &code, &ast); - let backend_opts = build_backend_options(llvm); - - let ir = match run_panic_guarded(|| unsafe { generate_ir(&ast, opt_flag, &backend_opts) }) { - Ok(ir) => ir, - Err((msg, loc)) => { - emit_codegen_panic_and_exit(file_path, &code, "llvm-ir-generation", msg, loc) - } - }; - - if debug.ir { - println!("\n===== LLVM IR =====\n{}", ir); - } - let file_stem = file_path.file_stem().unwrap().to_str().unwrap(); - let generated_object_path = - match run_panic_guarded(|| compile_ir_to_object(&ir, file_stem, opt_flag, &backend_opts)) { - Ok(path) => path, - Err((msg, loc)) => { - emit_codegen_panic_and_exit(file_path, &code, "object-emission", msg, loc) - } - }; - let object_path = materialize_output_path( - &generated_object_path, - output, + let default_object_path = PathBuf::from(format!("{}.o", file_stem)); + let output_path = output.unwrap_or(default_object_path.as_path()); + emit_wave_codegen_file_from_ast( file_path, &code, - "object-emission", + &ast, + opt_flag, + debug, + llvm, + output_path, + CodegenFileKind::Object, ); + let object_path = output_path.to_string_lossy().to_string(); if debug.mc { println!("\n===== MACHINE CODE PATH ====="); diff --git a/src/std.rs b/src/std.rs index ef4cfec5..ce4fb52b 100644 --- a/src/std.rs +++ b/src/std.rs @@ -48,7 +48,7 @@ fn install_or_update_std(is_update: bool) -> Result<(), CliError> { fn install_std_from_wave_repo_sparse(stage_dir: &Path) -> Result<(), CliError> { if !tool_exists("git") { - return Err(CliError::ExternalToolMissing("git")); + return Err(CliError::ExternalToolMissing("git".to_string())); } let repo = "https://github.com/wavefnd/Wave.git"; diff --git a/tools/llvm-config-wine.sh b/tools/llvm-config-wine.sh new file mode 100755 index 00000000..c8063b4b --- /dev/null +++ b/tools/llvm-config-wine.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# Run a Windows llvm-config.exe through Wine while returning paths usable by +# the Linux-hosted MinGW linker. +set -euo pipefail + +LLVM_CONFIG_EXE="${LLVM_CONFIG_EXE:-/opt/llvm-win/bin/llvm-config.exe}" + +wine "$LLVM_CONFIG_EXE" "$@" \ + 2> >(sed '/fixme:winediag:loader_init/d;/Please mention your exact version/d' >&2) \ + | sed -E 's#Z:/#/#g' diff --git a/tools/llvm-win-prefix/bin/llvm-config b/tools/llvm-win-prefix/bin/llvm-config new file mode 100755 index 00000000..1c811ffa --- /dev/null +++ b/tools/llvm-win-prefix/bin/llvm-config @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/../../llvm-config-wine.sh" "$@" diff --git a/tools/run_tests.py b/tools/run_tests.py index 46b830f6..dfa62f63 100644 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -44,7 +44,7 @@ "error[E", "failed to parse", "Failed to run", - "clang failed:", + "llc failed:", "compiler internal error during code generation", "thread 'main' panicked", "panicked at", diff --git a/utils/src/colorex.rs b/utils/src/colorex.rs index 03efa839..166b7783 100644 --- a/utils/src/colorex.rs +++ b/utils/src/colorex.rs @@ -12,6 +12,45 @@ pub struct Color(u8, u8, u8); +fn colors_enabled() -> bool { + if std::env::var_os("NO_COLOR").is_some() { + return false; + } + + if std::env::var("CLICOLOR").as_deref() == Ok("0") { + return false; + } + + if std::env::var("CLICOLOR_FORCE").is_ok_and(|v| v != "0") { + return true; + } + + windows_ansi_supported() +} + +#[cfg(windows)] +fn windows_ansi_supported() -> bool { + if std::env::var_os("WT_SESSION").is_some() + || std::env::var_os("ANSICON").is_some() + || std::env::var("ConEmuANSI").is_ok_and(|v| v.eq_ignore_ascii_case("ON")) + { + return true; + } + + std::env::var("TERM").is_ok_and(|term| { + let term = term.to_ascii_lowercase(); + term.contains("xterm") + || term.contains("ansi") + || term.contains("cygwin") + || term.contains("msys") + }) +} + +#[cfg(not(windows))] +fn windows_ansi_supported() -> bool { + true +} + impl Color { pub fn from_rgb(rgb: &str) -> Result { let parts: Vec<&str> = rgb.split(',').collect(); @@ -51,6 +90,10 @@ pub trait Colorize { impl Colorize for &str { fn color(self, color: &str) -> String { + if !colors_enabled() { + return self.to_string(); + } + let color = if color.starts_with('#') { Color::from_hex(color) } else { @@ -64,6 +107,10 @@ impl Colorize for &str { } fn bg_color(self, color: &str) -> String { + if !colors_enabled() { + return self.to_string(); + } + let color = if color.starts_with('#') { Color::from_hex(color) } else { @@ -77,26 +124,44 @@ impl Colorize for &str { } fn bold(self) -> String { + if !colors_enabled() { + return self.to_string(); + } format!("\x1b[1m{}\x1b[0m", self) } fn italic(self) -> String { + if !colors_enabled() { + return self.to_string(); + } format!("\x1b[3m{}\x1b[0m", self) } fn underline(self) -> String { + if !colors_enabled() { + return self.to_string(); + } format!("\x1b[4m{}\x1b[0m", self) } fn strikethrough(self) -> String { + if !colors_enabled() { + return self.to_string(); + } format!("\x1b[9m{}\x1b[0m", self) } fn dim(self) -> String { + if !colors_enabled() { + return self.to_string(); + } format!("\x1b[2m{}\x1b[0m", self) } fn invert(self) -> String { + if !colors_enabled() { + return self.to_string(); + } format!("\x1b[7m{}\x1b[0m", self) } } diff --git a/x.py b/x.py index dcc70d0b..e9136754 100644 --- a/x.py +++ b/x.py @@ -17,11 +17,13 @@ import sys import subprocess from pathlib import Path -import toml import shutil import platform -import tkinter as tk -from tkinter import ttk, messagebox + +try: + import tomllib +except ModuleNotFoundError: + tomllib = None # ------------------------------------------------------ # Basic Setting @@ -29,6 +31,7 @@ ROOT = Path(__file__).resolve().parent TARGET_DIR = ROOT / "target" +DIST_DIR = ROOT / "dist" BINARY_NAME = "wavec" NAME = "wave" WINDOWS_GNU_TARGET = "x86_64-pc-windows-gnu" @@ -42,7 +45,7 @@ TARGET_MATRIX = { "x86_64-unknown-linux-gnu": ["Linux"], - "aarch64-unknown-linux-gnu": ["Linux"], + #"aarch64-unknown-linux-gnu": ["Linux"], "x86_64-unknown-freebsd": ["FreeBSD"], "x86_64-unknown-openbsd": ["OpenBSD"], "x86_64-unknown-netbsd": ["NetBSD"], @@ -57,13 +60,88 @@ ALL_TARGETS = list(TARGET_MATRIX.keys()) +def target_darwin_arch(target): + if target.startswith("aarch64-"): + return "arm64" + if target.startswith("x86_64-"): + return "x86_64" + return None + +def early_llvm_prefix(): + for env_name in ["WAVE_LLVM_HOME", "LLVM_SYS_211_PREFIX"]: + value = os.environ.get(env_name) + if value: + path = Path(value) + if path.exists(): + return path + + llvm_config = os.environ.get("LLVM_CONFIG_PATH") or shutil.which("llvm-config") + if llvm_config: + result = subprocess.run( + [llvm_config, "--prefix"], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode == 0: + path = Path(result.stdout.strip()) + if path.exists(): + return path + return None + +def llvm_dylib_arches(): + prefix = early_llvm_prefix() + if prefix is None: + return set() + + lib_dir = prefix / "lib" + candidates = [ + *lib_dir.glob("libLLVM-*.dylib"), + lib_dir / "libLLVM.dylib", + lib_dir / "libLLVM-C.dylib", + ] + + for candidate in candidates: + if not candidate.exists(): + continue + result = subprocess.run( + ["lipo", "-info", str(candidate)], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + continue + + text = result.stdout.strip() + if "are:" in text: + return set(text.split("are:", 1)[1].strip().split()) + if "architecture:" in text: + return {text.rsplit("architecture:", 1)[1].strip()} + return set() + +def is_target_buildable_with_current_llvm(target): + if not target.endswith("apple-darwin"): + return True + + target_arch = target_darwin_arch(target) + if target_arch is None: + return True + + arches = llvm_dylib_arches() + if not arches: + return platform.machine() == target_arch + return target_arch in arches + def detect_targets(): os_name = platform.system() native = [ target for target, hosts in TARGET_MATRIX.items() - if os_name in hosts + if os_name in hosts and is_target_buildable_with_current_llvm(target) ] if not native: @@ -76,7 +154,12 @@ def detect_targets(): # Version Read def get_version(): - cargo = toml.load(str(ROOT / "Cargo.toml")) + if tomllib is None: + print("[!] Python 3.11+ is required, or install the 'tomli' package for older Python.") + sys.exit(1) + + with open(ROOT / "Cargo.toml", "rb") as f: + cargo = tomllib.load(f) return cargo["package"]["version"] VERSION = get_version() @@ -84,6 +167,12 @@ def get_version(): def is_windows_gnu_target(target): return target == WINDOWS_GNU_TARGET +def is_darwin_target(target): + return target.endswith("apple-darwin") + +def is_linux_target(target): + return "linux" in target + def require_tool(tool): if shutil.which(tool) is None: print(f"[!] Missing required tool: {tool}") @@ -110,6 +199,18 @@ def configure_windows_gnu_env(env): env["CC_x86_64_pc_windows_gnu"] = MINGW_CC env["CXX_x86_64_pc_windows_gnu"] = MINGW_CXX +def append_env_words(env, name, words): + current = env.get(name, "").strip() + addition = " ".join(words) + env[name] = f"{current} {addition}" if current else addition + +def configure_linux_release_env(env): + # Keep the distributed wavec binary self-contained with bundled llvm/lib. + append_env_words(env, "RUSTFLAGS", [ + "-C", "link-arg=-Wl,-z,origin", + "-C", "link-arg=-Wl,-rpath,$ORIGIN/llvm/lib", + ]) + def cargo_build_args(target): args = ["cargo", "build", "--target", target, "--release"] if is_windows_gnu_target(target): @@ -146,6 +247,448 @@ def windows_package_inputs(exe_path): files.append(path) return files +def llvm_prefix(): + for env_name in ["WAVE_LLVM_HOME", "LLVM_SYS_211_PREFIX"]: + value = os.environ.get(env_name) + if value: + path = Path(value) + if path.exists(): + return path + + llvm_config = os.environ.get("LLVM_CONFIG_PATH") or shutil.which("llvm-config") + if llvm_config: + result = subprocess.run( + [llvm_config, "--prefix"], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode == 0: + path = Path(result.stdout.strip()) + if path.exists(): + return path + return None + +def llvm_bin_dir(): + value = os.environ.get("WAVE_LLVM_BIN") + if value: + path = Path(value) + if path.exists(): + return path + + prefix = llvm_prefix() + if prefix is not None: + path = prefix / "bin" + if path.exists(): + return path + return None + +def llvm_lib_dir(): + prefix = llvm_prefix() + if prefix is not None: + path = prefix / "lib" + if path.exists(): + return path + return None + +def find_release_tool(tool): + names = [tool] + if platform.system() == "Windows" and not tool.endswith(".exe"): + names.insert(0, f"{tool}.exe") + + dirs = [] + bin_dir = llvm_bin_dir() + if bin_dir is not None: + dirs.append(bin_dir) + path_hit = shutil.which(tool) + if path_hit: + dirs.append(Path(path_hit).parent) + + for directory in dirs: + for name in names: + candidate = directory / name + if candidate.exists(): + return candidate.resolve() + return None + +def copy_executable(src, dst): + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + mode = dst.stat().st_mode + dst.chmod(mode | 0o755) + +def copy_optional(src, dst): + if src is None or not Path(src).exists(): + return None + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + return dst + +def copy_globbed_files(patterns, dst_dir): + copied = [] + dst_dir.mkdir(parents=True, exist_ok=True) + seen = set() + for pattern in patterns: + for src in pattern.parent.glob(pattern.name): + if src.name in seen or not src.is_file(): + continue + seen.add(src.name) + dst = dst_dir / src.name + shutil.copy2(src, dst) + copied.append(dst) + return copied + +def copy_named_runtime(src, dst_dir, dst_name=None): + if src is None: + return None + + src = Path(src) + if not src.exists() or not src.is_file(): + return None + + dst_dir.mkdir(parents=True, exist_ok=True) + dst = dst_dir / (dst_name or src.name) + if dst.exists(): + return dst + shutil.copy2(src, dst) + return dst + +def lld_tools_for_target(target): + common = ["llc", "llvm-as", "llvm-mc"] + if is_darwin_target(target): + return ["ld64.lld", "ld.lld", *common] + if is_windows_gnu_target(target): + return ["ld.lld", "lld-link", *common] + return ["ld.lld", *common] + +def copy_lld_tools(stage_dir, target): + copied = [] + tool_dir = stage_dir / "llvm" / "bin" + for tool in lld_tools_for_target(target): + src = find_release_tool(tool) + if src is None: + print(f"[!] Missing LLD tool for package: {tool}") + sys.exit(1) + dst = tool_dir / tool + copy_executable(src, dst) + copied.append((src, dst)) + return copied + +def ldd_shared_libs(binary): + if shutil.which("ldd") is None: + return [] + + result = subprocess.run( + ["ldd", str(binary)], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return [] + + libs = [] + for line in result.stdout.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("linux-vdso"): + continue + + path_text = None + if "=>" in stripped: + rhs = stripped.split("=>", 1)[1].strip() + if rhs.startswith("/"): + path_text = rhs.split(" ", 1)[0] + elif stripped.startswith("/"): + path_text = stripped.split(" ", 1)[0] + + if path_text: + path = Path(path_text) + if path.exists(): + libs.append(path) + + return libs + +def is_glibc_core_runtime(path): + name = path.name + return ( + name == "libc.so.6" + or name.startswith("ld-linux") + or name in { + "libdl.so.2", + "libm.so.6", + "libpthread.so.0", + "librt.so.1", + "libresolv.so.2", + "libutil.so.1", + } + ) + +def copy_linux_runtime_deps(stage_dir, binaries): + root_lib_dir = stage_dir / "llvm" / "lib" + copied = [] + queue = [Path(p) for p in binaries if Path(p).exists()] + seen = set() + + while queue: + current = queue.pop(0) + for dep in ldd_shared_libs(current): + resolved = dep.resolve() + if resolved in seen or is_glibc_core_runtime(dep): + continue + seen.add(resolved) + + dst = copy_named_runtime(dep, root_lib_dir) + if dst is not None: + copied.append(dst) + queue.append(dst) + + return copied + +def resolve_dylib_reference(ref, binary, extra_dirs=None): + path = Path(ref) + if path.is_absolute(): + return path if path.exists() else None + + name = path.name + search_dirs = [] + if ref.startswith("@loader_path/"): + search_dirs.append(Path(binary).parent) + if ref.startswith("@executable_path/"): + search_dirs.append(Path(binary).parent) + if extra_dirs: + search_dirs.extend(extra_dirs) + + for directory in search_dirs: + candidate = directory / name + if candidate.exists(): + return candidate + return None + +def copy_darwin_lld_runtime_refs(root_lib_dir, compiler_lib_dir, binaries): + copied = [] + compiler_llvm = None + if compiler_lib_dir is not None: + compiler_llvm = compiler_lib_dir / "libLLVM.dylib" + + for binary in binaries: + extra_dirs = [Path(binary).parent, Path(binary).parent.parent / "lib"] + for ref in dylib_references(binary): + name = Path(ref).name + src = resolve_dylib_reference(ref, binary, extra_dirs) + if src is None: + continue + + if name.startswith("liblld"): + copied.append(copy_named_runtime(src, root_lib_dir)) + elif name == "libLLVM.dylib": + if compiler_llvm is not None and compiler_llvm.exists() and src.resolve() != compiler_llvm.resolve(): + copied.append(copy_named_runtime(src, root_lib_dir, "libLLVM-lld.dylib")) + + return [p for p in copied if p is not None] + +def copy_llvm_runtime_libs(stage_dir, target, lld_tool_paths, runtime_roots=None): + copied = [] + root_lib_dir = stage_dir / "llvm" / "lib" + lib_dir = llvm_lib_dir() + + if is_windows_gnu_target(target): + dll_dirs = [] + bin_dir = llvm_bin_dir() + if bin_dir is not None: + dll_dirs.append(bin_dir) + target_runtime = TARGET_DIR / target / "release" + if target_runtime.exists(): + dll_dirs.append(target_runtime) + + root_dll_dir = stage_dir + tool_dll_dir = stage_dir / "llvm" / "bin" + for directory in dll_dirs: + for src in directory.glob("LLVM*.dll"): + copied.append(copy_optional(src, root_dll_dir / src.name)) + copied.append(copy_optional(src, tool_dll_dir / src.name)) + return [p for p in copied if p is not None] + + patterns = [] + if lib_dir is not None: + if is_darwin_target(target): + patterns.extend([lib_dir / "libLLVM*.dylib", lib_dir / "liblld*.dylib"]) + elif is_linux_target(target): + patterns.extend([lib_dir / "libLLVM*.so*", lib_dir / "liblld*.so*"]) + + for tool_src, _ in lld_tool_paths: + tool_lib_dir = tool_src.parent.parent / "lib" + if tool_lib_dir.exists(): + if is_darwin_target(target): + patterns.extend([tool_lib_dir / "liblld*.dylib"]) + elif is_linux_target(target): + patterns.extend([tool_lib_dir / "libLLVM*.so*", tool_lib_dir / "liblld*.so*"]) + + copied.extend(copy_globbed_files(patterns, root_lib_dir)) + if is_darwin_target(target): + lld_sources = [tool_src for tool_src, _ in lld_tool_paths] + lld_sources.extend(root_lib_dir.glob("liblld*.dylib")) + copied.extend(copy_darwin_lld_runtime_refs(root_lib_dir, lib_dir, lld_sources)) + elif is_linux_target(target): + dep_roots = list(runtime_roots or []) + dep_roots.extend(staged for _, staged in lld_tool_paths) + dep_roots.extend(root_lib_dir.glob("*.so*")) + copied.extend(copy_linux_runtime_deps(stage_dir, dep_roots)) + return copied + +def dylib_references(binary): + if shutil.which("otool") is None: + return [] + result = subprocess.run( + ["otool", "-L", str(binary)], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return [] + + refs = [] + for line in result.stdout.splitlines()[1:]: + ref = line.strip().split(" ", 1)[0] + if ref.startswith("/usr/lib/") or ref.startswith("/System/"): + continue + if "libLLVM" in ref or "liblld" in ref: + refs.append(ref) + return refs + +def patch_macos_binary(binary, loader_prefix): + if shutil.which("install_name_tool") is None: + print(f"[!] install_name_tool not found; {binary.name} may require host LLVM paths") + return + + subprocess.run( + ["install_name_tool", "-add_rpath", loader_prefix, str(binary)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + for ref in dylib_references(binary): + name = Path(ref).name + subprocess.run( + ["install_name_tool", "-change", ref, f"{loader_prefix}/{name}", str(binary)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + +def patch_macos_binary_with_lld_llvm(binary, loader_prefix): + if shutil.which("install_name_tool") is None: + print(f"[!] install_name_tool not found; {binary.name} may require host LLVM paths") + return + + subprocess.run( + ["install_name_tool", "-add_rpath", loader_prefix, str(binary)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + for ref in dylib_references(binary): + name = Path(ref).name + if name == "libLLVM.dylib": + name = "libLLVM-lld.dylib" + subprocess.run( + ["install_name_tool", "-change", ref, f"{loader_prefix}/{name}", str(binary)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + +def linux_binary_has_runpath(binary, expected): + if shutil.which("readelf") is None: + return True + + result = subprocess.run( + ["readelf", "-d", str(binary)], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return False + return expected in result.stdout + +def patch_linux_binary(binary, rpath): + if shutil.which("patchelf") is None: + return + subprocess.run(["patchelf", "--set-rpath", rpath, str(binary)], check=True) + +def codesign_macos_binary(binary): + if shutil.which("codesign") is None: + print(f"[!] codesign not found; {binary.name} may not run after install_name_tool") + return + + subprocess.run( + ["codesign", "--force", "--sign", "-", str(binary)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + +def patch_staged_runtime(stage_dir, target, binary_path, lld_tool_paths): + if is_darwin_target(target): + patch_macos_binary(binary_path, "@executable_path/llvm/lib") + has_lld_llvm = (stage_dir / "llvm" / "lib" / "libLLVM-lld.dylib").exists() + for _, staged in lld_tool_paths: + if staged.exists(): + if has_lld_llvm: + patch_macos_binary_with_lld_llvm(staged, "@executable_path/../lib") + else: + patch_macos_binary(staged, "@executable_path/../lib") + for dylib in (stage_dir / "llvm" / "lib").glob("liblld*.dylib"): + if has_lld_llvm: + patch_macos_binary_with_lld_llvm(dylib, "@loader_path") + else: + patch_macos_binary(dylib, "@loader_path") + codesign_macos_binary(binary_path) + for _, staged in lld_tool_paths: + if staged.exists(): + codesign_macos_binary(staged) + for dylib in (stage_dir / "llvm" / "lib").glob("*.dylib"): + codesign_macos_binary(dylib) + elif is_linux_target(target): + patch_linux_binary(binary_path, "$ORIGIN/llvm/lib") + if not linux_binary_has_runpath(binary_path, "$ORIGIN/llvm/lib"): + print(f"[!] Missing RUNPATH in {binary_path.name}") + print(" Linux release packages must keep wavec as an ELF binary and") + print(" resolve bundled LLVM from $ORIGIN/llvm/lib.") + print(" Rebuild with x.py build/release so Cargo embeds the release RUNPATH.") + sys.exit(1) + for _, staged in lld_tool_paths: + if staged.exists(): + patch_linux_binary(staged, "$ORIGIN/../lib") + +def stage_release_package(target, binary, out_name): + stage_dir = DIST_DIR / out_name + if stage_dir.exists(): + shutil.rmtree(stage_dir) + stage_dir.mkdir(parents=True) + + staged_binary = stage_dir / binary.name + copy_executable(binary, staged_binary) + + lld_tools = copy_lld_tools(stage_dir, target) + runtime_libs = copy_llvm_runtime_libs(stage_dir, target, lld_tools, [staged_binary]) + if not runtime_libs: + print("[!] Missing LLVM runtime libraries for package") + print(" Set WAVE_LLVM_HOME or LLVM_SYS_211_PREFIX to the LLVM release prefix.") + sys.exit(1) + patch_staged_runtime(stage_dir, target, staged_binary, lld_tools) + + if is_windows_gnu_target(target): + for src in windows_package_inputs(binary): + copy_optional(src, stage_dir / src.name) + + return stage_dir + # ------------------------------------------------------ # rustup target add # ------------------------------------------------------ @@ -163,12 +706,19 @@ def cmd_build(): for t in TARGETS: print(f" -> building for {t}") + if not is_target_buildable_with_current_llvm(t): + arches = ", ".join(sorted(llvm_dylib_arches())) or "unknown" + print(f"[!] Cannot build {t} with the current LLVM runtime architecture(s): {arches}") + print(" Use a matching/universal LLVM prefix, or select a compatible target explicitly.") + sys.exit(1) env = os.environ.copy() if is_windows_gnu_target(t): print(" [*] Applying MinGW + Windows LLVM environment") configure_windows_gnu_env(env) + elif is_linux_target(t): + configure_linux_release_env(env) subprocess.run( cargo_build_args(t), @@ -183,6 +733,9 @@ def cmd_build(): # ------------------------------------------------------ def cmd_package(): print("[*] Packaging release binaries...") + DIST_DIR.mkdir(exist_ok=True) + packaged = 0 + missing = [] for target in TARGETS: target_dir = TARGET_DIR / target / "release" @@ -196,40 +749,62 @@ def cmd_package(): bin_path = binary.with_suffix(".exe") if not bin_path.exists(): print(f"[!] Missing binary: {bin_path}") + missing.append(str(bin_path)) continue - package_files = [] - for src in windows_package_inputs(bin_path): - dst = ROOT / src.name - shutil.copy(src, dst) - package_files.append(dst.name) - + stage_dir = stage_release_package(target, bin_path, out_name) zip_path = ROOT / f"{out_name}.zip" - subprocess.run(["zip", "-q", zip_path, *package_files], check=True) - for name in package_files: - os.remove(ROOT / name) + subprocess.run( + ["zip", "-r", "-q", str(zip_path), stage_dir.name], + cwd=DIST_DIR, + check=True, + ) print(f"[+] Windows packaged → {zip_path}") + packaged += 1 else: if not binary.exists(): print(f"[!] Missing binary: {binary}") + missing.append(str(binary)) continue + stage_dir = stage_release_package(target, binary, out_name) tar_path = ROOT / f"{out_name}.tar.gz" subprocess.run([ "tar", "-czf", tar_path, - "-C", str(target_dir), - BINARY_NAME + "-C", str(DIST_DIR), + stage_dir.name ], check=True) print(f"[+] Packaged → {tar_path}") + packaged += 1 + + if missing: + print("[!] Packaging failed because required release binaries are missing:") + for path in missing: + print(f" {path}") + print(" Run x.py build for the selected target(s) before packaging.") + sys.exit(1) + + if packaged == 0: + print("[!] No release packages were produced.") + sys.exit(1) print("[+] Packaging complete.\n") def cmd_gui(): global TARGETS + try: + import tkinter as tk + from tkinter import ttk, messagebox + except ModuleNotFoundError: + print("[!] Python tkinter support is not available in this interpreter.") + print(" Non-GUI commands do not require tkinter. Use: x.py install|build|package|release|clean") + print(" To use x.py gui, install a Python build with Tk support.") + sys.exit(1) + root = tk.Tk() root.title("Wave Build Manager") root.geometry("700x720") @@ -414,6 +989,7 @@ def cmd_release(): def cmd_clean(): print("[*] Cleaning build artifacts...") shutil.rmtree("target", ignore_errors=True) + shutil.rmtree(DIST_DIR, ignore_errors=True) for f in os.listdir(ROOT): if f.endswith(".tar.gz") or f.endswith(".zip"):