use anyhow::Result;
use std::path::PathBuf;
use uuid::Uuid;
const REPO_URL: &str = "https://github.com/adolfousier/opencrabs.git";
pub fn running_binary_path() -> std::io::Result<PathBuf> {
Ok(strip_deleted_marker(std::env::current_exe()?))
}
pub(crate) fn strip_deleted_marker(exe: PathBuf) -> PathBuf {
match exe.to_str().and_then(|s| s.strip_suffix(" (deleted)")) {
Some(stripped) => PathBuf::from(stripped),
None => exe,
}
}
pub struct SelfUpdater {
project_root: PathBuf,
binary_path: PathBuf,
}
impl SelfUpdater {
pub fn new(project_root: PathBuf, binary_path: PathBuf) -> Self {
Self {
project_root,
binary_path,
}
}
pub fn auto_detect() -> Result<Self> {
let exe = running_binary_path()?;
let source_dir = crate::config::opencrabs_home().join("source");
let (project_root, binary_path) = Self::resolve_paths(&exe, source_dir)?;
tracing::info!(
"SelfUpdater: project_root={}, binary={}",
project_root.display(),
binary_path.display()
);
Ok(Self {
project_root,
binary_path,
})
}
pub(crate) fn resolve_paths(
exe: &std::path::Path,
source_dir: std::path::PathBuf,
) -> Result<(std::path::PathBuf, std::path::PathBuf)> {
let mut search_dir = exe
.parent()
.ok_or_else(|| anyhow::anyhow!("Cannot determine executable parent directory"))?
.to_path_buf();
loop {
if search_dir.join("Cargo.toml").exists() {
let binary_path = search_dir.join("target").join("release").join("opencrabs");
return Ok((search_dir, binary_path));
}
if !search_dir.pop() {
break;
}
}
Ok((source_dir, exe.to_path_buf()))
}
fn ensure_source_tree(&self) -> Result<()> {
if self.project_root.join("Cargo.toml").exists() {
tracing::info!("Updating source at {}", self.project_root.display());
let _ = std::process::Command::new("git")
.args(["pull", "--ff-only"])
.current_dir(&self.project_root)
.output();
return Ok(());
}
tracing::info!(
"Cloning OpenCrabs source to {}",
self.project_root.display()
);
let output = std::process::Command::new("git")
.args([
"clone",
"--depth",
"1",
REPO_URL,
&self.project_root.to_string_lossy(),
])
.output()
.map_err(|e| anyhow::anyhow!("Failed to clone source (is git installed?): {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("git clone failed: {}", stderr));
}
Ok(())
}
pub async fn build(&self) -> Result<PathBuf, String> {
self.build_streaming(|_| {}).await
}
pub async fn build_streaming<F>(&self, on_line: F) -> Result<PathBuf, String>
where
F: Fn(String) + Send + 'static,
{
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
if let Err(e) = self.ensure_source_tree() {
return Err(format!("Failed to prepare source tree: {e}"));
}
tracing::info!("Building OpenCrabs at {}", self.project_root.display());
let mut child = Command::new("cargo")
.args(["build", "--release"])
.env("RUSTFLAGS", "-C target-cpu=native")
.current_dir(&self.project_root)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to spawn cargo build: {}", e))?;
if let Some(stderr) = child.stderr.take() {
let mut lines = BufReader::new(stderr).lines();
while let Ok(Some(line)) = lines.next_line().await {
on_line(line);
}
}
let status = child
.wait()
.await
.map_err(|e| format!("Build process error: {}", e))?;
if status.success() {
let built_path = self
.project_root
.join("target")
.join("release")
.join("opencrabs");
let result_path = if built_path.exists() {
built_path
} else {
self.binary_path.clone()
};
tracing::info!("Build succeeded: {}", result_path.display());
Ok(result_path)
} else {
Err("Build failed — see output above".to_string())
}
}
pub async fn test(&self) -> Result<(), String> {
tracing::info!("Running tests at {}", self.project_root.display());
let output = tokio::process::Command::new("cargo")
.arg("test")
.current_dir(&self.project_root)
.output()
.await
.map_err(|e| format!("Failed to spawn cargo test: {}", e))?;
if output.status.success() {
tracing::info!("Tests passed");
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
tracing::warn!("Tests failed:\n{}\n{}", stderr, stdout);
Err(format!("{}\n{}", stderr, stdout))
}
}
#[cfg(unix)]
pub fn restart(&self, session_id: Uuid) -> Result<()> {
Self::restart_into(&self.binary_path, session_id)
}
#[cfg(not(unix))]
pub fn restart(&self, session_id: Uuid) -> Result<()> {
Self::restart_into(&self.binary_path, session_id)
}
#[cfg(unix)]
pub fn restart_into(binary_path: &std::path::Path, session_id: Uuid) -> Result<()> {
use std::os::unix::process::CommandExt;
tracing::info!(
"Restarting OpenCrabs: {} chat --session {}",
binary_path.display(),
session_id
);
let err = std::process::Command::new(binary_path)
.args(["chat", "--session", &session_id.to_string()])
.env("OPENCRABS_EVOLVED_FROM", crate::VERSION)
.exec();
Err(anyhow::anyhow!("exec() failed: {}", err))
}
#[cfg(not(unix))]
pub fn restart_into(_binary_path: &std::path::Path, _session_id: Uuid) -> Result<()> {
Err(anyhow::anyhow!(
"Hot restart via exec() is only supported on Unix platforms"
))
}
pub fn project_root(&self) -> &std::path::Path {
&self.project_root
}
pub fn binary_path(&self) -> &std::path::Path {
&self.binary_path
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let updater = SelfUpdater::new(
PathBuf::from("/tmp/project"),
PathBuf::from("/tmp/project/target/release/opencrabs"),
);
assert_eq!(updater.project_root(), std::path::Path::new("/tmp/project"));
assert_eq!(
updater.binary_path(),
std::path::Path::new("/tmp/project/target/release/opencrabs")
);
}
}