Skip to main content

zoi_project/
config.rs

1use anyhow::{Result, anyhow};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Deserialize, Default, Clone)]
8pub struct ProjectLocalConfig {
9    #[serde(default)]
10    pub local: bool,
11}
12
13#[derive(Debug, Deserialize, Clone, Default)]
14pub struct ShellSpec {
15    #[serde(default)]
16    pub env: PlatformOrEnvMap,
17}
18
19#[derive(Debug, Deserialize, Clone)]
20pub struct RegistrySpec {
21    pub url: String,
22    pub revision: Option<String>,
23    #[serde(rename = "type")]
24    pub registry_type: Option<String>,
25}
26
27#[derive(Debug, Serialize, Deserialize, Clone)]
28pub struct PackageSpec {
29    #[serde(rename = "type")]
30    pub package_type: Option<String>,
31    pub install_method: Option<String>,
32    pub sub_packages: Option<Vec<String>>,
33    pub version: Option<String>,
34    pub dependencies: Option<zoi_core::types::Dependencies>,
35    pub options: Option<Vec<String>>,
36    pub optionals: Option<Vec<String>>,
37}
38
39/// Represents the combined evaluation of a project's `zoi.lua` and `zoi.yaml` configuration.
40///
41/// This struct acts as the central definition for a project environment. It unifies:
42/// - The scriptable package and registry requirements defined in `zoi.lua`.
43/// - The declarative task (`commands`) and `environments` defined in `zoi.yaml`.
44#[derive(Debug, Deserialize, Clone)]
45#[allow(dead_code)]
46pub struct ProjectConfig {
47    /// The name of the project.
48    pub name: String,
49    /// Registries scoped specifically to this project.
50    #[serde(default)]
51    pub registries: HashMap<String, RegistrySpec>,
52    /// Declarative package checks (legacy v1).
53    #[serde(default)]
54    pub packages: Vec<PackageCheck>,
55    /// A flat list of simple package dependencies.
56    #[serde(default)]
57    pub pkgs: Vec<String>,
58    /// A map of packages defining explicit version requirements and options.
59    #[serde(default)]
60    pub pkgs_v2: HashMap<String, PackageSpec>,
61    /// Project-local configuration overrides (e.g. `--local` isolation).
62    #[serde(default)]
63    pub config: ProjectLocalConfig,
64    /// Declared task aliases and their underlying scripts.
65    #[serde(default)]
66    pub commands: Vec<CommandSpec>,
67    /// Full environment setup groups.
68    #[serde(default)]
69    pub environments: Vec<EnvironmentSpec>,
70    /// Ephemeral shell configurations.
71    #[serde(default)]
72    pub shell: Option<ShellSpec>,
73}
74
75#[derive(Debug, Deserialize, Clone)]
76pub struct PackageCheck {
77    pub name: String,
78    pub check: String,
79}
80
81#[derive(Debug, Deserialize, Clone)]
82#[serde(untagged)]
83pub enum PlatformOrString {
84    String(String),
85    Platform(HashMap<String, String>),
86}
87
88#[derive(Debug, Deserialize, Clone)]
89#[serde(untagged)]
90pub enum PlatformOrStringVec {
91    StringVec(Vec<String>),
92    Platform(HashMap<String, Vec<String>>),
93}
94
95#[derive(Debug, Deserialize, Clone)]
96#[serde(untagged)]
97pub enum PlatformOrEnvMap {
98    EnvMap(HashMap<String, String>),
99    Platform(HashMap<String, HashMap<String, String>>),
100}
101
102impl Default for PlatformOrEnvMap {
103    fn default() -> Self {
104        PlatformOrEnvMap::EnvMap(HashMap::new())
105    }
106}
107
108#[derive(Debug, Deserialize, Clone)]
109pub struct CommandSpec {
110    pub cmd: String,
111    pub run: PlatformOrString,
112    #[serde(default)]
113    pub env: PlatformOrEnvMap,
114    #[serde(default)]
115    pub depends_on: Option<Vec<String>>,
116    #[serde(default)]
117    pub cache_files: Option<Vec<String>>,
118}
119
120#[derive(Debug, Deserialize, Clone)]
121pub struct EnvironmentSpec {
122    pub name: String,
123    pub cmd: String,
124    pub run: PlatformOrStringVec,
125    #[serde(default)]
126    pub env: PlatformOrEnvMap,
127}
128
129pub fn load() -> Result<ProjectConfig> {
130    load_with_env(std::env::vars().collect())
131}
132
133pub fn load_with_env(env: HashMap<String, String>) -> Result<ProjectConfig> {
134    let lua_path = Path::new("zoi.lua");
135    if lua_path.exists() {
136        return crate::lua_config::load_zoi_lua(lua_path, env);
137    }
138
139    let config_path = Path::new("zoi.yaml");
140    if !config_path.exists() {
141        return Err(anyhow!(
142            "No 'zoi.lua' or 'zoi.yaml' file found in the current directory."
143        ));
144    }
145
146    let content = fs::read_to_string(config_path)?;
147    let config: ProjectConfig = serde_yaml::from_str(&content)?;
148    Ok(config)
149}
150
151pub fn add_packages_to_config(packages: &[String]) -> Result<()> {
152    if Path::new("zoi.lua").exists() {
153        return Err(anyhow!(
154            "Project uses zoi.lua. Automatic saving is not supported for Lua configurations."
155        ));
156    }
157    let config_path = Path::new("zoi.yaml");
158    if !config_path.exists() {
159        return Err(anyhow!(
160            "No 'zoi.yaml' file found in the current directory."
161        ));
162    }
163
164    let content = fs::read_to_string(config_path)?;
165    let mut yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)?;
166
167    if let Some(mapping) = yaml_value.as_mapping_mut() {
168        let pkgs_key = serde_yaml::Value::String("pkgs".to_string());
169        let pkgs_list = mapping
170            .entry(pkgs_key)
171            .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
172
173        if let Some(sequence) = pkgs_list.as_sequence_mut() {
174            for package in packages {
175                let new_pkg_value = serde_yaml::Value::String(package.clone());
176                if !sequence.contains(&new_pkg_value) {
177                    sequence.push(new_pkg_value);
178                }
179            }
180        }
181    }
182
183    let new_content = serde_yaml::to_string(&yaml_value)?;
184    fs::write(config_path, new_content)?;
185
186    Ok(())
187}
188
189pub fn remove_packages_from_config(packages_to_remove: &[String]) -> Result<()> {
190    if Path::new("zoi.lua").exists() {
191        return Ok(());
192    }
193    let config_path = Path::new("zoi.yaml");
194    if !config_path.exists() {
195        return Ok(());
196    }
197
198    let content = fs::read_to_string(config_path)?;
199    let mut yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)?;
200
201    if let Some(mapping) = yaml_value.as_mapping_mut()
202        && let Some(pkgs_list) = mapping.get_mut("pkgs")
203        && let Some(sequence) = pkgs_list.as_sequence_mut()
204    {
205        let packages_to_remove_names: Vec<_> = packages_to_remove
206            .iter()
207            .map(|p| {
208                zoi_resolver::resolve::parse_source_string(p)
209                    .map(|req| req.name)
210                    .unwrap_or_else(|_| p.to_string())
211            })
212            .collect();
213
214        sequence.retain(|v| {
215            if let Some(s) = v.as_str() {
216                if let Ok(req) = zoi_resolver::resolve::parse_source_string(s) {
217                    !packages_to_remove_names.contains(&req.name)
218                } else {
219                    true
220                }
221            } else {
222                true
223            }
224        });
225    }
226
227    let new_content = serde_yaml::to_string(&yaml_value)?;
228    fs::write(config_path, new_content)?;
229
230    Ok(())
231}