use crate::ZvError;
use color_eyre::{
Result,
eyre::{WrapErr, bail, eyre},
};
use semver::Version;
use std::{
borrow::Cow,
io,
path::{Path, PathBuf},
};
use yansi::Paint;
#[derive(Debug, Clone)]
pub struct ZvPaths {
pub data_dir: PathBuf,
pub bin_dir: PathBuf,
pub versions_dir: PathBuf,
pub config_dir: PathBuf,
pub config_file: PathBuf,
#[allow(dead_code)]
pub cache_dir: PathBuf,
pub downloads_dir: PathBuf,
pub index_file: PathBuf,
pub mirrors_file: PathBuf,
pub master_file: PathBuf,
pub public_bin_dir: Option<PathBuf>,
pub using_env_var: bool,
#[cfg(target_os = "macos")]
pub tier: u8,
}
impl ZvPaths {
pub fn resolve() -> Result<Self> {
let (data_dir, using_env_var) = fetch_zv_dir()?;
#[cfg(not(windows))]
let (config_dir, cache_dir, public_bin_dir) = if using_env_var {
(data_dir.clone(), data_dir.clone(), None)
} else {
#[cfg(not(target_os = "macos"))]
{
let config = xdg_config_home()
.unwrap_or_else(|_| data_dir.clone())
.join("zv");
let cache = xdg_cache_home()
.unwrap_or_else(|_| data_dir.clone())
.join("zv");
let public_bin = xdg_bin_home().ok();
(config, cache, public_bin)
}
#[cfg(target_os = "macos")]
if xdg_dirs_exist() {
let config = xdg_config_home()
.unwrap_or_else(|_| data_dir.clone())
.join("zv");
let cache = xdg_cache_home()
.unwrap_or_else(|_| data_dir.clone())
.join("zv");
let public_bin = xdg_bin_home().ok();
(config, cache, public_bin)
} else {
let home = home_dir()?;
let base = home.join("Library/Application Support/zv");
let cache = home.join("Library/Caches/zv");
let public_bin = Some(base.join("bin"));
(base.clone(), cache, public_bin)
}
};
#[cfg(windows)]
let (config_dir, cache_dir, public_bin_dir) = {
(data_dir.clone(), data_dir.clone(), None)
};
#[cfg(target_os = "macos")]
let tier = if using_env_var {
3
} else if xdg_dirs_exist() {
1
} else {
2
};
Ok(Self {
bin_dir: data_dir.join("bin"),
versions_dir: data_dir.join("versions"),
config_file: config_dir.join("zv.toml"),
downloads_dir: cache_dir.join("downloads"),
index_file: cache_dir.join("index.toml"),
mirrors_file: cache_dir.join("mirrors.toml"),
master_file: cache_dir.join("master"),
public_bin_dir,
config_dir,
cache_dir,
data_dir,
using_env_var,
#[cfg(target_os = "macos")]
tier,
})
}
pub fn env_file_default(&self) -> PathBuf {
self.data_dir.join("env")
}
}
#[cfg(target_os = "macos")]
fn xdg_dirs_exist() -> bool {
home_dir()
.map(|h| h.join(".local/share").is_dir() && h.join(".local/bin").is_dir())
.unwrap_or(false)
}
fn xdg_data_home() -> Result<PathBuf> {
if let Ok(val) = std::env::var("XDG_DATA_HOME") {
if !val.is_empty() {
return Ok(PathBuf::from(val));
}
}
home_dir().map(|h| h.join(".local/share"))
}
fn xdg_config_home() -> Result<PathBuf> {
if let Ok(val) = std::env::var("XDG_CONFIG_HOME") {
if !val.is_empty() {
return Ok(PathBuf::from(val));
}
}
home_dir().map(|h| h.join(".config"))
}
fn xdg_cache_home() -> Result<PathBuf> {
if let Ok(val) = std::env::var("XDG_CACHE_HOME") {
if !val.is_empty() {
return Ok(PathBuf::from(val));
}
}
home_dir().map(|h| h.join(".cache"))
}
fn xdg_bin_home() -> Result<PathBuf> {
if let Ok(val) = std::env::var("XDG_BIN_HOME") {
if !val.is_empty() {
return Ok(PathBuf::from(val));
}
}
home_dir().map(|h| h.join(".local/bin"))
}
fn home_dir() -> Result<PathBuf> {
let shell = crate::shell::Shell::detect();
shell
.get_home_dir()
.ok_or_else(|| eyre!("Unable to locate home directory"))
}
pub fn canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
dunce::canonicalize(path)
}
#[inline]
pub(crate) fn is_tty() -> bool {
yansi::is_enabled()
}
pub(crate) fn supports_interactive_prompts() -> bool {
if !is_tty() {
return false;
}
if std::env::var("CI").is_ok() {
return false;
}
if let Ok(term) = std::env::var("TERM")
&& term == "dumb"
{
return false;
}
if std::env::var("DEBIAN_FRONTEND").as_deref() == Ok("noninteractive") {
return false;
}
true
}
#[macro_export]
macro_rules! suggest {
($fmt:expr, cmd = $cmd:expr $(, $($args:tt)*)?) => {
println!(
"• {}",
format!($fmt, $crate::tools::format_cmd($cmd) $(, $($args)*)?)
);
};
($fmt:expr $(, $($args:tt)*)?) => {
println!("• {}", format!($fmt $(, $($args)*)?));
};
}
pub fn format_cmd(cmd: &str) -> String {
Paint::green(cmd).italic().to_string()
}
pub(crate) fn fetch_zv_dir() -> Result<(PathBuf, bool)> {
let zv_dir_env = match std::env::var("ZV_DIR") {
Ok(dir) if !dir.is_empty() => Some(dir),
Ok(_) => None,
Err(env_err) => match env_err {
std::env::VarError::NotPresent => None,
std::env::VarError::NotUnicode(ref str) => {
error(format!(
"Warning: ZV_DIR={str:?} is set but contains invalid Unicode."
));
return Err(eyre!(env_err));
}
},
};
let (zv_dir, using_env) = if let Some(zv_dir) = zv_dir_env {
(PathBuf::from(zv_dir), true )
} else {
(get_default_zv_dir()?, false )
};
match zv_dir.try_exists() {
Ok(true) => {
if !zv_dir.is_dir() {
error(format!(
"zv directory exists but is not a directory: {}. Please check ZV_DIR env var. Aborting...",
zv_dir.display()
));
bail!(eyre!("ZV_DIR exists but is not a directory"));
}
}
Ok(false) => {
if using_env {
std::fs::create_dir_all(&zv_dir)
.map_err(ZvError::Io)
.wrap_err_with(|| {
format!(
"Error creating ZV_DIR from env var ZV_DIR={}",
std::env::var("ZV_DIR").expect("Handled in fetch_zv_dir()")
)
})?;
} else {
std::fs::create_dir(&zv_dir)
.map_err(ZvError::Io)
.wrap_err_with(|| {
format!("Failed to create default .zv at {}", zv_dir.display())
})?;
}
}
Err(e) => {
error(format!(
"Failed to check zv directory at {:?}",
zv_dir.display(),
));
return Err(ZvError::Io(e).into());
}
};
let zv_dir = canonicalize(&zv_dir).map_err(ZvError::Io)?;
Ok((zv_dir, using_env))
}
pub(crate) fn get_default_zv_dir() -> Result<PathBuf> {
#[cfg(not(windows))]
{
xdg_data_home().map(|d| d.join("zv"))
}
#[cfg(windows)]
{
home_dir().map(|h| h.join(".zv"))
}
}
#[inline]
pub fn warn(message: impl Into<Cow<'static, str>>) {
let msg = message.into();
eprintln!("{}: {}", "Warning".yellow().bold(), msg);
}
#[inline]
pub fn error(message: impl Into<Cow<'static, str>>) {
let msg = message.into();
eprintln!("{}: {}", "Error".red().bold(), msg);
}
pub fn calculate_file_hash(path: &Path) -> Result<u32> {
use crc32fast::Hasher;
use std::io::Read;
let mut file = std::fs::File::open(path)
.wrap_err_with(|| format!("Failed to open file for hashing: {}", path.display()))?;
let mut hasher = Hasher::new();
let mut buffer = [0; 8192];
loop {
let bytes_read = file
.read(&mut buffer)
.wrap_err_with(|| format!("Failed to read file for hashing: {}", path.display()))?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(hasher.finalize())
}
pub fn files_have_same_hash(path1: &Path, path2: &Path) -> Result<bool> {
if !path1.exists() || !path2.exists() {
return Ok(false);
}
Ok(calculate_file_hash(path1)? == calculate_file_hash(path2)?)
}
pub fn sanitize_build_zig_zon_name(name: Option<&str>, zig_version: &Version) -> Option<String> {
if *zig_version < Version::new(0, 12, 0) {
return None; }
let default_name = "app";
let raw = name.unwrap_or(default_name).trim();
let mut sanitized = raw
.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => c,
'-' | ' ' | '.' => '_',
_ => '_', })
.collect::<String>()
.to_lowercase();
if let Some(first_char) = sanitized.chars().next()
&& first_char.is_ascii_digit()
{
sanitized = format!("_{}", sanitized);
}
Some(if *zig_version >= Version::new(0, 13, 0) {
format!(".{sanitized}") } else {
format!("\"{sanitized}\"") })
}
pub fn deduplicate_semver_variants(versions: Vec<crate::ZigVersion>) -> Vec<crate::ZigVersion> {
let mut seen_semvers: std::collections::HashMap<semver::Version, crate::ZigVersion> =
std::collections::HashMap::new();
let mut non_semver_versions: Vec<crate::ZigVersion> = Vec::new();
for version in versions {
match version {
crate::ZigVersion::Semver(v) => {
seen_semvers
.entry(v.clone())
.or_insert(crate::ZigVersion::Semver(v));
}
crate::ZigVersion::Latest(Some(v)) | crate::ZigVersion::Stable(Some(v)) => {
seen_semvers
.entry(v.clone())
.or_insert(crate::ZigVersion::Semver(v));
}
crate::ZigVersion::Latest(None)
| crate::ZigVersion::Stable(None)
| crate::ZigVersion::Master(_) => {
non_semver_versions.push(version);
}
}
}
let mut result: Vec<crate::ZigVersion> = seen_semvers.into_values().collect();
result.extend(non_semver_versions);
result
}