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