1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use crate::cli::args::ToolArg;
use crate::config::{Config, Settings};
use crate::file::display_path;
use crate::install_context::InstallContext;
use crate::toolset::ToolsetBuilder;
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::prompt;
use clap::ValueHint;
use console::style;
use eyre::{Result, bail, eyre};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
/// Install a tool version to a specific path
///
/// Used for building a tool to a directory for use outside of mise
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct InstallInto {
/// Tool to install
/// e.g.: node@20
#[clap(value_name = "TOOL@VERSION")]
tool: ToolArg,
/// Path to install the tool into
#[clap(value_hint = ValueHint::DirPath)]
path: PathBuf,
}
impl InstallInto {
pub async fn run(self) -> Result<()> {
let config = Config::get().await?;
let ts = Arc::new(
ToolsetBuilder::new()
.with_args(std::slice::from_ref(&self.tool))
.build(&config)
.await?,
);
let mut tv = ts
.versions
.get(self.tool.ba.as_ref())
.ok_or_else(|| eyre!("Tool not found"))?
.versions
.first()
.unwrap()
.clone();
let before_date = tv.before_date;
let backend = tv.backend()?;
let mpr = MultiProgressReport::get();
let install_ctx = InstallContext {
config: config.clone(),
ts: ts.clone(),
pr: mpr.add(&tv.style()),
force: true,
dry_run: false,
locked: false, // install-into doesn't support locked mode
before_date,
};
tv.install_path = Some(self.path.clone());
// install-into force-reinstalls, which uninstalls (rm -rf) whatever
// already exists at the install path. Check immediately before the
// install performs that deletion (rather than at the start of `run`) so
// a directory that became non-empty during tool resolution can't be
// clobbered without an explicit opt-in. Refuse to overwrite a non-empty
// directory (e.g. `.`) unless the user passes -y/--yes or confirms
// interactively; the prompt defaults to "no" since it is destructive.
// (#8115)
if path_has_contents(&self.path) {
let proceed = Settings::get().yes
|| prompt::confirm_with_default(
format!(
"{} is not empty; install-into will delete its contents. Continue?",
display_path(&self.path)
),
false,
)?;
if !proceed {
bail!(
"refusing to overwrite non-empty directory {}; pass {} or choose an empty/new path",
display_path(&self.path),
style("--yes").yellow().for_stderr()
);
}
}
backend.install_version(install_ctx, tv).await?;
Ok(())
}
}
/// True if `path` exists and is anything other than an empty directory
/// (a non-empty directory, or a regular file). Empty/new paths return false.
fn path_has_contents(path: &Path) -> bool {
match std::fs::read_dir(path) {
Ok(mut entries) => entries.next().is_some(), // non-empty dir
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false, // missing -> false
// A file (NotADirectory) or an unreadable dir (e.g. PermissionDenied):
// err toward "occupied" so we never silently clobber it.
Err(_) => path.exists(),
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
# install node@20.0.0 into ./mynode
$ <bold>mise install-into node@20.0.0 ./mynode && ./mynode/bin/node -v</bold>
20.0.0
"#
);