use std::path::{Path, PathBuf};
use std::process::Command;
use crate::runtime_cache;
use anyhow::{bail, Context, Result};
#[cfg(windows)]
use anyhow::anyhow;
pub fn compile_ir_to_object(ir: &Path, object: &Path) -> Result<()> {
let clang = std::env::var("NOVA_CLANG").unwrap_or_else(|_| "clang".to_string());
if tool_available(&clang) {
let mut cmd = Command::new(&clang);
cmd.arg("-O2").arg("-c").arg(ir).arg("-o").arg(object);
return run_tool(cmd, "clang");
}
let llc = std::env::var("NOVA_LLC").unwrap_or_else(|_| "llc".to_string());
if tool_available(&llc) {
let mut cmd = Command::new(&llc);
cmd.arg("-O2")
.arg("-filetype=obj")
.arg(ir)
.arg("-o")
.arg(object);
return run_tool(cmd, "llc");
}
bail!(
"no LLVM toolchain found for `--release`: install LLVM (so `clang` or \
`llc` is on PATH), or set NOVA_CLANG / NOVA_LLC to one"
)
}
fn tool_available(tool: &str) -> bool {
Command::new(tool).arg("--version").output().is_ok()
}
fn run_tool(mut cmd: Command, name: &str) -> Result<()> {
let output = cmd
.output()
.with_context(|| format!("failed to spawn {name}: {:?}", cmd.get_program()))?;
if !output.status.success() {
bail!(
"{name} failed ({}):\n{}\n{}",
output.status,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim(),
);
}
Ok(())
}
pub fn link_executable(object: &Path, output: &Path) -> Result<()> {
let runtime = find_runtime_lib()?;
if cfg!(windows) {
link_msvc(object, &runtime, output)
} else {
link_cc(object, &runtime, output)
}
}
fn runtime_lib_name() -> &'static str {
if cfg!(windows) {
"nova_runtime.lib"
} else {
"libnova_runtime.a"
}
}
fn find_runtime_lib() -> Result<PathBuf> {
let var = |name: &str| std::env::var_os(name).map(PathBuf::from);
runtime_cache::locate(
var("NOVA_RUNTIME_LIB"),
runtime_cache::embedded_runtime(),
|| {
runtime_cache::cache_root(
var("NOVA_HOME"),
var("USERPROFILE"),
var("HOME"),
cfg!(windows),
)
},
|| {
let exe = std::env::current_exe().context("locating the nova executable")?;
beside_exe(&exe)
},
)
}
fn beside_exe(exe: &Path) -> Result<PathBuf> {
let name = runtime_lib_name();
for dir in exe.ancestors().skip(1).take(3) {
let candidate = dir.join(name);
if candidate.exists() {
return Ok(candidate);
}
}
bail!(
"could not find the Nova runtime library ({name}) near {}, and this nova does not \
carry one. Install nova with the release profile, which is `cargo install`'s \
default, or build it with NOVA_EMBED_RUNTIME=1; in a checkout, \
`cargo build -p nova-runtime` puts the library beside it. NOVA_RUNTIME_LIB can \
also name one.",
exe.display()
)
}
#[cfg(windows)]
const MSVC_LIBS: [&str; 8] = [
"bcrypt.lib",
"advapi32.lib",
"kernel32.lib",
"ntdll.lib",
"userenv.lib",
"ws2_32.lib",
"dbghelp.lib",
"msvcrt.lib",
];
#[cfg(windows)]
fn link_msvc(object: &Path, runtime: &Path, output: &Path) -> Result<()> {
let target = match std::env::consts::ARCH {
"x86_64" => "x86_64-pc-windows-msvc",
"aarch64" => "aarch64-pc-windows-msvc",
other => bail!("unsupported Windows architecture for linking: {other}"),
};
let tool = cc::windows_registry::find_tool(target, "link.exe").ok_or_else(|| {
anyhow!(
"MSVC link.exe not found โ install the Visual Studio Build Tools \
(the same requirement as the Rust MSVC toolchain)"
)
})?;
let mut cmd = tool.to_command();
cmd.arg("/NOLOGO")
.arg(format!("/OUT:{}", output.display()))
.arg("/SUBSYSTEM:CONSOLE")
.arg(object)
.arg(runtime)
.args(MSVC_LIBS);
run_linker(cmd)
}
#[cfg(not(windows))]
fn link_msvc(_object: &Path, _runtime: &Path, _output: &Path) -> Result<()> {
unreachable!("MSVC linking is only used on Windows")
}
fn link_cc(object: &Path, runtime: &Path, output: &Path) -> Result<()> {
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".to_string());
let mut cmd = Command::new(cc);
cmd.arg("-o")
.arg(output)
.arg(object)
.arg(runtime)
.args(["-lpthread", "-ldl", "-lm"]);
run_linker(cmd)
}
fn run_linker(mut cmd: Command) -> Result<()> {
let output = cmd
.output()
.with_context(|| format!("failed to spawn linker: {:?}", cmd.get_program()))?;
if !output.status.success() {
bail!(
"linking failed ({}):\n{}\n{}",
output.status,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim(),
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_nothing_beside_it_the_error_says_how_to_get_a_runtime() {
let dir = std::env::temp_dir().join("nova-beside-exe");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("a/b/c")).unwrap();
let error = beside_exe(&dir.join("a/b/c/nova.exe"))
.unwrap_err()
.to_string();
assert!(error.contains("NOVA_EMBED_RUNTIME=1"), "{error}");
assert!(error.contains("NOVA_RUNTIME_LIB"), "{error}");
assert!(error.contains("cargo install"), "{error}");
}
}