use std::env;
use std::fs;
use std::io::Read;
use std::io::Write;
use std::io::{self};
use std::path::PathBuf;
use std::sync::Arc;
use rand::RngExt;
use rand_distr::Alphanumeric;
use std::process::{Command, Stdio};
use std::thread;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use crate::Equation;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[allow(unused_mut)]
fn new_command(program: &str) -> Command {
let mut cmd = Command::new(program);
#[cfg(windows)]
cmd.creation_flags(CREATE_NO_WINDOW);
cmd
}
fn find_cargo() -> String {
if let Ok(output) = Command::new("cargo").arg("--version").output() {
if output.status.success() {
return "cargo".to_string();
}
}
if let Ok(cargo_home) = env::var("CARGO_HOME") {
let cargo_path = PathBuf::from(&cargo_home)
.join("bin")
.join(cargo_exe_name());
if cargo_path.exists() {
return cargo_path.to_string_lossy().to_string();
}
}
let home = env::var("HOME")
.or_else(|_| env::var("USERPROFILE"))
.unwrap_or_default();
if !home.is_empty() {
let standard_path = PathBuf::from(&home)
.join(".cargo")
.join("bin")
.join(cargo_exe_name());
if standard_path.exists() {
return standard_path.to_string_lossy().to_string();
}
}
#[cfg(target_os = "windows")]
{
let candidates = [
"C:\\Program Files\\Rust stable MSVC\\bin\\cargo.exe",
"C:\\Program Files\\Rust stable GNU\\bin\\cargo.exe",
];
for candidate in &candidates {
if PathBuf::from(candidate).exists() {
return candidate.to_string();
}
}
}
#[cfg(target_os = "macos")]
{
let candidates = ["/opt/homebrew/bin/cargo", "/usr/local/bin/cargo"];
for candidate in &candidates {
if PathBuf::from(candidate).exists() {
return candidate.to_string();
}
}
}
#[cfg(target_os = "linux")]
{
let candidates = ["/usr/local/bin/cargo", "/usr/bin/cargo", "/snap/bin/cargo"];
for candidate in &candidates {
if PathBuf::from(candidate).exists() {
return candidate.to_string();
}
}
}
"cargo".to_string()
}
#[inline]
fn cargo_exe_name() -> &'static str {
#[cfg(target_os = "windows")]
{
"cargo.exe"
}
#[cfg(not(target_os = "windows"))]
{
"cargo"
}
}
pub fn compile<E: Equation>(
model_txt: String,
output: Option<PathBuf>,
params: Vec<String>,
template_path: PathBuf,
event_callback: impl Fn(String, String) + Send + Sync + 'static,
) -> Result<String, io::Error> {
let event_callback = Arc::new(event_callback);
let template_dir = match create_template(template_path.clone()) {
Ok(path) => path,
Err(e) => {
event_callback(
"build-log".into(),
format!("Failed to create template: {}", e),
);
return Err(e);
}
};
match inject_model::<E>(model_txt, params, template_dir.clone()) {
Ok(()) => (),
Err(e) => {
event_callback("build-log".into(), format!("Failed to inject model: {}", e));
return Err(e);
}
};
let dynlib_path = match build_template(template_dir.clone(), event_callback.clone()) {
Ok(path) => path,
Err(e) => {
event_callback(
"build-log".into(),
format!("Failed to build template: {}", e),
);
return Err(e);
}
};
event_callback(
"build-complete".into(),
"Compilation finished successfully".into(),
);
let output_path = output.unwrap_or_else(|| {
let random_suffix: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(5)
.map(char::from)
.collect();
let default_name = format!(
"model_{}_{}_{}.pkm",
env::consts::OS,
env::consts::ARCH,
random_suffix
);
let temp_dir = PathBuf::from(template_path);
temp_dir.with_file_name(default_name)
});
fs::copy(&dynlib_path, &output_path).expect("Failed to copy dynamic library to output path");
Ok(output_path.to_string_lossy().to_string())
}
pub fn dummy_compile(
template_path: PathBuf,
event_callback: impl Fn(String, String) + Send + Sync + 'static,
) -> Result<String, io::Error> {
let event_callback = Arc::new(event_callback);
let template_dir = create_template(template_path.clone())?;
build_template(template_dir.clone(), event_callback.clone())?;
event_callback(
"build-complete".into(),
"Compilation finished successfully".into(),
);
Ok(template_dir.to_string_lossy().to_string())
}
fn create_template(temp_dir: PathBuf) -> Result<PathBuf, io::Error> {
if !temp_dir.exists() {
fs::create_dir_all(&temp_dir)?;
}
let template_dir = temp_dir.join("template");
let cargo_toml_path = template_dir.join("Cargo.toml");
let pkg_version = env!("CARGO_PKG_VERSION");
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let pharmsol_dep = if std::env::var("PHARMSOL_LOCAL_EXA").is_ok() {
let manifest_path =
std::fs::canonicalize(manifest_dir).unwrap_or_else(|_| PathBuf::from(manifest_dir));
let manifest_str = manifest_path.to_string_lossy();
if manifest_str.contains('\'') {
let escaped = manifest_str.replace('\\', "\\\\").replace('"', "\\\"");
format!(r#"pharmsol = {{ path = "{}" }}"#, escaped)
} else {
format!(r#"pharmsol = {{ path = '{}' }}"#, manifest_str)
}
} else {
format!(r#"pharmsol = {{ version = "{}" }}"#, pkg_version)
};
let cargo_toml_content = format!(
r#"
[package]
name = "model_lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
{pharmsol_dep}
"#,
);
let src_dir = template_dir.join("src");
let needs_scaffold = !template_dir.exists() || !src_dir.exists();
if needs_scaffold {
if template_dir.exists() {
fs::remove_dir_all(&template_dir)?;
}
let output = new_command("cargo")
.arg("new")
.arg("template")
.arg("--lib")
.current_dir(&temp_dir)
.output()
.expect("Failed to create cargo project");
io::stderr().write_all(&output.stderr)?;
io::stdout().write_all(&output.stdout)?;
fs::write(&cargo_toml_path, &cargo_toml_content)?;
} else if !cargo_toml_path.exists() {
fs::write(&cargo_toml_path, &cargo_toml_content)?;
} else {
let existing_content = fs::read_to_string(&cargo_toml_path)?;
if existing_content.trim() != cargo_toml_content.trim() {
tracing::info!("pharmsol dependency changed, invalidating exa compilation cache");
fs::write(&cargo_toml_path, &cargo_toml_content)?;
let target_dir = template_dir.join("target");
if target_dir.exists() {
fs::remove_dir_all(&target_dir)?;
}
}
};
Ok(template_dir)
}
pub fn temp_path() -> PathBuf {
env::temp_dir().join("exa_tmp")
}
fn inject_model<E: Equation>(
model_txt: String,
params: Vec<String>,
template_dir: PathBuf,
) -> Result<(), io::Error> {
let lib_rs_path = template_dir.join("src").join("lib.rs");
let lib_rs_content = format!(
r#"
#![allow(dead_code)]
#![allow(unused_variables)]
use std::ffi::c_void;
use pharmsol::*;
pub fn eqn() -> impl Equation {{
{}
}}
#[no_mangle]
pub extern "C" fn create_eqn_ptr() -> *mut c_void {{
let eqn = Box::new(eqn());
Box::into_raw(eqn) as *mut c_void
}}
#[no_mangle]
pub extern "C" fn equation_kind() -> EqnKind{{
{}
}}
#[no_mangle]
pub extern "C" fn metadata_ptr() -> *mut c_void{{
let meta = Box::new(equation::Meta::new(vec![{}]));
Box::into_raw(meta) as *mut c_void
}}
"#,
model_txt,
E::kind().to_str(),
params
.iter()
.map(|p| format!("\"{}\"", p))
.collect::<Vec<String>>()
.join(", ")
);
fs::write(lib_rs_path, lib_rs_content)?;
let _ = new_command("cargo")
.arg("fmt")
.current_dir(&template_dir)
.output();
Ok(())
}
fn build_template(
template_path: PathBuf,
event_callback: Arc<dyn Fn(String, String) + Send + Sync + 'static>,
) -> Result<PathBuf, io::Error> {
let cargo_path = find_cargo();
let mut command = new_command(&cargo_path);
command
.arg("build")
.arg("--release")
.arg("--quiet")
.current_dir(&template_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn()?;
let stdout = child.stdout.take().expect("Failed to capture stdout");
let stderr = child.stderr.take().expect("Failed to capture stderr");
let stdout_handle = stream_output(stdout, event_callback.clone());
let stderr_handle = stream_output(stderr, event_callback.clone());
let status = child.wait()?;
stdout_handle
.join()
.expect("Failed to join stdout thread")?;
stderr_handle
.join()
.expect("Failed to join stderr thread")?;
if !status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Failed to build the template",
));
}
let dynlib_name = if cfg!(target_os = "windows") {
"model_lib.dll"
} else if cfg!(target_os = "macos") {
"libmodel_lib.dylib"
} else {
"libmodel_lib.so"
};
Ok(template_path
.join("target")
.join("release")
.join(dynlib_name))
}
fn stream_output<R: Read + Send + 'static>(
reader: R,
event_callback: Arc<dyn Fn(String, String) + Send + Sync + 'static>,
) -> thread::JoinHandle<Result<(), io::Error>> {
thread::spawn(move || {
let mut buffer = [0; 4096];
let mut reader = io::BufReader::new(reader);
loop {
let n = reader.read(&mut buffer)?;
if n == 0 {
break;
}
let output = String::from_utf8_lossy(&buffer[..n]).to_string();
event_callback("build-log-internal".into(), output);
}
Ok(())
})
}