use std::path::{Path, PathBuf};
use std::process::Command;
use soothfast_sdk::SdkKind;
use soothfast_sdk::target::Target;
pub struct Built {
pub target: &'static Target,
pub binary: PathBuf,
}
pub fn compile(
pkg: &str,
binary: &str,
targets: &[&'static Target],
release: bool,
target_dir: &Path,
) -> (Vec<Built>, Vec<String>) {
let mut built = Vec::new();
let mut skipped = Vec::new();
for target in targets {
match compile_one(pkg, binary, target, release, target_dir) {
Ok(path) => built.push(Built {
target,
binary: path,
}),
Err(why) => skipped.push(format!("{}: {why}", target.triple)),
}
}
(built, skipped)
}
fn compile_one(
pkg: &str,
binary: &str,
target: &Target,
release: bool,
target_dir: &Path,
) -> Result<PathBuf, String> {
let mut cmd = Command::new("cargo");
cmd.args([
"build",
"-p",
pkg,
"--bin",
binary,
"--target",
target.triple,
]);
if release {
cmd.arg("--release");
}
let status = cmd.status().map_err(|e| format!("cannot run cargo: {e}"))?;
if !status.success() {
return Err(format!(
"cargo build failed — is the target installed? \
(rustup target add {})",
target.triple
));
}
let path = binary_path(target_dir, binary, target, release);
if !path.is_file() {
return Err(format!("built, but {} is missing", path.display()));
}
Ok(path)
}
fn binary_path(target_dir: &Path, binary: &str, target: &Target, release: bool) -> PathBuf {
target_dir
.join(target.triple)
.join(if release { "release" } else { "debug" })
.join(format!("{binary}{}", target.exe_suffix))
}
pub fn stage_npm(
out_dir: &Path,
package: &str,
binary: &str,
built: &[Built],
) -> Result<Vec<String>, String> {
let mut staged = Vec::new();
for item in built {
let dir = out_dir
.join("platforms")
.join(format!("{package}-{}", item.target.npm_suffix()))
.join("bin");
std::fs::create_dir_all(&dir)
.map_err(|e| format!("cannot create {}: {e}", dir.display()))?;
let dest = dir.join(format!("{binary}{}", item.target.exe_suffix));
std::fs::copy(&item.binary, &dest)
.map_err(|e| format!("cannot stage {}: {e}", dest.display()))?;
make_executable(&dest)?;
staged.push(item.target.triple.to_string());
}
Ok(staged)
}
pub fn build_wheels(out_dir: &Path, built: &[Built]) -> Result<Vec<String>, String> {
let dist = out_dir.join("dist");
let _ = std::fs::remove_dir_all(&dist);
std::fs::create_dir_all(&dist).map_err(|e| format!("cannot create {}: {e}", dist.display()))?;
run_uv(&["build", "--sdist", "--out-dir", "dist"], out_dir, &[])?;
let mut wheels = Vec::new();
for item in built {
let absolute = std::fs::canonicalize(&item.binary)
.map_err(|e| format!("cannot resolve {}: {e}", item.binary.display()))?;
run_uv(
&["build", "--wheel", "--out-dir", "dist"],
out_dir,
&[
("SOOTHFAST_SERVER_BIN", absolute.to_string_lossy().as_ref()),
("SOOTHFAST_WHEEL_TAG", &item.target.wheel_tag()),
],
)?;
wheels.push(item.target.wheel_tag());
}
Ok(wheels)
}
pub fn manylinux_warning(built: &[Built]) -> Option<String> {
manylinux_warning_for(built, std::env::var_os("AUDITWHEEL_PLAT").is_some())
}
fn manylinux_warning_for(built: &[Built], in_manylinux: bool) -> Option<String> {
if in_manylinux {
return None;
}
let claimed: Vec<&str> = built
.iter()
.map(|b| b.target.wheel_platform)
.filter(|p| p.starts_with("manylinux"))
.collect();
if claimed.is_empty() {
return None;
}
Some(format!(
"tagging {} outside a manylinux container — the wheel claims an \
older glibc than it was linked against, and will fail to start on \
systems that honor the tag. Build these in the manylinux image \
(or switch those targets to musl) before publishing.",
claimed.join(", ")
))
}
fn run_uv(args: &[&str], dir: &Path, env: &[(&str, &str)]) -> Result<(), String> {
let mut cmd = Command::new("uv");
cmd.args(args).current_dir(dir);
for (key, value) in env {
cmd.env(key, value);
}
let status = cmd.status().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
"`uv` not found — curl -LsSf https://astral.sh/uv/install.sh | sh".to_string()
} else {
format!("cannot run uv: {e}")
}
})?;
if status.success() {
Ok(())
} else {
Err(format!("`uv {}` failed", args.join(" ")))
}
}
#[cfg(unix)]
fn make_executable(path: &Path) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
.map_err(|e| format!("cannot chmod {}: {e}", path.display()))
}
#[cfg(not(unix))]
fn make_executable(_path: &Path) -> Result<(), String> {
Ok(())
}
pub fn bundled_targets(kind: SdkKind, out_dir: &Path, package: &str, binary: &str) -> Vec<String> {
match kind {
SdkKind::TypeScript => Target::ALL
.iter()
.filter(|t| {
out_dir
.join("platforms")
.join(format!("{package}-{}", t.npm_suffix()))
.join("bin")
.join(format!("{binary}{}", t.exe_suffix))
.is_file()
})
.map(|t| t.triple.to_string())
.collect(),
SdkKind::Python => {
let dist = out_dir.join("dist");
let Ok(entries) = std::fs::read_dir(&dist) else {
return Vec::new();
};
let names: Vec<String> = entries
.flatten()
.map(|e| e.file_name().to_string_lossy().to_string())
.filter(|n| n.ends_with(".whl"))
.collect();
Target::ALL
.iter()
.filter(|t| names.iter().any(|n| n.contains(t.wheel_platform)))
.map(|t| t.triple.to_string())
.collect()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn target(triple: &str) -> &'static Target {
Target::find(triple).expect("known target")
}
fn built(triples: &[&str]) -> Vec<Built> {
triples
.iter()
.map(|t| Built {
target: target(t),
binary: PathBuf::from("unused"),
})
.collect()
}
fn scratch(name: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("soothfast-sdk-build-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch dir");
dir
}
#[test]
fn cross_compiled_output_is_looked_for_under_its_triple() {
let dir = Path::new("/w/target");
assert_eq!(
binary_path(dir, "srv", target("x86_64-unknown-linux-gnu"), true),
Path::new("/w/target/x86_64-unknown-linux-gnu/release/srv")
);
assert_eq!(
binary_path(dir, "srv", target("x86_64-unknown-linux-gnu"), false),
Path::new("/w/target/x86_64-unknown-linux-gnu/debug/srv")
);
assert_eq!(
binary_path(dir, "srv", target("x86_64-pc-windows-msvc"), true),
Path::new("/w/target/x86_64-pc-windows-msvc/release/srv.exe")
);
}
#[test]
fn a_manylinux_tag_built_outside_the_container_is_called_out() {
let warning =
manylinux_warning_for(&built(&["x86_64-unknown-linux-gnu"]), false).expect("warned");
assert!(warning.contains("manylinux_2_17_x86_64"), "{warning}");
}
#[test]
fn inside_the_container_the_tag_is_earned_and_nothing_is_said() {
assert_eq!(
manylinux_warning_for(&built(&["x86_64-unknown-linux-gnu"]), true),
None
);
}
#[test]
fn targets_that_make_no_glibc_claim_never_warn() {
for triple in [
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-musl",
] {
assert_eq!(
manylinux_warning_for(&built(&[triple]), false),
None,
"{triple}"
);
}
}
#[test]
fn npm_staging_is_only_reported_for_binaries_that_landed() {
let out = scratch("npm");
let source = out.join("server");
std::fs::write(&source, b"#!/bin/sh\n").expect("write");
let items = vec![Built {
target: target("x86_64-unknown-linux-gnu"),
binary: source,
}];
let staged = stage_npm(&out, "acme-items", "server", &items).expect("stages");
assert_eq!(staged, ["x86_64-unknown-linux-gnu"]);
let landed = out.join("platforms/acme-items-linux-x64/bin/server");
assert!(landed.is_file(), "{}", landed.display());
assert_eq!(
bundled_targets(SdkKind::TypeScript, &out, "acme-items", "server"),
["x86_64-unknown-linux-gnu"]
);
let _ = std::fs::remove_dir_all(&out);
}
#[test]
fn a_windows_binary_is_staged_under_its_exe_name() {
let out = scratch("win");
let source = out.join("server.exe");
std::fs::write(&source, b"MZ").expect("write");
let items = vec![Built {
target: target("x86_64-pc-windows-msvc"),
binary: source,
}];
stage_npm(&out, "acme-items", "server", &items).expect("stages");
assert!(
out.join("platforms/acme-items-win32-x64/bin/server.exe")
.is_file()
);
let _ = std::fs::remove_dir_all(&out);
}
#[test]
fn nothing_staged_means_nothing_bundled() {
let out = scratch("empty");
assert!(bundled_targets(SdkKind::TypeScript, &out, "acme-items", "server").is_empty());
assert!(bundled_targets(SdkKind::Python, &out, "acme-items", "server").is_empty());
let _ = std::fs::remove_dir_all(&out);
}
#[test]
fn python_coverage_is_read_from_the_wheel_platform_tags() {
let out = scratch("wheels");
std::fs::create_dir_all(out.join("dist")).expect("dist");
for name in [
"acme_items-1.2.3-py3-none-manylinux_2_17_x86_64.whl",
"acme_items-1.2.3-py3-none-macosx_11_0_arm64.whl",
"acme_items-1.2.3.tar.gz",
] {
std::fs::write(out.join("dist").join(name), b"").expect("write");
}
let bundled = bundled_targets(SdkKind::Python, &out, "acme-items", "server");
assert_eq!(
bundled,
["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]
);
let _ = std::fs::remove_dir_all(&out);
}
}