use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::path::{Path, PathBuf};
use color_eyre::eyre::{eyre, Result};
use confique::Partial;
use tool_versions::ToolVersions;
use crate::cli::args::tool::ToolArg;
use crate::config::config_file::rtx_toml::RtxToml;
use crate::config::settings::SettingsPartial;
use crate::config::{global_config_files, AliasMap, Config, Settings};
use crate::file::{display_path, replace_path};
use crate::hash::hash_to_str;
use crate::output::Output;
use crate::plugins::PluginName;
use crate::toolset::{ToolVersion, ToolVersionList, Toolset};
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::{dirs, env, file};
pub mod legacy_version;
pub mod rtx_toml;
pub mod tool_versions;
#[derive(Debug, PartialEq)]
pub enum ConfigFileType {
RtxToml,
ToolVersions,
LegacyVersion,
}
pub trait ConfigFile: Debug + Display + Send + Sync {
fn get_type(&self) -> ConfigFileType;
fn get_path(&self) -> &Path;
fn plugins(&self) -> HashMap<PluginName, String> {
Default::default()
}
fn env(&self) -> HashMap<String, String> {
Default::default()
}
fn env_remove(&self) -> Vec<String> {
Default::default()
}
fn path_dirs(&self) -> Vec<PathBuf> {
Default::default()
}
fn remove_plugin(&mut self, _plugin_name: &PluginName);
fn replace_versions(&mut self, plugin_name: &PluginName, versions: &[String]);
fn save(&self) -> Result<()>;
fn dump(&self) -> String;
fn to_toolset(&self) -> &Toolset;
fn settings(&self) -> Result<SettingsPartial> {
Ok(SettingsPartial::empty())
}
fn aliases(&self) -> AliasMap {
Default::default()
}
fn watch_files(&self) -> Vec<PathBuf> {
vec![self.get_path().to_path_buf()]
}
fn is_global(&self) -> bool {
global_config_files().iter().any(|p| p == self.get_path())
}
}
impl dyn ConfigFile {
pub fn add_runtimes(
&mut self,
config: &mut Config,
runtimes: &[ToolArg],
pin: bool,
) -> Result<()> {
let mpr = MultiProgressReport::new(config.show_progress_bars());
let mut ts = self.to_toolset().to_owned();
ts.latest_versions = true;
ts.resolve(config);
let mut plugins_to_update = HashMap::new();
for runtime in runtimes {
if let Some(tv) = &runtime.tvr {
plugins_to_update
.entry(runtime.plugin.clone())
.or_insert_with(Vec::new)
.push(tv);
}
}
for (plugin, versions) in &plugins_to_update {
let mut tvl =
ToolVersionList::new(plugin.to_string(), ts.source.as_ref().unwrap().clone());
for tv in versions {
tvl.requests.push(((*tv).clone(), Default::default()));
}
ts.versions.insert(plugin.clone(), tvl);
}
ts.resolve(config);
let versions: Vec<ToolVersion> = plugins_to_update
.iter()
.flat_map(|(pn, _)| ts.versions.get(pn).unwrap().versions.clone())
.collect();
ts.install_versions(config, versions, &mpr, false)?;
for (plugin, versions) in plugins_to_update {
let versions = versions
.into_iter()
.map(|tvr| {
if pin {
let plugin = config.plugins.get(&plugin).unwrap();
let tv = tvr.resolve(
config,
plugin.clone(),
Default::default(),
ts.latest_versions,
)?;
Ok(tv.version)
} else {
Ok(tvr.version())
}
})
.collect::<Result<Vec<_>>>()?;
self.replace_versions(&plugin, &versions);
}
Ok(())
}
pub fn display_runtime(&self, out: &mut Output, runtimes: &[ToolArg]) -> Result<bool> {
if runtimes.len() == 1 && runtimes[0].tvr.is_none() {
let plugin = &runtimes[0].plugin;
let tvl = self
.to_toolset()
.versions
.get(plugin)
.ok_or_else(|| {
eyre!(
"no version set for {} in {}",
plugin.to_string(),
display_path(self.get_path())
)
})?
.requests
.iter()
.map(|(tvr, _)| tvr.version())
.collect::<Vec<_>>();
rtxprintln!(out, "{}", tvl.join(" "));
return Ok(true);
}
if runtimes.iter().any(|r| r.tvr.is_none()) {
return Err(eyre!("invalid input, specify a version for each tool. Or just specify one tool to print the current version"));
}
Ok(false)
}
}
fn init(settings: &Settings, path: &Path) -> Box<dyn ConfigFile> {
let is_trusted = is_trusted(settings, path);
match detect_config_file_type(path) {
Some(ConfigFileType::RtxToml) => Box::new(RtxToml::init(path, is_trusted)),
Some(ConfigFileType::ToolVersions) => Box::new(ToolVersions::init(path, is_trusted)),
_ => panic!("Unknown config file type: {}", path.display()),
}
}
pub fn parse_or_init(settings: &Settings, path: &Path) -> Result<Box<dyn ConfigFile>> {
let cf = match path.exists() {
true => parse(settings, path)?,
false => init(settings, path),
};
Ok(cf)
}
pub fn parse(settings: &Settings, path: &Path) -> Result<Box<dyn ConfigFile>> {
let is_trusted = is_trusted(settings, path);
match detect_config_file_type(path) {
Some(ConfigFileType::RtxToml) => Ok(Box::new(RtxToml::from_file(path, is_trusted)?)),
Some(ConfigFileType::ToolVersions) => {
Ok(Box::new(ToolVersions::from_file(path, is_trusted)?))
}
#[allow(clippy::box_default)]
_ => Ok(Box::new(RtxToml::default())),
}
}
pub fn is_trusted(settings: &Settings, path: &Path) -> bool {
if settings
.trusted_config_paths
.iter()
.any(|p| path.starts_with(replace_path(p)))
{
return true;
}
match path.canonicalize() {
Ok(path) => trust_path(&path).exists(),
Err(_) => false,
}
}
pub fn trust(path: &Path) -> Result<()> {
let path = path.canonicalize()?;
let hashed_path = trust_path(&path);
if !hashed_path.exists() {
file::create_dir_all(hashed_path.parent().unwrap())?;
file::make_symlink(&path, &hashed_path)?;
}
Ok(())
}
pub fn untrust(path: &Path) -> Result<()> {
let path = path.canonicalize()?;
let hashed_path = trust_path(&path);
if hashed_path.exists() {
file::remove_file(hashed_path)?;
}
Ok(())
}
fn trust_path(path: &Path) -> PathBuf {
dirs::DATA.join("trusted-configs").join(hash_to_str(&path))
}
fn detect_config_file_type(path: &Path) -> Option<ConfigFileType> {
match path.file_name().unwrap().to_str().unwrap() {
f if f.ends_with(".toml") => Some(ConfigFileType::RtxToml),
f if env::RTX_DEFAULT_CONFIG_FILENAME.as_str() == f => Some(ConfigFileType::RtxToml),
f if env::RTX_DEFAULT_TOOL_VERSIONS_FILENAME.as_str() == f => {
Some(ConfigFileType::ToolVersions)
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_config_file_type() {
assert_eq!(
detect_config_file_type(Path::new("/foo/bar/.test-tool-versions")),
Some(ConfigFileType::ToolVersions)
);
assert_eq!(
detect_config_file_type(Path::new("/foo/bar/.tool-versions.toml")),
Some(ConfigFileType::RtxToml)
);
}
}