binary.rs
1 //! Binary installation from pre-built releases. 2 3 use acdc_core::Result; 4 use acdc_tui::output; 5 use sha2::{Digest, Sha256}; 6 use std::io::Read; 7 use std::path::Path; 8 use tokio::io::AsyncWriteExt; 9 use tracing::{info, warn}; 10 11 /// Release download base URL. 12 const RELEASE_BASE_URL: &str = "https://releases.ac-dc.network"; 13 14 /// Binary component information. 15 #[derive(Debug, Clone)] 16 pub struct BinaryInfo { 17 /// Component name. 18 pub name: String, 19 /// Download URL. 20 pub url: String, 21 /// Expected SHA256 checksum. 22 pub checksum: String, 23 /// Installation path. 24 pub install_path: std::path::PathBuf, 25 } 26 27 /// Get binary info for a component. 28 pub fn get_binary_info(component: &str, version: &str) -> BinaryInfo { 29 let arch = get_arch(); 30 let os = get_os(); 31 let filename = format!("{component}-{version}-{arch}-{os}.tar.gz"); 32 33 BinaryInfo { 34 name: component.to_string(), 35 url: format!("{RELEASE_BASE_URL}/{version}/{filename}"), 36 checksum: format!("{RELEASE_BASE_URL}/{version}/{filename}.sha256"), 37 install_path: std::path::PathBuf::from("/usr/local/bin").join(component), 38 } 39 } 40 41 /// Components to install. 42 const COMPONENTS: &[&str] = &["adnet", "alphaos", "deltaos"]; 43 44 /// Install pre-built binaries. 45 pub async fn install() -> Result<()> { 46 info!("Starting binary installation..."); 47 48 let version = get_latest_version().await?; 49 info!(version = %version, "Installing version"); 50 51 // Install all components 52 for component in COMPONENTS { 53 match install_component(component, &version).await { 54 Ok(()) => { 55 info!(component = component, "Component installed successfully"); 56 } 57 Err(e) => { 58 // Log error but continue with other components 59 warn!(component = component, error = %e, "Failed to install component"); 60 output::warning(&format!(" Failed to install {}: {}", component, e)); 61 } 62 } 63 } 64 65 output::success("Binary installation complete"); 66 Ok(()) 67 } 68 69 /// Install a single component. 70 pub async fn install_component(component: &str, version: &str) -> Result<()> { 71 let info = get_binary_info(component, version); 72 73 output::status(&format!("Installing {}", component)); 74 75 // Create temp directory 76 let temp_dir = tempfile::tempdir()?; 77 let archive_path = temp_dir.path().join(format!("{}.tar.gz", component)); 78 79 // Download binary 80 output::status(" Downloading..."); 81 download_with_progress(&info.url, &archive_path).await?; 82 83 // Fetch and verify checksum 84 output::status(" Verifying checksum..."); 85 let expected_checksum = fetch_checksum(&info.checksum).await?; 86 if !verify_checksum(&archive_path, &expected_checksum)? { 87 return Err(acdc_core::Error::Installation(format!( 88 "Checksum verification failed for {}", 89 component 90 ))); 91 } 92 output::success(" Checksum verified"); 93 94 // Extract and install 95 output::status(" Installing..."); 96 extract_and_install(&archive_path, &info.install_path).await?; 97 98 output::success(&format!(" {} installed successfully", component)); 99 Ok(()) 100 } 101 102 /// Get the latest release version. 103 async fn get_latest_version() -> Result<String> { 104 let client = reqwest::Client::new(); 105 let url = format!("{RELEASE_BASE_URL}/latest"); 106 107 let response = client.get(&url).send().await?; 108 109 if !response.status().is_success() { 110 // Fall back to default version if latest endpoint not available 111 warn!("Could not fetch latest version, using default"); 112 return Ok("0.2.0".to_string()); 113 } 114 115 let version = response.text().await?.trim().to_string(); 116 Ok(version) 117 } 118 119 /// Download a file with progress indication. 120 pub async fn download_with_progress(url: &str, dest: &Path) -> Result<()> { 121 let client = reqwest::Client::new(); 122 let response = client.get(url).send().await?; 123 124 if !response.status().is_success() { 125 return Err(acdc_core::Error::Network(format!( 126 "Failed to download {}: HTTP {}", 127 url, 128 response.status() 129 ))); 130 } 131 132 let total_size = response.content_length().unwrap_or(0); 133 134 let mut file = tokio::fs::File::create(dest).await?; 135 let mut downloaded: u64 = 0; 136 let mut stream = response.bytes_stream(); 137 138 use tokio_stream::StreamExt; 139 while let Some(chunk) = stream.next().await { 140 let chunk = chunk?; 141 file.write_all(&chunk).await?; 142 downloaded += chunk.len() as u64; 143 144 if total_size > 0 { 145 let percent = (downloaded as f64 / total_size as f64 * 100.0) as u8; 146 // Progress indication would go here in TUI mode 147 let _ = percent; // Suppress unused warning 148 } 149 } 150 151 file.flush().await?; 152 Ok(()) 153 } 154 155 /// Fetch checksum from URL. 156 async fn fetch_checksum(url: &str) -> Result<String> { 157 let client = reqwest::Client::new(); 158 let response = client.get(url).send().await?; 159 160 if !response.status().is_success() { 161 return Err(acdc_core::Error::Network(format!( 162 "Failed to fetch checksum from {}: HTTP {}", 163 url, 164 response.status() 165 ))); 166 } 167 168 // Checksum file format: "HASH filename" or just "HASH" 169 let text = response.text().await?; 170 let checksum = text 171 .split_whitespace() 172 .next() 173 .ok_or_else(|| acdc_core::Error::Installation("Invalid checksum format".to_string()))? 174 .to_string(); 175 176 Ok(checksum) 177 } 178 179 /// Verify SHA256 checksum of a file. 180 pub fn verify_checksum(path: &Path, expected: &str) -> Result<bool> { 181 let mut file = std::fs::File::open(path)?; 182 let mut hasher = Sha256::new(); 183 let mut buffer = [0u8; 8192]; 184 185 loop { 186 let bytes_read = file.read(&mut buffer)?; 187 if bytes_read == 0 { 188 break; 189 } 190 hasher.update(&buffer[..bytes_read]); 191 } 192 193 let result = hasher.finalize(); 194 let actual = hex::encode(result); 195 196 Ok(actual.eq_ignore_ascii_case(expected)) 197 } 198 199 /// Extract archive and install binary. 200 async fn extract_and_install(archive: &Path, install_path: &Path) -> Result<()> { 201 use flate2::read::GzDecoder; 202 use std::process::Command; 203 use tar::Archive; 204 205 let temp_dir = tempfile::tempdir()?; 206 207 // Extract archive 208 let file = std::fs::File::open(archive)?; 209 let decoder = GzDecoder::new(file); 210 let mut archive = Archive::new(decoder); 211 archive.unpack(temp_dir.path())?; 212 213 // Find the binary in extracted contents 214 let binary_name = install_path 215 .file_name() 216 .and_then(|n| n.to_str()) 217 .unwrap_or("binary"); 218 219 let extracted_binary = find_binary_in_dir(temp_dir.path(), binary_name)?; 220 221 // Install binary (may need sudo) 222 let install_dir = install_path.parent().unwrap_or(Path::new("/usr/local/bin")); 223 224 // Try direct copy first 225 if std::fs::copy(&extracted_binary, install_path).is_err() { 226 // Fall back to sudo 227 let status = Command::new("sudo") 228 .args([ 229 "install", 230 "-m", 231 "755", 232 extracted_binary.to_str().unwrap(), 233 install_path.to_str().unwrap(), 234 ]) 235 .status()?; 236 237 if !status.success() { 238 return Err(acdc_core::Error::Installation(format!( 239 "Failed to install binary to {}", 240 install_dir.display() 241 ))); 242 } 243 } 244 245 // Make executable 246 #[cfg(unix)] 247 { 248 use std::os::unix::fs::PermissionsExt; 249 if let Ok(mut perms) = std::fs::metadata(install_path).map(|m| m.permissions()) { 250 perms.set_mode(0o755); 251 let _ = std::fs::set_permissions(install_path, perms); 252 } 253 } 254 255 Ok(()) 256 } 257 258 /// Find a binary in a directory (possibly nested). 259 fn find_binary_in_dir(dir: &Path, name: &str) -> Result<std::path::PathBuf> { 260 for entry in walkdir::WalkDir::new(dir).max_depth(3) { 261 let entry = match entry { 262 Ok(e) => e, 263 Err(_) => continue, 264 }; 265 if entry.file_type().is_file() { 266 if let Some(file_name) = entry.file_name().to_str() { 267 if file_name == name || file_name.starts_with(name) { 268 return Ok(entry.path().to_path_buf()); 269 } 270 } 271 } 272 } 273 274 Err(acdc_core::Error::Installation(format!( 275 "Binary '{}' not found in archive", 276 name 277 ))) 278 } 279 280 /// Get current architecture string. 281 fn get_arch() -> &'static str { 282 #[cfg(target_arch = "x86_64")] 283 return "x86_64"; 284 285 #[cfg(target_arch = "aarch64")] 286 return "aarch64"; 287 288 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] 289 return "unknown"; 290 } 291 292 /// Get current OS string. 293 fn get_os() -> &'static str { 294 #[cfg(target_os = "linux")] 295 return "linux"; 296 297 #[cfg(target_os = "macos")] 298 return "darwin"; 299 300 #[cfg(not(any(target_os = "linux", target_os = "macos")))] 301 return "unknown"; 302 }