1use std::fmt;
2use std::path::PathBuf;
3
4use only_semantic::{
5 DirectiveAst, DocumentAst, InterpolationAst, ShellKind, ShellSelection, TaskAst,
6};
7
8use crate::dag::expand_execution_order;
9use crate::resolve::{
10 build_execution_nodes, build_task_index, document_shell, merge_parameter_inputs,
11 resolve_root_task, resolve_root_task_in_document,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Invocation<'a> {
16 Task {
17 target: &'a str,
18 args: Vec<&'a str>,
19 overrides: Vec<(&'a str, &'a str)>,
20 },
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ExecutionNode {
25 pub stage: usize,
26 pub name: String,
27 pub steps: Vec<ExecutionStep>,
28 pub params: Vec<PlanParam>,
29 pub result_params: Vec<PlanParam>,
30 pub pass: Option<String>,
31 pub fail: Option<String>,
32 pub shell: Option<ShellSelection>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ExecutionStep {
37 Command {
38 source: String,
39 interpolations: Vec<InterpolationAst>,
40 },
41 CommandBlock {
42 source: String,
43 line_count: usize,
44 interpolations: Vec<InterpolationAst>,
45 },
46}
47
48impl ExecutionStep {
49 pub fn source(&self) -> &str {
50 match self {
51 Self::Command { source, .. } => source,
52 Self::CommandBlock { source, .. } => source,
53 }
54 }
55
56 pub fn is_block(&self) -> bool {
57 matches!(self, Self::CommandBlock { .. })
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PlanParam {
63 pub name: String,
64 pub default_value: Option<String>,
65 pub value: Option<String>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
69pub struct ExecutionPlan {
70 pub nodes: Vec<ExecutionNode>,
71 pub shell: Option<ShellKind>,
72 pub working_dir: PathBuf,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum PlanError {
77 UnknownTask(String),
78 HelperTask(String),
79 TaskUnavailable(String),
80 MissingRequiredParameter(String),
81 DependencyMissingRequiredParameter {
82 dependency: String,
83 parameter: String,
84 },
85 ConflictingDependencyArguments(String),
86 UnknownParameter {
87 task: String,
88 name: String,
89 },
90 DuplicateOverride(String),
91 CyclicDependency(String),
92 TooManyArguments {
93 task: String,
94 expected: usize,
95 got: usize,
96 },
97}
98
99impl fmt::Display for PlanError {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 match self {
102 Self::UnknownTask(task) => write!(f, "task '{task}' does not exist"),
103 Self::HelperTask(task) => {
104 write!(f, "helper task '{task}' cannot be run directly")
105 }
106 Self::TaskUnavailable(task) => {
107 write!(f, "task '{task}' is not available on this system")
108 }
109 Self::MissingRequiredParameter(name) => {
110 write!(f, "parameter '{{{{{name}}}}}' is required")
111 }
112 Self::DependencyMissingRequiredParameter {
113 dependency,
114 parameter,
115 } => write!(
116 f,
117 "dependency `{dependency}` requires parameter `{parameter}`\nprovide it with `{dependency}(\"value\")`"
118 ),
119 Self::ConflictingDependencyArguments(dependency) => write!(
120 f,
121 "dependency `{dependency}` is called with different arguments"
122 ),
123 Self::UnknownParameter { task, name } => {
124 write!(f, "task '{task}' has no parameter named '{name}'")
125 }
126 Self::DuplicateOverride(name) => {
127 write!(f, "parameter '{name}' was given more than once")
128 }
129 Self::CyclicDependency(path) => write!(f, "dependency loop: {path}"),
130 Self::TooManyArguments {
131 task,
132 expected,
133 got,
134 } => match expected {
135 0 => write!(f, "task '{task}' accepts no arguments, but got {got}"),
136 1 => write!(f, "task '{task}' accepts 1 argument, but got {got}"),
137 _ => write!(
138 f,
139 "task '{task}' accepts {expected} arguments, but got {got}"
140 ),
141 },
142 }
143 }
144}
145
146impl std::error::Error for PlanError {}
147
148pub fn build_execution_plan(document: &DocumentAst, invocation: Invocation<'_>) -> ExecutionPlan {
157 let working_dir = std::env::current_dir().unwrap_or_default();
158 try_build_execution_plan_in_dir(document, invocation, working_dir).unwrap_or_default()
159}
160
161pub fn try_build_execution_plan(
170 document: &DocumentAst,
171 invocation: Invocation<'_>,
172) -> Result<ExecutionPlan, PlanError> {
173 let working_dir = std::env::current_dir().unwrap_or_default();
174 try_build_execution_plan_in_dir(document, invocation, working_dir)
175}
176
177pub fn try_build_execution_plan_in_dir(
187 document: &DocumentAst,
188 invocation: Invocation<'_>,
189 working_dir: PathBuf,
190) -> Result<ExecutionPlan, PlanError> {
191 let Invocation::Task {
192 target,
193 args,
194 overrides,
195 } = invocation;
196
197 let tasks = build_task_index(document);
198 let root = resolve_root_task(&tasks, target)?;
199 let globals = document
200 .directives
201 .iter()
202 .filter_map(|directive| match directive {
203 DirectiveAst::Variable { name, value, .. } => {
204 Some((name.to_string(), value.to_string()))
205 }
206 _ => None,
207 })
208 .collect::<std::collections::HashMap<_, _>>();
209 let overrides = merge_parameter_inputs(args, overrides, root, &globals)?;
210 let mut effective_globals = globals.clone();
211 for (name, value) in &overrides {
212 if globals.contains_key(name)
213 && !root.params.iter().any(|param| param.name.as_str() == name)
214 {
215 effective_globals.insert(name.clone(), value.clone());
216 }
217 }
218 let ordered = expand_execution_order(root, &overrides, &tasks, &effective_globals)?;
219
220 Ok(ExecutionPlan {
221 nodes: build_execution_nodes(ordered),
222 shell: document_shell(document),
223 working_dir,
224 })
225}
226
227pub fn select_root_task_variant<'a>(
236 document: &'a DocumentAst,
237 target: &str,
238) -> Result<&'a TaskAst, PlanError> {
239 resolve_root_task_in_document(document, target)
240}