/ crates / acdc-install / src / prerequisites.rs
prerequisites.rs
  1  //! Prerequisite installation.
  2  //!
  3  //! Installs system packages, Rust toolchain, and optional CUDA toolkit.
  4  
  5  use acdc_core::{Error, Result};
  6  use acdc_tui::output;
  7  use std::process::Command;
  8  use tracing::{debug, warn};
  9  
 10  /// Required apt packages for building from source.
 11  pub const APT_PACKAGES: &[&str] = &[
 12      "build-essential",
 13      "pkg-config",
 14      "libssl-dev",
 15      "git",
 16      "curl",
 17      "clang",
 18      "cmake",
 19      "libclang-dev",
 20      "llvm-dev",
 21  ];
 22  
 23  /// Optional packages for enhanced functionality.
 24  pub const APT_PACKAGES_OPTIONAL: &[&str] = &[
 25      "jq",    // JSON processing
 26      "htop",  // System monitoring
 27      "tmux",  // Terminal multiplexer
 28      "unzip", // Archive extraction
 29  ];
 30  
 31  /// CUDA apt packages for GPU provers.
 32  pub const CUDA_PACKAGES: &[&str] = &["nvidia-cuda-toolkit", "nvidia-cuda-dev"];
 33  
 34  /// Install required system prerequisites.
 35  pub async fn install(include_cuda: bool) -> Result<()> {
 36      output::section("Installing Prerequisites");
 37  
 38      // Check if we're on a Debian-based system
 39      if !is_debian_based() {
 40          output::warning("Non-Debian system detected. Manual package installation may be required.");
 41          output::info("Required packages", &APT_PACKAGES.join(", "));
 42      } else {
 43          // Update package lists
 44          update_apt().await?;
 45  
 46          // Install required packages
 47          install_apt_packages(APT_PACKAGES).await?;
 48  
 49          // Install optional packages (don't fail if these don't work)
 50          if let Err(e) = install_apt_packages(APT_PACKAGES_OPTIONAL).await {
 51              warn!("Optional packages failed to install: {}", e);
 52          }
 53  
 54          // Install CUDA if requested
 55          if include_cuda {
 56              output::subsection("CUDA Toolkit");
 57              if check_nvidia_gpu() {
 58                  install_cuda().await?;
 59              } else {
 60                  output::warning("No NVIDIA GPU detected, skipping CUDA installation");
 61              }
 62          }
 63      }
 64  
 65      // Install Rust if not present
 66      if !check_rust() {
 67          output::subsection("Rust Toolchain");
 68          install_rust().await?;
 69      } else {
 70          output::success("Rust toolchain already installed");
 71          // Ensure we have the right version
 72          update_rust().await?;
 73      }
 74  
 75      output::success("Prerequisites installation complete");
 76      Ok(())
 77  }
 78  
 79  /// Check if we're on a Debian-based system.
 80  fn is_debian_based() -> bool {
 81      std::path::Path::new("/etc/debian_version").exists()
 82          || Command::new("apt-get")
 83              .arg("--version")
 84              .output()
 85              .map(|o| o.status.success())
 86              .unwrap_or(false)
 87  }
 88  
 89  /// Update apt package lists.
 90  async fn update_apt() -> Result<()> {
 91      output::status("Updating package lists...");
 92  
 93      let output = Command::new("sudo")
 94          .args(["apt-get", "update", "-qq"])
 95          .output()
 96          .map_err(Error::Io)?;
 97  
 98      if !output.status.success() {
 99          let stderr = String::from_utf8_lossy(&output.stderr);
100          return Err(Error::Installation(format!(
101              "apt-get update failed: {}",
102              stderr
103          )));
104      }
105  
106      Ok(())
107  }
108  
109  /// Install apt packages.
110  async fn install_apt_packages(packages: &[&str]) -> Result<()> {
111      if packages.is_empty() {
112          return Ok(());
113      }
114  
115      output::status(&format!("Installing packages: {}", packages.join(", ")));
116  
117      let mut cmd = Command::new("sudo");
118      cmd.args(["apt-get", "install", "-y", "-qq"]);
119      cmd.args(packages);
120  
121      let output = cmd.output().map_err(Error::Io)?;
122  
123      if !output.status.success() {
124          let stderr = String::from_utf8_lossy(&output.stderr);
125          return Err(Error::Installation(format!(
126              "apt-get install failed: {}",
127              stderr
128          )));
129      }
130  
131      output::success(&format!("Installed {} packages", packages.len()));
132      Ok(())
133  }
134  
135  /// Check if an NVIDIA GPU is present.
136  fn check_nvidia_gpu() -> bool {
137      // Check lspci for NVIDIA
138      Command::new("lspci")
139          .output()
140          .map(|o| {
141              let stdout = String::from_utf8_lossy(&o.stdout);
142              stdout.to_lowercase().contains("nvidia")
143          })
144          .unwrap_or(false)
145  }
146  
147  /// Install CUDA toolkit.
148  async fn install_cuda() -> Result<()> {
149      output::status("Installing CUDA toolkit...");
150  
151      // First try the standard packages
152      let result = install_apt_packages(CUDA_PACKAGES).await;
153  
154      if result.is_err() {
155          output::warning("Standard CUDA packages not available");
156          output::info("Alternative", "Install CUDA from NVIDIA's repository");
157  
158          // Try to add NVIDIA repository and install
159          install_cuda_from_nvidia_repo().await?;
160      }
161  
162      // Verify CUDA installation
163      if check_cuda() {
164          output::success("CUDA toolkit installed successfully");
165      } else {
166          output::warning("CUDA installed but nvcc not in PATH. You may need to add it manually.");
167          output::info(
168              "Hint",
169              "Add to ~/.bashrc: export PATH=/usr/local/cuda/bin:$PATH",
170          );
171      }
172  
173      Ok(())
174  }
175  
176  /// Install CUDA from NVIDIA repository (fallback).
177  async fn install_cuda_from_nvidia_repo() -> Result<()> {
178      output::status("Setting up NVIDIA CUDA repository...");
179  
180      // Download and install the CUDA keyring
181      let keyring_url = "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb";
182  
183      let download = Command::new("curl")
184          .args(["-fsSL", "-o", "/tmp/cuda-keyring.deb", keyring_url])
185          .output()
186          .map_err(Error::Io)?;
187  
188      if !download.status.success() {
189          return Err(Error::Installation(
190              "Failed to download CUDA keyring".to_string(),
191          ));
192      }
193  
194      let install = Command::new("sudo")
195          .args(["dpkg", "-i", "/tmp/cuda-keyring.deb"])
196          .output()
197          .map_err(Error::Io)?;
198  
199      if !install.status.success() {
200          let stderr = String::from_utf8_lossy(&install.stderr);
201          return Err(Error::Installation(format!(
202              "Failed to install CUDA keyring: {}",
203              stderr
204          )));
205      }
206  
207      // Update and install CUDA
208      update_apt().await?;
209  
210      let cuda_install = Command::new("sudo")
211          .args(["apt-get", "install", "-y", "cuda-toolkit"])
212          .output()
213          .map_err(Error::Io)?;
214  
215      if !cuda_install.status.success() {
216          let stderr = String::from_utf8_lossy(&cuda_install.stderr);
217          return Err(Error::Installation(format!(
218              "Failed to install CUDA toolkit: {}",
219              stderr
220          )));
221      }
222  
223      Ok(())
224  }
225  
226  /// Check if Rust is installed.
227  pub fn check_rust() -> bool {
228      Command::new("rustc")
229          .arg("--version")
230          .output()
231          .map(|o| o.status.success())
232          .unwrap_or(false)
233  }
234  
235  /// Get installed Rust version.
236  pub fn get_rust_version() -> Option<String> {
237      Command::new("rustc")
238          .arg("--version")
239          .output()
240          .ok()
241          .and_then(|o| {
242              if o.status.success() {
243                  String::from_utf8(o.stdout)
244                      .ok()
245                      .map(|s| s.trim().to_string())
246              } else {
247                  None
248              }
249          })
250  }
251  
252  /// Install Rust via rustup.
253  async fn install_rust() -> Result<()> {
254      output::status("Installing Rust via rustup...");
255  
256      // Download and run rustup installer
257      let curl = Command::new("curl")
258          .args([
259              "--proto",
260              "=https",
261              "--tlsv1.2",
262              "-sSf",
263              "https://sh.rustup.rs",
264          ])
265          .output()
266          .map_err(Error::Io)?;
267  
268      if !curl.status.success() {
269          return Err(Error::Installation(
270              "Failed to download rustup installer".to_string(),
271          ));
272      }
273  
274      // Run the installer non-interactively
275      let install = Command::new("sh")
276          .args([
277              "-c",
278              "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y",
279          ])
280          .env("RUSTUP_INIT_SKIP_PATH_CHECK", "yes")
281          .output()
282          .map_err(Error::Io)?;
283  
284      if !install.status.success() {
285          let stderr = String::from_utf8_lossy(&install.stderr);
286          return Err(Error::Installation(format!(
287              "Rust installation failed: {}",
288              stderr
289          )));
290      }
291  
292      output::success("Rust installed successfully");
293      output::info("Note", "Run 'source ~/.cargo/env' or restart your shell");
294  
295      Ok(())
296  }
297  
298  /// Update Rust to latest stable.
299  async fn update_rust() -> Result<()> {
300      output::status("Updating Rust toolchain...");
301  
302      // Source cargo env if needed
303      let cargo_path = dirs::home_dir()
304          .map(|h| h.join(".cargo/bin/rustup"))
305          .unwrap_or_else(|| "rustup".into());
306  
307      let rustup = if cargo_path.exists() {
308          cargo_path.to_string_lossy().to_string()
309      } else {
310          "rustup".to_string()
311      };
312  
313      let output = Command::new(&rustup)
314          .args(["update", "stable"])
315          .output()
316          .map_err(Error::Io)?;
317  
318      if !output.status.success() {
319          // Non-fatal, just log
320          warn!("rustup update failed, continuing anyway");
321      } else {
322          debug!("Rust updated successfully");
323      }
324  
325      Ok(())
326  }
327  
328  /// Check if CUDA is installed.
329  pub fn check_cuda() -> bool {
330      Command::new("nvcc")
331          .arg("--version")
332          .output()
333          .map(|o| o.status.success())
334          .unwrap_or(false)
335  }
336  
337  /// Get installed CUDA version.
338  pub fn get_cuda_version() -> Option<String> {
339      Command::new("nvcc")
340          .arg("--version")
341          .output()
342          .ok()
343          .and_then(|o| {
344              if o.status.success() {
345                  let stdout = String::from_utf8_lossy(&o.stdout);
346                  // Parse version from output like "Cuda compilation tools, release 12.0, V12.0.140"
347                  stdout
348                      .lines()
349                      .find(|l| l.contains("release"))
350                      .and_then(|l| {
351                          l.split("release")
352                              .nth(1)
353                              .map(|s| s.trim().trim_matches(',').trim().to_string())
354                      })
355              } else {
356                  None
357              }
358          })
359  }
360  
361  /// Check if a specific command is available.
362  pub fn check_command(cmd: &str) -> bool {
363      Command::new("which")
364          .arg(cmd)
365          .output()
366          .map(|o| o.status.success())
367          .unwrap_or(false)
368  }
369  
370  /// Prerequisite check result.
371  #[derive(Debug, Clone)]
372  pub struct PrerequisiteStatus {
373      pub apt_packages: bool,
374      pub rust: bool,
375      pub rust_version: Option<String>,
376      pub cuda: bool,
377      pub cuda_version: Option<String>,
378      pub git: bool,
379      pub curl: bool,
380  }
381  
382  impl PrerequisiteStatus {
383      /// Check all prerequisites and return status.
384      pub fn check() -> Self {
385          Self {
386              apt_packages: check_command("gcc") && check_command("pkg-config"),
387              rust: check_rust(),
388              rust_version: get_rust_version(),
389              cuda: check_cuda(),
390              cuda_version: get_cuda_version(),
391              git: check_command("git"),
392              curl: check_command("curl"),
393          }
394      }
395  
396      /// Check if all required prerequisites are met.
397      pub fn is_ready(&self) -> bool {
398          self.rust && self.git && self.curl && self.apt_packages
399      }
400  
401      /// Check if CUDA prerequisites are met (for provers).
402      pub fn is_cuda_ready(&self) -> bool {
403          self.cuda
404      }
405  }
406  
407  /// Print prerequisite status.
408  pub fn print_status(status: &PrerequisiteStatus) {
409      output::subsection("Prerequisite Status");
410  
411      if status.apt_packages {
412          output::success("Build tools installed");
413      } else {
414          output::error("Build tools missing (run: sudo apt install build-essential pkg-config)");
415      }
416  
417      if status.git {
418          output::success("Git installed");
419      } else {
420          output::error("Git not found");
421      }
422  
423      if status.curl {
424          output::success("curl installed");
425      } else {
426          output::error("curl not found");
427      }
428  
429      if status.rust {
430          if let Some(ref version) = status.rust_version {
431              output::success(&format!("Rust: {}", version));
432          } else {
433              output::success("Rust installed");
434          }
435      } else {
436          output::error("Rust not installed");
437      }
438  
439      if status.cuda {
440          if let Some(ref version) = status.cuda_version {
441              output::success(&format!("CUDA: {}", version));
442          } else {
443              output::success("CUDA installed");
444          }
445      } else {
446          output::info("CUDA", "Not installed (optional, required for provers)");
447      }
448  }