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#[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
51pub 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
82fn 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
123pub(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 union_extend(&mut merged.modules, &spec.modules);
138
139 crate::merge_env(&mut merged.env, &spec.env);
141
142 crate::merge_aliases(&mut merged.aliases, &spec.aliases);
144
145 if let Some(ref pkgs) = spec.packages {
147 crate::composition::merge_packages(&mut merged.packages, pkgs);
148 }
149
150 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 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 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 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
213pub 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 for custom in &packages.custom {
283 if custom.name == manager_name {
284 return custom.packages.clone();
285 }
286 }
287 Vec::new()
288 }
289 }
290}