diff --git a/llvm/crt/linux/x86_64/crt1.s b/llvm/crt/linux/x86_64/crt1.s new file mode 100644 index 00000000..5b129683 --- /dev/null +++ b/llvm/crt/linux/x86_64/crt1.s @@ -0,0 +1,57 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# 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. + + .text + .globl _start + .type _start,@function +_start: + .cfi_startproc + .cfi_undefined %rip + xorl %ebp, %ebp + + # Linux x86_64 enters with: + # rdx = dynamic loader finalizer + # rsp = argc, argv..., NULL, envp..., NULL, auxv... + # + # Use glibc's hosted-program initializer rather than calling main + # directly. This keeps libc initialization, stdio flushing, TLS setup, + # argv/envp handling, and exit processing on the normal hosted path. + movq %rdx, %r9 + popq %rsi + movq %rsp, %rdx + andq $-16, %rsp + pushq %rax + pushq %rsp + xorl %r8d, %r8d + xorl %ecx, %ecx + leaq __wave_main_trampoline(%rip), %rdi + call __libc_start_main@PLT + hlt + .cfi_endproc + + .size _start, .-_start + + .type __wave_main_trampoline,@function +__wave_main_trampoline: + .cfi_startproc + subq $8, %rsp + .cfi_adjust_cfa_offset 8 + call main + addq $8, %rsp + .cfi_adjust_cfa_offset -8 + xorl %eax, %eax + ret + .cfi_endproc + + .size __wave_main_trampoline, .-__wave_main_trampoline + + .section .note.GNU-stack,"",@progbits diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index d4b97a6e..2db2403b 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -122,7 +122,7 @@ pub fn link_objects( let mut cmd = Command::new(&linker_bin); configure_bundled_llvm_tool_env(&mut cmd, &linker_bin); - if backend.linker.is_none() { + if backend.linker.is_none() && !(is_windows_gnu_target(Some(target)) && linker_bin == "gcc") { append_lld_target_args(&mut cmd, target, backend); } @@ -161,6 +161,10 @@ pub fn link_objects( fn default_lld_for_target(target: &str) -> String { if is_darwin_target(target) { resolve_bundled_tool("ld64.lld") + } else if is_windows_gnu_target(Some(target)) { + resolve_bundled_tool_path("ld.lld") + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "gcc".to_string()) } else { resolve_bundled_tool("ld.lld") } @@ -231,26 +235,42 @@ fn elf_lld_emulation(target: &str) -> Option<&'static str> { } fn resolve_bundled_tool(tool: &str) -> String { + if let Some(path) = resolve_bundled_tool_path(tool) { + return path.to_string_lossy().to_string(); + } + executable_tool_name(tool) +} + +fn resolve_bundled_tool_path(tool: &str) -> Option { 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(); + return Some(candidate); } } - executable_tool_name(tool) + None } fn configure_bundled_llvm_tool_env(cmd: &mut Command, bin: &str) { - let Some(lib_dir) = bundled_llvm_lib_dir(bin) else { + let Some(bin_dir) = bundled_llvm_bin_dir(bin) else { return; }; if cfg!(target_os = "linux") { - prepend_env_path(cmd, "LD_LIBRARY_PATH", lib_dir); + if let Some(lib_dir) = bin_dir.parent().map(|llvm_dir| llvm_dir.join("lib")) { + if lib_dir.is_dir() { + prepend_env_path(cmd, "LD_LIBRARY_PATH", lib_dir); + } + } + } else if cfg!(windows) { + if let Some(root_dir) = bin_dir.parent().and_then(|llvm_dir| llvm_dir.parent()) { + prepend_env_path(cmd, "PATH", root_dir.to_path_buf()); + } + prepend_env_path(cmd, "PATH", bin_dir); } } -fn bundled_llvm_lib_dir(bin: &str) -> Option { +fn bundled_llvm_bin_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") { @@ -262,8 +282,7 @@ fn bundled_llvm_lib_dir(bin: &str) -> Option { return None; } - let lib_dir = llvm_dir.join("lib"); - lib_dir.is_dir().then_some(lib_dir) + Some(bin_dir.to_path_buf()) } fn prepend_env_path(cmd: &mut Command, name: &str, first: PathBuf) { diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index 2b65cd3a..aaf2d23a 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -26,6 +26,7 @@ use parser::ast::{ StructNode, TypeAliasNode, VariableNode, WaveType, }; use std::collections::{HashMap, HashSet}; +use std::sync::Once; use crate::backend::BackendOptions; use crate::codegen::target::require_supported_target_from_triple; @@ -74,28 +75,46 @@ fn target_opt_level_from_flag(opt_flag: &str) -> OptimizationLevel { } } -fn initialize_llvm_targets() { - let config = InitializationConfig::default(); +static INIT_LLVM_TARGETS: Once = Once::new(); - #[cfg(feature = "llvm-target-all")] - { - Target::initialize_all(&config); +fn codegen_trace(step: &str) { + if std::env::var_os("WAVE_CODEGEN_TRACE").is_some() { + eprintln!("[wavec-codegen] {}", step); } +} - #[cfg(all(not(feature = "llvm-target-all"), feature = "llvm-target-x86"))] - { - Target::initialize_x86(&config); - } +fn initialize_llvm_targets() { + INIT_LLVM_TARGETS.call_once(|| { + let config = InitializationConfig::default(); - #[cfg(all(not(feature = "llvm-target-all"), feature = "llvm-target-aarch64"))] - { - Target::initialize_aarch64(&config); - } + #[cfg(feature = "llvm-target-all")] + { + Target::initialize_all(&config); + } - #[cfg(all(not(feature = "llvm-target-all"), feature = "llvm-target-riscv"))] - { - Target::initialize_riscv(&config); - } + #[cfg(all(not(feature = "llvm-target-all"), feature = "llvm-target-x86"))] + { + 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); + } + }); +} + +fn should_run_llvm_pass_pipeline() -> bool { + // LLVM 21's C pass pipeline can jump through a null callback in the + // MinGW-built Windows package. Code generation still uses the target + // machine's optimization level, so keep Windows codegen usable by skipping + // the in-process IR pass pipeline there. + !cfg!(target_os = "windows") } pub unsafe fn generate_ir( @@ -150,27 +169,36 @@ fn build_module( opt_flag: &str, backend: &BackendOptions, ) -> GeneratedModule { + codegen_trace("initialize targets"); + initialize_llvm_targets(); + + codegen_trace("create context"); let context: &'static Context = Box::leak(Box::new(Context::create())); + codegen_trace("create module"); let module: &'static _ = Box::leak(Box::new(context.create_module("main"))); + codegen_trace("create builder"); let builder: &'static _ = Box::leak(Box::new(context.create_builder())); + codegen_trace("resolve named types"); let named_types = collect_named_types(ast_nodes); let ast_nodes: Vec = ast_nodes .iter() .map(|n| resolve_ast_node(n, &named_types)) .collect(); - initialize_llvm_targets(); + codegen_trace("resolve target triple"); let triple = if let Some(raw) = &backend.target { TargetTriple::create(raw) } else { TargetMachine::get_default_triple() }; let abi_target = require_supported_target_from_triple(&triple); + codegen_trace("lookup target"); let target = Target::from_triple(&triple).unwrap(); let cpu = backend.cpu.as_deref().unwrap_or("generic"); let features = backend.features.as_deref().unwrap_or(""); + codegen_trace("create target machine"); let tm = target .create_target_machine( &triple, @@ -182,6 +210,7 @@ fn build_module( ) .unwrap(); + codegen_trace("set target metadata"); module.set_triple(&triple); let td_val: TargetData = tm.get_target_data(); @@ -495,13 +524,19 @@ fn build_module( } } - let pbo = PassBuilderOptions::create(); - let pipeline = pipeline_from_opt_flag(opt_flag); + if should_run_llvm_pass_pipeline() { + let pbo = PassBuilderOptions::create(); + let pipeline = pipeline_from_opt_flag(opt_flag); - module - .run_passes(pipeline, &tm, pbo) - .expect("failed to run optimization passes"); + codegen_trace("run optimization passes"); + module + .run_passes(pipeline, &tm, pbo) + .expect("failed to run optimization passes"); + } else { + codegen_trace("skip optimization passes"); + } + codegen_trace("finish module"); GeneratedModule { module, target_machine: tm, diff --git a/llvm/src/lib.rs b/llvm/src/lib.rs index 468a9ea0..7833bb21 100644 --- a/llvm/src/lib.rs +++ b/llvm/src/lib.rs @@ -15,8 +15,12 @@ pub mod codegen; pub mod expression; pub mod importgen; pub mod statement; +pub mod toolchain; pub fn backend() -> Option { - let (major, minor, patch) = (0_u32, 0_u32, 0_u32); + let (mut major, mut minor, mut patch) = (0_u32, 0_u32, 0_u32); + unsafe { + llvm_sys::core::LLVMGetVersion(&mut major, &mut minor, &mut patch); + } Some(format!("LLVM {}.{}.{}", major, minor, patch)) } diff --git a/llvm/src/toolchain.rs b/llvm/src/toolchain.rs new file mode 100644 index 00000000..97db5835 --- /dev/null +++ b/llvm/src/toolchain.rs @@ -0,0 +1,54 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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; + +pub fn find_bundled_linux_crt1(target: &str) -> Option { + bundled_linux_crt1_candidates(target) + .into_iter() + .find(|path| path.is_file()) +} + +pub fn expected_bundled_linux_crt1(target: &str) -> PathBuf { + bundled_linux_crt1_candidates(target) + .into_iter() + .next() + .unwrap_or_else(|| PathBuf::from(format!("crt/{}/crt1.o", target))) +} + +fn bundled_linux_crt1_candidates(target: &str) -> Vec { + let mut paths = Vec::new(); + + if let Ok(path) = env::var("WAVE_LINUX_CRT1_OBJECT") { + if !path.trim().is_empty() { + paths.push(PathBuf::from(path)); + } + } + + if let Ok(exe) = env::current_exe() { + if let Some(dir) = exe.parent() { + paths.push(dir.join("crt").join(target).join("crt1.o")); + if let Some(root) = dir.parent() { + paths.push( + root.join("lib") + .join("wave") + .join("crt") + .join(target) + .join("crt1.o"), + ); + } + } + } + + paths +} diff --git a/src/cli.rs b/src/cli.rs index a1c5e0f0..ca4aa7e2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1957,7 +1957,7 @@ fn link_objects( let out = command.args(&args).output().map_err(|e| { if e.kind() == ErrorKind::NotFound { - CliError::ExternalToolMissing(linker_tool_name(&bin)) + CliError::ExternalToolMissing(missing_linker_tool_name(global, &bin)) } else { CliError::Io(e) } @@ -1990,7 +1990,7 @@ fn build_linker_args( 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) + build_windows_gnu_linker_args(global, build, objects, output) } else { build_elf_lld_args(global, build, objects, output, &target) } @@ -2068,25 +2068,57 @@ fn build_darwin_lld_args( (resolve_bundled_tool("ld64.lld"), args) } -fn build_windows_gnu_lld_args( +fn build_windows_gnu_linker_args( global: &Global, build: &BuildRequest, objects: &[String], output: &Path, ) -> (String, Vec) { + let Some(linker) = resolve_bundled_tool_path("ld.lld") else { + return build_user_linker_args("gcc", global, build, objects, output); + }; + let mut args = vec!["-m".to_string(), "i386pep".to_string()]; + if !global.llvm.no_default_libs && !build.no_start_files { + args.push( + find_windows_mingw_runtime_file("crt2.o") + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "crt2.o".to_string()), + ); + } + for obj in objects { args.push(obj.clone()); } + append_windows_mingw_search_paths(&mut args); 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); + if !global.llvm.no_default_libs { + args.extend( + [ + "-lmingw32", + "-lgcc", + "-lgcc_eh", + "-lmoldname", + "-lmingwex", + "-lmsvcrt", + "-lkernel32", + "-luser32", + "-ladvapi32", + "-lshell32", + ] + .into_iter() + .map(String::from), + ); + } + args.push("-o".to_string()); args.push(output.to_string_lossy().to_string()); - (resolve_bundled_tool("ld.lld"), args) + (linker.to_string_lossy().to_string(), args) } fn build_elf_lld_args( @@ -2105,11 +2137,12 @@ fn build_elf_lld_args( if let Some(sysroot) = &global.llvm.sysroot { args.push(format!("--sysroot={}", sysroot)); } + let mut uses_elf_end_files = false; 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); + uses_elf_end_files = append_elf_start_files(&mut args, target, global, build); } for obj in objects { @@ -2123,9 +2156,10 @@ fn build_elf_lld_args( append_common_link_mode_args(&mut args, build, LinkerDialect::Gnu); 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); + append_elf_default_libs(&mut args, target, global); + if uses_elf_end_files { + append_elf_end_files(&mut args, target, global); + } } args.push("-o".to_string()); @@ -2257,9 +2291,9 @@ fn append_elf_start_files( target: &str, global: &Global, build: &BuildRequest, -) { +) -> bool { if build.no_start_files { - return; + return false; } let start_name = if build.pie == Some(true) { @@ -2267,11 +2301,17 @@ fn append_elf_start_files( } else { "crt1.o" }; - for file in [start_name, "crti.o"] { - if let Some(path) = find_elf_runtime_file(target, global, file) { - args.push(path); - } + + let start_file = find_elf_runtime_file(target, global, start_name); + let init_file = find_elf_runtime_file(target, global, "crti.o"); + if let (Some(start_file), Some(init_file)) = (start_file, init_file) { + args.push(start_file); + args.push(init_file); + return true; } + + append_bundled_linux_crt1(args, target); + false } fn append_elf_end_files(args: &mut Vec, target: &str, global: &Global) { @@ -2280,6 +2320,57 @@ fn append_elf_end_files(args: &mut Vec, target: &str, global: &Global) { } } +fn append_elf_default_libs(args: &mut Vec, target: &str, global: &Global) { + append_elf_default_lib( + args, + target, + global, + "c", + &["libc.so", "libc.a"], + &["libc.so.6"], + ); + append_elf_default_lib( + args, + target, + global, + "m", + &["libm.so", "libm.a"], + &["libm.so.6"], + ); +} + +fn append_elf_default_lib( + args: &mut Vec, + target: &str, + global: &Global, + link_name: &str, + development_names: &[&str], + runtime_names: &[&str], +) { + if find_elf_runtime_file_any(target, global, development_names).is_some() { + args.push(format!("-l{}", link_name)); + return; + } + + if let Some(path) = find_elf_runtime_file_any(target, global, runtime_names) { + args.push(path); + return; + } + + args.push(format!("-l{}", link_name)); +} + +fn append_bundled_linux_crt1(args: &mut Vec, target: &str) { + args.push("-e".to_string()); + args.push("_start".to_string()); + args.push( + llvm::toolchain::find_bundled_linux_crt1(target) + .unwrap_or_else(|| llvm::toolchain::expected_bundled_linux_crt1(target)) + .to_string_lossy() + .to_string(), + ); +} + fn append_elf_search_paths(args: &mut Vec, target: &str, global: &Global) { for path in elf_runtime_dirs(target, global) { if path.exists() { @@ -2288,6 +2379,42 @@ fn append_elf_search_paths(args: &mut Vec, target: &str, global: &Global } } +fn append_windows_mingw_search_paths(args: &mut Vec) { + for path in windows_mingw_runtime_dirs() { + if path.exists() { + args.push(format!("-L{}", path.display())); + } + } +} + +fn find_windows_mingw_runtime_file(name: &str) -> Option { + windows_mingw_runtime_dirs() + .into_iter() + .map(|dir| dir.join(name)) + .find(|path| path.exists()) +} + +fn windows_mingw_runtime_dirs() -> Vec { + let mut dirs = Vec::new(); + + if let Ok(path) = env::var("WAVE_WINDOWS_MINGW_LIB") { + if !path.trim().is_empty() { + dirs.push(PathBuf::from(path)); + } + } + + if let Ok(exe) = env::current_exe() { + if let Some(dir) = exe.parent() { + dirs.push(dir.join("mingw").join("lib")); + if let Some(root) = dir.parent() { + dirs.push(root.join("lib").join("wave").join("mingw").join("lib")); + } + } + } + + dirs +} + fn find_elf_runtime_file(target: &str, global: &Global, name: &str) -> Option { elf_runtime_dirs(target, global) .into_iter() @@ -2296,6 +2423,18 @@ fn find_elf_runtime_file(target: &str, global: &Global, name: &str) -> Option Option { + for dir in elf_runtime_dirs(target, global) { + for name in names { + let path = dir.join(name); + if path.exists() { + return Some(path.to_string_lossy().to_string()); + } + } + } + None +} + fn elf_runtime_dirs(target: &str, global: &Global) -> Vec { let sysroot = global.llvm.sysroot.as_deref().unwrap_or(""); let mut dirs = Vec::new(); @@ -2355,26 +2494,42 @@ fn detect_macos_sysroot_owned() -> Option { } fn resolve_bundled_tool(tool: &str) -> String { + if let Some(path) = resolve_bundled_tool_path(tool) { + return path.to_string_lossy().to_string(); + } + executable_tool_name(tool) +} + +fn resolve_bundled_tool_path(tool: &str) -> Option { 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(); + return Some(candidate); } } - executable_tool_name(tool) + None } fn configure_bundled_llvm_tool_env(cmd: &mut ProcessCommand, bin: &str) { - let Some(lib_dir) = bundled_llvm_lib_dir(bin) else { + let Some(bin_dir) = bundled_llvm_bin_dir(bin) else { return; }; if cfg!(target_os = "linux") { - prepend_env_path(cmd, "LD_LIBRARY_PATH", lib_dir); + if let Some(lib_dir) = bin_dir.parent().map(|llvm_dir| llvm_dir.join("lib")) { + if lib_dir.is_dir() { + prepend_env_path(cmd, "LD_LIBRARY_PATH", lib_dir); + } + } + } else if cfg!(windows) { + if let Some(root_dir) = bin_dir.parent().and_then(|llvm_dir| llvm_dir.parent()) { + prepend_env_path(cmd, "PATH", root_dir.to_path_buf()); + } + prepend_env_path(cmd, "PATH", bin_dir); } } -fn bundled_llvm_lib_dir(bin: &str) -> Option { +fn bundled_llvm_bin_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") { @@ -2386,8 +2541,7 @@ fn bundled_llvm_lib_dir(bin: &str) -> Option { return None; } - let lib_dir = llvm_dir.join("lib"); - lib_dir.is_dir().then_some(lib_dir) + Some(bin_dir.to_path_buf()) } fn prepend_env_path(cmd: &mut ProcessCommand, name: &str, first: PathBuf) { @@ -2445,6 +2599,14 @@ fn linker_tool_name(bin: &str) -> String { .to_string() } +fn missing_linker_tool_name(global: &Global, bin: &str) -> String { + if is_windows_gnu_target_global(global) && linker_tool_name(bin).eq_ignore_ascii_case("gcc") { + "Windows GNU linker (bundled ld.lld.exe, or gcc.exe in PATH)".to_string() + } else { + linker_tool_name(bin) + } +} + fn default_linker_name(global: &Global) -> String { if let Some(linker) = &global.llvm.linker { return linker.clone(); @@ -2453,6 +2615,10 @@ fn default_linker_name(global: &Global) -> String { let target = target_triple_for_global(global); if is_darwin_target(&target) { resolve_bundled_tool("ld64.lld") + } else if is_windows_gnu_target(&target) { + resolve_bundled_tool_path("ld.lld") + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "gcc".to_string()) } else { resolve_bundled_tool("ld.lld") } @@ -2617,9 +2783,11 @@ fn print_dry_run_human( } if let Some(link_output) = &plan.link_output { - let (bin, args) = build_linker_args(global, build, &plan.link_inputs, link_output); println!(" link:"); - println!(" - {}", shell_join(&bin, &args)); + println!( + " - {}", + render_link_command(global, build, &plan.link_inputs, link_output) + ); } if build.run { @@ -2768,7 +2936,6 @@ fn print_dry_run_json( text.push_str("\"link\":"); if let Some(link_output) = &plan.link_output { - let (bin, args) = build_linker_args(global, build, &plan.link_inputs, link_output); text.push('{'); append_json_field( &mut text, @@ -2776,7 +2943,16 @@ fn print_dry_run_json( &json_string(&link_output.to_string_lossy()), ); text.push(','); - append_json_field(&mut text, "command", &json_string(&shell_join(&bin, &args))); + append_json_field( + &mut text, + "command", + &json_string(&render_link_command( + global, + build, + &plan.link_inputs, + link_output, + )), + ); text.push('}'); } else { text.push_str("null"); @@ -2858,6 +3034,16 @@ fn build_mode_label(build: &BuildRequest) -> &'static str { "compile-only" } +fn render_link_command( + global: &Global, + build: &BuildRequest, + objects: &[String], + output: &Path, +) -> String { + let (bin, args) = build_linker_args(global, build, objects, output); + shell_join(&bin, &args) +} + fn render_emit_spec(spec: &EmitSpec) -> String { match spec { EmitSpec::Check => "check".to_string(), diff --git a/x.py b/x.py index e9136754..a8b9414a 100644 --- a/x.py +++ b/x.py @@ -40,6 +40,10 @@ "LLVM_CONFIG_EXE", "/opt/llvm-win/bin/llvm-config.exe", )) +WINDOWS_RUST_TOOLCHAIN = os.environ.get( + "WAVE_WINDOWS_RUST_TOOLCHAIN", + "stable-x86_64-pc-windows-gnu", +) MINGW_CC = "x86_64-w64-mingw32-gcc" MINGW_CXX = "x86_64-w64-mingw32-g++" @@ -205,8 +209,10 @@ def append_env_words(env, name, 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. + # Use legacy DT_RPATH intentionally: unlike DT_RUNPATH, it applies to + # transitive dependencies such as libLLVM -> libffi in clean containers. append_env_words(env, "RUSTFLAGS", [ + "-C", "link-arg=-Wl,--disable-new-dtags", "-C", "link-arg=-Wl,-z,origin", "-C", "link-arg=-Wl,-rpath,$ORIGIN/llvm/lib", ]) @@ -247,6 +253,27 @@ def windows_package_inputs(exe_path): files.append(path) return files +def llvm_config_path(): + return os.environ.get("LLVM_CONFIG_PATH") or shutil.which("llvm-config") + +def llvm_config_dir(flag): + llvm_config = llvm_config_path() + if not llvm_config: + return None + + result = subprocess.run( + [llvm_config, flag], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return None + + path = Path(result.stdout.strip()) + return path if path.exists() else None + def llvm_prefix(): for env_name in ["WAVE_LLVM_HOME", "LLVM_SYS_211_PREFIX"]: value = os.environ.get(env_name) @@ -255,20 +282,7 @@ def llvm_prefix(): 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 + return llvm_config_dir("--prefix") def llvm_bin_dir(): value = os.environ.get("WAVE_LLVM_BIN") @@ -277,6 +291,10 @@ def llvm_bin_dir(): if path.exists(): return path + path = llvm_config_dir("--bindir") + if path is not None: + return path + prefix = llvm_prefix() if prefix is not None: path = prefix / "bin" @@ -285,14 +303,193 @@ def llvm_bin_dir(): return None def llvm_lib_dir(): + value = os.environ.get("WAVE_LLVM_LIB") + if value: + path = Path(value) + if path.exists(): + return path + + path = llvm_config_dir("--libdir") + if path is not None: + return path + prefix = llvm_prefix() if prefix is not None: - path = prefix / "lib" + for name in ["lib64", "lib"]: + path = prefix / name + if path.exists(): + return path + return None + +def unique_existing_dirs(dirs): + out = [] + seen = set() + for directory in dirs: + if directory is None: + continue + path = Path(directory) + if not path.exists() or not path.is_dir(): + continue + resolved = path.resolve() + if resolved in seen: + continue + seen.add(resolved) + out.append(resolved) + return out + +def windows_llvm_bin_dirs(): + dirs = [] + value = os.environ.get("WAVE_WINDOWS_LLVM_BIN") + if value: + dirs.append(Path(value)) + if WINDOWS_LLVM_CONFIG_EXE.exists(): + dirs.append(WINDOWS_LLVM_CONFIG_EXE.parent) + return unique_existing_dirs(dirs) + +def windows_rust_toolchain_root(): + value = os.environ.get("WAVE_WINDOWS_RUST_ROOT") + if value: + path = Path(value) if path.exists(): return path + + rustup = shutil.which("rustup") + if rustup is None: + return None + + result = subprocess.run( + [rustup, "which", "--toolchain", WINDOWS_RUST_TOOLCHAIN, "rustc.exe"], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return None + + rustc = Path(result.stdout.strip()) + if rustc.exists(): + return rustc.parent.parent + return None + +def windows_rustlib_root(): + root = windows_rust_toolchain_root() + if root is None: + return None + path = root / "lib" / "rustlib" / WINDOWS_GNU_TARGET + return path if path.exists() else None + +def windows_rust_lld_path(): + root = windows_rustlib_root() + if root is None: + return None + + path = root / "bin" / "rust-lld.exe" + return path if path.exists() else None + +def windows_mingw_self_contained_lib_dir(): + value = os.environ.get("WAVE_WINDOWS_MINGW_LIB") + if value: + path = Path(value) + if path.exists(): + return path + + root = windows_rustlib_root() + if root is not None: + path = root / "lib" / "self-contained" + if path.exists(): + return path + return None + +def file_description(path): + if shutil.which("file") is None: + return "" + result = subprocess.run( + ["file", "-L", str(path)], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return "" + return result.stdout.strip() + +def is_windows_x86_64_binary(path): + desc = file_description(path) + if not desc: + return True + return "PE32+" in desc and "x86-64" in desc + +def expected_binary_arch_token(target): + arch = target.split("-", 1)[0] + if arch == "x86_64": + return "x86-64" + if arch == "aarch64": + return "ARM aarch64" + if arch == "riscv64": + return "RISC-V" + if arch in {"i386", "i586", "i686"}: + return "Intel 80386" return None -def find_release_tool(tool): +def is_binary_for_target(path, target): + desc = file_description(path) + if not desc: + return True + + if is_windows_gnu_target(target): + return "PE32+" in desc and "x86-64" in desc + + if is_linux_target(target) and "ELF" in desc: + expected = expected_binary_arch_token(target) + return expected is None or expected in desc + + return True + +def require_binary_for_target(path, target, label): + if is_binary_for_target(path, target): + return + + print(f"[!] {label} has the wrong architecture for {target}: {path}") + print(f" {file_description(path)}") + sys.exit(1) + +def windows_tool_names(tool): + if tool.lower().endswith(".exe"): + names = [tool] + else: + names = [f"{tool}.exe"] + if tool == "ld.lld": + names.append("lld.exe") + return names + +def packaged_tool_name(tool, target): + if is_windows_gnu_target(target) and not tool.lower().endswith(".exe"): + return f"{tool}.exe" + return tool + +def find_release_tool(tool, target): + if is_windows_gnu_target(target): + for directory in windows_llvm_bin_dirs(): + for name in windows_tool_names(tool): + candidate = directory / name + if candidate.exists(): + if not is_windows_x86_64_binary(candidate): + print(f"[!] Windows LLVM tool is not 64-bit PE: {candidate}") + print(f" {file_description(candidate)}") + sys.exit(1) + return candidate.resolve() + if tool == "ld.lld": + rust_lld = windows_rust_lld_path() + if rust_lld is not None: + if not is_windows_x86_64_binary(rust_lld): + print(f"[!] Windows rust-lld is not 64-bit PE: {rust_lld}") + print(f" {file_description(rust_lld)}") + sys.exit(1) + return rust_lld.resolve() + return None + names = [tool] if platform.system() == "Windows" and not tool.endswith(".exe"): names.insert(0, f"{tool}.exe") @@ -309,6 +506,7 @@ def find_release_tool(tool): for name in names: candidate = directory / name if candidate.exists(): + require_binary_for_target(candidate, target, "LLVM tool") return candidate.resolve() return None @@ -325,7 +523,7 @@ def copy_optional(src, dst): shutil.copy2(src, dst) return dst -def copy_globbed_files(patterns, dst_dir): +def copy_globbed_files(patterns, dst_dir, target=None): copied = [] dst_dir.mkdir(parents=True, exist_ok=True) seen = set() @@ -333,6 +531,10 @@ def copy_globbed_files(patterns, dst_dir): for src in pattern.parent.glob(pattern.name): if src.name in seen or not src.is_file(): continue + if target is not None and not is_binary_for_target(src, target): + print(f"[!] Skipping runtime with wrong architecture for {target}: {src}") + print(f" {file_description(src)}") + continue seen.add(src.name) dst = dst_dir / src.name shutil.copy2(src, dst) @@ -354,27 +556,124 @@ def copy_named_runtime(src, dst_dir, dst_name=None): shutil.copy2(src, dst) return dst -def lld_tools_for_target(target): +def llvm_tools_for_target(target): common = ["llc", "llvm-as", "llvm-mc"] if is_darwin_target(target): - return ["ld64.lld", "ld.lld", *common] + return [(tool, True) for tool in ["ld64.lld", "ld.lld", *common]] if is_windows_gnu_target(target): - return ["ld.lld", "lld-link", *common] - return ["ld.lld", *common] + return [ + ("ld.lld", True), + *[(tool, True) for tool in common], + ] + return [(tool, True) for tool in ["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) + missing_optional = [] + for tool, required in llvm_tools_for_target(target): + src = find_release_tool(tool, target) if src is None: - print(f"[!] Missing LLD tool for package: {tool}") + if not required: + missing_optional.append(tool) + continue + print(f"[!] Missing LLVM tool for package: {tool}") + if is_windows_gnu_target(target): + print(f" Expected a 64-bit Windows executable in: {', '.join(str(p) for p in windows_llvm_bin_dirs())}") + print(f" Install one with:") + print(f" rustup toolchain install {WINDOWS_RUST_TOOLCHAIN} --profile minimal --force-non-host") + print(" Or set WAVE_WINDOWS_LLVM_BIN to a complete x86_64 Windows LLVM/LLD bin directory.") sys.exit(1) - dst = tool_dir / tool + require_binary_for_target(src, target, "LLVM tool") + dst = tool_dir / packaged_tool_name(tool, target) copy_executable(src, dst) copied.append((src, dst)) + + if missing_optional and is_windows_gnu_target(target): + print("[!] Windows package is missing optional LLD tools:") + for tool in missing_optional: + print(f" missing optional tool: {tool}.exe") + return copied + +def copy_windows_mingw_self_contained_libs(stage_dir, target): + if not is_windows_gnu_target(target): + return [] + + src_dir = windows_mingw_self_contained_lib_dir() + if src_dir is None: + print("[!] Missing Windows MinGW self-contained runtime libraries") + print(f" Install them with:") + print(f" rustup toolchain install {WINDOWS_RUST_TOOLCHAIN} --profile minimal --force-non-host") + print(" Or set WAVE_WINDOWS_MINGW_LIB to a directory containing crt2.o and lib*.a.") + sys.exit(1) + + required = ["crt2.o", "libmingw32.a", "libgcc.a", "libmingwex.a", "libmsvcrt.a", "libkernel32.a"] + missing = [name for name in required if not (src_dir / name).exists()] + if missing: + print(f"[!] Incomplete Windows MinGW runtime library directory: {src_dir}") + for name in missing: + print(f" missing: {name}") + sys.exit(1) + + dst_dir = stage_dir / "mingw" / "lib" + dst_dir.mkdir(parents=True, exist_ok=True) + + copied = [] + for src in sorted(src_dir.glob("*.a")) + sorted(src_dir.glob("*.o")): + dst = dst_dir / src.name + shutil.copy2(src, dst) + copied.append(dst) return copied +def llvm_tool_run_env(tool): + env = os.environ.copy() + tool = Path(tool) + if tool.parent.name == "bin": + lib_dir = tool.parent.parent / "lib" + if lib_dir.is_dir(): + current = env.get("LD_LIBRARY_PATH", "") + env["LD_LIBRARY_PATH"] = f"{lib_dir}:{current}" if current else str(lib_dir) + return env + +def linux_crt_source_for_target(target): + if target == "x86_64-unknown-linux-gnu": + return ROOT / "llvm" / "crt" / "linux" / "x86_64" / "crt1.s" + return None + +def write_linux_crt_objects(stage_dir, target): + if not is_linux_target(target): + return [] + + source = linux_crt_source_for_target(target) + if source is None: + return [] + if not source.exists(): + print(f"[!] Missing Linux CRT source for {target}: {source}") + sys.exit(1) + + llvm_mc = find_release_tool("llvm-mc", target) + if llvm_mc is None: + print("[!] Missing LLVM tool for Linux CRT object: llvm-mc") + sys.exit(1) + + dst_dir = stage_dir / "crt" / target + dst_dir.mkdir(parents=True, exist_ok=True) + dst = dst_dir / "crt1.o" + + subprocess.run( + [ + str(llvm_mc), + f"-triple={target}", + "-filetype=obj", + str(source), + "-o", + str(dst), + ], + env=llvm_tool_run_env(llvm_mc), + check=True, + ) + return [dst] + def ldd_shared_libs(binary): if shutil.which("ldd") is None: return [] @@ -446,6 +745,119 @@ def copy_linux_runtime_deps(stage_dir, binaries): return copied +WINDOWS_SYSTEM_DLLS = { + "advapi32.dll", + "bcryptprimitives.dll", + "comdlg32.dll", + "crypt32.dll", + "gdi32.dll", + "kernel32.dll", + "msvcrt.dll", + "ntdll.dll", + "ole32.dll", + "oleaut32.dll", + "shell32.dll", + "user32.dll", + "userenv.dll", + "ws2_32.dll", +} + +def is_windows_system_dll(name): + lower = name.lower() + return lower in WINDOWS_SYSTEM_DLLS or lower.startswith("api-ms-win-") or lower.startswith("ext-ms-win-") + +def pe_imported_dlls(binary): + objdump = shutil.which("x86_64-w64-mingw32-objdump") or shutil.which("objdump") + if objdump is None: + return [] + + result = subprocess.run( + [objdump, "-p", str(binary)], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return [] + + dlls = [] + for line in result.stdout.splitlines(): + line = line.strip() + if line.startswith("DLL Name:"): + name = line.split(":", 1)[1].strip() + if name and not is_windows_system_dll(name): + dlls.append(name) + return dlls + +def windows_dll_search_dirs(): + dirs = [] + dirs.extend(windows_llvm_bin_dirs()) + target_runtime = TARGET_DIR / WINDOWS_GNU_TARGET / "release" + dirs.append(target_runtime) + for dll in ["libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"]: + path = mingw_print_file_name(dll) + if path is not None: + dirs.append(path.parent) + return unique_existing_dirs(dirs) + +def find_windows_dll(name): + lower = name.lower() + for directory in windows_dll_search_dirs(): + direct = directory / name + if direct.exists(): + return direct + for candidate in directory.glob("*.dll"): + if candidate.name.lower() == lower: + return candidate + return None + +def copy_windows_runtime_deps(stage_dir, binaries): + copied = [] + queue = [Path(p) for p in binaries if Path(p).exists()] + seen = set() + root_dir = stage_dir + tool_dir = stage_dir / "llvm" / "bin" + + for directory in windows_llvm_bin_dirs(): + for pattern in ["LLVM*.dll", "libLLVM*.dll", "liblld*.dll"]: + for dll in directory.glob(pattern): + queue.append(dll) + + while queue: + current = queue.pop(0) + resolved = current.resolve() + if resolved in seen: + continue + seen.add(resolved) + + if current.suffix.lower() == ".dll": + root_copy = copy_optional(current, root_dir / current.name) + tool_copy = copy_optional(current, tool_dir / current.name) + copied.extend(p for p in [root_copy, tool_copy] if p is not None) + + for dll_name in pe_imported_dlls(current): + dll = find_windows_dll(dll_name) + if dll is None: + print(f"[!] Missing Windows runtime DLL for package: {dll_name}") + print(f" referenced by: {current}") + sys.exit(1) + queue.append(dll) + + return copied + +def verify_packaged_runtime_arch(stage_dir, target): + paths = [] + for directory in [stage_dir, stage_dir / "llvm" / "bin", stage_dir / "llvm" / "lib"]: + if directory.exists(): + paths.extend(path for path in directory.iterdir() if path.is_file()) + + for path in paths: + desc = file_description(path) + if "ELF" not in desc and "PE32" not in desc: + continue + require_binary_for_target(path, target, "Packaged runtime") + def resolve_dylib_reference(ref, binary, extra_dirs=None): path = Path(ref) if path.is_absolute(): @@ -494,20 +906,9 @@ def copy_llvm_runtime_libs(stage_dir, target, lld_tool_paths, runtime_roots=None 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)) + dep_roots = list(runtime_roots or []) + dep_roots.extend(staged for _, staged in lld_tool_paths) + copied.extend(copy_windows_runtime_deps(stage_dir, dep_roots)) return [p for p in copied if p is not None] patterns = [] @@ -525,7 +926,7 @@ def copy_llvm_runtime_libs(stage_dir, target, lld_tool_paths, runtime_roots=None 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)) + copied.extend(copy_globbed_files(patterns, root_lib_dir, target)) 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")) @@ -619,7 +1020,7 @@ def linux_binary_has_runpath(binary, expected): def patch_linux_binary(binary, rpath): if shutil.which("patchelf") is None: return - subprocess.run(["patchelf", "--set-rpath", rpath, str(binary)], check=True) + subprocess.run(["patchelf", "--force-rpath", "--set-rpath", rpath, str(binary)], check=True) def codesign_macos_binary(binary): if shutil.which("codesign") is None: @@ -657,10 +1058,10 @@ def patch_staged_runtime(stage_dir, target, binary_path, lld_tool_paths): 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(f"[!] Missing RPATH/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.") + print(" Rebuild with x.py build/release so Cargo embeds the release runtime path.") sys.exit(1) for _, staged in lld_tool_paths: if staged.exists(): @@ -676,17 +1077,21 @@ def stage_release_package(target, binary, out_name): copy_executable(binary, staged_binary) lld_tools = copy_lld_tools(stage_dir, target) + write_linux_crt_objects(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) + copy_windows_mingw_self_contained_libs(stage_dir, target) 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) + verify_packaged_runtime_arch(stage_dir, target) + return stage_dir # ------------------------------------------------------ @@ -754,6 +1159,8 @@ def cmd_package(): stage_dir = stage_release_package(target, bin_path, out_name) zip_path = ROOT / f"{out_name}.zip" + if zip_path.exists(): + zip_path.unlink() subprocess.run( ["zip", "-r", "-q", str(zip_path), stage_dir.name], cwd=DIST_DIR,