zoi-rs 1.13.0

Universal Package Manager & Environment Setup Tool
Documentation
use crate::pkg::{config, db, local, resolve, types::Scope};
use crate::project;
use anyhow::{Result, anyhow};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use walkdir::WalkDir;

use crate::pkg::plugin::PluginManager;

pub fn run_shim(bin_name: &str, args: Vec<String>, plugin_manager: &PluginManager) -> Result<()> {
    let bin_path = resolve_to_installed_bin(bin_name, plugin_manager)?;

    let mut cmd = Command::new(bin_path);
    cmd.args(args);

    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        let err = cmd.exec();
        Err(anyhow!("Failed to execute binary '{}': {}", bin_name, err))
    }

    #[cfg(windows)]
    {
        let mut child = cmd.spawn()?;
        let status = child.wait()?;
        std::process::exit(status.code().unwrap_or(0));
    }
}

pub fn resolve_to_installed_bin(bin_name: &str, plugin_manager: &PluginManager) -> Result<PathBuf> {
    let desired_version = get_desired_version(bin_name, plugin_manager)?;

    let providers = db::find_provides("local", bin_name)?;
    if providers.is_empty() {
        return Err(anyhow!(
            "No installed package provides binary '{}'. Run 'zoi provides {}' to find providers.",
            bin_name,
            bin_name
        ));
    }

    if let Some(version) = &desired_version {
        for (pkg, _) in &providers {
            if let Some(path) = search_store_for_version(&pkg.name, version, bin_name)? {
                return Ok(path);
            }
        }
    }

    let (pkg, _) = &providers[0];
    let version = pkg
        .version
        .as_deref()
        .ok_or_else(|| anyhow!("Package '{}' has no version info in DB", pkg.name))?;

    if let Some(path) = search_store_for_version(&pkg.name, version, bin_name)? {
        return Ok(path);
    }

    for scope in [Scope::Project, Scope::User, Scope::System] {
        let store_root = local::get_store_base_dir(scope)?;
        if !store_root.exists() {
            continue;
        }

        for entry in fs::read_dir(store_root)? {
            let entry = entry?;
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }

            if let Some(dir_name) = path.file_name().and_then(|s| s.to_str())
                && dir_name.ends_with(&format!("-{}", pkg.name))
            {
                let latest_dir = path.join("latest");
                if latest_dir.exists()
                    && let Some(p) = find_bin_in_dir(&latest_dir, bin_name)
                {
                    return Ok(p);
                }
            }
        }
    }

    Err(anyhow!(
        "Could not locate binary '{}' in the Zoi store. Try reinstalling the provider package.",
        bin_name
    ))
}

fn get_desired_version(bin_name: &str, plugin_manager: &PluginManager) -> Result<Option<String>> {
    let env_var_name = format!("ZOI_{}_VERSION", bin_name.to_uppercase().replace('-', "_"));
    if let Ok(v) = env::var(&env_var_name) {
        return Ok(Some(v));
    }

    if let Ok(Some(v)) = plugin_manager.trigger_resolve_shim_version(bin_name) {
        return Ok(Some(v));
    }

    if let Ok(project_cfg) = project::config::load() {
        for pkg_spec in project_cfg.pkgs {
            if let Ok(req) = resolve::parse_source_string(&pkg_spec) {
                let is_match = req.name == bin_name || {
                    if let Ok(providers) = db::find_provides("local", bin_name) {
                        providers.iter().any(|(p, _)| p.name == req.name)
                    } else {
                        false
                    }
                };

                if is_match && let Some(v) = req.version_spec {
                    return Ok(Some(v));
                }
            }
        }
    }

    let cfg = config::read_config()?;
    if let Some(v) = cfg.versions.get(bin_name) {
        return Ok(Some(v.clone()));
    }

    Ok(None)
}

fn search_store_for_version(
    pkg_name: &str,
    version: &str,
    bin_name: &str,
) -> Result<Option<PathBuf>> {
    for scope in [Scope::Project, Scope::User, Scope::System] {
        let store_root = local::get_store_base_dir(scope)?;
        if !store_root.exists() {
            continue;
        }

        for entry in fs::read_dir(store_root)? {
            let entry = entry?;
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }

            if let Some(dir_name) = path.file_name().and_then(|s| s.to_str())
                && dir_name.ends_with(&format!("-{}", pkg_name))
            {
                let version_dir = path.join(version);
                if version_dir.exists()
                    && let Some(p) = find_bin_in_dir(&version_dir, bin_name)
                {
                    return Ok(Some(p));
                }

                for v_entry in fs::read_dir(&path)? {
                    let v_entry = v_entry?;
                    let v_name = v_entry.file_name().to_string_lossy().to_string();
                    if v_name.starts_with(version) && v_name != "latest" && v_name != "dependents" {
                        let v_dir = path.join(v_name);
                        if let Some(p) = find_bin_in_dir(&v_dir, bin_name) {
                            return Ok(Some(p));
                        }
                    }
                }
            }
        }
    }
    Ok(None)
}

fn find_bin_in_dir(dir: &std::path::Path, bin_name: &str) -> Option<PathBuf> {
    let bin_path = dir.join("bin").join(bin_name);
    if bin_path.exists() {
        return Some(bin_path);
    }

    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
        if entry.file_type().is_file() && entry.file_name().to_string_lossy() == bin_name {
            return Some(entry.path().to_path_buf());
        }
    }
    None
}

pub fn create_shim(link_path: &std::path::Path) -> Result<()> {
    let zoi_exe = env::current_exe()?;
    crate::utils::symlink_file(&zoi_exe, link_path)
        .map_err(|e| anyhow!("Failed to create shim: {}", e))
}