Skip to main content

zoi_project/
lua_config.rs

1use crate::config::{
2    CommandSpec, EnvironmentSpec, PackageSpec, ProjectConfig, ProjectLocalConfig, RegistrySpec,
3};
4use anyhow::{Result, anyhow};
5use mlua::{Lua, LuaSerdeExt, Table, Value};
6use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10/// Parses and executes a `zoi.lua` file to load project-specific configuration.
11///
12/// This is the core of Zoi Specification v2. It:
13/// - Exposes a declarative API (`project`, `packages`, `tasks`, etc.) to Lua.
14/// - Executes the script, capturing metadata into temporary thread-safe maps.
15/// - Resolves script-level choices into a static `ProjectConfig` struct.
16///
17/// This allows project environments to be programmable, enabling logic like
18/// conditionally selecting registries based on environment variables.
19pub fn load_zoi_lua(path: &Path, env: HashMap<String, String>) -> Result<ProjectConfig> {
20    let lua = Lua::new();
21    let content = fs::read_to_string(path)?;
22
23    let project_data = std::sync::Arc::new(std::sync::Mutex::new(HashMap::new()));
24    let packages_data = std::sync::Arc::new(std::sync::Mutex::new(HashMap::new()));
25    let registries_data = std::sync::Arc::new(std::sync::Mutex::new(HashMap::new()));
26    let tasks_data = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
27    let environments_data = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
28
29    let env_table = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
30    for (k, v) in &env {
31        env_table
32            .set(k.as_str(), v.as_str())
33            .map_err(|e| anyhow!(e.to_string()))?;
34    }
35    lua.globals()
36        .set("ENV", env_table)
37        .map_err(|e| anyhow!(e.to_string()))?;
38
39    let p_clone = project_data.clone();
40    let project_fn = lua
41        .create_function(move |lua, table: Table| {
42            let mut data = p_clone.lock().unwrap();
43            for pair in table.pairs::<String, Value>() {
44                let (k, v) = pair?;
45                data.insert(k, lua.from_value::<serde_json::Value>(v)?);
46            }
47            Ok(())
48        })
49        .map_err(|e| anyhow!(e.to_string()))?;
50    lua.globals()
51        .set("project", project_fn)
52        .map_err(|e| anyhow!(e.to_string()))?;
53
54    let pkgs_clone = packages_data.clone();
55    let packages_fn = lua
56        .create_function(move |lua, table: Table| {
57            let mut data = pkgs_clone.lock().unwrap();
58            for pair in table.pairs::<Value, Value>() {
59                let (k, v) = pair?;
60                match k {
61                    Value::String(s) => {
62                        let key = s.to_str()?.trim().to_string();
63                        let spec = lua.from_value::<PackageSpec>(v)?;
64                        data.insert(key, spec);
65                    }
66                    Value::Integer(_) => {
67                        if let Value::String(s) = v {
68                            data.insert(
69                                s.to_str()?.trim().to_string(),
70                                PackageSpec {
71                                    package_type: None,
72                                    install_method: None,
73                                    sub_packages: None,
74                                    version: None,
75                                    dependencies: None,
76                                    options: None,
77                                    optionals: None,
78                                },
79                            );
80                        }
81                    }
82                    _ => {}
83                }
84            }
85            Ok(())
86        })
87        .map_err(|e| anyhow!(e.to_string()))?;
88    lua.globals()
89        .set("packages", packages_fn)
90        .map_err(|e| anyhow!(e.to_string()))?;
91
92    let regs_clone = registries_data.clone();
93    let registries_fn = lua
94        .create_function(move |lua, table: Table| {
95            let mut data = regs_clone.lock().unwrap();
96            for pair in table.pairs::<String, Value>() {
97                let (k, v) = pair?;
98                let spec = lua.from_value::<RegistrySpec>(v)?;
99                data.insert(k, spec);
100            }
101            Ok(())
102        })
103        .map_err(|e| anyhow!(e.to_string()))?;
104    lua.globals()
105        .set("registries", registries_fn)
106        .map_err(|e| anyhow!(e.to_string()))?;
107
108    let tasks_clone = tasks_data.clone();
109    let tasks_fn = lua
110        .create_function(move |lua, table: Table| {
111            let mut data = tasks_clone.lock().unwrap();
112            for val in table.sequence_values::<Value>() {
113                let spec = lua.from_value::<CommandSpec>(val?)?;
114                data.push(spec);
115            }
116            Ok(())
117        })
118        .map_err(|e| anyhow!(e.to_string()))?;
119    lua.globals()
120        .set("tasks", tasks_fn)
121        .map_err(|e| anyhow!(e.to_string()))?;
122
123    let envs_clone = environments_data.clone();
124    let environments_fn = lua
125        .create_function(move |lua, table: Table| {
126            let mut data = envs_clone.lock().unwrap();
127            for val in table.sequence_values::<Value>() {
128                let spec = lua.from_value::<EnvironmentSpec>(val?)?;
129                data.push(spec);
130            }
131            Ok(())
132        })
133        .map_err(|e| anyhow!(e.to_string()))?;
134    lua.globals()
135        .set("environments", environments_fn)
136        .map_err(|e| anyhow!(e.to_string()))?;
137
138    lua.load(&content)
139        .exec()
140        .map_err(|e| anyhow!("Failed to execute zoi.lua: {}", e))?;
141
142    let project_map = project_data.lock().unwrap();
143    let name = project_map
144        .get("name")
145        .and_then(|v| v.as_str())
146        .map(|s| s.to_string())
147        .ok_or_else(|| anyhow!("zoi.lua must define project name"))?;
148
149    let local = project_map
150        .get("config")
151        .and_then(|v| v.as_object())
152        .and_then(|obj| obj.get("local"))
153        .and_then(|v| v.as_bool())
154        .or_else(|| project_map.get("local").and_then(|v| v.as_bool()))
155        .unwrap_or(false);
156
157    let mut pkgs = Vec::new();
158    let pkgs_v2 = packages_data.lock().unwrap().clone();
159    for (k, spec) in &pkgs_v2 {
160        let mut s = k.clone();
161        if let Some(v) = &spec.version {
162            s = format!("{}@{}", s, v);
163        }
164        pkgs.push(s);
165    }
166
167    Ok(ProjectConfig {
168        name,
169        registries: registries_data.lock().unwrap().clone(),
170        packages: Vec::new(),
171        pkgs,
172        pkgs_v2,
173        config: ProjectLocalConfig { local },
174        commands: tasks_data.lock().unwrap().clone(),
175        environments: environments_data.lock().unwrap().clone(),
176        shell: None,
177    })
178}