1use std::borrow::Cow;
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21
22use crate::pack::{
23 Action, Combiner, EnvArgs, ExecOnFail, ExecSpec, MkdirArgs, RequireOnFail, RequireSpec,
24 RmdirArgs, SymlinkArgs, UnlinkArgs, WhenSpec,
25};
26use crate::plugin::Registry;
27use crate::vars::{expand, VarEnv};
28
29use super::ctx::ExecCtx;
30use super::error::ExecError;
31use super::predicate::{evaluate, evaluate_when_gate};
32use super::step::{
33 ExecResult, ExecStep, PredicateOutcome, StepKind, ACTION_ENV, ACTION_EXEC, ACTION_MKDIR,
34 ACTION_REQUIRE, ACTION_RMDIR, ACTION_SYMLINK, ACTION_UNLINK, ACTION_WHEN,
35};
36use super::ActionExecutor;
37
38#[derive(Debug, Clone)]
50pub struct PlanExecutor {
51 registry: Arc<Registry>,
52}
53
54impl Default for PlanExecutor {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl PlanExecutor {
61 #[must_use]
65 pub fn new() -> Self {
66 Self { registry: Arc::new(Registry::bootstrap()) }
67 }
68
69 #[must_use]
73 pub fn with_registry(registry: Arc<Registry>) -> Self {
74 Self { registry }
75 }
76}
77
78impl ActionExecutor for PlanExecutor {
79 fn name(&self) -> &'static str {
80 "plan"
81 }
82
83 fn execute(&self, action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
84 let name = action.name();
85 if self.registry.get(name).is_none() {
91 return Err(ExecError::UnknownAction(name.to_string()));
92 }
93 let nested_ctx = ExecCtx {
97 vars: ctx.vars,
98 pack_root: ctx.pack_root,
99 workspace: ctx.workspace,
100 pack: ctx.pack,
102 platform: ctx.platform,
103 registry: Some(&self.registry),
104 pack_type_registry: ctx.pack_type_registry,
105 visited_meta: ctx.visited_meta,
106 scheduler: ctx.scheduler,
108 };
109 dispatch_plan(action, &nested_ctx)
110 }
111}
112
113fn dispatch_plan(action: &Action, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
121 match action {
122 Action::Symlink(s) => plan_symlink(s, ctx),
123 Action::Unlink(u) => plan_unlink(u, ctx),
124 Action::Env(e) => plan_env(e, ctx),
125 Action::Mkdir(m) => plan_mkdir(m, ctx),
126 Action::Rmdir(r) => plan_rmdir(r, ctx),
127 Action::Require(r) => plan_require(r, ctx),
128 Action::When(w) => plan_when(w, ctx),
129 Action::Exec(x) => plan_exec(x, ctx),
130 }
131}
132
133fn expand_field(raw: &str, env: &VarEnv, field: &'static str) -> Result<String, ExecError> {
135 expand(raw, env).map_err(|source| ExecError::VarExpand { field, source })
136}
137
138fn require_path(expanded: String) -> Result<PathBuf, ExecError> {
140 if expanded.is_empty() {
141 return Err(ExecError::InvalidPath(expanded));
142 }
143 Ok(PathBuf::from(expanded))
144}
145
146pub(crate) fn plan_symlink(args: &SymlinkArgs, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
147 let src = require_path(expand_field(&args.src, ctx.vars, "symlink.src")?)?;
148 let dst = require_path(expand_field(&args.dst, ctx.vars, "symlink.dst")?)?;
149 let result = classify_symlink(&src, &dst);
150 Ok(ExecStep {
151 action_name: Cow::Borrowed(ACTION_SYMLINK),
152 result,
153 details: StepKind::Symlink {
154 src,
155 dst,
156 kind: args.kind,
157 backup: args.backup,
158 normalize: args.normalize,
159 },
160 })
161}
162
163fn classify_symlink(src: &Path, dst: &Path) -> ExecResult {
164 match std::fs::symlink_metadata(dst) {
165 Ok(meta) if meta.file_type().is_symlink() => match std::fs::read_link(dst) {
166 Ok(target) if target == src => ExecResult::AlreadySatisfied,
167 _ => ExecResult::WouldPerformChange,
168 },
169 _ => ExecResult::WouldPerformChange,
170 }
171}
172
173pub(crate) fn plan_unlink(args: &UnlinkArgs, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
174 let dst = require_path(expand_field(&args.dst, ctx.vars, "unlink.dst")?)?;
175 let result = match std::fs::symlink_metadata(&dst) {
179 Ok(meta) if meta.file_type().is_symlink() => ExecResult::WouldPerformChange,
180 _ => ExecResult::AlreadySatisfied,
181 };
182 Ok(ExecStep {
183 action_name: Cow::Borrowed(ACTION_UNLINK),
184 result,
185 details: StepKind::Unlink { dst },
186 })
187}
188
189pub(crate) fn plan_env(args: &EnvArgs, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
190 let value = expand_field(&args.value, ctx.vars, "env.value")?;
191 let result = classify_env(&args.name, &value, ctx.vars);
192 Ok(ExecStep {
193 action_name: Cow::Borrowed(ACTION_ENV),
194 result,
195 details: StepKind::Env { name: args.name.clone(), value, scope: args.scope },
196 })
197}
198
199fn classify_env(name: &str, value: &str, vars: &VarEnv) -> ExecResult {
200 match vars.get(name) {
201 Some(existing) if existing == value => ExecResult::AlreadySatisfied,
202 _ => ExecResult::WouldPerformChange,
203 }
204}
205
206pub(crate) fn plan_mkdir(args: &MkdirArgs, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
207 let path = require_path(expand_field(&args.path, ctx.vars, "mkdir.path")?)?;
208 let result =
209 if path.is_dir() { ExecResult::AlreadySatisfied } else { ExecResult::WouldPerformChange };
210 Ok(ExecStep {
211 action_name: Cow::Borrowed(ACTION_MKDIR),
212 result,
213 details: StepKind::Mkdir { path, mode: args.mode.clone() },
214 })
215}
216
217pub(crate) fn plan_rmdir(args: &RmdirArgs, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
218 let path = require_path(expand_field(&args.path, ctx.vars, "rmdir.path")?)?;
219 let result =
220 if path.exists() { ExecResult::WouldPerformChange } else { ExecResult::AlreadySatisfied };
221 Ok(ExecStep {
222 action_name: Cow::Borrowed(ACTION_RMDIR),
223 result,
224 details: StepKind::Rmdir { path, backup: args.backup, force: args.force },
225 })
226}
227
228pub(crate) fn plan_require(spec: &RequireSpec, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
229 let satisfied = evaluate_combiner(&spec.combiner, ctx)?;
230 let outcome =
231 if satisfied { PredicateOutcome::Satisfied } else { PredicateOutcome::Unsatisfied };
232 let result = classify_require(satisfied, spec.on_fail)?;
233 Ok(ExecStep {
234 action_name: Cow::Borrowed(ACTION_REQUIRE),
235 result,
236 details: StepKind::Require { outcome, on_fail: spec.on_fail },
237 })
238}
239
240fn classify_require(satisfied: bool, on_fail: RequireOnFail) -> Result<ExecResult, ExecError> {
248 if satisfied {
249 return Ok(ExecResult::AlreadySatisfied);
250 }
251 match on_fail {
252 RequireOnFail::Error => {
253 Err(ExecError::RequireFailed { detail: "combiner evaluated to false".to_string() })
254 }
255 RequireOnFail::Skip | RequireOnFail::Warn => Ok(ExecResult::NoOp),
256 }
257}
258
259fn evaluate_combiner(combiner: &Combiner, ctx: &ExecCtx<'_>) -> Result<bool, ExecError> {
260 match combiner {
261 Combiner::AllOf(list) => {
262 for p in list {
263 if !evaluate(p, ctx)? {
264 return Ok(false);
265 }
266 }
267 Ok(true)
268 }
269 Combiner::AnyOf(list) => {
270 for p in list {
271 if evaluate(p, ctx)? {
272 return Ok(true);
273 }
274 }
275 Ok(false)
276 }
277 Combiner::NoneOf(list) => {
278 for p in list {
279 if evaluate(p, ctx)? {
280 return Ok(false);
281 }
282 }
283 Ok(true)
284 }
285 }
286}
287
288pub(crate) fn plan_when(spec: &WhenSpec, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
289 let branch_taken = evaluate_when_gate(spec, ctx)?;
290 let nested_steps = if branch_taken { plan_nested(&spec.actions, ctx)? } else { Vec::new() };
291 let result = if branch_taken { ExecResult::WouldPerformChange } else { ExecResult::NoOp };
292 Ok(ExecStep {
293 action_name: Cow::Borrowed(ACTION_WHEN),
294 result,
295 details: StepKind::When { branch_taken, nested_steps },
296 })
297}
298
299pub(crate) fn plan_nested(
300 actions: &[Action],
301 ctx: &ExecCtx<'_>,
302) -> Result<Vec<ExecStep>, ExecError> {
303 actions
310 .iter()
311 .map(|a| {
312 if let Some(reg) = ctx.registry {
313 if reg.get(a.name()).is_none() {
314 return Err(ExecError::UnknownAction(a.name().to_string()));
315 }
316 }
317 dispatch_plan(a, ctx)
318 })
319 .collect()
320}
321
322pub(crate) fn plan_exec(spec: &ExecSpec, ctx: &ExecCtx<'_>) -> Result<ExecStep, ExecError> {
323 let cwd = expand_optional_path(spec.cwd.as_deref(), ctx.vars, "exec.cwd")?;
324 let cmdline = build_exec_cmdline(spec, ctx.vars)?;
325 Ok(ExecStep {
326 action_name: Cow::Borrowed(ACTION_EXEC),
327 result: ExecResult::WouldPerformChange,
328 details: StepKind::Exec { cmdline, cwd, on_fail: spec.on_fail, shell: spec.shell },
329 })
330}
331
332fn expand_optional_path(
333 raw: Option<&str>,
334 env: &VarEnv,
335 field: &'static str,
336) -> Result<Option<PathBuf>, ExecError> {
337 match raw {
338 Some(s) => {
339 let expanded = expand_field(s, env, field)?;
340 Ok(Some(require_path(expanded)?))
341 }
342 None => Ok(None),
343 }
344}
345
346fn build_exec_cmdline(spec: &ExecSpec, env: &VarEnv) -> Result<String, ExecError> {
353 match (spec.shell, &spec.cmd, &spec.cmd_shell) {
354 (false, Some(argv), None) => join_argv(argv, env),
355 (true, None, Some(line)) => expand_field(line, env, "exec.cmd_shell"),
356 _ => Err(ExecError::ExecInvalid(
357 "exec requires cmd (shell=false) XOR cmd_shell (shell=true)".to_string(),
358 )),
359 }
360}
361
362fn join_argv(argv: &[String], env: &VarEnv) -> Result<String, ExecError> {
363 let mut parts = Vec::with_capacity(argv.len());
364 for a in argv {
365 parts.push(expand_field(a, env, "exec.cmd")?);
366 }
367 Ok(parts.join(" "))
368}
369
370#[allow(dead_code)]
373const _: Option<ExecOnFail> = None;