source.rs
1 //! Source installation (build from source). 2 //! 3 //! Supports cloning from Radicle (preferred) or Git fallback, 4 //! then building with cargo in release mode. 5 6 use acdc_core::{Error, Result}; 7 use acdc_tui::output; 8 use std::path::{Path, PathBuf}; 9 use std::process::Command; 10 use tempfile::TempDir; 11 use tracing::{debug, info, warn}; 12 13 /// Repository sources for building from source. 14 #[derive(Debug, Clone)] 15 pub struct RepoSource { 16 /// Repository name 17 pub name: &'static str, 18 /// Radicle RID (preferred) 19 pub radicle_rid: &'static str, 20 /// Git URL fallback 21 pub git_url: &'static str, 22 /// Binary name to install 23 pub binary_name: &'static str, 24 } 25 26 /// Default repositories for source build. 27 pub const REPOS: &[RepoSource] = &[ 28 RepoSource { 29 name: "adnet", 30 radicle_rid: "rad:zynPtE1i1VaRsJjSEd7fZjBKxaZL", 31 git_url: "https://source.ac-dc.network/alpha-delta-network/adnet.git", 32 binary_name: "adnet", 33 }, 34 RepoSource { 35 name: "alphaos", 36 radicle_rid: "rad:z2Ag9vY11gXdqF7Bpj4uMwaK7VA3i", 37 git_url: "https://source.ac-dc.network/alpha-delta-network/alphaos.git", 38 binary_name: "alphaos", 39 }, 40 RepoSource { 41 name: "deltaos", 42 radicle_rid: "rad:z2vzrzyNghNJioXj4oTi6QNxLyLt6", 43 git_url: "https://source.ac-dc.network/alpha-delta-network/deltaos.git", 44 binary_name: "deltaos", 45 }, 46 ]; 47 48 /// Build and install from source. 49 pub async fn install() -> Result<()> { 50 output::section("Building from Source"); 51 52 // Create temporary build directory 53 let build_dir = TempDir::new().map_err(Error::Io)?; 54 info!("Using build directory: {:?}", build_dir.path()); 55 56 for repo in REPOS { 57 output::status(&format!("Building {}...", repo.name)); 58 59 let repo_path = build_dir.path().join(repo.name); 60 61 // Clone repository (try Radicle first, then Git) 62 clone_repo_with_fallback(repo, &repo_path).await?; 63 64 // Build with cargo 65 cargo_build(&repo_path, true).await?; 66 67 // Install binary 68 install_binary(&repo_path, repo.binary_name)?; 69 70 output::success(&format!("{} built and installed successfully", repo.name)); 71 } 72 73 output::success("Source build complete"); 74 Ok(()) 75 } 76 77 /// Clone a repository, trying Radicle first then Git fallback. 78 async fn clone_repo_with_fallback(repo: &RepoSource, dest: &Path) -> Result<()> { 79 // Try Radicle first if rad CLI is available 80 if check_rad_cli() { 81 debug!("Attempting Radicle clone for {}", repo.name); 82 match clone_from_radicle(repo.radicle_rid, dest).await { 83 Ok(()) => { 84 output::info("Source", "Radicle"); 85 return Ok(()); 86 } 87 Err(e) => { 88 warn!("Radicle clone failed, falling back to Git: {}", e); 89 } 90 } 91 } 92 93 // Fallback to Git 94 output::info("Source", "Git"); 95 clone_from_git(repo.git_url, dest).await 96 } 97 98 /// Check if rad CLI is available. 99 fn check_rad_cli() -> bool { 100 Command::new("rad") 101 .arg("--version") 102 .output() 103 .map(|o| o.status.success()) 104 .unwrap_or(false) 105 } 106 107 /// Clone from Radicle. 108 async fn clone_from_radicle(rid: &str, dest: &Path) -> Result<()> { 109 let output = Command::new("rad") 110 .args(["clone", rid, "--no-confirm"]) 111 .arg(dest) 112 .output() 113 .map_err(Error::Io)?; 114 115 if !output.status.success() { 116 let stderr = String::from_utf8_lossy(&output.stderr); 117 return Err(Error::Installation(format!( 118 "Radicle clone failed: {}", 119 stderr 120 ))); 121 } 122 123 Ok(()) 124 } 125 126 /// Clone from Git. 127 async fn clone_from_git(url: &str, dest: &Path) -> Result<()> { 128 let output = Command::new("git") 129 .args(["clone", "--depth", "1", url]) 130 .arg(dest) 131 .output() 132 .map_err(Error::Io)?; 133 134 if !output.status.success() { 135 let stderr = String::from_utf8_lossy(&output.stderr); 136 return Err(Error::Installation(format!("Git clone failed: {}", stderr))); 137 } 138 139 Ok(()) 140 } 141 142 /// Clone a repository from URL to destination. 143 pub async fn clone_repo(url: &str, dest: &Path) -> Result<()> { 144 clone_from_git(url, dest).await 145 } 146 147 /// Build a Rust project with cargo. 148 pub async fn cargo_build(path: &Path, release: bool) -> Result<()> { 149 output::status("Compiling (this may take a while)..."); 150 151 let mut cmd = Command::new("cargo"); 152 cmd.arg("build"); 153 154 if release { 155 cmd.arg("--release"); 156 } 157 158 cmd.current_dir(path); 159 160 // Set environment for optimized build 161 cmd.env("CARGO_INCREMENTAL", "0"); 162 cmd.env("RUSTFLAGS", "-C target-cpu=native"); 163 164 let output = cmd.output().map_err(Error::Io)?; 165 166 if !output.status.success() { 167 let stderr = String::from_utf8_lossy(&output.stderr); 168 return Err(Error::Installation(format!( 169 "Cargo build failed: {}", 170 stderr 171 ))); 172 } 173 174 Ok(()) 175 } 176 177 /// Install a binary from build directory to system. 178 fn install_binary(repo_path: &Path, binary_name: &str) -> Result<()> { 179 let profile = "release"; 180 let source = repo_path.join("target").join(profile).join(binary_name); 181 let dest = PathBuf::from("/usr/local/bin").join(binary_name); 182 183 if !source.exists() { 184 return Err(Error::Installation(format!( 185 "Binary not found at {:?}", 186 source 187 ))); 188 } 189 190 // Try to copy directly first, then sudo if needed 191 if std::fs::copy(&source, &dest).is_err() { 192 // Need elevated permissions 193 output::status("Installing binary (requires sudo)..."); 194 195 let output = Command::new("sudo") 196 .args(["cp", "-f"]) 197 .arg(&source) 198 .arg(&dest) 199 .output() 200 .map_err(Error::Io)?; 201 202 if !output.status.success() { 203 let stderr = String::from_utf8_lossy(&output.stderr); 204 return Err(Error::Installation(format!( 205 "Failed to install binary: {}", 206 stderr 207 ))); 208 } 209 210 // Make executable 211 let output = Command::new("sudo") 212 .args(["chmod", "+x"]) 213 .arg(&dest) 214 .output() 215 .map_err(Error::Io)?; 216 217 if !output.status.success() { 218 let stderr = String::from_utf8_lossy(&output.stderr); 219 return Err(Error::Installation(format!( 220 "Failed to set permissions: {}", 221 stderr 222 ))); 223 } 224 } 225 226 debug!("Installed {} to {:?}", binary_name, dest); 227 Ok(()) 228 } 229 230 /// Get the default install path for binaries. 231 pub fn default_install_path() -> PathBuf { 232 PathBuf::from("/usr/local/bin") 233 } 234 235 /// Build options for source installation. 236 #[derive(Debug, Clone)] 237 pub struct BuildOptions { 238 /// Build in release mode 239 pub release: bool, 240 /// Target architecture (default: native) 241 pub target: Option<String>, 242 /// Additional cargo features 243 pub features: Vec<String>, 244 /// Number of parallel jobs 245 pub jobs: Option<u32>, 246 } 247 248 impl Default for BuildOptions { 249 fn default() -> Self { 250 Self { 251 release: true, 252 target: None, 253 features: Vec::new(), 254 jobs: None, 255 } 256 } 257 } 258 259 /// Build with custom options. 260 pub async fn build_with_options(path: &Path, options: &BuildOptions) -> Result<()> { 261 output::status("Compiling with custom options..."); 262 263 let mut cmd = Command::new("cargo"); 264 cmd.arg("build"); 265 266 if options.release { 267 cmd.arg("--release"); 268 } 269 270 if let Some(ref target) = options.target { 271 cmd.args(["--target", target]); 272 } 273 274 if !options.features.is_empty() { 275 cmd.args(["--features", &options.features.join(",")]); 276 } 277 278 if let Some(jobs) = options.jobs { 279 cmd.args(["-j", &jobs.to_string()]); 280 } 281 282 cmd.current_dir(path); 283 cmd.env("CARGO_INCREMENTAL", "0"); 284 285 let output = cmd.output().map_err(Error::Io)?; 286 287 if !output.status.success() { 288 let stderr = String::from_utf8_lossy(&output.stderr); 289 return Err(Error::Installation(format!( 290 "Cargo build failed: {}", 291 stderr 292 ))); 293 } 294 295 Ok(()) 296 }