Skip to main content

cfgd_core/config/
resolve.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use serde::Serialize;
5
6use super::parse::{find_profile_path, load_profile};
7use super::profile_spec::{
8    FilesSpec, PackagesSpec, ProfileSpec, ScriptSpec, SecretSpec, validate_secret_specs,
9};
10use super::source::{EnvVar, ShellAlias};
11use crate::errors::{ConfigError, Result};
12use crate::{deep_merge_yaml, union_extend};
13
14// --- Profile Resolution ---
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17pub enum LayerPolicy {
18    Local,
19    Required,
20    Recommended,
21    Optional,
22}
23
24#[derive(Debug, Clone, Serialize)]
25pub struct ProfileLayer {
26    pub source: String,
27    pub profile_name: String,
28    pub priority: u32,
29    pub policy: LayerPolicy,
30    pub spec: ProfileSpec,
31}
32
33#[derive(Debug, Clone, Serialize)]
34pub struct ResolvedProfile {
35    pub layers: Vec<ProfileLayer>,
36    pub merged: MergedProfile,
37}
38
39#[derive(Debug, Clone, Default, Serialize)]
40pub struct MergedProfile {
41    pub modules: Vec<String>,
42    pub env: Vec<EnvVar>,
43    pub aliases: Vec<ShellAlias>,
44    pub packages: PackagesSpec,
45    pub files: FilesSpec,
46    pub system: HashMap<String, serde_yaml::Value>,
47    pub secrets: Vec<SecretSpec>,
48    pub scripts: ScriptSpec,
49}
50
51/// Resolve a profile by loading it and its full inheritance chain, then merging.
52pub fn resolve_profile(profile_name: &str, profiles_dir: &Path) -> Result<ResolvedProfile> {
53    let resolution_order = resolve_inheritance_order(profile_name, profiles_dir, &mut vec![])?;
54
55    let mut layers = Vec::new();
56    for name in &resolution_order {
57        let path = find_profile_path(profiles_dir, name);
58        let doc = load_profile(&path).map_err(|e| match e {
59            crate::errors::CfgdError::Config(ConfigError::NotFound { .. }) => {
60                crate::errors::CfgdError::Config(ConfigError::ProfileNotFound {
61                    name: name.clone(),
62                })
63            }
64            other => other,
65        })?;
66        layers.push(ProfileLayer {
67            source: "local".to_string(),
68            profile_name: name.clone(),
69            priority: 1000,
70            policy: LayerPolicy::Local,
71            spec: doc.spec,
72        });
73    }
74
75    let merged = merge_layers(&layers);
76
77    validate_secret_specs(&merged.secrets)?;
78
79    Ok(ResolvedProfile { layers, merged })
80}
81
82/// Recursively resolve the inheritance order (depth-first, left-to-right).
83/// Returns profiles in resolution order: earliest ancestor first, active profile last.
84fn resolve_inheritance_order(
85    profile_name: &str,
86    profiles_dir: &Path,
87    visited: &mut Vec<String>,
88) -> Result<Vec<String>> {
89    if visited.contains(&profile_name.to_string()) {
90        let mut chain = visited.clone();
91        chain.push(profile_name.to_string());
92        return Err(ConfigError::CircularInheritance { chain }.into());
93    }
94
95    visited.push(profile_name.to_string());
96
97    let path = find_profile_path(profiles_dir, profile_name);
98    let doc = load_profile(&path).map_err(|e| match e {
99        crate::errors::CfgdError::Config(ConfigError::NotFound { .. }) => {
100            crate::errors::CfgdError::Config(ConfigError::ProfileNotFound {
101                name: profile_name.to_string(),
102            })
103        }
104        other => other,
105    })?;
106
107    let mut order = Vec::new();
108    for parent in &doc.spec.inherits {
109        let parent_order = resolve_inheritance_order(parent, profiles_dir, visited)?;
110        for name in parent_order {
111            if !order.contains(&name) {
112                order.push(name);
113            }
114        }
115    }
116
117    order.push(profile_name.to_string());
118    visited.pop();
119
120    Ok(order)
121}
122
123/// Merge profile layers according to merge rules:
124/// - packages: union
125/// - files: overlay (later overrides earlier for same target)
126/// - env: override (later replaces earlier for same name)
127/// - secrets: append (deduplicated by target)
128/// - scripts: append in order
129/// - system: deep merge (later overrides at leaf level)
130pub(super) fn merge_layers(layers: &[ProfileLayer]) -> MergedProfile {
131    let mut merged = MergedProfile::default();
132
133    for layer in layers {
134        let spec = &layer.spec;
135
136        // Modules: union
137        union_extend(&mut merged.modules, &spec.modules);
138
139        // Env: later layer overrides earlier by name
140        crate::merge_env(&mut merged.env, &spec.env);
141
142        // Aliases: later layer overrides earlier by name
143        crate::merge_aliases(&mut merged.aliases, &spec.aliases);
144
145        // Packages: union (delegated to composition::merge_packages)
146        if let Some(ref pkgs) = spec.packages {
147            crate::composition::merge_packages(&mut merged.packages, pkgs);
148        }
149
150        // Files: overlay (later layer overrides earlier for same target)
151        if let Some(ref files) = spec.files {
152            for managed in &files.managed {
153                if let Some(existing) = merged
154                    .files
155                    .managed
156                    .iter_mut()
157                    .find(|m| m.target == managed.target)
158                {
159                    *existing = managed.clone();
160                } else {
161                    merged.files.managed.push(managed.clone());
162                }
163            }
164            for (path, mode) in &files.permissions {
165                merged.files.permissions.insert(path.clone(), mode.clone());
166            }
167        }
168
169        // System: deep merge at leaf level
170        for (key, value) in &spec.system {
171            deep_merge_yaml(
172                merged
173                    .system
174                    .entry(key.clone())
175                    .or_insert(serde_yaml::Value::Null),
176                value,
177            );
178        }
179
180        // Secrets: append, deduplicate by source (later layer overrides)
181        for secret in &spec.secrets {
182            if let Some(existing) = merged
183                .secrets
184                .iter_mut()
185                .find(|s| s.source == secret.source)
186            {
187                *existing = secret.clone();
188            } else {
189                merged.secrets.push(secret.clone());
190            }
191        }
192
193        // Scripts: append in order
194        if let Some(ref scripts) = spec.scripts {
195            merged.scripts.pre_apply.extend(scripts.pre_apply.clone());
196            merged.scripts.post_apply.extend(scripts.post_apply.clone());
197            merged
198                .scripts
199                .pre_reconcile
200                .extend(scripts.pre_reconcile.clone());
201            merged
202                .scripts
203                .post_reconcile
204                .extend(scripts.post_reconcile.clone());
205            merged.scripts.on_drift.extend(scripts.on_drift.clone());
206            merged.scripts.on_change.extend(scripts.on_change.clone());
207        }
208    }
209
210    merged
211}
212
213/// Get the list of desired packages for a specific package manager from a merged profile.
214pub fn desired_packages_for(manager_name: &str, profile: &MergedProfile) -> Vec<String> {
215    desired_packages_for_spec(manager_name, &profile.packages)
216}
217
218pub fn desired_packages_for_spec(manager_name: &str, packages: &PackagesSpec) -> Vec<String> {
219    match manager_name {
220        "brew" => packages
221            .brew
222            .as_ref()
223            .map(|b| b.formulae.clone())
224            .unwrap_or_default(),
225        "brew-tap" => packages
226            .brew
227            .as_ref()
228            .map(|b| b.taps.clone())
229            .unwrap_or_default(),
230        "brew-cask" => packages
231            .brew
232            .as_ref()
233            .map(|b| b.casks.clone())
234            .unwrap_or_default(),
235        "apt" => packages
236            .apt
237            .as_ref()
238            .map(|a| a.packages.clone())
239            .unwrap_or_default(),
240        "cargo" => packages
241            .cargo
242            .as_ref()
243            .map(|c| c.packages.clone())
244            .unwrap_or_default(),
245        "npm" => packages
246            .npm
247            .as_ref()
248            .map(|n| n.global.clone())
249            .unwrap_or_default(),
250        "pipx" => packages.pipx.clone(),
251        "dnf" => packages.dnf.clone(),
252        "apk" => packages.apk.clone(),
253        "pacman" => packages.pacman.clone(),
254        "zypper" => packages.zypper.clone(),
255        "yum" => packages.yum.clone(),
256        "pkg" => packages.pkg.clone(),
257        "snap" => packages
258            .snap
259            .as_ref()
260            .map(|s| {
261                let mut all = s.packages.clone();
262                for p in &s.classic {
263                    if !all.contains(p) {
264                        all.push(p.clone());
265                    }
266                }
267                all
268            })
269            .unwrap_or_default(),
270        "flatpak" => packages
271            .flatpak
272            .as_ref()
273            .map(|f| f.packages.clone())
274            .unwrap_or_default(),
275        "nix" => packages.nix.clone(),
276        "go" => packages.go.clone(),
277        "winget" => packages.winget.clone(),
278        "chocolatey" => packages.chocolatey.clone(),
279        "scoop" => packages.scoop.clone(),
280        _ => {
281            // Check custom managers
282            for custom in &packages.custom {
283                if custom.name == manager_name {
284                    return custom.packages.clone();
285                }
286            }
287            Vec::new()
288        }
289    }
290}