Skip to content

Commit

Permalink
feat(doctor): identify missing/extra shims (#1524)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ajpantuso authored Jan 25, 2024
1 parent c97bb79 commit 0737239
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 73 deletions.
114 changes: 75 additions & 39 deletions src/cli/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,29 @@ use crate::file::display_path;
use crate::git::Git;
use crate::plugins::PluginType;
use crate::shell::ShellType;
use crate::toolset::Toolset;
use crate::toolset::ToolsetBuilder;
use crate::ui::style;
use crate::{cli, cmd, dirs, forge};
use crate::{duration, env};
use crate::{file, shims};

/// Check mise installation for possible problems.
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Doctor {}
pub struct Doctor {
#[clap(skip)]
checks: Vec<String>,
}

impl Doctor {
pub fn run(self) -> Result<()> {
let mut checks = Vec::new();
if let Err(err) = Config::try_get() {
checks.push(format!("failed to load config: {}", err));
}

pub fn run(mut self) -> Result<()> {
miseprintln!("{}", mise_version());
miseprintln!("{}", build_info());
miseprintln!("{}", shell());
miseprintln!("{}", mise_data_dir());
miseprintln!("{}", mise_env_vars());

match Settings::try_get() {
Ok(settings) => {
miseprintln!(
Expand All @@ -44,58 +46,92 @@ impl Doctor {
}
Err(err) => warn!("failed to load settings: {}", err),
}

match Config::try_get() {
Ok(config) => {
miseprintln!("{}", render_config_files(&config));
miseprintln!("{}", render_plugins());
for plugin in forge::list() {
if !plugin.is_installed() {
checks.push(format!("plugin {} is not installed", &plugin.id()));
continue;
}
}
if !config.is_activated() && !shims_on_path() {
let cmd = style("mise help activate").yellow().for_stderr();
let url = style("https://mise.jdx.dev").underlined().for_stderr();
let shims = style(dirs::SHIMS.display()).cyan().for_stderr();
checks.push(formatdoc!(
r#"mise is not activated, run {cmd} or
read documentation at {url} for activation instructions.
Alternatively, add the shims directory {shims} to PATH.
Using the shims directory is preferred for non-interactive setups."#
));
}
match ToolsetBuilder::new().build(&config) {
Ok(ts) => {
miseprintln!("{}\n{}\n", style("toolset:").bold(), indent(ts.to_string()))
}
Err(err) => warn!("failed to load toolset: {}", err),
}
Ok(config) => self.analyze_config(config)?,
Err(err) => {
self.checks.push(format!("failed to load config: {}", err));
}
Err(err) => warn!("failed to load config: {}", err),
}

if let Some(latest) = cli::version::check_for_new_version(duration::HOURLY) {
checks.push(format!(
self.checks.push(format!(
"new mise version {latest} available, currently on {}",
*version::V
));
}

if checks.is_empty() {
if self.checks.is_empty() {
miseprintln!("No problems found");
} else {
let checks_plural = if checks.len() == 1 { "" } else { "s" };
let summary = format!("{} problem{checks_plural} found:", checks.len());
let checks_plural = if self.checks.len() == 1 { "" } else { "s" };
let summary = format!("{} problem{checks_plural} found:", self.checks.len());
miseprintln!("{}", style(summary).red().bold());
for check in &checks {
for check in &self.checks {
miseprintln!("{}\n", check);
}
exit(1);
}

Ok(())
}

fn analyze_config(&mut self, config: impl AsRef<Config>) -> Result<()> {
let config = config.as_ref();

miseprintln!("{}", render_config_files(config));
miseprintln!("{}", render_plugins());

for plugin in forge::list() {
if !plugin.is_installed() {
self.checks
.push(format!("plugin {} is not installed", &plugin.id()));
continue;
}
}

if !config.is_activated() && !shims_on_path() {
let cmd = style::estyle("mise help activate");
let url = style::eunderline("https://mise.jdx.dev");
let shims = style::ecyan(dirs::SHIMS.display());
self.checks.push(formatdoc!(
r#"mise is not activated, run {cmd} or
read documentation at {url} for activation instructions.
Alternatively, add the shims directory {shims} to PATH.
Using the shims directory is preferred for non-interactive setups."#
));
}

match ToolsetBuilder::new().build(config) {
Ok(ts) => {
self.analyze_shims(&ts);

miseprintln!("{}\n{}\n", style("toolset:").bold(), indent(ts.to_string()));
}
Err(err) => self.checks.push(format!("failed to load toolset: {}", err)),
}

Ok(())
}

fn analyze_shims(&mut self, toolset: &Toolset) {
let mise_bin = file::which("mise").unwrap_or(env::MISE_BIN.clone());

if let Ok((missing, extra)) = shims::get_shim_diffs(mise_bin, toolset) {
let cmd = style::eyellow("mise reshim");

if !missing.is_empty() {
self.checks
.push(format!("shims are missing, run {cmd} to create them"));
}

if !extra.is_empty() {
self.checks.push(format!(
"unused shims are present, run {cmd} to remove them"
));
}
}
}
}

fn shims_on_path() -> bool {
Expand Down
89 changes: 55 additions & 34 deletions src/shims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,7 @@ pub fn reshim(ts: &Toolset) -> Result<()> {

create_dir_all(&*dirs::SHIMS)?;

let existing_shims = list_executables_in_dir(&dirs::SHIMS)?
.into_par_iter()
.filter(|bin| {
dirs::SHIMS
.join(bin)
.read_link()
.is_ok_and(|p| p == mise_bin)
})
.collect::<HashSet<_>>();

let shims: HashSet<String> = ts
.list_installed_versions()?
.into_par_iter()
.flat_map(|(t, tv)| {
list_tool_bins(t.clone(), &tv).unwrap_or_else(|e| {
warn!("Error listing bin paths for {}: {:#}", tv, e);
Vec::new()
})
})
.collect();

let shims_to_add = shims.difference(&existing_shims);
let shims_to_remove = existing_shims.difference(&shims);
let (shims_to_add, shims_to_remove) = get_shim_diffs(&mise_bin, ts)?;

for shim in shims_to_add {
let symlink_path = dirs::SHIMS.join(shim);
Expand Down Expand Up @@ -143,17 +121,34 @@ pub fn reshim(ts: &Toolset) -> Result<()> {
Ok(())
}

// lists all the paths to bins in a tv that shims will be needed for
fn list_tool_bins(t: Arc<dyn Forge>, tv: &ToolVersion) -> Result<Vec<String>> {
Ok(t.list_bin_paths(tv)?
.into_iter()
.par_bridge()
.filter(|path| path.exists())
.map(|dir| list_executables_in_dir(&dir))
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect())
// get_shim_diffs contrasts the actual shims on disk
// with the desired shims specified by the Toolset
// and returns a tuple of (missing shims, extra shims)
pub fn get_shim_diffs(
mise_bin: impl AsRef<Path>,
toolset: &Toolset,
) -> Result<(Vec<String>, Vec<String>)> {
let actual_shims = get_actual_shims(&mise_bin)?;
let desired_shims = get_desired_shims(toolset)?;

Ok((
desired_shims.difference(&actual_shims).cloned().collect(),
actual_shims.difference(&desired_shims).cloned().collect(),
))
}

fn get_actual_shims(mise_bin: impl AsRef<Path>) -> Result<HashSet<String>> {
let mise_bin = mise_bin.as_ref();

Ok(list_executables_in_dir(&dirs::SHIMS)?
.into_par_iter()
.filter(|bin| {
dirs::SHIMS
.join(bin)
.read_link()
.is_ok_and(|p| p == mise_bin)
})
.collect::<HashSet<_>>())
}

fn list_executables_in_dir(dir: &Path) -> Result<HashSet<String>> {
Expand All @@ -171,6 +166,32 @@ fn list_executables_in_dir(dir: &Path) -> Result<HashSet<String>> {
Ok(out)
}

fn get_desired_shims(toolset: &Toolset) -> Result<HashSet<String>> {
Ok(toolset
.list_installed_versions()?
.into_par_iter()
.flat_map(|(t, tv)| {
list_tool_bins(t.clone(), &tv).unwrap_or_else(|e| {
warn!("Error listing bin paths for {}: {:#}", tv, e);
Vec::new()
})
})
.collect())
}

// lists all the paths to bins in a tv that shims will be needed for
fn list_tool_bins(t: Arc<dyn Forge>, tv: &ToolVersion) -> Result<Vec<String>> {
Ok(t.list_bin_paths(tv)?
.into_iter()
.par_bridge()
.filter(|path| path.exists())
.map(|dir| list_executables_in_dir(&dir))
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect())
}

fn make_shim(target: &Path, shim: &Path) -> Result<()> {
if shim.exists() {
file::remove_file(shim)?;
Expand Down

0 comments on commit 0737239

Please sign in to comment.