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 const PREFERRING_RELEASE_ORDER: [BuildProfile; 2] = [BuildProfile::Release, BuildProfile::Debug];
pub fn find_built_artifact(
workspace_root: &Path,
target: &crate::publish::platform::RustTarget,
filename: &str,
profile: BuildProfile,
) -> Result<PathBuf> {
find_built_artifact_impl(workspace_root, target, &[filename], profile, &[])
}
pub fn find_built_artifact_with_extra_dirs(
workspace_root: &Path,
target: &crate::publish::platform::RustTarget,
filename: &str,
profile: BuildProfile,
extra_dirs: &[PathBuf],
) -> Result<PathBuf> {
find_built_artifact_impl(workspace_root, target, &[filename], profile, extra_dirs)
}
pub fn find_built_artifact_any_with_extra_dirs(
workspace_root: &Path,
target: &crate::publish::platform::RustTarget,
filenames: &[&str],
profile: BuildProfile,
extra_dirs: &[PathBuf],
) -> Result<PathBuf> {
find_built_artifact_impl(workspace_root, target, filenames, profile, extra_dirs)
}
fn find_built_artifact_impl(
workspace_root: &Path,
target: &crate::publish::platform::RustTarget,
filenames: &[&str],
profile: BuildProfile,
extra_dirs: &[PathBuf],
) -> 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].into_iter().chain(extra_dirs.iter()) {
for filename in filenames {
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()
.flat_map(|deps_dir| filenames.iter().map(move |filename| deps_dir.join(*filename)))
.find(|candidate| candidate.exists());
let filenames_display = filenames.join(" or ");
let extra_dirs_note = if extra_dirs.is_empty() {
String::new()
} else {
let listed = extra_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>();
format!(" or in {}", listed.join(", "))
};
match untrusted_deps_copy {
Some(deps_path) => anyhow::bail!(
"{filenames_display} not found in target/{}/{profile}/ or target/{profile}/{extra_dirs_note}; an \
unused deps/-only copy exists at {} — run `cargo build -p <crate>{}` (or `alef build{}`) to build \
it as an explicit top-level target",
target.triple,
deps_path.display(),
profile.cargo_flag(),
profile.cargo_flag(),
),
None => anyhow::bail!(
"{filenames_display} not found in target/{}/{profile}/ or target/{profile}/{extra_dirs_note}",
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}"
);
let portable_message = message.replace('\\', "/");
assert!(
portable_message.contains(&crate::test_support::portable_path_string(
&deps_dir.join("libsample_ffi.so")
)),
"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"));
}
#[test]
fn preferring_release_order_puts_release_first_and_only_lists_the_two_real_profiles() {
assert_eq!(
super::PREFERRING_RELEASE_ORDER,
[BuildProfile::Release, BuildProfile::Debug],
"release must be tried before debug, and no third profile may appear here"
);
}
#[test]
fn with_extra_dirs_checks_extra_dir_when_canonical_locations_miss() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let extra_dir = root.join("crates/sample-node");
std::fs::create_dir_all(&extra_dir).expect("create extra dir");
std::fs::write(extra_dir.join("sample.node"), b"in-crate-output").expect("write fixture");
let found = super::find_built_artifact_with_extra_dirs(
root,
&target,
"sample.node",
BuildProfile::Release,
std::slice::from_ref(&extra_dir),
)
.expect("must find artifact in extra_dirs");
assert_eq!(
found,
extra_dir.join("sample.node"),
"expected the extra_dirs copy at {}, got {}",
extra_dir.join("sample.node").display(),
found.display()
);
}
#[test]
fn extra_dirs_does_not_shadow_the_canonical_uplifted_location() {
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("sample.node"), b"canonical-uplift").expect("write fixture");
let extra_dir = root.join("crates/sample-node");
std::fs::create_dir_all(&extra_dir).expect("create extra dir");
std::fs::write(extra_dir.join("sample.node"), b"in-crate-output").expect("write fixture");
let found = super::find_built_artifact_with_extra_dirs(
root,
&target,
"sample.node",
BuildProfile::Release,
std::slice::from_ref(&extra_dir),
)
.expect("must find canonical artifact");
assert_eq!(
found,
release_dir.join("sample.node"),
"expected the canonical uplifted copy at {}, got {}",
release_dir.join("sample.node").display(),
found.display()
);
}
#[test]
fn find_built_artifact_without_extra_dirs_ignores_the_extra_location() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let extra_dir = root.join("crates/sample-node");
std::fs::create_dir_all(&extra_dir).expect("create extra dir");
std::fs::write(extra_dir.join("sample.node"), b"in-crate-output").expect("write fixture");
let result = find_built_artifact(root, &target, "sample.node", BuildProfile::Release);
assert!(
result.is_err(),
"find_built_artifact must not silently check extra_dirs-only locations, got: {result:?}"
);
}
#[test]
fn find_built_artifact_any_searches_every_tier_before_the_next_filename() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let target = RustTarget::parse("x86_64-unknown-linux-gnu").expect("parse target");
let cross_dir = root.join("target").join(&target.triple).join("release");
std::fs::create_dir_all(&cross_dir).expect("create cross dir");
std::fs::write(cross_dir.join("second.node"), b"cross-tier-second-name").expect("write fixture");
let extra_dir = root.join("crates/sample-node");
std::fs::create_dir_all(&extra_dir).expect("create extra dir");
std::fs::write(extra_dir.join("first.node"), b"extra-tier-first-name").expect("write fixture");
let found = super::find_built_artifact_any_with_extra_dirs(
root,
&target,
&["first.node", "second.node"],
BuildProfile::Release,
std::slice::from_ref(&extra_dir),
)
.expect("must find an artifact");
assert_eq!(
found,
cross_dir.join("second.node"),
"the higher-priority tier must win over a lower-priority tier under a preferred name; expected {}, \
got {}",
cross_dir.join("second.node").display(),
found.display()
);
}
}