rtx-cli 1.3.0

Polyglot runtime manager (asdf rust clone)
use std::collections::HashMap;
use std::env::join_paths;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use color_eyre::eyre::{eyre, Result, WrapErr};
use color_eyre::Report;
use indexmap::IndexMap;
use itertools::Itertools;
use rayon::prelude::*;

pub use plugin_source::PluginSource;
pub use settings::{MissingRuntimeBehavior, Settings};

use crate::cli::args::runtime::RuntimeArg;
use crate::config::config_file::legacy_version::LegacyVersionFile;
use crate::config::config_file::rtxrc::RTXFile;
use crate::config::config_file::ConfigFile;
use crate::config::toolset::Toolset;
use crate::plugins::{Plugin, PluginName};
use crate::{dirs, env, file};

pub mod config_file;
pub mod plugin_source;
mod settings;
mod toolset;

type AliasMap = IndexMap<PluginName, IndexMap<String, String>>;

#[derive(Debug, Default)]
pub struct Config {
    pub settings: Settings,
    pub rtxrc: RTXFile,
    pub ts: Toolset,
    pub config_files: Vec<PathBuf>,
    pub aliases: AliasMap,
}

impl Config {
    pub fn load() -> Result<Self> {
        let rtxrc = load_rtxrc()?;
        let settings = rtxrc.settings();
        let mut ts = Toolset::default();
        load_installed_plugins(&mut ts)?;
        load_installed_runtimes(&mut ts)?;
        let legacy_filenames = load_legacy_filenames(&settings, &ts)?;
        let config_files = find_all_config_files(&legacy_filenames);
        load_config_files(&mut ts, &config_files, &legacy_filenames)?;
        load_runtime_env(&mut ts, env::vars().collect())?;
        let aliases = load_aliases(&settings, &ts)?;
        ts.resolve_all_versions(&aliases)?;

        let config = Self {
            settings,
            ts,
            config_files,
            aliases,
            rtxrc,
        };

        debug!("{}", &config);

        Ok(config)
    }

    pub fn env(&self) -> Result<IndexMap<String, String>> {
        let mut entries = self
            .ts
            .list_current_installed_versions()
            .into_par_iter()
            .map(|p| p.exec_env())
            .collect::<Result<Vec<HashMap<String, String>>>>()?
            .into_iter()
            .flatten()
            .collect_vec();
        entries.par_sort();
        Ok(entries.into_iter().collect())
    }

    pub fn list_paths(&self) -> Result<Vec<PathBuf>> {
        let paths = self
            .ts
            .list_current_installed_versions()
            .into_par_iter()
            .map(|rtv| rtv.list_bin_paths())
            .collect::<Result<Vec<Vec<PathBuf>>>>()?
            .into_iter()
            .flatten()
            .collect::<Vec<PathBuf>>();
        Ok(paths)
    }

    pub fn path_env(&self) -> Result<String> {
        let installs = self.list_paths()?;
        let other = env::PATH
            .clone()
            .into_iter()
            .filter(|p| !p.starts_with(dirs::INSTALLS.deref()))
            .collect_vec();
        Ok(join_paths([installs, other].concat())?
            .to_string_lossy()
            .into())
    }

    pub fn with_runtime_args(mut self, args: &[RuntimeArg]) -> Result<Self> {
        let args_by_plugin = &args.iter().group_by(|arg| arg.plugin.clone());
        for (plugin_name, args) in args_by_plugin {
            match self.ts.plugins.get(&plugin_name) {
                Some(plugin) => plugin,
                _ => {
                    let plugin = Plugin::load_ensure_installed(&plugin_name, &self.settings)?;
                    self.ts
                        .plugins
                        .entry(plugin_name.clone())
                        .or_insert_with(|| Arc::new(plugin))
                }
            };
            let args = args.collect_vec();
            let source = PluginSource::Argument(args[0].clone());
            let versions = args.iter().map(|arg| arg.version.clone()).collect();
            self.ts
                .set_current_runtime_versions(&plugin_name, versions, source)?;
        }
        if !args.is_empty() {
            self.ts.resolve_all_versions(&self.aliases)?;
        }
        Ok(self)
    }

    pub fn ensure_installed(&self) -> Result<()> {
        for rtv in self.ts.list_current_versions() {
            if rtv.plugin.is_installed() {
                rtv.ensure_installed(self)?;
            }
        }
        Ok(())
    }

    pub fn resolve_alias(&self, plugin: &str, version: String) -> String {
        if let Some(plugin_aliases) = self.aliases.get(plugin) {
            if let Some(alias) = plugin_aliases.get(&version) {
                return alias.clone();
            }
        }
        version
    }
}

fn load_rtxrc() -> Result<RTXFile> {
    let settings_path = dirs::CONFIG.join("config.toml");
    let rtxrc = if !settings_path.exists() {
        trace!("settings does not exist {:?}", settings_path);
        RTXFile::init(&settings_path)
    } else {
        let rtxrc = RTXFile::from_file(&settings_path)
            .wrap_err_with(|| err_load_settings(&settings_path))?;
        trace!("Settings: {:#?}", rtxrc.settings());
        rtxrc
    };

    Ok(rtxrc)
}

fn load_installed_plugins(ts: &mut Toolset) -> Result<()> {
    let plugins = file::dir_subdirs(&dirs::PLUGINS)?
        .into_par_iter()
        .map(|p| {
            let plugin = Plugin::load(&p)?;
            Ok((p, Arc::new(plugin)))
        })
        .collect::<Result<Vec<_>>>()?;
    for (name, plugin) in plugins {
        ts.plugins.entry(name).or_insert(plugin);
    }
    Ok(())
}

fn load_installed_runtimes(ts: &mut Toolset) -> Result<()> {
    let plugin_versions = ts
        .list_plugins()
        .into_par_iter()
        .map(|p| Ok((p.clone(), p.list_installed_versions()?)))
        .collect::<Result<Vec<(Arc<Plugin>, Vec<String>)>>>()?;
    for (plugin, versions) in plugin_versions {
        ts.add_runtime_versions(&plugin.name, versions)?;
    }
    Ok(())
}

fn load_legacy_filenames(settings: &Settings, ts: &Toolset) -> Result<HashMap<String, PluginName>> {
    if !settings.legacy_version_file {
        return Ok(HashMap::new());
    }
    let filenames = ts
        .list_plugins()
        .into_par_iter()
        .map(|plugin| {
            let mut legacy_filenames = vec![];
            for filename in plugin.legacy_filenames()? {
                legacy_filenames.push((filename, plugin.name.clone()));
            }
            Ok(legacy_filenames)
        })
        .collect::<Result<Vec<Vec<(String, PluginName)>>>>()?
        .into_iter()
        .flatten()
        .collect::<HashMap<String, PluginName>>();
    Ok(filenames)
}

fn find_all_config_files(legacy_filenames: &HashMap<String, PluginName>) -> Vec<PathBuf> {
    let mut filenames = vec![
        // ".rtxrc.toml",
        // ".rtxrc",
        env::RTX_DEFAULT_TOOL_VERSIONS_FILENAME.as_str(),
    ];
    for filename in legacy_filenames.keys() {
        filenames.push(filename.as_str());
    }
    filenames.reverse();

    let mut config_files = file::FindUp::new(&dirs::CURRENT, &filenames).collect::<Vec<_>>();

    let home_config = dirs::HOME.join(env::RTX_DEFAULT_TOOL_VERSIONS_FILENAME.as_str());
    if home_config.is_file() {
        config_files.push(home_config);
    }

    config_files.into_iter().unique().collect()
}

fn load_config_files(
    ts: &mut Toolset,
    config_files: &Vec<PathBuf>,
    legacy_filenames: &HashMap<String, PluginName>,
) -> Result<()> {
    let parsed_config_files = config_files
        .into_par_iter()
        .rev()
        .map(|path| {
            let filename = path.file_name().unwrap().to_string_lossy().to_string();
            match legacy_filenames.get(&filename) {
                Some(plugin) => {
                    let plugin = ts.find_plugin(plugin).unwrap();
                    let cf = LegacyVersionFile::parse(path.into(), &plugin)?;
                    Ok(Box::new(cf) as Box<dyn ConfigFile>)
                }
                None => config_file::parse(path),
            }
        })
        .collect::<Result<Vec<_>>>()?;

    for cf in parsed_config_files {
        let path = cf.get_path().to_path_buf();
        load_config_file(ts, cf)
            .with_context(|| eyre!("error loading file: {}", path.display()))?;
    }

    Ok(())
}

fn load_config_file(ts: &mut Toolset, cf: Box<dyn ConfigFile>) -> Result<()> {
    trace!("config file: {:#?}", cf);
    for (plugin, versions) in cf.plugins() {
        ts.set_current_runtime_versions(&plugin, versions.clone(), cf.source())?;
    }

    Ok(())
}

fn load_runtime_env(ts: &mut Toolset, env: HashMap<String, String>) -> Result<()> {
    for (k, v) in env {
        if k.starts_with("RTX_") && k.ends_with("_VERSION") {
            let plugin_name = k[4..k.len() - 8].to_lowercase();
            if let Some(plugin) = ts.find_plugin(&plugin_name) {
                if plugin.is_installed() {
                    let source = PluginSource::Environment(k, v.clone());
                    ts.set_current_runtime_versions(&plugin.name, vec![v], source)?;
                }
            }
        }
    }
    Ok(())
}

fn load_aliases(settings: &Settings, ts: &Toolset) -> Result<AliasMap> {
    let mut aliases = IndexMap::new();
    for plugin in ts.list_installed_plugins() {
        for (from, to) in plugin.list_aliases()? {
            aliases
                .entry(plugin.name.clone())
                .or_insert_with(IndexMap::new)
                .insert(from, to);
        }
    }

    for (plugin, plugin_aliases) in &settings.aliases {
        for (from, to) in plugin_aliases {
            aliases
                .entry(plugin.clone())
                .or_insert_with(IndexMap::new)
                .insert(from.clone(), to.clone());
        }
    }

    Ok(aliases)
}

fn err_load_settings(settings_path: &Path) -> Report {
    eyre!(
        "error loading settings from {}",
        settings_path.to_string_lossy()
    )
}

impl Display for Config {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Config:")?;
        writeln!(f, "  Installed Plugins:")?;
        for plugin in self.ts.list_installed_plugins() {
            writeln!(f, "    {}", plugin.name)?;
        }
        writeln!(f, "  Active Versions:")?;
        for rtv in self.ts.list_current_versions() {
            writeln!(f, "    {rtv}")?;
        }
        Ok(())
    }
}