use std::ffi::OsStr;
use std::fmt::{Debug, Display};
use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, Once};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use crate::cli::args::{BackendArg, ToolArg};
use crate::config::config_file::min_version::MinVersionSpec;
use crate::config::config_file::mise_toml::{MiseToml, MonorepoConfig};
use crate::config::env_directive::EnvDirective;
use crate::config::{AliasMap, Settings, settings};
use crate::errors::Error::UntrustedConfig;
use crate::file::display_path;
use crate::hash::hash_to_str;
use crate::hooks::Hook;
use crate::prepare::PrepareConfig;
use crate::redactions::Redactions;
use crate::task::{Task, TaskTemplate};
use crate::toolset::{ToolRequest, ToolRequestSet, ToolSource, ToolVersionList, Toolset};
use crate::ui::{prompt, style};
use crate::watch_files::WatchFile;
use crate::{
backend::{self, Backend},
config, dirs, env, file, hash,
};
use eyre::{Result, eyre};
use idiomatic_version::IdiomaticVersionFile;
use indexmap::IndexMap;
use serde_derive::Deserialize;
use std::sync::LazyLock as Lazy;
use tool_versions::ToolVersions;
use super::Config;
pub mod config_root;
pub mod diagnostic;
pub mod idiomatic_version;
pub mod min_version;
pub mod mise_toml;
pub mod toml;
pub mod tool_versions;
#[derive(Debug, PartialEq)]
pub enum ConfigFileType {
MiseToml,
ToolVersions,
IdiomaticVersion(Vec<Arc<dyn Backend>>),
}
pub trait ConfigFile: Debug + Send + Sync {
fn get_path(&self) -> &Path;
fn min_version(&self) -> Option<&MinVersionSpec> {
None
}
fn project_root(&self) -> Option<PathBuf> {
let p = self.get_path();
if config::is_global_config(p) {
return None;
}
match p.parent() {
Some(dir) => match dir {
dir if dir.starts_with(*dirs::CONFIG) => None,
dir if dir.starts_with(*dirs::SYSTEM_CONFIG) => None,
dir if dir == *dirs::HOME => None,
_ => Some(config_root::config_root(p)),
},
None => None,
}
}
fn config_type(&self) -> ConfigFileType;
fn config_root(&self) -> PathBuf {
config_root::config_root(self.get_path())
}
fn plugins(&self) -> Result<HashMap<String, String>> {
Ok(Default::default())
}
fn env_entries(&self) -> Result<Vec<EnvDirective>> {
Ok(Default::default())
}
fn vars_entries(&self) -> Result<Vec<EnvDirective>> {
Ok(Default::default())
}
fn tasks(&self) -> Vec<&Task> {
Default::default()
}
fn remove_tool(&self, ba: &BackendArg) -> eyre::Result<()>;
fn replace_versions(&self, ba: &BackendArg, versions: Vec<ToolRequest>) -> eyre::Result<()>;
fn save(&self) -> eyre::Result<()>;
fn dump(&self) -> eyre::Result<String>;
fn source(&self) -> ToolSource;
fn to_toolset(&self) -> eyre::Result<Toolset> {
Ok(self.to_tool_request_set()?.into())
}
fn to_tool_request_set(&self) -> eyre::Result<ToolRequestSet>;
fn aliases(&self) -> eyre::Result<AliasMap> {
Ok(Default::default())
}
fn shell_aliases(&self) -> eyre::Result<IndexMap<String, String>> {
Ok(Default::default())
}
fn task_config(&self) -> &TaskConfig {
static DEFAULT_TASK_CONFIG: Lazy<TaskConfig> = Lazy::new(TaskConfig::default);
&DEFAULT_TASK_CONFIG
}
fn task_templates(&self) -> IndexMap<String, TaskTemplate> {
IndexMap::new()
}
fn experimental_monorepo_root(&self) -> Option<bool> {
None
}
fn monorepo(&self) -> Option<&MonorepoConfig> {
None
}
fn redactions(&self) -> &Redactions {
static DEFAULT_REDACTIONS: Lazy<Redactions> = Lazy::new(Redactions::default);
&DEFAULT_REDACTIONS
}
fn watch_files(&self) -> Result<Vec<WatchFile>> {
Ok(Default::default())
}
fn hooks(&self) -> Result<Vec<Hook>> {
Ok(Default::default())
}
fn prepare_config(&self) -> Option<PrepareConfig> {
None
}
}
impl dyn ConfigFile {
pub async fn add_runtimes(
&self,
config: &Arc<Config>,
tools: &[ToolArg],
pin: bool,
) -> eyre::Result<()> {
let mut ts = self.to_toolset()?.to_owned();
ts.resolve(config).await?;
trace!("resolved toolset");
let mut plugins_to_update = HashMap::new();
for ta in tools {
if let Some(tv) = &ta.tvr {
plugins_to_update
.entry(ta.ba.clone())
.or_insert_with(Vec::new)
.push(tv);
}
}
trace!("plugins to update: {plugins_to_update:?}");
for (ba, versions) in &plugins_to_update {
let mut tvl = ToolVersionList::new(
ba.clone(),
ts.source.clone().unwrap_or(ToolSource::Argument),
);
for tv in versions {
tvl.requests.push((*tv).clone());
}
ts.versions.insert(ba.clone(), tvl);
}
trace!("resolving toolset 2");
ts.resolve(config).await?;
trace!("resolved toolset 2");
for (ba, versions) in plugins_to_update {
let mut new = vec![];
for tr in versions {
let mut tr = tr.clone();
if pin {
let tv = tr.resolve(config, &Default::default()).await?;
if let ToolRequest::Version {
version: _version,
source,
options,
backend,
} = tr
{
tr = ToolRequest::Version {
version: tv.version,
source,
options,
backend,
};
}
}
new.push(tr);
}
trace!("replacing versions {new:?}");
self.replace_versions(&ba, new)?;
}
trace!("done adding runtimes");
Ok(())
}
pub fn display_runtime(&self, runtimes: &[ToolArg]) -> eyre::Result<bool> {
if runtimes.len() == 1 && runtimes[0].tvr.is_none() {
let fa = &runtimes[0].ba;
let tvl = self
.to_toolset()?
.versions
.get(fa)
.ok_or_else(|| {
eyre!(
"no version set for {} in {}",
fa.to_string(),
display_path(self.get_path())
)
})?
.requests
.iter()
.map(|tvr| tvr.version())
.collect::<Vec<_>>();
miseprintln!("{}", 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)
}
}
async fn init(path: &Path) -> Arc<dyn ConfigFile> {
match detect_config_file_type(path).await {
Some(ConfigFileType::MiseToml) => Arc::new(MiseToml::init(path)),
Some(ConfigFileType::ToolVersions) => Arc::new(ToolVersions::init(path)),
Some(ConfigFileType::IdiomaticVersion(backends)) => Arc::new(
IdiomaticVersionFile::parse(path.to_path_buf(), backends)
.await
.expect("failed to parse idiomatic version file"),
),
_ => panic!("Unknown config file type: {}", path.display()),
}
}
pub async fn parse_or_init(path: &Path) -> eyre::Result<Arc<dyn ConfigFile>> {
let path = if path.is_dir() {
path.join(&*env::MISE_DEFAULT_CONFIG_FILENAME)
} else {
path.into()
};
let cf = match path.exists() {
true => parse(&path).await?,
false => init(&path).await,
};
Ok(cf)
}
pub async fn parse(path: &Path) -> Result<Arc<dyn ConfigFile>> {
if let Ok(settings) = Settings::try_get()
&& settings.paranoid
{
trust_check(path)?;
}
match detect_config_file_type(path).await {
Some(ConfigFileType::MiseToml) => Ok(Arc::new(MiseToml::from_file(path)?)),
Some(ConfigFileType::ToolVersions) => Ok(Arc::new(ToolVersions::from_file(path)?)),
Some(ConfigFileType::IdiomaticVersion(backends)) => Ok(Arc::new(
IdiomaticVersionFile::parse(path.to_path_buf(), backends).await?,
)),
#[allow(clippy::box_default)]
_ => Ok(Arc::new(MiseToml::default())),
}
}
pub fn config_trust_root(path: &Path) -> PathBuf {
if settings::is_loaded() && Settings::get().paranoid {
path.to_path_buf()
} else {
config_root::config_root(path)
}
}
pub fn trust_check(path: &Path) -> eyre::Result<()> {
static MUTEX: Mutex<()> = Mutex::new(());
let _lock = MUTEX.lock().unwrap(); let config_root = config_trust_root(path);
let default_cmd = String::new();
let args = env::ARGS.read().unwrap();
let cmd = args.get(1).unwrap_or(&default_cmd).as_str();
if is_trusted(&config_root) || is_trusted(path) || cmd == "trust" || cfg!(test) {
return Ok(());
}
if cmd != "hook-env" && !is_ignored(&config_root) && !is_ignored(path) {
let ans = (settings::is_loaded() && Settings::get().yes)
|| prompt::confirm_with_all(format!(
"{} config files in {} are not trusted. Trust them?",
style::eyellow("mise"),
style::epath(&config_root)
))?;
if ans {
trust(&config_root)?;
return Ok(());
} else if console::user_attended_stderr() {
add_ignored(config_root.to_path_buf())?;
}
}
Err(UntrustedConfig(path.into()))?
}
pub fn is_trusted(path: &Path) -> bool {
let canonicalized_path = match path.canonicalize() {
Ok(p) => p,
Err(err) => {
debug!("trust canonicalize: {err}");
return false;
}
};
if is_ignored(canonicalized_path.as_path()) {
return false;
}
if IS_TRUSTED
.lock()
.unwrap()
.contains(canonicalized_path.as_path())
{
return true;
}
if config::is_global_config(path) {
add_trusted(canonicalized_path.to_path_buf());
return true;
}
let settings = Settings::get();
for p in settings.trusted_config_paths() {
if canonicalized_path.starts_with(p) {
add_trusted(canonicalized_path.to_path_buf());
return true;
}
}
if settings.experimental
&& let Some(parent) = canonicalized_path.parent()
{
let mut current = parent;
while let Some(dir) = current.parent() {
let monorepo_marker = with_appended_extension(&trust_path(dir), "monorepo");
if monorepo_marker.exists() {
add_trusted(canonicalized_path.to_path_buf());
return true;
}
current = dir;
}
}
if settings.paranoid {
let trusted = trust_file_hash(path).unwrap_or_else(|e| {
warn!("trust_file_hash: {e}");
false
});
if !trusted {
return false;
}
} else if cfg!(test) || ci_info::is_ci() {
return true;
} else if !trust_path(path).exists() {
return false;
}
add_trusted(canonicalized_path.to_path_buf());
true
}
static IS_TRUSTED: Lazy<Mutex<HashSet<PathBuf>>> = Lazy::new(|| Mutex::new(HashSet::new()));
static IS_IGNORED: Lazy<Mutex<HashSet<PathBuf>>> = Lazy::new(|| Mutex::new(HashSet::new()));
fn add_trusted(path: PathBuf) {
IS_TRUSTED.lock().unwrap().insert(path);
}
pub fn add_ignored(path: PathBuf) -> Result<()> {
let path = path.canonicalize()?;
file::create_dir_all(&*dirs::IGNORED_CONFIGS)?;
file::make_symlink_or_file(&path, &ignore_path(&path))?;
IS_IGNORED.lock().unwrap().insert(path);
Ok(())
}
pub fn rm_ignored(path: PathBuf) -> Result<()> {
let path = path.canonicalize()?;
let ignore_path = ignore_path(&path);
if ignore_path.exists() {
file::remove_file(&ignore_path)?;
}
IS_IGNORED.lock().unwrap().remove(&path);
Ok(())
}
pub fn is_ignored(path: &Path) -> bool {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
if !dirs::IGNORED_CONFIGS.exists() {
return;
}
let mut is_ignored = IS_IGNORED.lock().unwrap();
for entry in file::ls(&dirs::IGNORED_CONFIGS).unwrap_or_default() {
if let Ok(canonicalized_path) = entry.canonicalize() {
is_ignored.insert(canonicalized_path);
}
}
});
if let Ok(path) = path.canonicalize() {
env::MISE_IGNORED_CONFIG_PATHS
.iter()
.any(|p| path.starts_with(p))
|| IS_IGNORED.lock().unwrap().contains(&path)
} else {
debug!("is_ignored: path canonicalize failed");
true
}
}
pub fn trust(path: &Path) -> Result<()> {
rm_ignored(path.to_path_buf())?;
let hashed_path = trust_path(path);
if !hashed_path.exists() {
file::create_dir_all(hashed_path.parent().unwrap())?;
file::make_symlink_or_file(path.canonicalize()?.as_path(), &hashed_path)?;
}
if Settings::get().paranoid {
let trust_hash_path = with_appended_extension(&hashed_path, "hash");
let hash = hash::file_hash_sha256(path, None)?;
file::write(trust_hash_path, hash)?;
}
Ok(())
}
pub fn mark_as_monorepo_root(path: &Path) -> Result<()> {
let config_root = config_trust_root(path);
let hashed_path = trust_path(&config_root);
let monorepo_marker = with_appended_extension(&hashed_path, "monorepo");
if !monorepo_marker.exists() {
file::create_dir_all(monorepo_marker.parent().unwrap())?;
file::write(&monorepo_marker, "")?;
}
Ok(())
}
pub fn untrust(path: &Path) -> eyre::Result<()> {
rm_ignored(path.to_path_buf())?;
let hashed_path = trust_path(path);
if hashed_path.exists() {
file::remove_file(&hashed_path)?;
}
let hash_path = with_appended_extension(&hashed_path, "hash");
if hash_path.exists() {
file::remove_file(&hash_path)?;
}
let monorepo_path = with_appended_extension(&hashed_path, "monorepo");
if monorepo_path.exists() {
file::remove_file(&monorepo_path)?;
}
Ok(())
}
fn trust_path(path: &Path) -> PathBuf {
dirs::TRUSTED_CONFIGS.join(hashed_path_filename(path))
}
fn ignore_path(path: &Path) -> PathBuf {
dirs::IGNORED_CONFIGS.join(hashed_path_filename(path))
}
fn with_appended_extension(path: &Path, ext: &str) -> PathBuf {
let mut os_string = path.as_os_str().to_owned();
os_string.push(".");
os_string.push(ext);
PathBuf::from(os_string)
}
fn hashed_path_filename(path: &Path) -> String {
let canonicalized_path = path.canonicalize().unwrap();
let hash = hash_to_str(&canonicalized_path);
let trunc_str = |s: &OsStr| {
let mut s = s.to_str().unwrap().to_string();
s = s.chars().take(20).collect();
s
};
let trust_path = dirs::TRUSTED_CONFIGS.join(hash_to_str(&hash));
if trust_path.exists() {
return trust_path
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
}
let parent = canonicalized_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_default()
.file_name()
.map(trunc_str);
let filename = canonicalized_path.file_name().map(trunc_str);
[parent, filename, Some(hash)]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join("-")
}
fn trust_file_hash(path: &Path) -> eyre::Result<bool> {
let trust_path = trust_path(path);
let trust_hash_path = with_appended_extension(&trust_path, "hash");
if !trust_hash_path.exists() {
return Ok(false);
}
let hash = file::read_to_string(&trust_hash_path)?;
let actual = hash::file_hash_sha256(path, None)?;
Ok(hash == actual)
}
async fn filename_is_idiomatic(file_name: String) -> Option<Vec<Arc<dyn Backend>>> {
let mut backends = vec![];
for b in backend::list() {
match b.idiomatic_filenames().await {
Ok(filenames) if filenames.contains(&file_name) => backends.push(b),
Err(e) => debug!("idiomatic_filenames failed for {}: {:?}", b, e),
_ => {}
}
}
if backends.is_empty() {
None
} else {
Some(backends)
}
}
async fn detect_config_file_type(path: &Path) -> Option<ConfigFileType> {
match path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("mise.toml")
{
f if env::MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES
.as_ref()
.is_some_and(|o| o.contains(f)) =>
{
Some(ConfigFileType::ToolVersions)
}
f if env::MISE_DEFAULT_TOOL_VERSIONS_FILENAME.as_str() == f => {
Some(ConfigFileType::ToolVersions)
}
f if env::MISE_OVERRIDE_CONFIG_FILENAMES.contains(f) => Some(ConfigFileType::MiseToml),
f if env::MISE_DEFAULT_CONFIG_FILENAME.as_str() == f => Some(ConfigFileType::MiseToml),
f => {
if let Some(backends) = filename_is_idiomatic(f.to_string()).await {
Some(ConfigFileType::IdiomaticVersion(backends))
} else if f.ends_with(".toml") {
Some(ConfigFileType::MiseToml)
} else {
None
}
}
}
}
impl Display for dyn ConfigFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let toolset = self.to_toolset().unwrap().to_string();
write!(f, "{}: {toolset}", &display_path(self.get_path()))
}
}
impl PartialEq for dyn ConfigFile {
fn eq(&self, other: &Self) -> bool {
self.get_path() == other.get_path()
}
}
impl Eq for dyn ConfigFile {}
impl Hash for dyn ConfigFile {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.get_path().hash(state);
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct TaskConfig {
pub includes: Option<Vec<String>>,
pub dir: Option<String>,
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[tokio::test]
async fn test_detect_config_file_type() {
env::set_var("MISE_EXPERIMENTAL", "true");
assert!(matches!(
detect_config_file_type(Path::new("/foo/bar/.nvmrc")).await,
Some(ConfigFileType::IdiomaticVersion(_))
));
assert!(matches!(
detect_config_file_type(Path::new("/foo/bar/.ruby-version")).await,
Some(ConfigFileType::IdiomaticVersion(_))
));
assert_eq!(
detect_config_file_type(Path::new("/foo/bar/.test-tool-versions")).await,
Some(ConfigFileType::ToolVersions)
);
assert_eq!(
detect_config_file_type(Path::new("/foo/bar/mise.toml")).await,
Some(ConfigFileType::MiseToml)
);
assert!(matches!(
detect_config_file_type(Path::new("/foo/bar/rust-toolchain.toml")).await,
Some(ConfigFileType::IdiomaticVersion(_))
));
}
#[test]
fn test_with_appended_extension() {
let path = Path::new("/tmp/trusted/infra-mise.toml-a1b2c3d4e5f67890");
let result = with_appended_extension(path, "hash");
assert_eq!(
result,
Path::new("/tmp/trusted/infra-mise.toml-a1b2c3d4e5f67890.hash")
);
let result2 = with_appended_extension(path, "monorepo");
assert_eq!(
result2,
Path::new("/tmp/trusted/infra-mise.toml-a1b2c3d4e5f67890.monorepo")
);
}
}