use anyhow::{Context, Result};
use crate::armory_lock;
use crate::config::Config;
use crate::edit::{self, EditSession};
use crate::exec;
use crate::nixfile;
use crate::output;
use crate::resolve::{self, Resolution, Source};
pub enum InstallMode {
Nix,
Cask,
Brew,
Auto,
}
pub fn run(
config: &Config,
mode: InstallMode,
packages: &[String],
dry_run: bool,
lock_only: bool,
) -> Result<()> {
tracing::info!(packages = ?packages, dry_run, "install");
if matches!(mode, InstallMode::Auto) && packages.len() == 1 {
if let Ok(package_ref) = crate::armory::PackageRef::parse(&packages[0]) {
return armory_lock::install(config, &package_ref, dry_run, lock_only);
}
}
if packages.is_empty() {
anyhow::bail!("no packages specified");
}
ensure_mode_enabled(config, &mode)?;
let mut session = EditSession::new();
let mut any_added = false;
for pkg in packages {
if is_already_declared(config, pkg)? {
tracing::debug!(pkg, "already declared, skipping");
output::already(pkg);
continue;
}
let source = match &mode {
InstallMode::Nix => Source::Nix,
InstallMode::Cask => Source::BrewCask,
InstallMode::Brew => Source::BrewFormula,
InstallMode::Auto => resolve_source(pkg, dry_run, config)?,
};
tracing::debug!(pkg, source = %source, "resolved install source");
if dry_run {
let target = target_description(&source, config);
output::dry_run(&format!("would add {pkg} to {target} (via {source})"));
continue;
}
if matches!(mode, InstallMode::Nix) {
let attr = crate::aliases::nixpkgs_attr(pkg);
if !exec::nix_eval_exists(attr)? && (attr == pkg || !exec::nix_eval_exists(pkg)?) {
let hint = if config.homebrew_cask_target().is_some() {
"not in nixpkgs — try: nex install --cask"
} else {
"not in nixpkgs — check the attribute with `nix search nixpkgs`"
};
output::not_found(pkg, hint);
continue;
}
}
match install_as(config, &mut session, pkg, &source)? {
true => {
output::added_with_source(pkg, &source.to_string());
any_added = true;
}
false => output::already(pkg),
}
}
if dry_run || !any_added {
return Ok(());
}
output::status("switching...");
match exec::system_rebuild_switch(&config.repo, &config.hostname, config.platform) {
Ok(()) => {
session.commit_all()?;
output::status("done");
Ok(())
}
Err(e) => {
output::error("switch failed, reverting changes");
session.revert_all()?;
Err(e)
}
}
}
fn ensure_mode_enabled(config: &Config, mode: &InstallMode) -> Result<()> {
match mode {
InstallMode::Brew if config.homebrew_formula_target().is_none() => anyhow::bail!(
"Homebrew formula provider is not enabled for this profile; configure a formula target before using --brew"
),
InstallMode::Cask if config.homebrew_cask_target().is_none() => anyhow::bail!(
"Homebrew cask provider is not enabled for this profile; configure a cask target before using --cask"
),
_ => Ok(()),
}
}
fn is_already_declared(config: &Config, pkg: &str) -> Result<bool> {
let alias_list = crate::aliases::all_names_for(pkg);
let mut all_names: Vec<&str> = vec![pkg];
for alias in &alias_list {
if *alias != pkg {
all_names.push(alias);
}
}
for nix_file in config.all_nix_package_files() {
if edit::contains_any(nix_file, &nixfile::NIX_PACKAGES, &all_names)?.is_some() {
return Ok(true);
}
}
if let Some(target) = config.homebrew_cask_target() {
if edit::contains_any(target, &nixfile::HOMEBREW_CASKS, &all_names)?.is_some() {
return Ok(true);
}
}
if let Some(target) = config.homebrew_formula_target() {
if edit::contains_any(target, &nixfile::HOMEBREW_BREWS, &all_names)?.is_some() {
return Ok(true);
}
}
Ok(false)
}
fn resolve_source(pkg: &str, dry_run: bool, config: &Config) -> Result<Source> {
let result = resolve::resolve_with_sources(pkg, &config.auto_install_sources())?;
if config.has_homebrew_provider() && !result.brew_checked {
if let Resolution::Single(ref c) = result.resolution {
if c.source == Source::Nix {
output::warn(
"brew not available — cask/formula check skipped; \
use --cask or --brew to install via homebrew",
);
}
}
}
match result.resolution {
Resolution::Single(candidate) => Ok(candidate.source),
Resolution::Conflict {
candidates,
recommended,
reason,
versions_equal,
} => {
if versions_equal && config.prefer_nix_on_equal {
return Ok(Source::Nix);
}
if versions_equal && recommended == Source::BrewCask {
return Ok(Source::BrewCask);
}
if dry_run {
return Ok(recommended);
}
match resolve::prompt_resolution(
pkg,
&candidates,
&recommended,
&reason,
versions_equal,
)? {
Some(result) => {
if result.remember_nix {
if let Err(e) = crate::config::set_preference("prefer_nix_on_equal", "true")
{
output::warn(&format!("could not save preference: {e}"));
} else {
eprintln!(
" {} saved — won't ask again for equal versions",
console::style("✓").green()
);
}
}
Ok(result.source)
}
None => anyhow::bail!("cancelled"),
}
}
Resolution::NotFound => {
let hint = if !config.has_homebrew_provider() {
"not found in enabled package providers"
} else if result.brew_checked {
"not found in nixpkgs or homebrew"
} else {
"not found in nixpkgs (brew unavailable for the enabled Homebrew provider)"
};
output::not_found(pkg, hint);
anyhow::bail!("package {pkg} not found");
}
}
}
fn install_as(
config: &Config,
session: &mut EditSession,
pkg: &str,
source: &Source,
) -> Result<bool> {
match source {
Source::Nix => {
let attr = crate::aliases::nixpkgs_attr(pkg);
session.backup(&config.nix_packages_file)?;
edit::insert(&config.nix_packages_file, &nixfile::NIX_PACKAGES, attr)
}
Source::BrewCask => {
let cask = crate::aliases::brew_cask_name(pkg).unwrap_or(pkg);
let target = config
.homebrew_cask_target()
.context("Homebrew cask provider is not enabled for this profile")?;
session.backup(target)?;
edit::insert(target, &nixfile::HOMEBREW_CASKS, cask)
}
Source::BrewFormula => {
let target = config
.homebrew_formula_target()
.context("Homebrew formula provider is not enabled for this profile")?;
session.backup(target)?;
edit::insert(target, &nixfile::HOMEBREW_BREWS, pkg)
}
}
}
fn target_description(source: &Source, config: &Config) -> String {
match source {
Source::Nix => config.nix_packages_file.display().to_string(),
Source::BrewCask => config
.homebrew_cask_target()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "disabled Homebrew cask provider".to_string()),
Source::BrewFormula => config
.homebrew_formula_target()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "disabled Homebrew formula provider".to_string()),
}
}