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 shell: Option<ShellSelection>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ExecutionStep {
32 Command(String),
33 CommandBlock { source: String, line_count: usize },
34}
35
36impl ExecutionStep {
37 pub fn source(&self) -> &str {
38 match self {
39 Self::Command(command) => command,
40 Self::CommandBlock { source, .. } => source,
41 }
42 }
43
44 pub fn is_block(&self) -> bool {
45 matches!(self, Self::CommandBlock { .. })
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct PlanParam {
51 pub name: String,
52 pub default_value: Option<String>,
53 pub value: Option<String>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Default)]
57pub struct ExecutionPlan {
58 pub nodes: Vec<ExecutionNode>,
59 pub shell: Option<ShellKind>,
60 pub working_dir: PathBuf,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum PlanError {
65 UnknownTask(String),
66 HelperTask(String),
67 TaskUnavailable(String),
68 MissingRequiredParameter(String),
69 UnknownParameter {
70 task: String,
71 name: String,
72 },
73 DuplicateOverride(String),
74 CyclicDependency(String),
75 TooManyArguments {
76 task: String,
77 expected: usize,
78 got: usize,
79 },
80}
81
82impl fmt::Display for PlanError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::UnknownTask(task) => write!(f, "task '{task}' does not exist"),
86 Self::HelperTask(task) => {
87 write!(f, "helper task '{task}' cannot be run directly")
88 }
89 Self::TaskUnavailable(task) => {
90 write!(f, "task '{task}' is not available on this system")
91 }
92 Self::MissingRequiredParameter(name) => {
93 write!(f, "parameter '{{{{{name}}}}}' is required")
94 }
95 Self::UnknownParameter { task, name } => {
96 write!(f, "task '{task}' has no parameter named '{name}'")
97 }
98 Self::DuplicateOverride(name) => {
99 write!(f, "parameter '{name}' was given more than once")
100 }
101 Self::CyclicDependency(path) => write!(f, "dependency loop: {path}"),
102 Self::TooManyArguments {
103 task,
104 expected,
105 got,
106 } => match expected {
107 0 => write!(f, "task '{task}' accepts no arguments, but got {got}"),
108 1 => write!(f, "task '{task}' accepts 1 argument, but got {got}"),
109 _ => write!(
110 f,
111 "task '{task}' accepts {expected} arguments, but got {got}"
112 ),
113 },
114 }
115 }
116}
117
118impl std::error::Error for PlanError {}
119
120pub fn build_execution_plan(document: &DocumentAst, invocation: Invocation<'_>) -> ExecutionPlan {
129 let working_dir = std::env::current_dir().unwrap_or_default();
130 try_build_execution_plan_in_dir(document, invocation, working_dir).unwrap_or_default()
131}
132
133pub fn try_build_execution_plan(
142 document: &DocumentAst,
143 invocation: Invocation<'_>,
144) -> Result<ExecutionPlan, PlanError> {
145 let working_dir = std::env::current_dir().unwrap_or_default();
146 try_build_execution_plan_in_dir(document, invocation, working_dir)
147}
148
149pub fn try_build_execution_plan_in_dir(
159 document: &DocumentAst,
160 invocation: Invocation<'_>,
161 working_dir: PathBuf,
162) -> Result<ExecutionPlan, PlanError> {
163 let Invocation::Task {
164 target,
165 args,
166 overrides,
167 } = invocation;
168
169 let tasks = build_task_index(document);
170 let root = resolve_root_task(&tasks, target)?;
171 let globals = document
172 .directives
173 .iter()
174 .filter_map(|directive| match directive {
175 DirectiveAst::Variable { name, value, .. } => {
176 Some((name.to_string(), value.to_string()))
177 }
178 _ => None,
179 })
180 .collect::<std::collections::HashMap<_, _>>();
181 let overrides = merge_parameter_inputs(args, overrides, root, &globals)?;
182 let mut effective_globals = globals.clone();
183 for (name, value) in &overrides {
184 if globals.contains_key(name)
185 && !root.params.iter().any(|param| param.name.as_str() == name)
186 {
187 effective_globals.insert(name.clone(), value.clone());
188 }
189 }
190 let ordered = expand_execution_order(root, &overrides, &tasks, &effective_globals)?;
191
192 Ok(ExecutionPlan {
193 nodes: build_execution_nodes(ordered),
194 shell: document_shell(document),
195 working_dir,
196 })
197}
198
199pub fn select_root_task_variant<'a>(
208 document: &'a DocumentAst,
209 target: &str,
210) -> Result<&'a TaskAst, PlanError> {
211 resolve_root_task_in_document(document, target)
212}