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,
#[serde(default)]
package_id: Option<String>,
target: ArtifactTarget,
filenames: Vec<PathBuf>,
}
#[derive(Debug, Deserialize)]
struct ArtifactTarget {
name: String,
#[serde(default)]
kind: Vec<String>,
#[serde(default)]
crate_types: 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_matches_crate(&artifact, crate_name) {
continue;
}
if !artifact_is_dylib(&artifact.target) {
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
}
fn artifact_matches_crate(artifact: &CompilerArtifact, crate_name: &str) -> bool {
if let Some(package_id) = artifact.package_id.as_deref() {
return package_id_matches_crate(package_id, crate_name);
}
artifact.target.name == crate_name
}
fn artifact_is_dylib(target: &ArtifactTarget) -> bool {
if !target.crate_types.is_empty() {
return target.crate_types.iter().any(|k| k == "dylib");
}
target.kind.iter().any(|k| k == "dylib")
}
fn package_id_matches_crate(package_id: &str, crate_name: &str) -> bool {
let Some((source, suffix)) = package_id.rsplit_once('#') else {
return false;
};
if package_fragment_matches_crate(suffix, crate_name) {
return true;
}
let source_tail = source
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or_default();
package_fragment_matches_crate(source_tail, crate_name)
}
fn package_fragment_matches_crate(fragment: &str, crate_name: &str) -> bool {
fragment == crate_name
|| fragment.replace('-', "_") == crate_name
|| fragment
.strip_prefix(crate_name)
.is_some_and(|rest| rest.starts_with('@'))
}
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()),
)
})?;
}
crate::util::remove_path_race_free(managed, "prior managed dylib")?;
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()),
)
})?;
}
crate::util::remove_path_race_free(managed, "prior managed dylib")?;
#[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_crate_type_and_extension() {
#[cfg(target_os = "linux")]
let line = r#"{"reason":"compiler-artifact","package_id":"path+file:///p#consumer@0.1.0","target":{"name":"consumer","kind":["lib"],"crate_types":["rlib","dylib"]},"filenames":["/p/target/release/deps/libconsumer-abc.so"]}"#;
#[cfg(target_os = "macos")]
let line = r#"{"reason":"compiler-artifact","package_id":"path+file:///p#consumer@0.1.0","target":{"name":"consumer","kind":["lib"],"crate_types":["rlib","dylib"]},"filenames":["/p/target/release/deps/libconsumer-abc.dylib"]}"#;
#[cfg(target_os = "windows")]
let line = r#"{"reason":"compiler-artifact","package_id":"path+file:///p#consumer@0.1.0","target":{"name":"consumer","kind":["lib"],"crate_types":["rlib","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","package_id":"path+file:///p#consumer@0.1.0","target":{"name":"consumer","kind":["lib"],"crate_types":["rlib","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 parse_dylib_path_can_match_package_id_when_lib_target_name_differs() {
let stream = r#"{"reason":"compiler-artifact","package_id":"path+file:///p#consumer@0.1.0","target":{"name":"consumer_lib","kind":["lib"],"crate_types":["rlib","dylib"]},"filenames":["/p/libconsumer_lib-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 parse_dylib_path_prefers_package_id_over_target_name_collision() {
let ext = dylib_extensions()[0];
let stream = format!(
"\
{{\"reason\":\"compiler-artifact\",\"package_id\":\"path+file:///p#consumer@0.1.0\",\"target\":{{\"name\":\"consumer_lib\",\"kind\":[\"lib\"],\"crate_types\":[\"rlib\",\"dylib\"]}},\"filenames\":[\"/p/libconsumer_lib-right.{ext}\"]}}
{{\"reason\":\"compiler-artifact\",\"package_id\":\"path+file:///p#other@0.1.0\",\"target\":{{\"name\":\"consumer\",\"kind\":[\"lib\"],\"crate_types\":[\"rlib\",\"dylib\"]}},\"filenames\":[\"/p/libconsumer-wrong.{ext}\"]}}
"
);
let p = parse_dylib_path(&stream, "consumer").unwrap();
assert!(p.to_string_lossy().contains("right"));
}
#[test]
fn parse_dylib_path_uses_crate_types_when_cargo_provides_them() {
let ext = dylib_extensions()[0];
let stream = format!(
r#"{{"reason":"compiler-artifact","package_id":"path+file:///p#consumer@0.1.0","target":{{"name":"consumer","kind":["dylib"],"crate_types":["rlib"]}},"filenames":["/p/libconsumer-not-dylib.{ext}"]}}"#
);
assert!(parse_dylib_path(&stream, "consumer").is_none());
}
#[test]
fn parse_dylib_path_matches_path_package_id_source_tail() {
let ext = dylib_extensions()[0];
let stream = format!(
r#"{{"reason":"compiler-artifact","package_id":"path+file:///workspace/consumer#0.1.0","target":{{"name":"consumer","kind":["dylib"],"crate_types":["dylib"]}},"filenames":["/p/libconsumer.{ext}"]}}"#
);
let p = parse_dylib_path(&stream, "consumer").unwrap();
assert!(p.to_string_lossy().contains("libconsumer"));
}
#[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_default_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"));
}
}