use std::path::{Path, PathBuf};
use std::process::Command;
use serde::Deserialize;
use crate::config::Suite;
use crate::error::{Error, Outcome};
use crate::toolchain::Toolchain;
#[derive(Debug, Clone)]
pub struct BuildOutput {
pub cargo_dylib_path: PathBuf,
pub deps_dir: PathBuf,
#[allow(dead_code)] pub invocation: String,
}
#[derive(Debug, Clone)]
pub struct BuildParams<'a> {
pub crate_name: &'a str,
pub features: &'a [String],
pub manifest_path: &'a Path,
pub target_dir: &'a Path,
pub toolchain: &'a Toolchain,
}
pub fn build(params: &BuildParams<'_>) -> Result<BuildOutput, Error> {
std::fs::create_dir_all(params.target_dir).map_err(|e| {
Error::io(
e,
"creating lihaaf-build target dir",
Some(params.target_dir.to_path_buf()),
)
})?;
let mut cmd = Command::new("cargo");
cmd.arg("rustc")
.arg("-p")
.arg(params.crate_name)
.arg("--lib")
.arg("--release")
.arg("--crate-type=dylib")
.arg("--message-format=json-render-diagnostics")
.arg("--manifest-path")
.arg(params.manifest_path)
.arg("--target-dir")
.arg(params.target_dir);
for f in params.features {
cmd.arg("--features").arg(f);
}
let prior_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
let new_rustflags = if prior_rustflags.is_empty() {
"-C prefer-dynamic".to_string()
} else {
format!("{prior_rustflags} -C prefer-dynamic")
};
cmd.env("RUSTFLAGS", &new_rustflags);
let invocation = format!(
"RUSTFLAGS={:?} cargo rustc -p {} --lib --release --crate-type=dylib \
--message-format=json-render-diagnostics --manifest-path {:?} --target-dir {:?}{}",
new_rustflags,
params.crate_name,
params.manifest_path,
params.target_dir,
if params.features.is_empty() {
String::new()
} else {
format!(" --features {}", params.features.join(","))
}
);
let output = cmd.output().map_err(|e| Error::SubprocessSpawn {
program: "cargo".into(),
source: e,
})?;
if !output.status.success() {
return Err(Error::Session(Outcome::DylibBuildFailed {
invocation: invocation.clone(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let dylib_path = parse_dylib_path(&stdout, params.crate_name).ok_or_else(|| {
Error::Session(Outcome::DylibNotFound {
invocation: invocation.clone(),
crate_name: params.crate_name.to_string(),
})
})?;
let deps_dir = dylib_path
.parent()
.map(|p| p.join("deps"))
.unwrap_or_else(|| params.target_dir.join("release/deps"));
let _ = params.toolchain;
Ok(BuildOutput {
cargo_dylib_path: dylib_path,
deps_dir,
invocation,
})
}
#[derive(Debug, Deserialize)]
struct CompilerArtifact {
reason: String,
target: ArtifactTarget,
filenames: Vec<PathBuf>,
}
#[derive(Debug, Deserialize)]
struct ArtifactTarget {
name: String,
kind: Vec<String>,
}
pub fn parse_dylib_path(stdout: &str, crate_name: &str) -> Option<PathBuf> {
let extensions = dylib_extensions();
let mut last_match: Option<PathBuf> = None;
for line in stdout.lines() {
if !line.starts_with('{') {
continue;
}
let artifact: CompilerArtifact = match serde_json::from_str(line) {
Ok(a) => a,
Err(_) => continue,
};
if artifact.reason != "compiler-artifact" {
continue;
}
if artifact.target.name != crate_name {
continue;
}
if !artifact.target.kind.iter().any(|k| k == "dylib") {
continue;
}
for filename in &artifact.filenames {
if let Some(ext) = filename.extension().and_then(|e| e.to_str())
&& extensions.contains(&ext)
{
last_match = Some(filename.clone());
break;
}
}
}
last_match
}
pub fn dylib_extensions() -> &'static [&'static str] {
if cfg!(target_os = "linux") || cfg!(target_os = "android") {
&["so"]
} else if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
&["dylib"]
} else if cfg!(target_os = "windows") {
&["dll"]
} else {
&["so"]
}
}
pub fn managed_dylib_path(workspace_target: &Path, cargo_dylib: &Path) -> PathBuf {
let lihaaf_dir = workspace_target.join("lihaaf");
let stem = cargo_dylib
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("lib");
let ext = cargo_dylib
.extension()
.and_then(|s| s.to_str())
.unwrap_or("so");
let (crate_part, hash_part) = match stem.strip_prefix("lib") {
Some(rest) => match rest.rfind('-') {
Some(idx) => (&rest[..idx], &rest[idx + 1..]),
None => (rest, "0"),
},
None => (stem, "0"),
};
lihaaf_dir.join(format!("lib{crate_part}-current-{hash_part}.{ext}"))
}
pub fn copy_dylib(cargo_dylib: &Path, managed: &Path) -> Result<(), Error> {
if let Some(parent) = managed.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
Error::io(
e,
"creating managed dylib parent",
Some(parent.to_path_buf()),
)
})?;
}
if managed.exists() || managed.symlink_metadata().is_ok() {
std::fs::remove_file(managed).map_err(|e| {
Error::io(
e,
"removing prior managed dylib",
Some(managed.to_path_buf()),
)
})?;
}
std::fs::copy(cargo_dylib, managed).map_err(|e| {
Error::io(
e,
"copying cargo dylib to managed location",
Some(managed.to_path_buf()),
)
})?;
Ok(())
}
pub fn symlink_dylib(cargo_dylib: &Path, managed: &Path) -> Result<(), Error> {
if let Some(parent) = managed.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
Error::io(
e,
"creating managed dylib parent",
Some(parent.to_path_buf()),
)
})?;
}
if managed.exists() || managed.symlink_metadata().is_ok() {
std::fs::remove_file(managed).map_err(|e| {
Error::io(
e,
"removing prior managed dylib",
Some(managed.to_path_buf()),
)
})?;
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(cargo_dylib, managed)
.map_err(|e| Error::io(e, "symlinking cargo dylib", Some(managed.to_path_buf())))?;
}
#[cfg(windows)]
{
std::os::windows::fs::symlink_file(cargo_dylib, managed)
.map_err(|e| Error::io(e, "symlinking cargo dylib", Some(managed.to_path_buf())))?;
}
#[cfg(not(any(unix, windows)))]
{
copy_dylib(cargo_dylib, managed)?;
}
Ok(())
}
pub fn mtime_unix_secs(path: &Path) -> Result<i64, Error> {
let meta = std::fs::metadata(path)
.map_err(|e| Error::io(e, "stat file for mtime", Some(path.to_path_buf())))?;
let mtime = meta
.modified()
.map_err(|e| Error::io(e, "reading mtime", Some(path.to_path_buf())))?;
let dur = mtime
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
Ok(dur.as_secs() as i64)
}
pub fn workspace_target_dir(manifest_path: &Path) -> PathBuf {
if let Ok(env_dir) = std::env::var("CARGO_TARGET_DIR")
&& !env_dir.is_empty()
{
return PathBuf::from(env_dir);
}
let crate_dir = manifest_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
crate_dir.join("target")
}
pub fn build_dir_for_suite(workspace_target: &Path, suite_name: &str) -> PathBuf {
if suite_name == crate::config::DEFAULT_SUITE_NAME {
workspace_target.join("lihaaf-build")
} else {
workspace_target.join(format!("lihaaf-build-{suite_name}"))
}
}
#[allow(dead_code)]
pub fn validate_params(_params: &BuildParams<'_>, _suite: &Suite) -> Result<(), Error> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dylib_path_picks_dylib_kind_and_extension() {
#[cfg(target_os = "linux")]
let line = r#"{"reason":"compiler-artifact","target":{"name":"consumer","kind":["dylib"]},"filenames":["/p/target/release/deps/libconsumer-abc.so"]}"#;
#[cfg(target_os = "macos")]
let line = r#"{"reason":"compiler-artifact","target":{"name":"consumer","kind":["dylib"]},"filenames":["/p/target/release/deps/libconsumer-abc.dylib"]}"#;
#[cfg(target_os = "windows")]
let line = r#"{"reason":"compiler-artifact","target":{"name":"consumer","kind":["dylib"]},"filenames":["C:/p/target/release/deps/consumer-abc.dll"]}"#;
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
let line = r#"{"reason":"compiler-artifact","target":{"name":"consumer","kind":["dylib"]},"filenames":["/p/target/release/deps/libconsumer-abc.so"]}"#;
let path = parse_dylib_path(line, "consumer").unwrap();
assert!(path.to_string_lossy().contains("consumer-abc"));
}
#[test]
fn parse_dylib_path_skips_unrelated_artifacts() {
let stream = "\
{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"unrelated\",\"kind\":[\"lib\"]},\"filenames\":[\"/p/x.rlib\"]}
{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"consumer\",\"kind\":[\"lib\"]},\"filenames\":[\"/p/libconsumer.rlib\"]}
{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"consumer\",\"kind\":[\"dylib\"]},\"filenames\":[\"/p/libconsumer-abc.so\"]}
";
#[cfg(target_os = "windows")]
let _ = stream;
#[cfg(not(target_os = "windows"))]
{
let p = parse_dylib_path(stream, "consumer").unwrap();
assert!(p.to_string_lossy().ends_with(".so"));
}
}
#[test]
fn managed_dylib_path_preserves_hash() {
let p = managed_dylib_path(
Path::new("/p/target"),
Path::new("/p/target/release/deps/libconsumer-abc123.so"),
);
assert!(p.ends_with("libconsumer-current-abc123.so"));
assert!(p.starts_with("/p/target/lihaaf"));
}
#[test]
fn managed_dylib_path_handles_missing_hash() {
let p = managed_dylib_path(
Path::new("/p/target"),
Path::new("/p/target/release/deps/libconsumer.so"),
);
assert!(p.ends_with("libconsumer-current-0.so"));
}
#[test]
fn build_dir_for_default_suite_uses_legacy_name() {
let p = build_dir_for_suite(Path::new("/p/target"), crate::config::DEFAULT_SUITE_NAME);
assert_eq!(p, PathBuf::from("/p/target/lihaaf-build"));
}
#[test]
fn build_dir_for_named_suite_includes_name() {
let p = build_dir_for_suite(Path::new("/p/target"), "spatial");
assert_eq!(p, PathBuf::from("/p/target/lihaaf-build-spatial"));
}
}