diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff4ab4..0eec5ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ * The `update-hashes/` directory is not used anymore, deleting `toolchains/` or direct subdirectories does not break elan anymore. - More useful download and installation info messages +- Experimental `elan toolchain gc` command. See `elan toolchain gc --help` for documentation. + `lean-toolchain` files will only be known to the GC after being used at least once with this + version of elan. # 3.1.1 - 2024-02-22 diff --git a/src/elan-cli/elan_mode.rs b/src/elan-cli/elan_mode.rs index d53ee56..b5ce498 100644 --- a/src/elan-cli/elan_mode.rs +++ b/src/elan-cli/elan_mode.rs @@ -1,6 +1,6 @@ use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand}; use common; -use elan::{command, lookup_toolchain_desc, Cfg, Toolchain}; +use elan::{command, gc, lookup_toolchain_desc, Cfg, Toolchain}; use elan_dist::dist::ToolchainDesc; use elan_utils::utils; use errors::*; @@ -30,6 +30,7 @@ pub fn main() -> Result<()> { ("list", Some(_)) => list_toolchains(cfg)?, ("link", Some(m)) => toolchain_link(cfg, m)?, ("uninstall", Some(m)) => toolchain_remove(cfg, m)?, + ("gc", Some(m)) => toolchain_gc(cfg, m)?, (_, _) => unreachable!(), }, ("override", Some(c)) => match c.subcommand() { @@ -126,7 +127,13 @@ pub fn cli() -> App<'static, 'static> { .help(TOOLCHAIN_ARG_HELP) .required(true)) .arg(Arg::with_name("path") - .required(true)))) + .required(true))) + .subcommand(SubCommand::with_name("gc") + .about("Garbage-collect toolchains not used by any known project") + .after_help(TOOLCHAIN_GC_HELP) + .arg(Arg::with_name("delete") + .long("delete") + .help("Delete collected toolchains instead of only reporting them")))) .subcommand(SubCommand::with_name("override") .about("Modify directory toolchain overrides") .after_help(OVERRIDE_HELP) @@ -418,6 +425,26 @@ fn toolchain_remove(cfg: &Cfg, m: &ArgMatches) -> Result<()> { Ok(()) } +fn toolchain_gc(cfg: &Cfg, m: &ArgMatches<'_>) -> Result<()> { + let toolchains = gc::get_unreachable_toolchains(cfg)?; + if toolchains.is_empty() { + println!("No unused toolchains found"); + return Ok(()) + } + let delete = m.is_present("delete"); + if !delete { + println!("The following toolchains are not used by any known project; rerun with `--delete` to delete them:"); + } + for t in toolchains.into_iter() { + if delete { + t.remove()?; + } else { + println!("- {}", t.path().display()); + } + } + Ok(()) +} + fn override_add(cfg: &Cfg, m: &ArgMatches) -> Result<()> { let ref toolchain = m.value_of("toolchain").expect(""); let desc = lookup_toolchain_desc(cfg, toolchain)?; diff --git a/src/elan-cli/help.rs b/src/elan-cli/help.rs index 62ef536..d704c7d 100644 --- a/src/elan-cli/help.rs +++ b/src/elan-cli/help.rs @@ -61,6 +61,17 @@ pub static TOOLCHAIN_LINK_HELP: &'static str = r"DISCUSSION: If you now compile a crate in the current directory, the custom toolchain 'master' will be used."; +pub static TOOLCHAIN_GC_HELP: &'static str = r"DISCUSSION: + Experimental. A toolchain is classified as 'in use' if + * it is the default toolchain, + * it is registered as an override, or + * there is a directory with a `lean-toolchain` file referencing the + toolchain and elan has been used in the directory before. + + For safety reasons, the command currently requires passing `--delete` + to actually remove toolchains but this may be relaxed in the future + when the implementation is deemed stable."; + pub static OVERRIDE_HELP: &'static str = r"DISCUSSION: Overrides configure elan to use a specific toolchain when running in a specific directory. diff --git a/src/elan/config.rs b/src/elan/config.rs index 19bb970..8229aca 100644 --- a/src/elan/config.rs +++ b/src/elan/config.rs @@ -9,13 +9,14 @@ use elan_dist::dist::ToolchainDesc; use elan_dist::temp; use elan_utils::utils; use errors::*; +use itertools::Itertools; use notifications::*; use settings::{Settings, SettingsFile}; use toolchain::Toolchain; use toml; -use crate::lookup_toolchain_desc; +use crate::{gc, lookup_toolchain_desc}; #[derive(Debug)] pub enum OverrideReason { @@ -238,6 +239,7 @@ impl Cfg { let toolchain_name = s.trim(); let desc = lookup_toolchain_desc(&self, toolchain_name)?; let reason = OverrideReason::ToolchainFile(toolchain_file); + gc::add_root(self, d)?; return Ok(Some((desc, reason))); } } @@ -297,6 +299,12 @@ impl Cfg { ) } + pub fn get_overrides(&self) -> Result> { + self.settings_file.with(|s| { + Ok(s.overrides.clone().into_iter().collect_vec()) + }) + } + pub fn list_toolchains(&self) -> Result> { if utils::is_directory(&self.toolchains_dir) { let mut toolchains: Vec<_> = utils::read_dir("toolchains", &self.toolchains_dir)? @@ -311,7 +319,6 @@ impl Cfg { utils::toolchain_sort(&mut toolchains); - // ignore legacy toolchains in non-resolved format let toolchains: Vec<_> = toolchains.iter().flat_map(|s| ToolchainDesc::from_resolved_str(&s)).collect(); Ok(toolchains) } else { diff --git a/src/elan/gc.rs b/src/elan/gc.rs new file mode 100644 index 0000000..26480a7 --- /dev/null +++ b/src/elan/gc.rs @@ -0,0 +1,57 @@ +use std::{collections::HashSet, path::{Path, PathBuf}}; + +use itertools::Itertools; + +use crate::{lookup_toolchain_desc, Cfg, Toolchain}; + +fn get_root_file(cfg: &Cfg) -> PathBuf { + cfg.elan_dir.join("known-projects") +} + +fn get_roots(cfg: &Cfg) -> elan_utils::Result> { + let path = get_root_file(cfg); + if path.exists() { + let roots = std::fs::read_to_string(&path)?; + Ok(roots.split("\n").map(|s| s.to_string()).collect_vec()) + } else { + Ok(vec![]) + } +} + +pub fn add_root(cfg: &Cfg, root: &Path) -> elan_utils::Result<()> { + let path = get_root_file(cfg); + let mut roots = get_roots(cfg)?; + let root = root.to_str().unwrap().to_string(); + if !roots.contains(&root) { + roots.push(root); + let roots = roots.join("\n"); + std::fs::write(path, roots)?; + } + Ok(()) +} + +pub fn get_unreachable_toolchains(cfg: &Cfg) -> crate::Result> { + let roots = get_roots(cfg)?; + let mut used_toolchains = roots.into_iter().filter_map(|r| { + let path = PathBuf::from(r).join("lean-toolchain"); + if path.exists() { + Some(std::fs::read_to_string(path).unwrap().trim().to_string()) + } else { + None + } + }).collect::>(); + if let Some(default) = cfg.get_default()? { + let default = lookup_toolchain_desc(cfg, &default)?; + used_toolchains.insert(default.to_string()); + } + if let Some(ref env_override) = cfg.env_override { + used_toolchains.insert(env_override.clone()); + } + for o in cfg.get_overrides()? { + used_toolchains.insert(o.1.to_string()); + } + Ok(cfg.list_toolchains()?.into_iter() + .map(|t| Toolchain::from(cfg, &t)) + .filter(|t| !t.is_custom() && !used_toolchains.contains(&t.desc.to_string())) + .collect_vec()) +} diff --git a/src/elan/lib.rs b/src/elan/lib.rs index b1549dd..a5c70ab 100644 --- a/src/elan/lib.rs +++ b/src/elan/lib.rs @@ -29,3 +29,4 @@ pub mod install; mod notifications; pub mod settings; mod toolchain; +pub mod gc;