zoi-project 1.24.2

Advanced Package Manager & Environment Orchestrator
Documentation
use crate::config::{
    CommandSpec, EnvironmentSpec, PackageSpec, ProjectConfig, ProjectLocalConfig, RegistrySpec,
};
use anyhow::{Result, anyhow};
use mlua::{Lua, LuaSerdeExt, Table, Value};
use std::collections::HashMap;
use std::fs;
use std::path::Path;

/// Parses and executes a `zoi.lua` file to load project-specific configuration.
///
/// This is the core of Zoi Specification v2. It:
/// - Exposes a declarative API (`project`, `packages`, `tasks`, etc.) to Lua.
/// - Executes the script, capturing metadata into temporary thread-safe maps.
/// - Resolves script-level choices into a static `ProjectConfig` struct.
///
/// This allows project environments to be programmable, enabling logic like
/// conditionally selecting registries based on environment variables.
pub fn load_zoi_lua(path: &Path, env: HashMap<String, String>) -> Result<ProjectConfig> {
    let lua = Lua::new();
    let content = fs::read_to_string(path)?;

    let project_data = std::sync::Arc::new(std::sync::Mutex::new(HashMap::new()));
    let packages_data = std::sync::Arc::new(std::sync::Mutex::new(HashMap::new()));
    let registries_data = std::sync::Arc::new(std::sync::Mutex::new(HashMap::new()));
    let tasks_data = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
    let environments_data = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));

    let env_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
    for (k, v) in &env {
        env_table
            .set(k.as_str(), v.as_str())
            .map_err(|e| anyhow!(e.to_string()))?;
    }
    lua.globals()
        .set("ENV", env_table)
        .map_err(|e| anyhow!(e.to_string()))?;

    let p_clone = project_data.clone();
    let project_fn = lua
        .create_function(move |lua, table: Table| {
            let mut data = p_clone.lock().unwrap();
            for pair in table.pairs::<String, Value>() {
                let (k, v) = pair?;
                data.insert(k, lua.from_value::<serde_json::Value>(v)?);
            }
            Ok(())
        })
        .map_err(|e| anyhow!(e.to_string()))?;
    lua.globals()
        .set("project", project_fn)
        .map_err(|e| anyhow!(e.to_string()))?;

    let pkgs_clone = packages_data.clone();
    let packages_fn = lua
        .create_function(move |lua, table: Table| {
            let mut data = pkgs_clone.lock().unwrap();
            for pair in table.pairs::<Value, Value>() {
                let (k, v) = pair?;
                match k {
                    Value::String(s) => {
                        let key = s.to_str()?.trim().to_string();
                        let spec = lua.from_value::<PackageSpec>(v)?;
                        data.insert(key, spec);
                    }
                    Value::Integer(_) => {
                        if let Value::String(s) = v {
                            data.insert(
                                s.to_str()?.trim().to_string(),
                                PackageSpec {
                                    package_type: None,
                                    install_method: None,
                                    sub_packages: None,
                                    version: None,
                                    dependencies: None,
                                    options: None,
                                    optionals: None,
                                },
                            );
                        }
                    }
                    _ => {}
                }
            }
            Ok(())
        })
        .map_err(|e| anyhow!(e.to_string()))?;
    lua.globals()
        .set("packages", packages_fn)
        .map_err(|e| anyhow!(e.to_string()))?;

    let regs_clone = registries_data.clone();
    let registries_fn = lua
        .create_function(move |lua, table: Table| {
            let mut data = regs_clone.lock().unwrap();
            for pair in table.pairs::<String, Value>() {
                let (k, v) = pair?;
                let spec = lua.from_value::<RegistrySpec>(v)?;
                data.insert(k, spec);
            }
            Ok(())
        })
        .map_err(|e| anyhow!(e.to_string()))?;
    lua.globals()
        .set("registries", registries_fn)
        .map_err(|e| anyhow!(e.to_string()))?;

    let tasks_clone = tasks_data.clone();
    let tasks_fn = lua
        .create_function(move |lua, table: Table| {
            let mut data = tasks_clone.lock().unwrap();
            for val in table.sequence_values::<Value>() {
                let spec = lua.from_value::<CommandSpec>(val?)?;
                data.push(spec);
            }
            Ok(())
        })
        .map_err(|e| anyhow!(e.to_string()))?;
    lua.globals()
        .set("tasks", tasks_fn)
        .map_err(|e| anyhow!(e.to_string()))?;

    let envs_clone = environments_data.clone();
    let environments_fn = lua
        .create_function(move |lua, table: Table| {
            let mut data = envs_clone.lock().unwrap();
            for val in table.sequence_values::<Value>() {
                let spec = lua.from_value::<EnvironmentSpec>(val?)?;
                data.push(spec);
            }
            Ok(())
        })
        .map_err(|e| anyhow!(e.to_string()))?;
    lua.globals()
        .set("environments", environments_fn)
        .map_err(|e| anyhow!(e.to_string()))?;

    lua.load(&content)
        .exec()
        .map_err(|e| anyhow!("Failed to execute zoi.lua: {}", e))?;

    let project_map = project_data.lock().unwrap();
    let name = project_map
        .get("name")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| anyhow!("zoi.lua must define project name"))?;

    let local = project_map
        .get("config")
        .and_then(|v| v.as_object())
        .and_then(|obj| obj.get("local"))
        .and_then(|v| v.as_bool())
        .or_else(|| project_map.get("local").and_then(|v| v.as_bool()))
        .unwrap_or(false);

    let mut pkgs = Vec::new();
    let pkgs_v2 = packages_data.lock().unwrap().clone();
    for (k, spec) in &pkgs_v2 {
        let mut s = k.clone();
        if let Some(v) = &spec.version {
            s = format!("{}@{}", s, v);
        }
        pkgs.push(s);
    }

    Ok(ProjectConfig {
        name,
        registries: registries_data.lock().unwrap().clone(),
        packages: Vec::new(),
        pkgs,
        pkgs_v2,
        config: ProjectLocalConfig { local },
        commands: tasks_data.lock().unwrap().clone(),
        environments: environments_data.lock().unwrap().clone(),
        shell: None,
    })
}