use crate::pkg::resolve::SourceType;
use anyhow::anyhow;
use colored::*;
use crossterm::tty::IsTty;
use std::fmt::Display;
use std::fs;
use std::io::{Write, stdin, stdout};
use std::process::Command;
use std::time::Duration;
use walkdir::WalkDir;
#[cfg(windows)]
use junction;
pub fn format_bytes(bytes: u64) -> String {
const KIB: u64 = 1024;
const MIB: u64 = 1024 * KIB;
const GIB: u64 = 1024 * MIB;
if bytes >= GIB {
format!("{:.2} GiB", bytes as f64 / GIB as f64)
} else if bytes >= MIB {
format!("{:.2} MiB", bytes as f64 / MIB as f64)
} else if bytes >= KIB {
format!("{:.2} KiB", bytes as f64 / KIB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn format_size_diff(diff: i64) -> String {
if diff == 0 {
return "0 B".to_string();
}
let sign = if diff > 0 { "+" } else { "-" };
let bytes = diff.unsigned_abs();
format!("{} {}", sign, format_bytes(bytes))
}
use crate::pkg::types::Scope;
use clap_complete::Shell;
use std::path::{Path, PathBuf};
pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
}
Ok(())
}
pub fn is_safe_path(base: &Path, path: &Path) -> bool {
let base = base.canonicalize().unwrap_or_else(|_| base.to_path_buf());
let joined = if path.is_absolute() {
path.to_path_buf()
} else {
base.join(path)
};
let mut normalized = PathBuf::new();
for component in joined.components() {
match component {
std::path::Component::Prefix(_) => normalized.push(component),
std::path::Component::RootDir => normalized.push(component),
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
if !normalized.pop() {
return false;
}
}
std::path::Component::Normal(p) => normalized.push(p),
}
}
normalized.starts_with(&base)
}
pub fn symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
if link.exists() || link.is_symlink() {
fs::remove_file(link)?;
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
{
if std::os::windows::fs::symlink_file(target, link).is_err() {
if fs::hard_link(target, link).is_err() {
fs::copy(target, link)?;
}
}
Ok(())
}
}
pub fn symlink_dir(target: &Path, link: &Path) -> std::io::Result<()> {
if link.exists() || link.is_symlink() {
if link.is_dir() && !link.is_symlink() {
fs::remove_dir_all(link)?;
} else {
fs::remove_file(link)?;
}
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
{
junction::create(target, link)
}
}
pub fn is_admin() -> bool {
#[cfg(windows)]
{
use std::mem;
use std::ptr;
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::GetCurrentProcess;
use winapi::um::processthreadsapi::OpenProcessToken;
use winapi::um::securitybaseapi::CheckTokenMembership;
use winapi::um::winnt::{PSID, TOKEN_QUERY};
let mut token = ptr::null_mut();
let process = unsafe { GetCurrentProcess() };
if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
return false;
}
let mut sid: [u8; 8] = [0; 8];
let mut sid_size = mem::size_of_val(&sid) as u32;
if unsafe {
winapi::um::securitybaseapi::CreateWellKnownSid(
winapi::um::winnt::WinBuiltinAdministratorsSid,
ptr::null_mut(),
sid.as_mut_ptr() as PSID,
&mut sid_size,
)
} == 0
{
unsafe { CloseHandle(token) };
return false;
}
let mut is_member = 0;
let result =
unsafe { CheckTokenMembership(token, sid.as_mut_ptr() as PSID, &mut is_member) };
unsafe { CloseHandle(token) };
result != 0 && is_member != 0
}
#[cfg(unix)]
{
nix::unistd::getuid().is_root()
}
}
pub fn print_info<T: Display>(key: &str, value: T) {
println!("{}: {}", key, value);
}
pub fn format_version_summary(branch: &str, status: &str, number: &str) -> String {
let branch_short = if branch == "Production" {
"Prod."
} else if branch == "Development" {
"Dev."
} else if branch == "Public" {
"Pub."
} else if branch == "Special" {
"Spec."
} else {
branch
};
format!(
"{} {} {}",
branch_short.blue().bold().italic(),
status,
number,
)
}
pub fn format_version_full(branch: &str, status: &str, number: &str, commit: &str) -> String {
format!(
"{} {}",
format_version_summary(branch, status, number),
commit.green()
)
}
pub fn print_aligned_info(key: &str, value: &str) {
let key_with_colon = format!("{}:", key);
println!("{:<18}{}", key_with_colon.cyan(), value);
}
pub fn run_shell_command(command_str: &str) -> anyhow::Result<()> {
let status = if cfg!(target_os = "windows") {
Command::new("pwsh")
.arg("-Command")
.arg(command_str)
.status()?
} else {
Command::new("bash").arg("-c").arg(command_str).status()?
};
if !status.success() {
return Err(anyhow!("Command failed: {}", command_str));
}
Ok(())
}
pub fn run_shell_command_quietly(command_str: &str) -> anyhow::Result<()> {
let status = if cfg!(target_os = "windows") {
Command::new("pwsh")
.arg("-Command")
.arg(command_str)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()?
} else {
Command::new("bash")
.arg("-c")
.arg(command_str)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()?
};
if !status.success() {
return Err(anyhow!("Command failed: {}", command_str));
}
Ok(())
}
pub fn command_exists(command: &str) -> bool {
if cfg!(target_os = "windows") {
Command::new("where")
.arg(command)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
} else {
Command::new("bash")
.arg("-c")
.arg(format!("command -v {}", command))
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
}
pub fn is_mini_mode() -> bool {
std::env::var("ZOI_MINI_MODE").is_ok_and(|v| v == "1")
}
pub fn ask_for_confirmation(prompt: &str, yes: bool) -> bool {
if yes {
return true;
}
if std::env::var("ZOI_TEST").is_ok() || !stdin().is_tty() {
return false;
}
print!("{} [y/N]: ", prompt.yellow());
let _ = stdout().flush();
let mut input = String::new();
if stdin().read_line(&mut input).is_err() {
return false;
}
input.trim().eq_ignore_ascii_case("y")
}
pub fn set_path_read_only(path: &Path) -> anyhow::Result<()> {
if !path.exists() {
return Ok(());
}
for entry in WalkDir::new(path) {
let entry = entry?;
let mut perms = fs::metadata(entry.path())?.permissions();
if !perms.readonly() {
perms.set_readonly(true);
fs::set_permissions(entry.path(), perms)?;
}
}
Ok(())
}
pub fn set_path_writable(path: &Path) -> anyhow::Result<()> {
if !path.exists() {
return Ok(());
}
for entry in WalkDir::new(path) {
let entry = entry?;
let mut perms = fs::metadata(entry.path())?.permissions();
if perms.readonly() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = perms.mode();
perms.set_mode(mode | 0o200);
}
#[cfg(not(unix))]
{
perms.set_readonly(false);
}
fs::set_permissions(entry.path(), perms)?;
}
}
Ok(())
}
#[cfg(unix)]
pub fn set_path_owner(path: &Path, owner: &str, group: &str) -> anyhow::Result<()> {
use nix::unistd::{Gid, Group, Uid, User, chown};
let uid = if let Ok(u) = owner.parse::<u32>() {
Some(Uid::from_raw(u))
} else if !owner.is_empty() {
Some(
User::from_name(owner)
.map_err(|e| anyhow!("Error looking up user '{}': {}", owner, e))?
.ok_or_else(|| anyhow!("User not found: {}", owner))?
.uid,
)
} else {
None
};
let gid = if let Ok(g) = group.parse::<u32>() {
Some(Gid::from_raw(g))
} else if !group.is_empty() {
Some(
Group::from_name(group)
.map_err(|e| anyhow!("Error looking up group '{}': {}", group, e))?
.ok_or_else(|| anyhow!("Group not found: {}", group))?
.gid,
)
} else {
None
};
chown(path, uid, gid).map_err(|e| anyhow!("Failed to chown '{}': {}", path.display(), e))?;
Ok(())
}
#[cfg(not(unix))]
pub fn set_path_owner(_path: &Path, _owner: &str, _group: &str) -> anyhow::Result<()> {
Ok(())
}
use std::collections::HashMap;
pub fn get_linux_distribution_info() -> Option<HashMap<String, String>> {
if let Ok(contents) = fs::read_to_string("/etc/os-release") {
let info: HashMap<String, String> = contents
.lines()
.filter_map(|line| {
let mut parts = line.splitn(2, '=');
let key = parts.next()?;
let value = parts.next()?.trim_matches('"').to_string();
if key.is_empty() {
None
} else {
Some((key.to_string(), value))
}
})
.collect();
if info.is_empty() { None } else { Some(info) }
} else {
None
}
}
pub fn get_linux_distro_family() -> Option<String> {
if let Some(info) = get_linux_distribution_info() {
if let Some(id_like) = info.get("ID_LIKE") {
let families: Vec<&str> = id_like.split_whitespace().collect();
if families.contains(&"debian") {
return Some("debian".to_string());
}
if families.contains(&"arch") {
return Some("arch".to_string());
}
if families.contains(&"fedora") {
return Some("fedora".to_string());
}
if families.contains(&"rhel") {
return Some("fedora".to_string());
}
if families.contains(&"suse") {
return Some("suse".to_string());
}
if families.contains(&"gentoo") {
return Some("gentoo".to_string());
}
}
if let Some(id) = info.get("ID") {
return match id.as_str() {
"debian" | "ubuntu" | "linuxmint" | "pop" | "kali" | "kubuntu" | "lubuntu"
| "xubuntu" | "zorin" | "elementary" => Some("debian".to_string()),
"arch" | "manjaro" | "cachyos" | "endeavouros" | "garuda" => {
Some("arch".to_string())
}
"fedora" | "centos" | "rhel" | "rocky" | "almalinux" => Some("fedora".to_string()),
"opensuse" | "opensuse-tumbleweed" | "opensuse-leap" => Some("suse".to_string()),
"gentoo" => Some("gentoo".to_string()),
"alpine" => Some("alpine".to_string()),
"void" => Some("void".to_string()),
"solus" => Some("solus".to_string()),
"guix" => Some("guix".to_string()),
_ => None,
};
}
}
None
}
pub fn get_linux_distribution() -> Option<String> {
get_linux_distribution_info().and_then(|info| info.get("ID").cloned())
}
pub fn get_desktop_environment() -> Option<String> {
if cfg!(target_os = "windows") {
return Some("windows".to_string());
}
if let Ok(de) = std::env::var("XDG_CURRENT_DESKTOP")
&& !de.is_empty()
{
return Some(de.to_lowercase());
}
if let Ok(ds) = std::env::var("DESKTOP_SESSION")
&& !ds.is_empty()
{
return Some(ds.to_lowercase());
}
None
}
pub fn get_display_server() -> Option<String> {
if cfg!(target_os = "windows") {
return Some("windows".to_string());
}
if cfg!(target_os = "macos") {
return Some("quartz".to_string());
}
if let Ok(st) = std::env::var("XDG_SESSION_TYPE")
&& !st.is_empty()
{
return Some(st.to_lowercase());
}
None
}
pub fn get_kernel_version() -> Option<String> {
if cfg!(unix) {
let output = Command::new("uname").arg("-r").output().ok()?;
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
} else if cfg!(target_os = "windows") {
let output = Command::new("pwsh")
.arg("-Command")
.arg("(Get-CimInstance Win32_OperatingSystem).Version")
.output()
.ok()?;
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
}
None
}
pub fn get_distro_version() -> Option<String> {
if let Some(info) = get_linux_distribution_info()
&& let Some(vid) = info.get("VERSION_ID")
{
return Some(vid.clone());
}
if cfg!(target_os = "macos") {
let output = Command::new("sw_vers")
.arg("-productVersion")
.output()
.ok()?;
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
} else if cfg!(target_os = "windows") {
let output = Command::new("pwsh")
.arg("-Command")
.arg("(Get-CimInstance Win32_OperatingSystem).Version")
.output()
.ok()?;
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
}
None
}
pub fn get_cpu_info() -> Option<String> {
if cfg!(target_os = "linux") {
if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") {
for line in cpuinfo.lines() {
if line.starts_with("model name")
&& let Some((_, model)) = line.split_once(':')
{
return Some(model.trim().to_string());
}
}
}
} else if cfg!(target_os = "macos") {
let output = Command::new("sysctl")
.arg("-n")
.arg("machdep.cpu.brand_string")
.output()
.ok()?;
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
} else if cfg!(target_os = "windows") {
let output = Command::new("pwsh")
.arg("-Command")
.arg("(Get-CimInstance Win32_Processor).Name")
.output()
.ok()?;
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
}
None
}
pub fn get_gpu_info() -> Option<String> {
if cfg!(target_os = "linux") {
if let Ok(output) = Command::new("lspci").output()
&& output.status.success()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if (line.contains("VGA compatible controller") || line.contains("3D controller"))
&& let Some((_, model)) = line.split_once(": ")
{
return Some(model.trim().to_string());
}
}
}
} else if cfg!(target_os = "macos") {
let output = Command::new("system_profiler")
.arg("SPDisplaysDataType")
.output()
.ok()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.trim().starts_with("Chipset Model:")
&& let Some((_, model)) = line.split_once(':')
{
return Some(model.trim().to_string());
}
}
}
} else if cfg!(target_os = "windows") {
let output = Command::new("pwsh")
.arg("-Command")
.arg("(Get-CimInstance Win32_VideoController).Name")
.output()
.ok()?;
if output.status.success() {
return Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
}
None
}
pub fn get_native_package_manager() -> Option<String> {
let os = std::env::consts::OS;
match os {
"linux" => get_linux_distro_family()
.map(|family| {
match family.as_str() {
"debian" => "apt",
"arch" => "pacman",
"fedora" => "dnf",
"suse" => "zypper",
"gentoo" => "portage",
"alpine" => "apk",
"void" => "xbps-install",
"solus" => "eopkg",
"guix" => "guix",
_ => "unknown",
}
.to_string()
})
.filter(|s| s != "unknown"),
"macos" => {
if command_exists("brew") {
Some("brew".to_string())
} else if command_exists("port") {
Some("macports".to_string())
} else {
None
}
}
"windows" => {
if command_exists("scoop") {
Some("scoop".to_string())
} else if command_exists("choco") {
Some("choco".to_string())
} else if command_exists("winget") {
Some("winget".to_string())
} else {
None
}
}
_ => None,
}
}
pub fn print_repo_warning(repo_name: &str) {
if is_mini_mode() {
if let Ok(index) = crate::pkg::mini_resolve::fetch_registry_index()
&& let Some(pkg_info) = index.packages.values().find(|p| p.repo == repo_name)
{
let warning_message = match pkg_info.repo_type.as_str() {
"unofficial" => {
Some("This package is from an unofficial repository and is not trusted.")
}
"community" => {
Some("This package is from a community repository. Use with caution.")
}
"test" => Some(
"This package is from a testing repository and may not function correctly.",
),
"archive" => {
Some("This package is from an archive repository and is no longer maintained.")
}
_ => None,
};
if let Some(message) = warning_message {
println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
}
}
return;
}
if let Ok(db_path) = crate::pkg::resolve::get_db_root()
&& let Ok(repo_config) = crate::pkg::config::read_repo_config(&db_path)
{
let major_repo = repo_name.split('/').next().unwrap_or_default();
if let Some(repo_entry) = repo_config.repos.iter().find(|r| r.name == major_repo) {
let warning_message = match repo_entry.repo_type.as_str() {
"unofficial" => {
Some("This package is from an unofficial repository and is not trusted.")
}
"community" => {
Some("This package is from a community repository. Use with caution.")
}
"test" => Some(
"This package is from a testing repository and may not function correctly.",
),
"archive" => {
Some("This package is from an archive repository and is no longer maintained.")
}
_ => None,
};
if let Some(message) = warning_message {
println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
}
}
}
}
pub fn confirm_untrusted_source(source_type: &SourceType, yes: bool) -> anyhow::Result<()> {
if is_mini_mode() {
return Ok(());
}
if source_type == &SourceType::OfficialRepo {
return Ok(());
}
let warning_message = match source_type {
SourceType::UntrustedRepo(repo) => {
format!(
"The package from repository '@{}' is not an official Zoi repository.",
repo
)
}
SourceType::LocalFile => "You are installing from a local file.".to_string(),
SourceType::Url => "You are installing from a remote URL. This script will be executed with your user's permissions, which could lead to remote code execution if the source is malicious.".to_string(),
SourceType::GitRepo(repo) => format!("You are installing from an external git repository '{}'. This script will be executed with your user's permissions.", repo),
_ => return Ok(()),
};
println!(
"\n{}: {}",
"SECURITY WARNING".yellow().bold(),
warning_message
);
if ask_for_confirmation(
"This source is not trusted. Are you sure you want to continue?",
yes,
) {
Ok(())
} else {
Err(anyhow!("Operation aborted by user."))
}
}
pub fn is_platform_compatible(current_platform: &str, allowed_platforms: &[String]) -> bool {
let os_part = current_platform
.split('-')
.next()
.unwrap_or(current_platform);
let os = match os_part {
"darwin" => "macos",
other => other,
};
allowed_platforms.iter().any(|p| {
let p_norm = if p == "darwin" { "macos" } else { p };
p_norm == "all" || p_norm == os || p_norm == current_platform
})
}
pub fn setup_path(scope: Scope) -> anyhow::Result<()> {
if scope == Scope::Project {
return Ok(());
}
let zoi_bin_dir = match scope {
Scope::User => {
let home = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
crate::pkg::sysroot::apply_sysroot(home.join(".zoi").join("pkgs").join("bin"))
}
Scope::System => {
if cfg!(target_os = "windows") {
crate::pkg::sysroot::apply_sysroot(PathBuf::from("C:\\ProgramData\\zoi\\pkgs\\bin"))
} else {
crate::pkg::sysroot::apply_sysroot(PathBuf::from("/usr/local/bin"))
}
}
Scope::Project => return Ok(()),
};
if !zoi_bin_dir.exists() {
fs::create_dir_all(&zoi_bin_dir)?;
}
if scope == Scope::System && cfg!(unix) {
println!(
"{}",
"System-wide installation complete. Binaries are in the system PATH.".green()
);
return Ok(());
}
#[cfg(unix)]
{
use std::fs::{File, OpenOptions};
let home = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
let zoi_bin_str = "$HOME/.zoi/pkgs/bin";
let shell_name = std::env::var("SHELL").unwrap_or_default();
let (profile_file_path, cmd_to_write) = if shell_name.contains("bash") {
let path = if cfg!(target_os = "macos") {
home.join(".bash_profile")
} else {
home.join(".bashrc")
};
let cmd = format!(
"\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
} else if shell_name.contains("zsh") {
let path = home.join(".zshrc");
let cmd = format!(
"\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
} else if shell_name.contains("fish") {
let path = home.join(".config/fish/config.fish");
let cmd = format!("\n# Added by Zoi\nset -gx PATH \"{}\" $PATH\n", zoi_bin_str);
(path, cmd)
} else if shell_name.contains("elvish") {
let path = home.join(".config/elvish/rc.elv");
let cmd = "
# Added by Zoi
set paths = [ ~/.zoi/pkgs/bin $paths... ]
"
.to_string();
(path, cmd)
} else if shell_name.contains("csh") || shell_name.contains("tcsh") {
let path = home.join(".cshrc");
let cmd = format!(
"\n# Added by Zoi\nsetenv PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
} else {
let path = home.join(".profile");
let cmd = format!(
"\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
zoi_bin_str, "$PATH"
);
(path, cmd)
};
if !profile_file_path.exists() {
if let Some(parent) = profile_file_path.parent() {
fs::create_dir_all(parent)?;
}
File::create(&profile_file_path)?;
}
let content = fs::read_to_string(&profile_file_path)?;
if content.contains(zoi_bin_str) {
println!("Zoi bin directory is already in your shell's config.");
return Ok(());
}
let mut file = OpenOptions::new().append(true).open(&profile_file_path)?;
file.write_all(cmd_to_write.as_bytes())?;
println!(
"{} Zoi bin directory has been added to your PATH in '{}'.",
"Success:".green(),
profile_file_path.display()
);
println!(
"Please restart your shell or run `source {}` for the changes to take effect.",
profile_file_path.display()
);
}
#[cfg(windows)]
{
use winreg::RegKey;
use winreg::enums::*;
let zoi_bin_path_str = zoi_bin_dir
.to_str()
.ok_or_else(|| anyhow!("Invalid path string"))?;
let (root, subkey, scope_name) = if scope == Scope::System {
if !is_admin() {
return Err(anyhow!(
"Administrator privileges required to modify system PATH."
));
}
(
HKEY_LOCAL_MACHINE,
"System\\CurrentControlSet\\Control\\Session Manager\\Environment",
"system",
)
} else {
(HKEY_CURRENT_USER, "Environment", "user")
};
let key = RegKey::predef(root);
let env = key.open_subkey_with_flags(subkey, KEY_READ | KEY_WRITE)?;
let current_path: String = env.get_value("Path")?;
if current_path
.split(';')
.any(|p| p.eq_ignore_ascii_case(zoi_bin_path_str))
{
println!("Zoi bin directory is already in your PATH.");
return Ok(());
}
let new_path = if current_path.is_empty() {
zoi_bin_path_str.to_string()
} else {
format!("{};{}", current_path, zoi_bin_path_str)
};
env.set_value("Path", &new_path)?;
println!(
"{} Zoi bin directory has been added to your {} PATH environment variable.",
"Success:".green(),
scope_name
);
println!(
"Please restart your shell or log out and log back in for the changes to take effect."
);
}
Ok(())
}
pub fn check_path() {
if let Some(home) = home::home_dir() {
let zoi_bin_dir = crate::pkg::sysroot::apply_sysroot(home.join(".zoi/pkgs/bin"));
if !zoi_bin_dir.exists() {
return;
}
} else {
return;
}
let command_output = if cfg!(target_os = "windows") {
Command::new("pwsh")
.arg("-Command")
.arg("echo $env:Path")
.output()
} else {
Command::new("bash").arg("-c").arg("echo $PATH").output()
};
let is_in_path = match command_output {
Ok(output) => {
if output.status.success() {
let path_var = String::from_utf8_lossy(&output.stdout);
path_var.contains(".zoi/pkgs/bin")
} else {
false
}
}
Err(_) => false,
};
if !is_in_path {
eprintln!(
"Please run 'zoi shell <shell>' or add it to your PATH manually for commands to be available."
);
}
}
pub fn get_platform() -> anyhow::Result<String> {
let os = match std::env::consts::OS {
"linux" => "linux",
"macos" | "darwin" => "macos",
"windows" => "windows",
unsupported_os => return Err(anyhow!("Unsupported operating system: {}", unsupported_os)),
};
let arch = match std::env::consts::ARCH {
"x86_64" => "amd64",
"aarch64" => "arm64",
unsupported_arch => return Err(anyhow!("Unsupported architecture: {}", unsupported_arch)),
};
Ok(format!("{}-{}", os, arch))
}
pub fn get_all_available_package_managers() -> Vec<String> {
let mut managers = Vec::new();
let all_possible_managers = [
"apt",
"pacman",
"yay",
"paru",
"pikaur",
"trizen",
"dnf",
"yum",
"zypper",
"portage",
"apk",
"snap",
"flatpak",
"nix",
"brew",
"port",
"scoop",
"choco",
"winget",
"pkg",
"pkg_add",
"xbps-install",
"eopkg",
"guix",
"mas",
];
for manager in &all_possible_managers {
if command_exists(manager) {
managers.push(manager.to_string());
}
}
managers.sort();
managers.dedup();
managers
}
use std::sync::OnceLock;
static HTTP_CLIENT: OnceLock<reqwest::blocking::Client> = OnceLock::new();
pub fn get_http_client() -> anyhow::Result<&'static reqwest::blocking::Client> {
if crate::pkg::offline::is_offline() {
return Err(anyhow!(
"Cannot create HTTP client: Zoi is in offline mode."
));
}
if let Some(client) = HTTP_CLIENT.get() {
return Ok(client);
}
let client = reqwest::blocking::Client::builder()
.user_agent("zoi")
.timeout(Duration::from_secs(60))
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?;
let _ = HTTP_CLIENT.set(client);
HTTP_CLIENT
.get()
.ok_or_else(|| anyhow!("HTTP_CLIENT should be set but was missing"))
}
pub fn build_blocking_http_client(timeout_secs: u64) -> anyhow::Result<reqwest::blocking::Client> {
if crate::pkg::offline::is_offline() {
return Err(anyhow!(
"Cannot create HTTP client: Zoi is in offline mode."
));
}
let client = reqwest::blocking::Client::builder()
.user_agent("zoi")
.timeout(Duration::from_secs(timeout_secs))
.build()?;
Ok(client)
}
pub fn retry_backoff_sleep(attempt: u32) {
let base_ms = 500u64.saturating_mul(1u64 << (attempt.saturating_sub(1)));
let jitter = (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.subsec_millis()
% 200) as u64;
let sleep_ms = (base_ms + jitter).min(8000);
std::thread::sleep(Duration::from_millis(sleep_ms));
}
pub fn check_license(license: &str) {
if license.is_empty() {
println!(
"{}",
"Warning: Package does not have a license specified.".yellow()
);
return;
}
if license.eq_ignore_ascii_case("Proprietary") {
println!(
"{}",
"Warning: Package is using a proprietary license.".red()
);
return;
}
if license.eq_ignore_ascii_case("Unkown") {
println!("{}", "Warning: Package license is unkown.".red());
return;
}
match spdx::Expression::parse(license) {
Ok(expr) => {
if !expr.evaluate(|req| match req.license {
spdx::LicenseItem::Spdx { id, .. } => id.is_osi_approved(),
spdx::LicenseItem::Other { .. } => false,
}) {
println!(
"{}{}{}",
"Warning: License '".yellow(),
license.yellow().bold(),
"' is not an OSI approved license.".yellow()
);
}
}
Err(_) => {
println!(
"{}{}{}",
"Warning: Could not parse license expression '".yellow(),
license.yellow().bold(),
"' It may not be a valid SPDX identifier.".yellow()
);
}
}
}
pub struct PackageCompletion {
pub display: String,
pub repo: String,
pub description: String,
}
pub fn get_all_packages_for_completion() -> Vec<PackageCompletion> {
let mut completions = Vec::new();
let config = if let Ok(cfg) = crate::pkg::config::read_config() {
cfg
} else {
return completions;
};
let mut registries = Vec::new();
if let Some(default) = &config.default_registry {
registries.push(default.handle.clone());
}
for reg in &config.added_registries {
registries.push(reg.handle.clone());
}
let default_handle = config.default_registry.as_ref().map(|r| &r.handle);
for handle in registries {
if handle.is_empty() {
continue;
}
if let Ok(entries) = crate::pkg::db::get_packages_for_completion(&handle) {
let is_default = default_handle == Some(&handle);
for entry in entries {
let base_name = if is_default {
format!("@{}/{}", entry.repo, entry.name)
} else {
format!("#{}@{}/{}", handle, entry.repo, entry.name)
};
let display = if let Some(sub) = entry.sub_package {
format!("{}:{}", base_name, sub)
} else {
base_name
};
completions.push(PackageCompletion {
display,
repo: entry.repo,
description: entry.description,
});
}
}
}
completions.sort_by(|a, b| a.display.cmp(&b.display));
completions
}
pub fn get_current_shell() -> Option<Shell> {
if cfg!(windows) {
return Some(Shell::PowerShell);
}
if let Ok(shell_path) = std::env::var("SHELL") {
let shell_name = Path::new(&shell_path).file_name()?.to_str()?;
match shell_name {
"bash" => Some(Shell::Bash),
"zsh" => Some(Shell::Zsh),
"fish" => Some(Shell::Fish),
"elvish" => Some(Shell::Elvish),
"pwsh" => Some(Shell::PowerShell),
_ => None,
}
} else {
None
}
}