1#![allow(dead_code)]
35
36use crate::paths;
37use anyhow::{Context, Result};
38use indicatif::{ProgressBar, ProgressStyle};
39use std::path::PathBuf;
40
41const FRAMEWORK_VERSION: &str = env!("CARGO_PKG_VERSION");
43
44pub struct BinaryManager {
46 cache_dir: PathBuf,
48 version: String,
50 target: String,
52}
53
54impl BinaryManager {
55 pub fn new() -> Result<Self> {
57 let cache_dir = paths::user::bin_dir();
58 let version = FRAMEWORK_VERSION.to_string();
59 let target = mecha10_binary_naming::detect_target()?;
60
61 Ok(Self {
62 cache_dir,
63 version,
64 target,
65 })
66 }
67
68 #[allow(dead_code)]
70 pub fn with_version(version: String) -> Result<Self> {
71 let cache_dir = paths::user::bin_dir();
72 let target = mecha10_binary_naming::detect_target()?;
73
74 Ok(Self {
75 cache_dir,
76 version,
77 target,
78 })
79 }
80
81 pub fn is_prebuilt_available(&self) -> bool {
83 mecha10_binary_naming::is_prebuilt_available(&self.target)
84 }
85
86 #[allow(dead_code)]
88 pub fn cache_dir(&self) -> &PathBuf {
89 &self.cache_dir
90 }
91
92 #[allow(dead_code)]
94 pub fn version(&self) -> &str {
95 &self.version
96 }
97
98 #[allow(dead_code)]
100 pub fn target(&self) -> &str {
101 &self.target
102 }
103
104 pub async fn resolve(&self, node_name: &str) -> Result<PathBuf> {
119 self.resolve_with_version(node_name, None).await
120 }
121
122 pub async fn resolve_with_version(&self, node_name: &str, version: Option<&str>) -> Result<PathBuf> {
134 if let Some(v) = version {
136 if let Some(path) = self.find_cached_version(node_name, v) {
137 tracing::debug!("Using cached binary: {}", path.display());
138 return Ok(path);
139 }
140 } else if let Some(cached) = self.find_cached(node_name) {
141 tracing::debug!("Using cached binary: {}", cached.display());
142 return Ok(cached);
143 }
144
145 if self.is_prebuilt_available() {
147 let result = if let Some(v) = version {
148 self.download_version(node_name, v).await
149 } else {
150 self.download(node_name).await
151 };
152
153 match result {
154 Ok(path) => {
155 tracing::info!("Downloaded binary for {}: {}", node_name, path.display());
156 return Ok(path);
157 }
158 Err(e) => {
159 tracing::warn!("Failed to download binary for {}: {}", node_name, e);
160 if self.target.ends_with("-apple-darwin") {
162 println!("⚠️ macOS binaries for '{}' v{} not found.", node_name, self.version);
163 println!(" Compiling from source instead (this may take a few minutes)...");
164 }
165 }
167 }
168 } else {
169 tracing::info!("Pre-built binary not available for {} on {}", node_name, self.target);
170 }
171
172 self.cargo_install(node_name).await
174 }
175
176 fn find_cached_version(&self, node_name: &str, version: &str) -> Option<PathBuf> {
178 mecha10_binary_naming::find_cached_version(&self.cache_dir, node_name, version, &self.target)
179 }
180
181 fn find_cached(&self, node_name: &str) -> Option<PathBuf> {
186 mecha10_binary_naming::find_cached(&self.cache_dir, node_name, &self.target)
187 }
188
189 fn binary_name_with_version(&self, node_name: &str, version: &str) -> String {
191 mecha10_binary_naming::binary_name(node_name, version, &self.target)
192 }
193
194 fn binary_name(&self, node_name: &str) -> String {
196 self.binary_name_with_version(node_name, &self.version)
197 }
198
199 fn tarball_name_with_version(&self, node_name: &str, version: &str) -> String {
201 mecha10_binary_naming::tarball_name(node_name, version, &self.target)
202 }
203
204 #[allow(dead_code)]
206 fn tarball_name(&self, node_name: &str) -> String {
207 self.tarball_name_with_version(node_name, &self.version)
208 }
209
210 async fn download(&self, node_name: &str) -> Result<PathBuf> {
212 self.download_version(node_name, &self.version.clone()).await
213 }
214
215 async fn download_version(&self, node_name: &str, version: &str) -> Result<PathBuf> {
217 let tarball = self.tarball_name_with_version(node_name, version);
218 let (os, arch) = mecha10_binary_naming::os_arch_query(&self.target)?;
219 let url = format!(
220 "{}/downloads/node/{}?os={}&arch={}&version={}",
221 paths::urls::mecha10_api_base_url(),
222 node_name,
223 os,
224 arch,
225 version
226 );
227
228 println!("📦 Downloading {} v{}...", node_name, version);
229 tracing::info!("Downloading binary from mecha10-api: {}", url);
230
231 let client = Self::build_client()?;
232 let response = client
233 .get(&url)
234 .send()
235 .await
236 .context(format!("Failed to connect to mecha10-api for binary '{}'", node_name))?;
237
238 if !response.status().is_success() {
239 return Err(anyhow::anyhow!(
240 "Binary '{}' not found: HTTP {}\n\
241 URL: {}\n\
242 Falling back to cargo install...",
243 tarball,
244 response.status(),
245 url
246 ));
247 }
248
249 let size = response.content_length().unwrap_or(0);
250 self.download_and_extract_versioned(node_name, version, response, size)
251 .await
252 }
253
254 async fn download_and_extract_versioned(
256 &self,
257 node_name: &str,
258 version: &str,
259 response: reqwest::Response,
260 size: u64,
261 ) -> Result<PathBuf> {
262 let pb = ProgressBar::new(size);
264 pb.set_style(
265 ProgressStyle::default_bar()
266 .template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
267 .unwrap()
268 .progress_chars("#>-"),
269 );
270
271 let temp_dir = tempfile::tempdir()?;
273 let temp_file = temp_dir.path().join("binary.tar.gz");
274
275 let mut file = tokio::fs::File::create(&temp_file).await?;
276 let mut stream = response.bytes_stream();
277
278 use futures_util::StreamExt;
279 use tokio::io::AsyncWriteExt;
280
281 while let Some(chunk) = stream.next().await {
282 let chunk = chunk.context("Error downloading chunk")?;
283 file.write_all(&chunk).await?;
284 pb.inc(chunk.len() as u64);
285 }
286
287 file.flush().await?;
288 pb.finish_with_message("Download complete");
289
290 tokio::fs::create_dir_all(&self.cache_dir).await?;
292
293 let tar_gz = std::fs::File::open(&temp_file)?;
295 let tar = flate2::read::GzDecoder::new(tar_gz);
296 let mut archive = tar::Archive::new(tar);
297
298 let extract_dir = temp_dir.path().join("extract");
300 std::fs::create_dir_all(&extract_dir)?;
301 archive.unpack(&extract_dir)?;
302
303 let extracted_binary = extract_dir.join(node_name);
305 if !extracted_binary.exists() {
306 return Err(anyhow::anyhow!("Binary '{}' not found in tarball", node_name));
307 }
308
309 let binary_name = self.binary_name_with_version(node_name, version);
311 let dest_path = self.cache_dir.join(&binary_name);
312
313 tokio::fs::copy(&extracted_binary, &dest_path).await?;
314
315 #[cfg(unix)]
317 {
318 use std::os::unix::fs::PermissionsExt;
319 let mut perms = tokio::fs::metadata(&dest_path).await?.permissions();
320 perms.set_mode(0o755);
321 tokio::fs::set_permissions(&dest_path, perms).await?;
322 }
323
324 let symlink_path = self.cache_dir.join(node_name);
326 if symlink_path.exists() || symlink_path.is_symlink() {
327 tokio::fs::remove_file(&symlink_path).await.ok();
328 }
329
330 #[cfg(unix)]
331 {
332 std::os::unix::fs::symlink(&binary_name, &symlink_path)?;
333 }
334
335 #[cfg(windows)]
336 {
337 std::os::windows::fs::symlink_file(&dest_path, &symlink_path)?;
338 }
339
340 println!("✅ Installed {} v{} to {}", node_name, version, dest_path.display());
341
342 Ok(dest_path)
343 }
344
345 async fn cargo_install(&self, node_name: &str) -> Result<PathBuf> {
347 println!(
348 "⚠️ Pre-built binary not available for {} on {}",
349 node_name, self.target
350 );
351 println!("🔨 Compiling via cargo install...");
352
353 let crate_name = format!("mecha10-nodes-{}", node_name);
354
355 let output = tokio::process::Command::new("cargo")
356 .args([
357 "install",
358 &crate_name,
359 "--version",
360 &self.version,
361 "--root",
362 &self.cache_dir.to_string_lossy(),
363 ])
364 .output()
365 .await
366 .context("Failed to run cargo install")?;
367
368 if !output.status.success() {
369 let stderr = String::from_utf8_lossy(&output.stderr);
370 return Err(anyhow::anyhow!("cargo install failed for {}: {}", crate_name, stderr));
371 }
372
373 let binary_path = self.cache_dir.join("bin").join(node_name);
375 if !binary_path.exists() {
376 return Err(anyhow::anyhow!(
377 "Binary not found after cargo install: {}",
378 binary_path.display()
379 ));
380 }
381
382 println!("✅ Compiled {} via cargo install", node_name);
383
384 Ok(binary_path)
385 }
386
387 fn build_client() -> Result<reqwest::Client> {
389 reqwest::Client::builder()
390 .user_agent("mecha10-cli")
391 .build()
392 .context("Failed to build HTTP client")
393 }
394
395 pub async fn remove(&self, node_name: &str) -> Result<()> {
397 let binary_name = self.binary_name(node_name);
398 let binary_path = self.cache_dir.join(&binary_name);
399 let symlink_path = self.cache_dir.join(node_name);
400
401 if binary_path.exists() {
402 tokio::fs::remove_file(&binary_path).await?;
403 println!("Removed {}", binary_path.display());
404 }
405
406 if symlink_path.exists() || symlink_path.is_symlink() {
407 tokio::fs::remove_file(&symlink_path).await?;
408 println!("Removed symlink {}", symlink_path.display());
409 }
410
411 Ok(())
412 }
413
414 pub async fn clear_cache(&self) -> Result<()> {
416 if self.cache_dir.exists() {
417 tokio::fs::remove_dir_all(&self.cache_dir).await?;
418 println!("Cleared binary cache at {}", self.cache_dir.display());
419 }
420 Ok(())
421 }
422}
423
424impl Default for BinaryManager {
425 fn default() -> Self {
426 Self::new().expect("Failed to create BinaryManager")
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
439 fn test_binary_name() {
440 let manager = BinaryManager {
441 cache_dir: PathBuf::from("/tmp"),
442 version: "0.1.44".to_string(),
443 target: "aarch64-apple-darwin".to_string(),
444 };
445
446 assert_eq!(manager.binary_name("speaker"), "speaker-0.1.44-aarch64-apple-darwin");
447 }
448
449 #[test]
450 fn test_tarball_name() {
451 let manager = BinaryManager {
452 cache_dir: PathBuf::from("/tmp"),
453 version: "0.1.44".to_string(),
454 target: "x86_64-apple-darwin".to_string(),
455 };
456
457 assert_eq!(manager.tarball_name("motor"), "motor-0.1.44-x86_64-apple-darwin.tar.gz");
458 }
459
460 #[test]
461 fn test_prebuilt_available() {
462 let manager = BinaryManager {
463 cache_dir: PathBuf::from("/tmp"),
464 version: "0.1.44".to_string(),
465 target: "aarch64-apple-darwin".to_string(),
466 };
467 assert!(manager.is_prebuilt_available());
468
469 let manager_arm_linux = BinaryManager {
471 cache_dir: PathBuf::from("/tmp"),
472 version: "0.1.44".to_string(),
473 target: "aarch64-unknown-linux-gnu".to_string(),
474 };
475 assert!(manager_arm_linux.is_prebuilt_available());
476
477 let manager_unsupported = BinaryManager {
479 cache_dir: PathBuf::from("/tmp"),
480 version: "0.1.44".to_string(),
481 target: "aarch64-unknown-linux-musl".to_string(),
482 };
483 assert!(!manager_unsupported.is_prebuilt_available());
484 }
485}