pub mod alias;
mod cli;
pub mod config;
mod directories;
pub mod env;
mod trace;
pub mod ui;
use anyhow::{Context, Result};
use cnf_lib::{
prelude::*,
provider::{apt, cargo, cwd, dnf, flatpak, pacman, path},
};
use logerr::LoggableError;
use std::{str::FromStr, sync::Arc};
use tracing::{debug, info};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
pub use cli::Args;
pub use env::Env;
macro_rules! arc_provider {
($provider:ty) => {
Arc::new(<$provider>::new().into())
};
}
#[doc(hidden)]
pub async fn main(args: cli::Args) -> Result<()> {
config::load();
if let Some(hook) = args.hooks {
cli::install_hook(hook);
return Ok(());
}
let mut guards: Vec<Box<dyn trace::Guard>> = vec![];
let registry = tracing_subscriber::registry();
let log_layer = trace::logfile().context("failed to enable application logging")?;
let registry = registry.with(log_layer.layer);
guards.push(log_layer.guard);
#[cfg(feature = "debug-flame")]
let registry = {
let flame_file = trace::flame_file()?;
let (writer, guard) = tracing_appender::non_blocking(flame_file);
guards.push(Box::new(guard));
registry.with(
tracing_flame::FlameLayer::new(writer)
.with_empty_samples(true)
.with_threads_collapsed(true)
.with_file_and_line(true),
)
};
registry.init();
info!("Launching application");
let cur_env = Arc::new(cnf_lib::environment::current());
info!("Running in environment '{}'", cur_env);
if let Some(recursion_depth) = Env::RecursionDepth.get::<usize>() {
debug!("cnf execution at recursion depth {}", recursion_depth);
let max_recursion = config::get().max_recursion_depth;
if recursion_depth >= max_recursion {
anyhow::bail!(
"current recursion depth {} exceeds configured maximum recursion depth {}",
recursion_depth,
max_recursion
);
}
Env::RecursionDepth.set(recursion_depth + 1);
} else {
Env::RecursionDepth.set(0)
}
let command = args.command[0].to_string();
let alias = args.alias_target_env.and_then(|target_env| {
let mut cmd = CommandLine::new(&args.command);
cmd.needs_privileges(args.alias_privileged);
cmd.is_interactive(args.alias_interactive);
let alias = alias::Alias {
source_env: "".to_string(),
target_env,
command: command.clone(),
alias: cmd,
};
if args
.alias_source_env
.is_some_and(|env| env != cur_env.to_json())
{
None
} else {
Some(alias)
}
});
if let Some(alias) = alias {
info!(command, "executing command alias");
let target_env = Environment::from_str(&alias.target_env)
.with_context(|| format!("failed to resolve alias '{:?}'", alias))?;
target_env.start()?;
let mut cmd = alias.alias.clone();
if Env::AliasPrivileged.is_set() {
debug!("privileged alias execution requested by env variable");
cmd.needs_privileges(true);
}
let status = target_env
.execute(cmd)
.await
.with_context(|| format!("failed to prepare alias execution in env '{}'", target_env))?
.status()
.await
.map_err(anyhow::Error::new)?;
match status.code() {
Some(code) if code != 0 => std::process::exit(code),
None => std::process::exit(255),
_ => return Ok(()),
}
}
if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
anyhow::bail!("command not found: {}", command);
}
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut envs = vec![
cur_env,
Arc::new(cnf_lib::Environment::Host(cnf_lib::env::host::Host::new())),
];
let mut toolbx_names = config::get()
.toolbx_names
.clone()
.into_iter()
.map(|n| if n.is_empty() { None } else { Some(n) })
.collect::<Vec<_>>();
if toolbx_names.is_empty() {
toolbx_names.push(None);
}
for toolbx_name in toolbx_names {
if let Ok(toolbx) = cnf_lib::env::toolbx::Toolbx::new(toolbx_name)
.context("cannot search in 'toolbx' environment")
.to_log()
{
if toolbx.exists().await {
envs.push(Arc::new(toolbx.into()));
}
}
}
let mut distrobox_names = config::get()
.distrobox_names
.clone()
.into_iter()
.map(|n| if n.is_empty() { None } else { Some(n) })
.collect::<Vec<_>>();
if distrobox_names.is_empty() {
distrobox_names.push(None)
}
for distrobox_name in distrobox_names {
if let Ok(distrobox) = cnf_lib::env::distrobox::Distrobox::new(distrobox_name)
.context("cannot search in 'distrobox' environment")
.to_log()
{
if distrobox.exists().await {
envs.push(Arc::new(distrobox.into()));
}
}
}
envs.sort();
envs.dedup();
for origin in &config::get().query_origins {
let _ = origin.check_env_exists(&envs).to_log();
}
let mut providers: Vec<Arc<Provider>> = vec![
arc_provider!(path::Path),
arc_provider!(dnf::Dnf),
arc_provider!(cargo::Cargo),
arc_provider!(pacman::Pacman),
arc_provider!(apt::Apt),
arc_provider!(flatpak::Flatpak),
];
if let Ok(val) = cwd::Cwd::new()
.context("cannot search in provider 'cwd'")
.to_log()
{
providers.push(Arc::new(val.into()));
}
config::get()
.custom_providers
.iter()
.for_each(|provider| providers.push(Arc::new(Provider::from(provider.clone()))));
for origin in &config::get().query_origins {
let _ = origin.check_providers_exist(&providers).to_log();
}
for env in envs {
let env_config = config::get()
.query_origins
.iter()
.find(|origin| origin.environment == env.to_string());
let empty: Vec<String> = vec![];
let disabled_providers = env_config
.map(|config| &config.disabled_providers)
.unwrap_or(&empty);
let enabled_providers = env_config
.map(|config| &config.enabled_providers)
.unwrap_or(&empty);
if env_config.map(|config| config.enabled).unwrap_or(true) {
for prov in &providers {
let prov_name = prov.to_string();
if disabled_providers.is_empty() && enabled_providers.is_empty() {
} else if disabled_providers.contains(&prov_name) {
debug!(
"provider '{}' for env '{}' set to inactive in config",
prov.to_string(),
env.to_string()
);
continue;
} else if disabled_providers.is_empty() && !enabled_providers.contains(&prov_name) {
debug!(
"provider '{}' for env '{}' not set to active in config",
prov.to_string(),
env.to_string()
);
continue;
}
let cloned_env = env.clone();
let env_name = env.to_string();
let cloned_sender = tx.clone();
let cloned_provider = prov.clone();
let provider_name = prov.to_string();
let cloned_cmd = command.clone();
tokio::task::spawn(async move {
let result = search_in(cloned_provider, &cloned_cmd, cloned_env).await;
let _ = cloned_sender
.send(result)
.with_context(|| {
format!(
"failed to report results from '{}' in '{}'",
&provider_name, &env_name
)
})
.to_log();
});
}
} else {
debug!("env '{}' is ignored according to config", env.to_string());
}
}
drop(tx);
ui::tui(rx, &args.command).await
}