/ crates / acdc-install / src / source.rs
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  }