use std::env;
use std::ffi::OsStr;
use std::io;
use std::io::prelude::*;
use std::io::StdoutLock;
use std::path::PathBuf;
use std::process;
use std::sync::OnceLock;
use anstream::AutoStream;
use thiserror::Error as ThisError;
use crate::collections::Uvec;
use crate::process::ExecType;
macro_rules! debug {
($stream:expr, $($arg:tt)+) => {{
if let Some(stream) = $stream {
writeln!(stream, "[meadows::config] {}", format_args!($($arg)+))
} else {
Ok(())
}
}}
}
type AutoStreamStdoutLock = AutoStream<StdoutLock<'static>>;
#[derive(Debug, ThisError)]
pub enum FindError {
#[error("File not found")]
FileNotFound,
#[error("Invalid file-name pattern `{0}`")]
InvalidFileNamePattern(String),
#[error("I/O error")]
Io(#[from] io::Error),
}
impl FindError {
#[must_use]
pub fn should_print(&self) -> bool { !matches!(self, Self::FileNotFound) }
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ConfigLevel {
Executable,
System,
User,
Local,
Package,
Instance,
Path,
}
#[allow(clippy::missing_panics_doc)]
pub fn find_config_file(
exec_type: ExecType,
file_name_pattern: &str,
is_debug: bool,
name: &OsStr,
paths: Option<&OsStr>,
set_env_vars: bool,
) -> Result<(ConfigLevel, PathBuf), FindError> {
let files =
find_config_files_impl(true, exec_type, file_name_pattern, is_debug, name, paths, set_env_vars)?;
Ok(files.into_iter().next().unwrap())
}
pub fn find_config_files(
exec_type: ExecType,
file_name_pattern: &str,
is_debug: bool,
name: &OsStr,
paths: Option<&OsStr>,
set_env_vars: bool,
) -> Result<impl IntoIterator<Item = (ConfigLevel, PathBuf)>, FindError> {
find_config_files_impl(false, exec_type, file_name_pattern, is_debug, name, paths, set_env_vars)
}
fn find_config_files_impl(
find_one: bool,
exec_type: ExecType,
file_name_pattern: &str,
is_debug: bool,
name: &OsStr,
paths: Option<&OsStr>,
set_env_vars: bool,
) -> Result<impl IntoIterator<Item = (ConfigLevel, PathBuf)>, FindError> {
use ConfigLevel::*;
use ExecType::*;
let mut stdout = if is_debug { Some(crate::io::stdout().lock()) } else { None };
let stdout = &mut stdout;
debug!(stdout, "Checking paths for {} executable", match exec_type {
Binary => "binary",
Example => "example",
DocTest => "doc-test",
UnitTest => "unit-test",
IntegTest => "integration-test",
BenchTest => "benchmark-test",
})?;
debug!(stdout, "Current directory: {}", {
match env::current_dir() {
Ok(dir) => format!("{dir:?}"),
Err(_) => String::from("-"),
}
})?;
if set_env_vars {
self::set_env_vars(stdout, exec_type)?;
}
let name = name.to_string_lossy();
let bare_file_name = replace_in_pattern(file_name_pattern, "")?;
let file_name = replace_in_pattern(file_name_pattern, &name)?;
let hidden_file_name = format!(".{file_name}");
let relative_file = PathBuf::from(name.as_ref()).join(&bare_file_name);
let hidden_relative_file = PathBuf::from(format!(".{name}")).join(&bare_file_name);
let mut file_paths = Vec::new();
let mut add_file_path =
|level: ConfigLevel, path: PathBuf| -> io::Result<Option<(ConfigLevel, PathBuf)>> {
file_paths.push((level, path.clone()));
if is_debug {
let level_str = format!("{level:?}");
let bullet = if path.is_file() { "*" } else { "" };
debug!(stdout, "{level_str:<10} | {bullet:<1} {path:?}")?;
Ok(None)
} else if find_one && path.is_file() {
Ok(Some((level, path)))
} else {
Ok(None)
}
};
macro_rules! add {
($level:expr, $path:expr) => {{
if let Some(val) = add_file_path($level, $path)? {
return Ok(vec![val].into_iter());
}
}};
}
if let Some(paths) = paths {
for path in env::split_paths(paths) {
if path.is_file() {
add!(Path, path);
} else {
add!(Path, path.join(&hidden_file_name));
add!(Path, path.join(&hidden_relative_file));
}
}
}
if exec_type == Binary {
let mut dir = env::current_dir().ok();
while let Some(val) = dir {
add!(Instance, val.join(&hidden_file_name));
add!(Instance, val.join(&hidden_relative_file));
dir = val.parent().map(PathBuf::from);
}
}
let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from);
if let Some(dir) = manifest_dir {
match exec_type {
Binary => {
add!(Package, dir.join("src").join(&file_name));
add!(Package, dir.join("src").join("bin").join(&file_name));
}
Example => {
add!(Package, dir.join("examples").join(&file_name));
add!(Package, dir.join("examples").join(&bare_file_name));
}
DocTest | UnitTest => {
add!(Package, dir.join("src").join(&file_name));
add!(Package, dir.join("src").join(&bare_file_name));
}
IntegTest => {
add!(Package, dir.join("tests").join(&file_name));
add!(Package, dir.join("tests").join(&bare_file_name));
}
BenchTest => {
add!(Package, dir.join("benches").join(&file_name));
add!(Package, dir.join("benches").join(&bare_file_name));
}
}
}
if exec_type == Binary {
if let Some(dir) = dirs::home_dir() {
add!(Local, dir.join(&hidden_file_name));
add!(Local, dir.join(&hidden_relative_file));
}
if let Some(dir) = dirs::config_local_dir() {
add!(Local, dir.join(&relative_file));
}
}
if exec_type == Binary {
if let Some(dir) = dirs::config_dir() {
add!(User, dir.join(&relative_file));
}
}
if exec_type == Binary {
if let Some(dir) = crate::env::system_config_dir() {
add!(System, dir.join(&file_name));
add!(System, dir.join(&relative_file));
}
}
if exec_type == Binary {
add!(Executable, crate::env::inv_dir().join(&file_name));
}
let mut files = Uvec::with_key(&|val: &(ConfigLevel, PathBuf)| dunce::canonicalize(&val.1).ok());
files.extend(file_paths);
if files.is_empty() {
Err(FindError::FileNotFound)
} else {
Ok(files.into_iter())
}
}
fn replace_in_pattern(pattern: &str, to: &str) -> Result<String, FindError> {
let from = "{}";
if let Some(index) = pattern.find(from) {
let ldot = index > 0;
let ldot_str = if ldot && !to.is_empty() { "." } else { "" };
let rdot = index < pattern.len() - from.len();
let rdot_str = if rdot && !to.is_empty() { "." } else { "" };
let to = format!("{ldot_str}{to}{rdot_str}");
Ok(pattern.replacen(from, &to, 1))
} else {
Err(FindError::InvalidFileNamePattern(pattern.to_owned()))
}
}
fn set_env_vars(stdout: &mut Option<AutoStreamStdoutLock>, exec_type: ExecType) -> io::Result<()> {
static VAL: OnceLock<io::Result<()>> = OnceLock::new();
match VAL.get_or_init(|| set_env_vars_impl(stdout, exec_type)) {
Ok(()) => Ok(()),
Err(err) => Err(io::Error::new(err.kind(), err.to_string())),
}
}
fn set_env_vars_impl(stdout: &mut Option<AutoStreamStdoutLock>, exec_type: ExecType) -> io::Result<()> {
let mut set_env_var = |name: &str, val: &OsStr| -> io::Result<()> {
debug!(stdout, "Setting `{name}` to {val:?}")?;
env::set_var(name, val);
Ok(())
};
set_env_var("dir", crate::env::dir().as_ref())?;
if let Some(dir) = dirs::home_dir() {
set_env_var("home_dir", dir.as_ref())?;
}
set_env_var("name", crate::env::name())?;
set_env_var("path", crate::env::path().as_ref())?;
set_env_var("pid", process::id().to_string().as_ref())?;
if exec_type == ExecType::Binary {
set_env_var("inv_dir", crate::env::inv_dir().as_ref())?;
set_env_var("inv_name", crate::env::inv_name())?;
set_env_var("inv_path", crate::env::inv_path().as_ref())?;
}
if exec_type.is_test() {
set_env_var("test_name", crate::env::test_name())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replace_in_pattern() -> Result<(), FindError> {
assert!(matches!(replace_in_pattern("", "name"), Err(FindError::InvalidFileNamePattern(_))));
assert!(matches!(replace_in_pattern("begend", "name"), Err(FindError::InvalidFileNamePattern(_))));
assert_eq!(replace_in_pattern("{}", "name")?, "name");
assert_eq!(replace_in_pattern("{}", "")?, "");
assert_eq!(replace_in_pattern("beg{}", "name")?, "beg.name");
assert_eq!(replace_in_pattern("beg{}", "")?, "beg");
assert_eq!(replace_in_pattern("{}end", "name")?, "name.end");
assert_eq!(replace_in_pattern("{}end", "")?, "end");
assert_eq!(replace_in_pattern("beg{}end", "name")?, "beg.name.end");
assert_eq!(replace_in_pattern("beg{}end", "")?, "begend");
Ok(())
}
}