use crate::cli::args::ToolArg;
use crate::config::{Config, Settings};
use crate::task::task_context_builder::TaskContextBuilder;
use crate::task::task_helpers::canonicalize_path;
use crate::task::{Deps, Task};
use crate::toolset::{InstallOptions, ToolSource, ToolVersion, Toolset};
use eyre::Result;
use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;
pub(crate) struct TaskToolInstaller<'a> {
context_builder: &'a TaskContextBuilder,
cli_tools: &'a [ToolArg],
}
impl<'a> TaskToolInstaller<'a> {
pub(crate) fn new(context_builder: &'a TaskContextBuilder, cli_tools: &'a [ToolArg]) -> Self {
Self {
context_builder,
cli_tools,
}
}
pub(crate) async fn install_tools(
&self,
config: &mut Arc<Config>,
tasks: &Deps,
dry_run: bool,
previewed_tools: &HashSet<ToolVersion>,
) -> Result<()> {
let all_tasks: Vec<_> = tasks.all().collect();
let all_tool_requests = self.collect_tool_requests(config, all_tasks).await?;
let toolset = self
.build_toolset(config, self.cli_tools.to_vec(), all_tool_requests)
.await?;
self.install_toolset(config, toolset, dry_run, previewed_tools)
.await?;
Ok(())
}
pub(crate) async fn collect_tool_requests<'t>(
&self,
config: &Arc<Config>,
tasks: impl IntoIterator<Item = &'t Task>,
) -> Result<Vec<crate::toolset::ToolRequest>> {
let tasks = tasks.into_iter().collect::<Vec<_>>();
let mut requests = vec![];
let mut seen_config_roots = HashSet::new();
trace!("Collecting tools from {} tasks", tasks.len());
for task in tasks {
requests.extend(task.tool_args()?.into_iter().filter_map(|tool| tool.tvr));
let config_root = task
.cf(config)
.map(|task_cf| task_cf.config_root())
.or_else(|| task.config_root.clone());
if let Some(config_root) = config_root {
let config_root = canonicalize_path(&config_root);
if seen_config_roots.insert(config_root.clone()) {
requests.extend(
self.collect_tools_from_dir(&config_root, &task.name)
.await?,
);
}
}
}
Ok(requests)
}
async fn collect_tools_from_dir(
&self,
dir: &Path,
task_name: &str,
) -> Result<Vec<crate::toolset::ToolRequest>> {
let (config_paths, idiomatic_filenames) =
crate::config::load_config_hierarchy_from_dir(dir).await?;
let task_config_files =
crate::config::load_config_files_from_paths(&config_paths, &idiomatic_filenames)
.await?;
let mut tool_requests: Vec<crate::toolset::ToolRequest> = vec![];
let mut seen_tools: std::collections::HashSet<String> = std::collections::HashSet::new();
for (source, cf) in task_config_files.iter() {
let config_path = canonicalize_path(source);
let trs = {
let cache = self
.context_builder
.tool_request_set_cache()
.read()
.expect("tool_request_set_cache RwLock poisoned");
cache.get(&config_path).cloned()
};
let trs = if let Some(cached) = trs {
trace!(
"Using cached tool request set from {}",
config_path.display()
);
cached
} else {
match cf.to_tool_request_set() {
Ok(trs) => {
let trs = Arc::new(trs);
let mut cache = self
.context_builder
.tool_request_set_cache()
.write()
.expect("tool_request_set_cache RwLock poisoned");
cache.insert(config_path.clone(), Arc::clone(&trs));
trace!("Cached tool request set from {}", config_path.display());
trs
}
Err(e) => {
warn!(
"Failed to parse tools from {} for task {}: {}",
source.display(),
task_name,
e
);
continue;
}
}
};
for (ba, reqs) in trs.tools.iter() {
let tool_key = ba.to_string();
if !seen_tools.contains(&tool_key) {
trace!(
"Adding tool {} from {} for task {}",
ba,
source.display(),
task_name
);
tool_requests.extend(reqs.iter().cloned());
seen_tools.insert(tool_key);
}
}
}
trace!(
"Found {} tool requests in config hierarchy for task {}",
tool_requests.len(),
task_name
);
Ok(tool_requests)
}
async fn build_toolset(
&self,
config: &Arc<Config>,
all_tools: Vec<ToolArg>,
all_tool_requests: Vec<crate::toolset::ToolRequest>,
) -> Result<Toolset> {
let source = ToolSource::Argument;
let mut ts = Toolset::new(source.clone());
for tool_arg in all_tools {
if let Some(tvr) = tool_arg.tvr {
ts.add_version(tvr);
}
}
for tr in all_tool_requests {
trace!("Adding tool from config: {}", tr);
ts.add_version(tr);
}
ts.resolve(config).await?;
Ok(ts)
}
async fn install_toolset(
&self,
config: &mut Arc<Config>,
mut ts: Toolset,
dry_run: bool,
previewed_tools: &HashSet<ToolVersion>,
) -> Result<()> {
if dry_run {
for tvl in ts.versions.values_mut() {
tvl.versions.retain(|tv| !previewed_tools.contains(tv));
}
}
let (_, missing) = ts
.install_missing_versions(
config,
&InstallOptions {
dry_run,
missing_args_only: !Settings::get().task.run_auto_install,
skip_auto_install: !Settings::get().task.run_auto_install
|| !Settings::get().auto_install,
..Default::default()
},
)
.await?;
if !dry_run && let Err(err) = crate::shims::ensure_lazy_shims(&missing) {
warn!("failed to create shims for lazy tools: {err:#}");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_tool_installer_new() {
let context_builder = TaskContextBuilder::new();
let cli_tools: Vec<ToolArg> = vec![];
let installer = TaskToolInstaller::new(&context_builder, &cli_tools);
assert_eq!(installer.cli_tools.len(), 0);
}
}