use crate::core::config::ResolvedCrateConfig;
use crate::core::config::extras::Language;
use crate::publish::platform::RustTarget;
use anyhow::{Context, Result, bail};
use std::fs;
use std::path::{Path, PathBuf};
pub fn stage_ffi(
config: &ResolvedCrateConfig,
lang: Language,
target: &RustTarget,
workspace_root: &Path,
) -> Result<PathBuf> {
let lib_name = config.ffi_lib_name();
let shared_lib = target.shared_lib_name(&lib_name);
let lib_path = find_built_library(workspace_root, target, &shared_lib)?;
let dest_dir = staging_dir(config, lang, target, workspace_root)?;
fs::create_dir_all(&dest_dir).with_context(|| format!("creating {}", dest_dir.display()))?;
let dest_path = dest_dir.join(&shared_lib);
fs::copy(&lib_path, &dest_path)
.with_context(|| format!("copying {} to {}", lib_path.display(), dest_path.display()))?;
tracing::info!(
lang = %lang,
lib = %shared_lib,
dest = %dest_dir.display(),
"staged FFI library"
);
Ok(dest_path)
}
pub fn stage_header(
config: &ResolvedCrateConfig,
lang: Language,
target: &RustTarget,
workspace_root: &Path,
) -> Result<Option<PathBuf>> {
let header_name = config.ffi_header_name();
let ffi_crate_dir = find_ffi_crate_dir(config, workspace_root);
let header_src = ffi_crate_dir.join("include").join(&header_name);
if !header_src.exists() {
return Ok(None);
}
let dest_dir = staging_dir(config, lang, target, workspace_root)?;
let include_dir = dest_dir.join("include");
fs::create_dir_all(&include_dir)?;
let dest_path = include_dir.join(&header_name);
fs::copy(&header_src, &dest_path)?;
Ok(Some(dest_path))
}
fn find_built_library(workspace_root: &Path, target: &RustTarget, shared_lib: &str) -> Result<PathBuf> {
crate::publish::package::find_built_artifact(workspace_root, target, shared_lib)
}
fn staging_dir(
config: &ResolvedCrateConfig,
lang: Language,
target: &RustTarget,
workspace_root: &Path,
) -> Result<PathBuf> {
let pkg_dir = config.package_dir(lang);
let platform = target.platform_for(lang);
let rel = match lang {
Language::Go => PathBuf::from(&pkg_dir).join(".lib").join(&platform),
Language::Java => PathBuf::from(&pkg_dir)
.join("src/main/resources/natives")
.join(&platform),
Language::Csharp => {
let namespace = config.csharp_namespace();
PathBuf::from(&pkg_dir)
.join(&namespace)
.join("runtimes")
.join(&platform)
.join("native")
}
other => bail!("FFI staging not supported for {other}"),
};
Ok(workspace_root.join(rel))
}
pub fn find_ffi_crate_dir_pub(config: &ResolvedCrateConfig, workspace_root: &Path) -> PathBuf {
find_ffi_crate_dir(config, workspace_root)
}
fn find_ffi_crate_dir(config: &ResolvedCrateConfig, workspace_root: &Path) -> PathBuf {
if let Some(ffi_output) = config.explicit_output.ffi.as_ref() {
let p = Path::new(ffi_output);
for ancestor in p.ancestors() {
if ancestor.join("Cargo.toml").exists() || ancestor.join("include").exists() {
return workspace_root.join(ancestor);
}
}
if let Some(parent) = p.parent() {
return workspace_root.join(parent);
}
}
let crate_name = &config.name;
workspace_root.join(format!("crates/{crate_name}-ffi"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NewAlefConfig;
use std::fs;
use tempfile::TempDir;
fn minimal_config() -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["go", "java", "csharp"]
[[crates]]
name = "my-lib"
sources = ["crates/my-lib/src/lib.rs"]
[crates.ffi]
prefix = "mylib"
lib_name = "my_lib_ffi"
header_name = "my_lib.h"
[crates.csharp]
namespace = "MyLib"
"#,
)
.unwrap();
cfg.resolve().unwrap().remove(0)
}
fn setup_built_ffi(root: &Path, target_triple: &str) {
let target = RustTarget::parse(target_triple).unwrap();
let lib_name = target.shared_lib_name("my_lib_ffi");
let release_dir = root.join("target").join(target_triple).join("release");
fs::create_dir_all(&release_dir).unwrap();
fs::write(release_dir.join(lib_name), "fake-lib").unwrap();
}
fn setup_header(root: &Path) {
let include_dir = root.join("crates/my-lib-ffi/include");
fs::create_dir_all(&include_dir).unwrap();
fs::write(include_dir.join("my_lib.h"), "#pragma once").unwrap();
}
#[test]
fn stage_ffi_go() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let config = minimal_config();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
setup_built_ffi(root, "x86_64-unknown-linux-gnu");
fs::create_dir_all(root.join("packages/go")).unwrap();
let result = stage_ffi(&config, Language::Go, &target, root).unwrap();
assert!(result.exists());
assert!(
result
.to_string_lossy()
.replace('\\', "/")
.contains("packages/go/.lib/linux-x86_64")
);
}
#[test]
fn stage_ffi_java() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let config = minimal_config();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
setup_built_ffi(root, "x86_64-unknown-linux-gnu");
fs::create_dir_all(root.join("packages/java")).unwrap();
let result = stage_ffi(&config, Language::Java, &target, root).unwrap();
assert!(result.exists());
assert!(
result
.to_string_lossy()
.replace('\\', "/")
.contains("natives/linux-x86_64")
);
}
#[test]
fn stage_ffi_csharp() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let config = minimal_config();
let target = RustTarget::parse("aarch64-apple-darwin").unwrap();
setup_built_ffi(root, "aarch64-apple-darwin");
fs::create_dir_all(root.join("packages/csharp")).unwrap();
let result = stage_ffi(&config, Language::Csharp, &target, root).unwrap();
assert!(result.exists());
assert!(
result
.to_string_lossy()
.replace('\\', "/")
.contains("runtimes/osx-arm64/native")
);
}
#[test]
fn stage_ffi_not_found() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let config = minimal_config();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
let result = stage_ffi(&config, Language::Go, &target, root);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn stage_header_present() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let config = minimal_config();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
setup_built_ffi(root, "x86_64-unknown-linux-gnu");
setup_header(root);
fs::create_dir_all(root.join("packages/go")).unwrap();
stage_ffi(&config, Language::Go, &target, root).unwrap();
let result = stage_header(&config, Language::Go, &target, root).unwrap();
assert!(result.is_some());
assert!(result.unwrap().exists());
}
#[test]
fn stage_header_missing() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let config = minimal_config();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
setup_built_ffi(root, "x86_64-unknown-linux-gnu");
fs::create_dir_all(root.join("packages/go")).unwrap();
stage_ffi(&config, Language::Go, &target, root).unwrap();
let result = stage_header(&config, Language::Go, &target, root).unwrap();
assert!(result.is_none());
}
#[test]
fn stage_ffi_native_build_fallback() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let config = minimal_config();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
let lib_name = target.shared_lib_name("my_lib_ffi");
let release_dir = root.join("target/release");
fs::create_dir_all(&release_dir).unwrap();
fs::write(release_dir.join(&lib_name), "fake-lib").unwrap();
fs::create_dir_all(root.join("packages/go")).unwrap();
let result = stage_ffi(&config, Language::Go, &target, root).unwrap();
assert!(result.exists());
assert!(
result
.to_string_lossy()
.replace('\\', "/")
.contains(".lib/linux-x86_64")
);
}
}