RFC 0008: Status updates now sync to markdown files, not just DB RFC 0009: Add Audit as first-class document type, rename blue_audit to blue_health_check to avoid naming collision Also includes: - Update RFC 0005 with Ollama auto-detection and bundled Goose support - Mark RFCs 0001-0006 as Implemented - Add spikes documenting investigations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
212 lines
6.5 KiB
Rust
212 lines
6.5 KiB
Rust
//! Build script for Blue CLI
|
|
//!
|
|
//! Downloads Goose binary for the target platform during build.
|
|
//! Binary is placed in OUT_DIR and copied to target dir post-build.
|
|
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
#[allow(unused_imports)]
|
|
use std::io::Write;
|
|
|
|
const GOOSE_VERSION: &str = "1.21.1";
|
|
|
|
fn main() {
|
|
println!("cargo:rerun-if-changed=build.rs");
|
|
println!("cargo:rerun-if-env-changed=BLUE_SKIP_DOWNLOAD");
|
|
println!("cargo:rerun-if-env-changed=BLUE_GOOSE_PATH");
|
|
|
|
// Skip download if explicitly disabled (for CI caching)
|
|
if env::var("BLUE_SKIP_DOWNLOAD").is_ok() {
|
|
println!("cargo:warning=Skipping Goose download (BLUE_SKIP_DOWNLOAD set)");
|
|
return;
|
|
}
|
|
|
|
// Use pre-downloaded binary if specified
|
|
if let Ok(path) = env::var("BLUE_GOOSE_PATH") {
|
|
println!("cargo:warning=Using pre-downloaded Goose from {}", path);
|
|
copy_goose_binary(&PathBuf::from(path));
|
|
return;
|
|
}
|
|
|
|
// Check if we already have the binary
|
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
|
let goose_binary = out_dir.join(if cfg!(windows) { "goose.exe" } else { "goose" });
|
|
|
|
if goose_binary.exists() {
|
|
println!("cargo:warning=Goose binary already exists");
|
|
copy_goose_binary(&goose_binary);
|
|
return;
|
|
}
|
|
|
|
// Download Goose for target platform
|
|
if let Err(e) = download_goose() {
|
|
println!("cargo:warning=Failed to download Goose: {}", e);
|
|
println!("cargo:warning=blue agent will check for system Goose at runtime");
|
|
}
|
|
}
|
|
|
|
fn download_goose() -> Result<(), Box<dyn std::error::Error>> {
|
|
let target = env::var("TARGET")?;
|
|
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
|
|
|
let (url, archive_name) = get_goose_url(&target)?;
|
|
|
|
println!("cargo:warning=Downloading Goose {} for {}", GOOSE_VERSION, target);
|
|
|
|
// Download to OUT_DIR
|
|
let archive_path = out_dir.join(&archive_name);
|
|
download_file(&url, &archive_path)?;
|
|
|
|
// Extract binary
|
|
let goose_binary = extract_goose(&archive_path, &out_dir, &target)?;
|
|
|
|
// Copy to cargo output location
|
|
copy_goose_binary(&goose_binary);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_goose_url(target: &str) -> Result<(String, String), Box<dyn std::error::Error>> {
|
|
let base = format!(
|
|
"https://github.com/block/goose/releases/download/v{}",
|
|
GOOSE_VERSION
|
|
);
|
|
|
|
let (archive, name) = match target {
|
|
// macOS ARM64 (M1/M2/M3/M4)
|
|
t if t.contains("aarch64") && t.contains("apple") => (
|
|
format!("{}/goose-aarch64-apple-darwin.tar.bz2", base),
|
|
"goose-aarch64-apple-darwin.tar.bz2".to_string(),
|
|
),
|
|
// macOS x86_64
|
|
t if t.contains("x86_64") && t.contains("apple") => (
|
|
format!("{}/goose-x86_64-apple-darwin.tar.bz2", base),
|
|
"goose-x86_64-apple-darwin.tar.bz2".to_string(),
|
|
),
|
|
// Linux x86_64
|
|
t if t.contains("x86_64") && t.contains("linux") => (
|
|
format!("{}/goose-x86_64-unknown-linux-gnu.tar.bz2", base),
|
|
"goose-x86_64-unknown-linux-gnu.tar.bz2".to_string(),
|
|
),
|
|
// Linux ARM64
|
|
t if t.contains("aarch64") && t.contains("linux") => (
|
|
format!("{}/goose-aarch64-unknown-linux-gnu.tar.bz2", base),
|
|
"goose-aarch64-unknown-linux-gnu.tar.bz2".to_string(),
|
|
),
|
|
// Windows x86_64
|
|
t if t.contains("x86_64") && t.contains("windows") => (
|
|
format!("{}/goose-x86_64-pc-windows-gnu.zip", base),
|
|
"goose-x86_64-pc-windows-gnu.zip".to_string(),
|
|
),
|
|
_ => return Err(format!("Unsupported target: {}", target).into()),
|
|
};
|
|
|
|
Ok((archive, name))
|
|
}
|
|
|
|
fn download_file(url: &str, dest: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Use curl for simplicity - available on all platforms
|
|
let status = std::process::Command::new("curl")
|
|
.args(["-L", "-o"])
|
|
.arg(dest)
|
|
.arg(url)
|
|
.status()?;
|
|
|
|
if !status.success() {
|
|
return Err(format!("curl failed with status: {}", status).into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn extract_goose(
|
|
archive: &PathBuf,
|
|
out_dir: &PathBuf,
|
|
target: &str,
|
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
let binary_name = if target.contains("windows") {
|
|
"goose.exe"
|
|
} else {
|
|
"goose"
|
|
};
|
|
|
|
if archive.to_string_lossy().ends_with(".tar.bz2") {
|
|
// Extract tar.bz2
|
|
let status = std::process::Command::new("tar")
|
|
.args(["-xjf"])
|
|
.arg(archive)
|
|
.arg("-C")
|
|
.arg(out_dir)
|
|
.status()?;
|
|
|
|
if !status.success() {
|
|
return Err("tar extraction failed".into());
|
|
}
|
|
} else if archive.to_string_lossy().ends_with(".zip") {
|
|
// Extract zip
|
|
let status = std::process::Command::new("unzip")
|
|
.args(["-o"])
|
|
.arg(archive)
|
|
.arg("-d")
|
|
.arg(out_dir)
|
|
.status()?;
|
|
|
|
if !status.success() {
|
|
return Err("unzip extraction failed".into());
|
|
}
|
|
}
|
|
|
|
// Find the goose binary (might be in a subdirectory)
|
|
let binary_path = find_binary(out_dir, binary_name)?;
|
|
|
|
// Make executable on Unix
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mut perms = fs::metadata(&binary_path)?.permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(&binary_path, perms)?;
|
|
}
|
|
|
|
Ok(binary_path)
|
|
}
|
|
|
|
fn find_binary(dir: &PathBuf, name: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
// Check direct path first
|
|
let direct = dir.join(name);
|
|
if direct.exists() {
|
|
return Ok(direct);
|
|
}
|
|
|
|
// Search subdirectories
|
|
for entry in fs::read_dir(dir)? {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
if let Ok(found) = find_binary(&path, name) {
|
|
return Ok(found);
|
|
}
|
|
} else if path.file_name().map(|n| n == name).unwrap_or(false) {
|
|
return Ok(path);
|
|
}
|
|
}
|
|
|
|
Err(format!("Binary {} not found in {:?}", name, dir).into())
|
|
}
|
|
|
|
fn copy_goose_binary(source: &PathBuf) {
|
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
|
|
|
// Tell Cargo where the binary is
|
|
println!("cargo:rustc-env=GOOSE_BINARY_PATH={}", source.display());
|
|
|
|
// Also copy to a known location in OUT_DIR for runtime discovery
|
|
let dest = out_dir.join("goose");
|
|
if source != &dest {
|
|
if let Err(e) = fs::copy(source, &dest) {
|
|
println!("cargo:warning=Failed to copy Goose binary: {}", e);
|
|
}
|
|
}
|
|
}
|