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 successors: Vec<Vec<usize>>,
72 pub shell: Option<ShellKind>,
73 pub working_dir: PathBuf,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum PlanError {
78 UnknownTask(String),
79 HelperTask(String),
80 TaskUnavailable(String),
81 MissingRequiredParameter(String),
82 DependencyMissingRequiredParameter {
83 dependency: String,
84 parameter: String,
85 },
86 ConflictingDependencyArguments(String),
87 UnknownParameter {
88 task: String,
89 name: String,
90 },
91 DuplicateOverride(String),
92 CyclicDependency(String),
93 TooManyArguments {
94 task: String,
95 expected: usize,
96 got: usize,
97 },
98}
99
100impl fmt::Display for PlanError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::UnknownTask(task) => write!(f, "task '{task}' does not exist"),
104 Self::HelperTask(task) => {
105 write!(f, "helper task '{task}' cannot be run directly")
106 }
107 Self::TaskUnavailable(task) => {
108 write!(f, "task '{task}' is not available on this system")
109 }
110 Self::MissingRequiredParameter(name) => {
111 write!(f, "parameter '{{{{{name}}}}}' is required")
112 }
113 Self::DependencyMissingRequiredParameter {
114 dependency,
115 parameter,
116 } => write!(
117 f,
118 "dependency `{dependency}` requires parameter `{parameter}`\nprovide it with `{dependency}(\"value\")`"
119 ),
120 Self::ConflictingDependencyArguments(dependency) => write!(
121 f,
122 "dependency `{dependency}` is called with different arguments"
123 ),
124 Self::UnknownParameter { task, name } => {
125 write!(f, "task '{task}' has no parameter named '{name}'")
126 }
127 Self::DuplicateOverride(name) => {
128 write!(f, "parameter '{name}' was given more than once")
129 }
130 Self::CyclicDependency(path) => write!(f, "dependency loop: {path}"),
131 Self::TooManyArguments {
132 task,
133 expected,
134 got,
135 } => match expected {
136 0 => write!(f, "task '{task}' accepts no arguments, but got {got}"),
137 1 => write!(f, "task '{task}' accepts 1 argument, but got {got}"),
138 _ => write!(
139 f,
140 "task '{task}' accepts {expected} arguments, but got {got}"
141 ),
142 },
143 }
144 }
145}
146
147impl std::error::Error for PlanError {}
148
149pub fn build_execution_plan(document: &DocumentAst, invocation: Invocation<'_>) -> ExecutionPlan {
158 let working_dir = std::env::current_dir().unwrap_or_default();
159 try_build_execution_plan_in_dir(document, invocation, working_dir).unwrap_or_default()
160}
161
162pub fn try_build_execution_plan(
171 document: &DocumentAst,
172 invocation: Invocation<'_>,
173) -> Result<ExecutionPlan, PlanError> {
174 let working_dir = std::env::current_dir().unwrap_or_default();
175 try_build_execution_plan_in_dir(document, invocation, working_dir)
176}
177
178pub fn try_build_execution_plan_in_dir(
188 document: &DocumentAst,
189 invocation: Invocation<'_>,
190 working_dir: PathBuf,
191) -> Result<ExecutionPlan, PlanError> {
192 let Invocation::Task {
193 target,
194 args,
195 overrides,
196 } = invocation;
197
198 let tasks = build_task_index(document);
199 let root = resolve_root_task(&tasks, target)?;
200 let globals = document
201 .directives
202 .iter()
203 .filter_map(|directive| match directive {
204 DirectiveAst::Variable { name, value, .. } => {
205 Some((name.to_string(), value.to_string()))
206 }
207 _ => None,
208 })
209 .collect::<std::collections::HashMap<_, _>>();
210 let overrides = merge_parameter_inputs(args, overrides, root, &globals)?;
211 let mut effective_globals = globals.clone();
212 for (name, value) in &overrides {
213 if globals.contains_key(name)
214 && !root.params.iter().any(|param| param.name.as_str() == name)
215 {
216 effective_globals.insert(name.clone(), value.clone());
217 }
218 }
219 let expanded = expand_execution_order(root, &overrides, &tasks, &effective_globals)?;
220
221 Ok(ExecutionPlan {
222 nodes: build_execution_nodes(expanded.ordered),
223 successors: expanded.successors,
224 shell: document_shell(document),
225 working_dir,
226 })
227}
228
229pub fn select_root_task_variant<'a>(
238 document: &'a DocumentAst,
239 target: &str,
240) -> Result<&'a TaskAst, PlanError> {
241 resolve_root_task_in_document(document, target)
242}