pub mod c_ffi;
pub mod cli;
pub mod csharp;
pub mod dart;
pub mod elixir;
pub mod gleam;
pub mod go;
pub mod java;
pub mod kotlin;
pub mod node;
pub mod php;
pub mod python;
pub mod ruby;
pub mod swift;
pub(crate) mod template_env;
pub mod util;
pub mod wasm;
pub mod zig;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct PackageArtifact {
pub path: PathBuf,
pub name: String,
pub checksum: Option<String>,
}
pub fn create_tar_gz(staging_dir: &Path, output_path: &Path) -> Result<()> {
let file_name = staging_dir
.file_name()
.context("staging dir has no file name")?
.to_string_lossy();
let status = std::process::Command::new("tar")
.arg("czf")
.arg(output_path)
.arg("-C")
.arg(staging_dir.parent().unwrap_or(Path::new(".")))
.arg(file_name.as_ref())
.status()?;
if !status.success() {
anyhow::bail!("tar failed with exit code {}", status.code().unwrap_or(-1));
}
Ok(())
}
pub fn create_tar_gz_flat(staging_dir: &Path, output_path: &Path) -> Result<()> {
let mut entries: Vec<String> = std::fs::read_dir(staging_dir)
.with_context(|| format!("reading staging dir {}", staging_dir.display()))?
.map(|res| {
res.map(|entry| entry.file_name().to_string_lossy().into_owned())
.map_err(anyhow::Error::from)
})
.collect::<Result<Vec<_>>>()?;
if entries.is_empty() {
anyhow::bail!(
"staging dir {} is empty; refusing to create empty archive",
staging_dir.display()
);
}
entries.sort();
let status = std::process::Command::new("tar")
.arg("czf")
.arg(output_path)
.arg("-C")
.arg(staging_dir)
.args(&entries)
.status()?;
if !status.success() {
anyhow::bail!("tar failed with exit code {}", status.code().unwrap_or(-1));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuildProfile {
Release,
Debug,
}
impl BuildProfile {
pub(crate) fn dir_name(self) -> &'static str {
match self {
BuildProfile::Release => "release",
BuildProfile::Debug => "debug",
}
}
pub(crate) fn cargo_flag(self) -> &'static str {
match self {
BuildProfile::Release => " --release",
BuildProfile::Debug => "",
}
}
}
impl std::fmt::Display for BuildProfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.dir_name())
}
}
pub fn find_built_artifact(
workspace_root: &Path,
target: &crate::publish::platform::RustTarget,
filename: &str,
profile: BuildProfile,
) -> Result<PathBuf> {
let cross_dir = workspace_root
.join("target")
.join(&target.triple)
.join(profile.dir_name());
let native_dir = workspace_root.join("target").join(profile.dir_name());
for candidate_dir in [&cross_dir, &native_dir] {
let candidate = candidate_dir.join(filename);
if candidate.exists() {
tracing::debug!(path = %candidate.display(), %profile, "found uplifted build artifact");
return Ok(candidate);
}
}
let untrusted_deps_copy = [cross_dir.join("deps"), native_dir.join("deps")]
.into_iter()
.map(|deps_dir| deps_dir.join(filename))
.find(|candidate| candidate.exists());
match untrusted_deps_copy {
Some(deps_path) => anyhow::bail!(
"{filename} not found in target/{}/{profile}/ or target/{profile}/ (the only locations cargo \
uplifts an explicit build target into); an untrusted deps/-only copy exists at {} but was not \
used because a crate compiled only as a transitive dependency of something else's build may not \
carry the feature set the bindings were generated against — run `cargo build -p <crate>{}` (or \
`alef build{}`) to build it as an explicit top-level target and produce a trustworthy artifact",
target.triple,
deps_path.display(),
profile.cargo_flag(),
profile.cargo_flag(),
),
None => anyhow::bail!(
"{filename} not found in target/{}/{profile}/ or target/{profile}/",
target.triple
),
}
}
#[cfg(test)]
mod find_built_artifact_tests {
use super::{BuildProfile, find_built_artifact};
use crate::publish::platform::RustTarget;
#[test]
fn finds_uplifted_release_artifact() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let release_dir = root.join("target").join(&target.triple).join("release");
std::fs::create_dir_all(&release_dir).expect("create release dir");
std::fs::write(release_dir.join("libsample_ffi.so"), b"uplifted-release").expect("write fixture");
let found = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Release)
.expect("must find uplifted release artifact");
assert_eq!(found, release_dir.join("libsample_ffi.so"));
}
#[test]
fn finds_uplifted_debug_artifact_in_native_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let debug_dir = root.join("target/debug");
std::fs::create_dir_all(&debug_dir).expect("create debug dir");
std::fs::write(debug_dir.join("libsample_ffi.so"), b"uplifted-debug").expect("write fixture");
let found = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Debug)
.expect("must find uplifted debug artifact");
assert_eq!(found, debug_dir.join("libsample_ffi.so"));
}
#[test]
fn release_profile_does_not_fall_back_to_debug_uplift() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let debug_dir = root.join("target/debug");
std::fs::create_dir_all(&debug_dir).expect("create debug dir");
std::fs::write(debug_dir.join("libsample_ffi.so"), b"uplifted-debug").expect("write fixture");
let result = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Release);
assert!(
result.is_err(),
"a debug-only artifact must not satisfy a release request"
);
}
#[test]
fn debug_profile_does_not_fall_back_to_release_uplift() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let release_dir = root.join("target/release");
std::fs::create_dir_all(&release_dir).expect("create release dir");
std::fs::write(release_dir.join("libsample_ffi.so"), b"uplifted-release").expect("write fixture");
let result = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Debug);
assert!(
result.is_err(),
"a release-only artifact must not satisfy a debug request"
);
}
#[test]
fn rejects_deps_only_artifact_even_though_it_is_real_cargo_output() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let deps_dir = root.join("target/release/deps");
std::fs::create_dir_all(&deps_dir).expect("create deps dir");
std::fs::write(deps_dir.join("libsample_ffi.so"), b"deps-only-artifact").expect("write fixture");
let result = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Release);
assert!(result.is_err(), "a deps/-only artifact must never be silently staged");
}
#[test]
fn error_names_the_untrusted_deps_copy_when_rejecting() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let deps_dir = root.join("target/release/deps");
std::fs::create_dir_all(&deps_dir).expect("create deps dir");
std::fs::write(deps_dir.join("libsample_ffi.so"), b"deps-only-artifact").expect("write fixture");
let error = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Release)
.expect_err("deps-only artifact must be rejected");
let message = error.to_string();
assert!(
message.contains("deps"),
"error should name the rejected deps/ copy, got: {message}"
);
assert!(
message.contains(&deps_dir.join("libsample_ffi.so").display().to_string()),
"error should include the deps/ copy's path so an operator can inspect it, got: {message}"
);
}
#[test]
fn prefers_uplifted_artifact_over_deps_copy() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let release_dir = root.join("target/release");
std::fs::create_dir_all(&release_dir).expect("create release dir");
std::fs::write(release_dir.join("libsample_ffi.so"), b"uplifted").expect("write uplifted fixture");
let deps_dir = release_dir.join("deps");
std::fs::create_dir_all(&deps_dir).expect("create deps dir");
std::fs::write(deps_dir.join("libsample_ffi.so"), b"deps-copy").expect("write deps fixture");
let found = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Release)
.expect("must find uplifted artifact");
assert_eq!(found, release_dir.join("libsample_ffi.so"));
}
#[test]
fn errors_without_mentioning_deps_when_nothing_exists_anywhere() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let result = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Release);
let error = result.expect_err("must error when nothing exists");
let message = error.to_string();
assert!(message.contains("not found"), "got: {message}");
assert!(
!message.contains("deps/-only copy exists"),
"must not claim a deps/ copy exists when none does, got: {message}"
);
}
#[test]
fn still_errors_when_absent_everywhere_including_deps() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let result = find_built_artifact(root, &target, "libsample_ffi.so", BuildProfile::Release);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
}