use crate::utils::package_manager::PackageManagerImpl;
use crate::utils::profile_manifest::Package;
use anyhow::Result;
use std::io::{BufRead, BufReader};
use std::process::{Child, Stdio};
use std::sync::mpsc;
use std::thread;
use tracing::debug;
pub struct PackageInstaller;
#[allow(dead_code)] pub struct InstallationHandle {
pub child: Child,
pub output_rx: mpsc::Receiver<String>,
}
use crate::ui::InstallationStatus;
impl PackageInstaller {
pub fn install(package: &Package, tx: mpsc::Sender<InstallationStatus>) {
match Self::start_install(package) {
Ok(handle) => {
let InstallationHandle {
mut child,
output_rx,
} = handle;
for line in output_rx {
let _ = tx.send(InstallationStatus::Output(line));
}
match child.wait() {
Ok(status) => {
let success = status.success();
let error = if success {
None
} else {
Some("Installation failed".to_string())
};
let _ = tx.send(InstallationStatus::Complete { success, error });
}
Err(e) => {
let _ = tx.send(InstallationStatus::Complete {
success: false,
error: Some(e.to_string()),
});
}
}
}
Err(e) => {
let _ = tx.send(InstallationStatus::Complete {
success: false,
error: Some(e.to_string()),
});
}
}
}
#[allow(dead_code)] pub fn start_install(package: &Package) -> Result<InstallationHandle> {
if PackageManagerImpl::check_sudo_required(&package.manager) {
return Err(anyhow::anyhow!(
"sudo password required. Please run this in a terminal or configure passwordless sudo."
));
}
if matches!(
package.manager,
crate::utils::profile_manifest::PackageManager::Custom
) {
if let Some(cmd_str) = &package.install_command {
if cmd_str.contains("sudo") {
let sudo_needs_password = std::process::Command::new("sudo")
.arg("-n")
.arg("true")
.output()
.map(|o| !o.status.success())
.unwrap_or(true);
if sudo_needs_password {
return Err(anyhow::anyhow!(
"This command requires sudo password. Please run it manually in a terminal:\n\n {cmd_str}"
));
}
}
}
}
let mut cmd = PackageManagerImpl::get_install_command_builder(package);
let mut child = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let (tx, rx) = mpsc::channel::<String>();
let tx_stdout = tx.clone();
let tx_stderr = tx.clone();
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
thread::spawn(move || {
let reader = BufReader::new(stdout);
#[allow(clippy::unnecessary_lazy_evaluations, clippy::lines_filter_map_ok)]
for line in reader.lines().flatten() {
let _ = tx_stdout.send(line);
}
});
let stderr = child
.stderr
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to capture stderr"))?;
thread::spawn(move || {
let reader = BufReader::new(stderr);
#[allow(clippy::unnecessary_lazy_evaluations, clippy::lines_filter_map_ok)]
for line in reader.lines().flatten() {
let _ = tx_stderr.send(format!("[stderr] {line}"));
}
});
Ok(InstallationHandle {
child,
output_rx: rx,
})
}
#[allow(dead_code)] pub fn check_installation_status(handle: &mut InstallationHandle) -> Result<Option<bool>> {
match handle.child.try_wait()? {
Some(status) => Ok(Some(status.success())),
None => Ok(None), }
}
#[allow(dead_code)] #[must_use]
pub fn read_output(handle: &InstallationHandle) -> Vec<String> {
let mut lines = Vec::new();
while let Ok(line) = handle.output_rx.try_recv() {
lines.push(line);
}
lines
}
pub fn check_exists(package: &Package) -> Result<(bool, Option<String>, Option<String>)> {
let mut check_attempts: Vec<String> = Vec::new();
if let Some(existence_check) = package.existence_check.as_ref().map(|s| s.trim()) {
if !existence_check.is_empty() {
debug!(
"Running user-provided existence_check for package {}",
package.name
);
let output = std::process::Command::new("sh")
.arg("-c")
.arg(existence_check)
.output()?;
let found = output.status.success();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined_output = format!("STDOUT:\n{stdout}\nSTDERR:\n{stderr}");
debug!("existence_check for {}: {}", package.name, found);
return Ok((
found,
Some(existence_check.to_string()),
Some(combined_output),
));
}
}
debug!(
"Checking if binary '{}' exists in PATH for package {}",
package.binary_name, package.name
);
let binary_check_cmd = format!("which {}", package.binary_name);
if PackageManagerImpl::check_binary_in_path(&package.binary_name) {
debug!("Package {} found via binary check", package.name);
return Ok((
true,
Some(binary_check_cmd),
Some("Binary found in PATH".to_string()),
));
}
debug!("Binary '{}' not found in PATH", package.binary_name);
check_attempts.push(format!(
"Binary check: `{binary_check_cmd}` - not found in PATH"
));
if let Some(manager_check) = &package.manager_check {
debug!("Trying custom manager check for package {}", package.name);
let output = std::process::Command::new("sh")
.arg("-c")
.arg(manager_check)
.output()?;
let found = output.status.success();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined_output = format!("STDOUT:\n{stdout}\nSTDERR:\n{stderr}");
debug!("Custom manager check for {}: {}", package.name, found);
return Ok((found, Some(manager_check.clone()), Some(combined_output)));
}
if let Some(package_name) = &package.package_name {
if PackageManagerImpl::is_manager_installed(&package.manager) {
debug!(
"Trying auto-generated manager check for package {} (manager: {:?})",
package.name, package.manager
);
if let Some(mut manager_cmd) =
PackageManagerImpl::build_manager_check_command(&package.manager, package_name)
{
let cmd_str = format!("{manager_cmd:?}");
let output = manager_cmd.output()?;
let found = output.status.success();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined_output = format!("STDOUT:\n{stdout}\nSTDERR:\n{stderr}");
debug!(
"Auto-generated manager check for {}: {}",
package.name, found
);
return Ok((found, Some(cmd_str), Some(combined_output)));
}
} else {
check_attempts.push(format!(
"Manager check: {:?} not installed, skipped",
package.manager
));
debug!(
"Manager {:?} not installed, skipping manager check for {}",
package.manager, package.name
);
}
}
debug!("All checks failed for package {}", package.name);
let output = if check_attempts.is_empty() {
"No suitable check method found".to_string()
} else {
format!("Checks attempted:\n{}", check_attempts.join("\n"))
};
Ok((false, None, Some(output)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::profile_manifest::{Package, PackageManager};
fn custom_pkg(name: &str, binary_name: &str, existence_check: Option<&str>) -> Package {
Package {
name: name.to_string(),
description: None,
manager: PackageManager::Custom,
package_name: None,
binary_name: binary_name.to_string(),
install_command: Some("echo installed".to_string()),
existence_check: existence_check.map(str::to_string),
manager_check: None,
}
}
#[test]
fn existence_check_overrides_failing_binary() {
let pkg = custom_pkg("test-pkg", "nonexistent-binary-xyz-9001", Some("true"));
let (found, cmd, _) = PackageInstaller::check_exists(&pkg).unwrap();
assert!(found, "existence_check `true` should report installed");
assert_eq!(cmd.as_deref(), Some("true"));
}
#[test]
fn existence_check_overrides_passing_binary() {
let pkg = custom_pkg("fake-pkg", "sh", Some("false"));
let (found, cmd, _) = PackageInstaller::check_exists(&pkg).unwrap();
assert!(
!found,
"existence_check `false` should report not installed"
);
assert_eq!(cmd.as_deref(), Some("false"));
}
#[test]
fn empty_existence_check_falls_through_to_binary() {
let pkg = custom_pkg("sh-pkg", "sh", Some(" "));
let (found, _, _) = PackageInstaller::check_exists(&pkg).unwrap();
assert!(
found,
"binary check for `sh` should succeed when existence_check is empty"
);
}
#[test]
fn none_existence_check_falls_through_to_binary() {
let pkg = custom_pkg("missing-pkg", "nonexistent-binary-xyz-9001", None);
let (found, _, _) = PackageInstaller::check_exists(&pkg).unwrap();
assert!(
!found,
"no existence_check, no binary, no manager → not installed"
);
}
}