use crate::{OptLevel, RUNTIME_LIB};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use std::time::{Duration, SystemTime};
const MIN_CLANG_VERSION: u32 = 15;
static CLANG_VERSION_CHECKED: OnceLock<Result<u32, String>> = OnceLock::new();
pub fn check_clang_version() -> Result<u32, String> {
CLANG_VERSION_CHECKED
.get_or_init(|| {
let output = Command::new("clang")
.arg("--version")
.output()
.map_err(|e| {
format!(
"Failed to run clang: {e}.\n\
plgc needs clang {MIN_CLANG_VERSION}+ to link compiled binaries \
(the binaries themselves need nothing).\n\
Install it with:\n\
\x20 debian/ubuntu: sudo apt install clang\n\
\x20 fedora: sudo dnf install clang\n\
\x20 macOS: xcode-select --install"
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"clang --version failed with exit code {:?}: {stderr}",
output.status.code(),
));
}
let version_str = String::from_utf8_lossy(&output.stdout);
let version = parse_clang_version(&version_str).ok_or_else(|| {
format!(
"Could not parse clang version from: {}\n\
plgc requires clang {MIN_CLANG_VERSION} or later (opaque pointer support).",
version_str.lines().next().unwrap_or(&version_str),
)
})?;
let is_apple = version_str.contains("Apple clang");
let effective_min = if is_apple { 14 } else { MIN_CLANG_VERSION };
if version < effective_min {
return Err(format!(
"clang version {version} detected, but plgc requires {} {effective_min} or later.\n\
The generated LLVM IR uses opaque pointers (requires LLVM 15+).",
if is_apple { "Apple clang" } else { "clang" },
));
}
Ok(version)
})
.clone()
}
fn parse_clang_version(output: &str) -> Option<u32> {
for line in output.lines() {
if line.contains("clang version")
&& let Some(idx) = line.find("version ")
{
let after_version = &line[idx + 8..];
let major: String = after_version
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if !major.is_empty() {
return major.parse().ok();
}
}
}
None
}
static RUNTIME_EXTRACTED: OnceLock<Result<std::path::PathBuf, String>> = OnceLock::new();
const STALE_TEMP_AGE: Duration = Duration::from_secs(10 * 60);
const STALE_SIBLING_AGE: Duration = Duration::from_secs(7 * 24 * 60 * 60);
enum RuntimeArchive {
Cached(std::path::PathBuf),
Ephemeral(#[allow(dead_code)] tempfile::TempDir, std::path::PathBuf),
}
impl RuntimeArchive {
fn lib_path(&self) -> &Path {
match self {
RuntimeArchive::Cached(path) => path,
RuntimeArchive::Ephemeral(_, path) => path,
}
}
}
fn extracted_runtime() -> Result<RuntimeArchive, String> {
let Some(base) = cache_base() else {
let dir = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
let path = dir.path().join("libplg_runtime.a");
fs::write(&path, RUNTIME_LIB).map_err(|e| format!("Failed to write runtime lib: {e}"))?;
return Ok(RuntimeArchive::Ephemeral(dir, path));
};
RUNTIME_EXTRACTED
.get_or_init(|| {
let dir = base.join(concat!("runtime-", env!("PLG_RUNTIME_HASH")));
let path = dir.join("libplg_runtime.a");
sweep_stale(&base, &dir);
if let Ok(meta) = fs::metadata(&path)
&& meta.len() == RUNTIME_LIB.len() as u64
{
return Ok(path);
}
fs::create_dir_all(&dir)
.map_err(|e| format!("Failed to create runtime cache dir: {e}"))?;
let tmp = dir.join(format!(".libplg_runtime.a.{}", std::process::id()));
fs::write(&tmp, RUNTIME_LIB)
.map_err(|e| format!("Failed to write runtime lib: {e}"))?;
fs::rename(&tmp, &path).map_err(|e| format!("Failed to install runtime lib: {e}"))?;
Ok(path)
})
.clone()
.map(RuntimeArchive::Cached)
}
fn cache_base() -> Option<std::path::PathBuf> {
if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME")
&& !xdg.is_empty()
{
return Some(std::path::PathBuf::from(xdg).join("plgc"));
}
if let Some(home) = std::env::var_os("HOME")
&& !home.is_empty()
{
return Some(std::path::PathBuf::from(home).join(".cache").join("plgc"));
}
None
}
fn sweep_stale(base: &Path, keep: &Path) {
if let Ok(entries) = fs::read_dir(base) {
for entry in entries.flatten() {
let path = entry.path();
if path != keep
&& path
.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with("runtime-"))
&& older_than(&path, STALE_SIBLING_AGE)
{
let _ = fs::remove_dir_all(&path);
}
}
}
if let Ok(entries) = fs::read_dir(keep) {
for entry in entries.flatten() {
let path = entry.path();
if path
.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with(".libplg_runtime.a."))
&& older_than(&path, STALE_TEMP_AGE)
{
let _ = fs::remove_file(&path);
}
}
}
}
fn older_than(path: &Path, age: Duration) -> bool {
let Ok(meta) = fs::metadata(path) else {
return false;
};
let Ok(modified) = meta.modified() else {
return false;
};
SystemTime::now()
.duration_since(modified)
.is_ok_and(|elapsed| elapsed > age)
}
pub fn link_ir(ir_path: &Path, output_path: &Path, opt: OptLevel) -> Result<(), String> {
check_clang_version()?;
let runtime = extracted_runtime()?;
let opt_flag = match opt {
OptLevel::O0 => "-O0",
OptLevel::O3 => "-O3",
};
let mut clang = Command::new("clang");
clang.arg(opt_flag);
if opt == OptLevel::O0 {
clang.arg("-g");
}
clang
.arg(ir_path)
.arg("-o")
.arg(output_path)
.arg("-L")
.arg(runtime.lib_path().parent().unwrap())
.arg("-lplg_runtime")
.arg("-lm");
if cfg!(target_os = "macos") {
clang.arg("-Wl,-dead_strip");
} else if cfg!(target_os = "linux") {
clang.arg("-Wl,--gc-sections");
if opt != OptLevel::O0 {
clang.arg("-Wl,--strip-debug");
}
}
let output = clang
.output()
.map_err(|e| format!("Failed to run clang: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Clang compilation failed:\n{stderr}"));
}
Ok(())
}
pub fn link_wasm(ir_path: &Path, output_path: &Path, opt: OptLevel) -> Result<(), String> {
let wasm_runtime = crate::WASM_RUNTIME_LIB.ok_or_else(|| {
"this plgc was built without wasm support.\n\
Reinstall with the wasm runtime embedded: just install-wasm\n\
(or: cargo install --features wasm --path crates/compiler)"
.to_string()
})?;
let (llc, lld, self_contained) = wasm_toolchain()?;
let work = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
let obj = work.path().join("prog.o");
let runtime = work.path().join("libplg_runtime.a");
fs::write(&runtime, wasm_runtime).map_err(|e| format!("Failed to write wasm runtime: {e}"))?;
let opt_flag = match opt {
OptLevel::O0 => "-O0",
OptLevel::O3 => "-O2",
};
let llc_out = Command::new(&llc)
.args(["-mtriple=wasm32-wasi", "-mattr=+tail-call", "-filetype=obj"])
.arg(opt_flag)
.arg(ir_path)
.arg("-o")
.arg(&obj)
.output()
.map_err(|e| format!("Failed to run llc: {e}"))?;
if !llc_out.status.success() {
return Err(format!(
"llc (wasm) failed:\n{}",
String::from_utf8_lossy(&llc_out.stderr)
));
}
let lld_out = Command::new(&lld)
.args(["-flavor", "wasm"])
.arg("-L")
.arg(&self_contained)
.arg(self_contained.join("crt1-command.o"))
.arg(&obj)
.arg(&runtime)
.arg("-lc")
.arg("--allow-undefined")
.arg("-o")
.arg(output_path)
.output()
.map_err(|e| format!("Failed to run wasm-ld (rust-lld): {e}"))?;
if !lld_out.status.success() {
return Err(format!(
"wasm-ld failed:\n{}",
String::from_utf8_lossy(&lld_out.stderr)
));
}
Ok(())
}
const REACTOR_EXPORTS: &[&str] = &[
"plg_init",
"plg_rt_run_query",
"plg_rt_alloc",
"plg_rt_free",
];
pub fn link_wasm_reactor(ir_path: &Path, output_path: &Path, opt: OptLevel) -> Result<(), String> {
let worker_runtime = crate::WORKER_RUNTIME_LIB.ok_or_else(|| {
"this plgc was built without wasm support.\n\
Reinstall with the wasm runtimes embedded: just install-wasm\n\
(or: cargo install --features wasm --path crates/compiler)"
.to_string()
})?;
let (llc, lld) = llvm_tools()?;
let work = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
let obj = work.path().join("prog.o");
let runtime = work.path().join("libplg_runtime.a");
fs::write(&runtime, worker_runtime)
.map_err(|e| format!("Failed to write reactor runtime: {e}"))?;
let opt_flag = match opt {
OptLevel::O0 => "-O0",
OptLevel::O3 => "-O2",
};
let llc_out = Command::new(&llc)
.args([
"-mtriple=wasm32-unknown-unknown",
"-mattr=+tail-call",
"-filetype=obj",
])
.arg(opt_flag)
.arg(ir_path)
.arg("-o")
.arg(&obj)
.output()
.map_err(|e| format!("Failed to run llc: {e}"))?;
if !llc_out.status.success() {
return Err(format!(
"llc (wasm reactor) failed:\n{}",
String::from_utf8_lossy(&llc_out.stderr)
));
}
let mut lld_cmd = Command::new(&lld);
lld_cmd.args(["-flavor", "wasm", "--no-entry", "--allow-undefined"]);
for sym in REACTOR_EXPORTS {
lld_cmd.arg(format!("--export={sym}"));
}
let lld_out = lld_cmd
.arg(&obj)
.arg(&runtime)
.arg("-o")
.arg(output_path)
.output()
.map_err(|e| format!("Failed to run wasm-ld (rust-lld): {e}"))?;
if !lld_out.status.success() {
return Err(format!(
"wasm-ld (reactor) failed:\n{}",
String::from_utf8_lossy(&lld_out.stderr)
));
}
Ok(())
}
fn llvm_tools() -> Result<(PathBuf, PathBuf), String> {
let sysroot = rustc_print(&["--print", "sysroot"])?;
let host = rustc_print(&["--print", "host-tuple"])?;
let bin = Path::new(&sysroot)
.join("lib/rustlib")
.join(&host)
.join("bin");
let llc = bin.join("llc");
let lld = bin.join("rust-lld");
if !llc.exists() || !lld.exists() {
return Err(format!(
"wasm target needs the LLVM tools that ship with rustup:\n \
rustup component add llvm-tools-preview\n\
(looked under {})",
bin.display()
));
}
Ok((llc, lld))
}
fn wasm_toolchain() -> Result<(PathBuf, PathBuf, PathBuf), String> {
let (llc, lld) = llvm_tools()?;
let sysroot = rustc_print(&["--print", "sysroot"])?;
let self_contained = Path::new(&sysroot).join("lib/rustlib/wasm32-wasip1/lib/self-contained");
if !self_contained.exists() {
return Err("wasm target not installed (wasm32-wasip1 std):\n \
rustup target add wasm32-wasip1"
.to_string());
}
if !self_contained.join("libc.a").exists() {
return Err(format!(
"wasm32-wasip1 std looks partially installed — wasi-libc (libc.a) \
missing under {}.\n \
Try: rustup target remove wasm32-wasip1 && rustup target add wasm32-wasip1",
self_contained.display()
));
}
Ok((llc, lld, self_contained))
}
fn rustc_print(args: &[&str]) -> Result<String, String> {
let out = Command::new("rustc")
.args(args)
.output()
.map_err(|e| format!("Failed to run rustc (needed for the wasm target): {e}"))?;
if !out.status.success() {
return Err(format!("rustc {args:?} failed"));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}