#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const MIN_BD_VERSION: &str = "0.5.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinaryVersion {
pub binary: String,
pub path: PathBuf,
pub version: String,
pub commit: Option<String>,
pub build_date: Option<String>,
#[serde(default)]
pub raw_output: String,
}
impl BinaryVersion {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"binary": self.binary,
"path": self.path.display().to_string(),
"version": self.version,
"commit": self.commit,
"build_date": self.build_date,
})
}
}
#[derive(Debug, Clone)]
pub struct DiscoveredBinaries {
pub br: BinaryVersion,
pub bd: Option<BinaryVersion>,
}
impl DiscoveredBinaries {
pub const fn bd_available(&self) -> bool {
self.bd.is_some()
}
pub fn require_bd(&self) -> Result<&BinaryVersion, String> {
self.bd.as_ref().ok_or_else(|| {
"bd (Go beads) binary not found. Conformance tests require bd to be installed.\n\
Install from: https://github.com/steveyegge/beads\n\
Or set BD_BINARY env var to the path."
.to_string()
})
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"br": self.br.to_json(),
"bd": self.bd.as_ref().map(BinaryVersion::to_json),
"conformance_ready": self.bd_available(),
})
}
}
fn discover_br() -> Result<BinaryVersion, String> {
if let Ok(br_path) = std::env::var("BR_BINARY") {
let path = PathBuf::from(&br_path);
if path.exists() {
return probe_binary("br", &path);
}
return Err(format!("BR_BINARY={br_path} does not exist"));
}
let cargo_bin = assert_cmd::cargo::cargo_bin!("br");
if cargo_bin.exists() {
return probe_binary("br", cargo_bin);
}
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let release_bin = manifest_dir.join("target/release/br");
if release_bin.exists() {
return probe_binary("br", &release_bin);
}
if let Some(path) = which("br") {
return probe_binary("br", &path);
}
Err("br binary not found. Build with `cargo build` first.".to_string())
}
fn discover_bd() -> Option<BinaryVersion> {
if let Ok(bd_path) = std::env::var("BD_BINARY") {
let path = PathBuf::from(&bd_path);
if path.exists() {
return probe_binary("bd", &path).ok();
}
eprintln!("Warning: BD_BINARY={bd_path} does not exist");
return None;
}
let home = std::env::var("HOME")
.ok()
.map(PathBuf::from)
.unwrap_or_default();
let mut common_paths = vec![
PathBuf::from("/usr/local/bin/bd"),
PathBuf::from("/usr/bin/bd"),
];
if !home.as_os_str().is_empty() {
common_paths.push(home.join(".local/bin/bd"));
common_paths.push(home.join("go/bin/bd"));
}
for path in common_paths {
if path.exists()
&& let Ok(version) = probe_binary("bd", &path)
{
return Some(version);
}
}
if let Some(path) = which("bd") {
return probe_binary("bd", &path).ok();
}
None
}
fn probe_binary(name: &str, path: &Path) -> Result<BinaryVersion, String> {
if name == "bd"
&& let Some(output) = run_version_command(path, &["version"])
&& looks_like_br(&output)
{
return Err(format!(
"bd binary at {} appears to be br; set BD_BINARY to real bd",
path.display()
));
}
let json_output = run_version_command(path, &["version", "--json"]);
if let Some(output) = json_output
&& let Ok(parsed) = parse_json_version(&output)
{
return Ok(BinaryVersion {
binary: name.to_string(),
path: path.to_path_buf(),
version: parsed.version,
commit: parsed.commit,
build_date: parsed.build_date,
raw_output: output,
});
}
let plain_output = run_version_command(path, &["--version"]);
if let Some(output) = plain_output {
let version = parse_plain_version(&output);
return Ok(BinaryVersion {
binary: name.to_string(),
path: path.to_path_buf(),
version,
commit: None,
build_date: None,
raw_output: output,
});
}
let check_output = run_version_command(path, &["--help"]);
if check_output.is_some() {
return Ok(BinaryVersion {
binary: name.to_string(),
path: path.to_path_buf(),
version: "unknown".to_string(),
commit: None,
build_date: None,
raw_output: check_output.unwrap_or_default(),
});
}
Err(format!(
"Binary at {} does not respond to version commands",
path.display()
))
}
fn looks_like_br(output: &str) -> bool {
output.trim_start().starts_with("br ")
}
fn run_version_command(binary: &Path, args: &[&str]) -> Option<String> {
let output = Command::new(binary)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).to_string())
} else {
None
}
}
#[derive(Debug, Deserialize)]
struct JsonVersion {
version: String,
commit: Option<String>,
build_date: Option<String>,
}
fn parse_json_version(output: &str) -> Result<JsonVersion, serde_json::Error> {
let json_start = output.find('{').unwrap_or(0);
serde_json::from_str(&output[json_start..])
}
fn parse_plain_version(output: &str) -> String {
let output = output.trim();
for word in output.split_whitespace() {
if word.chars().next().is_some_and(|c| c.is_ascii_digit()) {
let version: String = word
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '-')
.collect();
if !version.is_empty() {
return version;
}
}
}
"unknown".to_string()
}
fn which(name: &str) -> Option<PathBuf> {
std::env::var_os("PATH").and_then(|paths| {
std::env::split_paths(&paths).find_map(|dir| {
let path = dir.join(name);
if path.exists() && path.is_file() {
Some(path)
} else {
None
}
})
})
}
pub fn discover_binaries() -> Result<DiscoveredBinaries, String> {
let br = discover_br()?;
let bd = discover_bd();
Ok(DiscoveredBinaries { br, bd })
}
pub fn check_bd_version(version: &BinaryVersion) -> Result<(), String> {
let current = &version.version;
if current == "unknown" || current.contains("dev") {
return Ok(());
}
if compare_versions(current, MIN_BD_VERSION).is_lt() {
return Err(format!(
"bd version {current} is below minimum required version {MIN_BD_VERSION}. Please upgrade bd."
));
}
Ok(())
}
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
let parse = |s: &str| -> Vec<u32> {
s.split(|c: char| !c.is_ascii_digit())
.filter_map(|p| p.parse().ok())
.collect()
};
let av = parse(a);
let bv = parse(b);
av.cmp(&bv)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discover_br() {
let result = discover_br();
assert!(result.is_ok(), "br should be discoverable: {result:?}");
let version = result.unwrap();
assert_eq!(version.binary, "br");
assert!(version.path.exists());
}
#[test]
fn test_discover_binaries() {
let result = discover_binaries();
assert!(result.is_ok(), "Binary discovery failed: {result:?}");
let binaries = result.unwrap();
assert_eq!(binaries.br.binary, "br");
if binaries.bd_available() {
let bd = binaries.bd.as_ref().unwrap();
assert_eq!(bd.binary, "bd");
}
}
#[test]
fn test_parse_plain_version() {
assert_eq!(parse_plain_version("br 0.1.0"), "0.1.0");
assert_eq!(parse_plain_version("beads 0.5.2"), "0.5.2");
assert_eq!(parse_plain_version("0.1.0-dev"), "0.1.0-dev");
assert_eq!(parse_plain_version("no version"), "unknown");
}
#[test]
fn test_compare_versions() {
use std::cmp::Ordering;
assert_eq!(compare_versions("0.1.0", "0.1.0"), Ordering::Equal);
assert_eq!(compare_versions("0.2.0", "0.1.0"), Ordering::Greater);
assert_eq!(compare_versions("0.1.0", "0.2.0"), Ordering::Less);
assert_eq!(compare_versions("1.0.0", "0.5.0"), Ordering::Greater);
}
#[test]
fn test_discovered_binaries_json() {
let binaries = discover_binaries().expect("discovery failed");
let json = binaries.to_json();
assert!(json.get("br").is_some());
assert!(json.get("conformance_ready").is_some());
}
}