Skip to main content

zoi_project/
config.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use anyhow::{Result, anyhow};
6use serde::{Deserialize, Serialize};
7
8/// Project-local configuration overrides.
9#[derive(Debug, Deserialize, Default, Clone)]
10pub struct ProjectLocalConfig {
11    /// Whether the project is isolated from the system registry.
12    #[serde(default)]
13    pub local: bool
14}
15
16/// Shell configuration for the project.
17#[derive(Debug, Deserialize, Clone, Default)]
18pub struct ShellSpec {
19    /// Environment variables for the shell, potentially platform-specific.
20    #[serde(default)]
21    pub env: PlatformOrEnvMap
22}
23
24/// Specification for a project-scoped registry.
25#[derive(Debug, Deserialize, Clone)]
26pub struct RegistrySpec {
27    /// The URL of the registry.
28    pub url: String,
29    /// The git revision of the registry.
30    pub revision: Option<String>,
31    /// The type of registry (e.g. "git").
32    #[serde(rename = "type")]
33    pub registry_type: Option<String>
34}
35
36/// Specification for a package dependency.
37#[derive(Debug, Serialize, Deserialize, Clone)]
38pub struct PackageSpec {
39    /// The type of package.
40    #[serde(rename = "type")]
41    pub package_type: Option<String>,
42    /// The method used to install the package.
43    pub install_method: Option<String>,
44    /// List of sub-packages to install.
45    pub sub_packages: Option<Vec<String>>,
46    /// The version requirement for the package.
47    pub version: Option<String>,
48    /// Dependencies specific to this package.
49    pub dependencies: Option<zoi_core::types::Dependencies>,
50    /// List of build/install options.
51    pub options: Option<Vec<String>>,
52    /// List of optional features to enable.
53    pub optionals: Option<Vec<String>>
54}
55
56/// Represents the combined evaluation of a project's `zoi.lua` and `zoi.yaml`
57/// configuration.
58///
59/// This struct acts as the central definition for a project environment. It
60/// unifies:
61/// - The scriptable package and registry requirements defined in `zoi.lua`.
62/// - The declarative task (`commands`) and `environments` defined in
63///   `zoi.yaml`.
64#[derive(Debug, Deserialize, Clone)]
65#[allow(dead_code)]
66pub struct ProjectConfig {
67    /// The name of the project.
68    pub name: String,
69    /// Registries scoped specifically to this project.
70    #[serde(default)]
71    pub registries: HashMap<String, RegistrySpec>,
72    /// Declarative package checks (legacy v1).
73    #[serde(default)]
74    pub packages: Vec<PackageCheck>,
75    /// A flat list of simple package dependencies.
76    #[serde(default)]
77    pub pkgs: Vec<String>,
78    /// A map of packages defining explicit version requirements and options.
79    #[serde(default)]
80    pub pkgs_v2: HashMap<String, PackageSpec>,
81    /// Project-local configuration overrides (e.g. `--local` isolation).
82    #[serde(default)]
83    pub config: ProjectLocalConfig,
84    /// Declared task aliases and their underlying scripts.
85    #[serde(default)]
86    pub commands: Vec<CommandSpec>,
87    /// Full environment setup groups.
88    #[serde(default)]
89    pub environments: Vec<EnvironmentSpec>,
90    /// Ephemeral shell configurations.
91    #[serde(default)]
92    pub shell: Option<ShellSpec>
93}
94
95/// A declarative package check.
96#[derive(Debug, Deserialize, Clone)]
97pub struct PackageCheck {
98    /// The name of the package.
99    pub name: String,
100    /// The check command or requirement.
101    pub check: String
102}
103
104/// A value that can be a single string or a map of platform-specific strings.
105#[derive(Debug, Deserialize, Clone)]
106#[serde(untagged)]
107pub enum PlatformOrString {
108    /// A simple string value.
109    String(String),
110    /// A map of platform names to string values.
111    Platform(HashMap<String, String>)
112}
113
114/// A value that can be a list of strings or a map of platform-specific string
115/// lists.
116#[derive(Debug, Deserialize, Clone)]
117#[serde(untagged)]
118pub enum PlatformOrStringVec {
119    /// A simple list of strings.
120    StringVec(Vec<String>),
121    /// A map of platform names to lists of strings.
122    Platform(HashMap<String, Vec<String>>)
123}
124
125/// A value that can be an environment map or a map of platform-specific
126/// environment maps.
127#[derive(Debug, Deserialize, Clone)]
128#[serde(untagged)]
129pub enum PlatformOrEnvMap {
130    /// A simple environment map.
131    EnvMap(HashMap<String, String>),
132    /// A map of platform names to environment maps.
133    Platform(HashMap<String, HashMap<String, String>>)
134}
135
136impl Default for PlatformOrEnvMap {
137    fn default() -> Self {
138        PlatformOrEnvMap::EnvMap(HashMap::new())
139    }
140}
141
142/// Specification for a declarative task/command.
143#[derive(Debug, Deserialize, Clone)]
144pub struct CommandSpec {
145    /// The name of the task.
146    pub cmd: String,
147    /// The command to run, potentially platform-specific.
148    pub run: PlatformOrString,
149    /// Environment variables for the task.
150    #[serde(default)]
151    pub env: PlatformOrEnvMap,
152    /// List of task names this task depends on.
153    #[serde(default)]
154    pub depends_on: Option<Vec<String>>,
155    /// List of files that contribute to the task's cache hash.
156    #[serde(default)]
157    pub cache_files: Option<Vec<String>>
158}
159
160/// Specification for a project environment setup.
161#[derive(Debug, Deserialize, Clone)]
162pub struct EnvironmentSpec {
163    /// The name of the environment.
164    pub name: String,
165    /// The command associated with this environment.
166    pub cmd: String,
167    /// The commands to run to setup the environment.
168    pub run: PlatformOrStringVec,
169    /// Environment variables for this setup.
170    #[serde(default)]
171    pub env: PlatformOrEnvMap
172}
173
174/// Loads the project configuration from the current directory.
175///
176/// # Errors
177///
178/// Returns an error if no configuration file is found or if the configuration
179/// is invalid.
180pub fn load() -> Result<ProjectConfig> {
181    let env: HashMap<String, String> = std::env::vars().collect();
182    load_with_env(&env)
183}
184
185/// Loads the project configuration with a custom set of environment variables.
186///
187/// # Errors
188///
189/// Returns an error if no configuration file is found or if the configuration
190/// is invalid.
191pub fn load_with_env<S: ::std::hash::BuildHasher>(
192    env: &HashMap<String, String, S>
193) -> Result<ProjectConfig> {
194    let lua_path = Path::new("zoi.lua");
195    if lua_path.exists() {
196        return crate::lua_config::load_zoi_lua(lua_path, env);
197    }
198
199    let config_path = Path::new("zoi.yaml");
200    if !config_path.exists() {
201        return Err(anyhow!(
202            "No 'zoi.lua' or 'zoi.yaml' file found in the current directory."
203        ));
204    }
205
206    let content = fs::read_to_string(config_path)?;
207    let config: ProjectConfig = serde_yaml::from_str(&content)?;
208    Ok(config)
209}
210
211/// Adds packages to the `zoi.yaml` configuration file.
212///
213/// # Errors
214///
215/// Returns an error if the project uses `zoi.lua`, the `zoi.yaml` file is
216/// missing, or if there is an error reading or writing the file.
217pub fn add_packages_to_config(packages: &[String]) -> Result<()> {
218    if Path::new("zoi.lua").exists() {
219        return Err(anyhow!(
220            "Project uses zoi.lua. Automatic saving is not supported for Lua \
221             configurations."
222        ));
223    }
224    let config_path = Path::new("zoi.yaml");
225    if !config_path.exists() {
226        return Err(anyhow!(
227            "No 'zoi.yaml' file found in the current directory."
228        ));
229    }
230
231    let content = fs::read_to_string(config_path)?;
232    let mut yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)?;
233
234    if let Some(mapping) = yaml_value.as_mapping_mut() {
235        let pkgs_key = serde_yaml::Value::String("pkgs".to_string());
236        let pkgs_list = mapping
237            .entry(pkgs_key)
238            .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
239
240        if let Some(sequence) = pkgs_list.as_sequence_mut() {
241            for package in packages {
242                let new_pkg_value = serde_yaml::Value::String(package.clone());
243                if !sequence.contains(&new_pkg_value) {
244                    sequence.push(new_pkg_value);
245                }
246            }
247        }
248    }
249
250    let new_content = serde_yaml::to_string(&yaml_value)?;
251    fs::write(config_path, new_content)?;
252
253    Ok(())
254}
255
256/// Removes packages from the `zoi.yaml` configuration file.
257///
258/// # Errors
259///
260/// Returns an error if there is an issue reading or writing the `zoi.yaml`
261/// file.
262pub fn remove_packages_from_config(
263    packages_to_remove: &[String]
264) -> Result<()> {
265    if Path::new("zoi.lua").exists() {
266        return Ok(());
267    }
268    let config_path = Path::new("zoi.yaml");
269    if !config_path.exists() {
270        return Ok(());
271    }
272
273    let content = fs::read_to_string(config_path)?;
274    let mut yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)?;
275
276    if let Some(mapping) = yaml_value.as_mapping_mut()
277        && let Some(pkgs_list) = mapping.get_mut("pkgs")
278        && let Some(sequence) = pkgs_list.as_sequence_mut()
279    {
280        let packages_to_remove_names: Vec<_> = packages_to_remove
281            .iter()
282            .map(|p| {
283                zoi_resolver::resolve::parse_source_string(p)
284                    .map_or_else(|_| p.clone(), |req| req.name)
285            })
286            .collect();
287
288        sequence.retain(|v| {
289            if let Some(s) = v.as_str() {
290                if let Ok(req) = zoi_resolver::resolve::parse_source_string(s) {
291                    !packages_to_remove_names.contains(&req.name)
292                } else {
293                    true
294                }
295            } else {
296                true
297            }
298        });
299    }
300
301    let new_content = serde_yaml::to_string(&yaml_value)?;
302    fs::write(config_path, new_content)?;
303
304    Ok(())
305}