1use crate::task_options::TaskOptions;
2use moon_common::{Id, cacheable, path::WorkspaceRelativePathBuf};
3use moon_config::{
4 EnvMap, Input, Output, TaskDependencyConfig, TaskOptionRunInCI, TaskPreset, TaskType, is_false,
5 schematic::RegexSetting,
6};
7use moon_target::Target;
8use rustc_hash::{FxHashMap, FxHashSet};
9use starbase_utils::glob::{self, GlobWalkOptions, split_patterns};
10use std::fmt;
11use std::path::{Path, PathBuf};
12
13cacheable!(
14 #[derive(Clone, Debug, Default, Eq, PartialEq)]
15 #[serde(default)]
16 pub struct TaskState {
17 #[serde(skip_serializing_if = "is_false")]
19 pub default_inputs: bool,
20
21 #[serde(skip_serializing_if = "is_false")]
23 pub empty_inputs: bool,
24
25 #[serde(skip_serializing_if = "is_false")]
27 pub expanded: bool,
28
29 #[serde(skip_serializing_if = "is_false")]
31 pub root_level: bool,
32 }
33);
34
35cacheable!(
36 #[derive(Clone, Debug, Default, Eq, PartialEq)]
37 #[serde(default)]
38 pub struct TaskFileInput {
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub content: Option<RegexSetting>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub optional: Option<bool>,
44 }
45);
46
47cacheable!(
48 #[derive(Clone, Debug, Eq, PartialEq)]
49 #[serde(default)]
50 pub struct TaskGlobInput {
51 #[serde(skip_serializing_if = "is_false")]
52 pub cache: bool,
53 }
54);
55
56impl Default for TaskGlobInput {
57 fn default() -> Self {
58 Self { cache: true }
59 }
60}
61
62cacheable!(
63 #[derive(Clone, Debug, Default, Eq, PartialEq)]
64 #[serde(default)]
65 pub struct TaskFileOutput {
66 pub optional: bool,
67 }
68);
69
70cacheable!(
71 #[derive(Clone, Debug, Default, Eq, PartialEq)]
72 #[serde(default)]
73 pub struct TaskGlobOutput {}
74);
75
76cacheable!(
77 #[derive(Clone, Debug, Eq, PartialEq)]
78 #[serde(default)]
79 pub struct Task {
80 #[serde(skip_serializing_if = "Vec::is_empty")]
81 pub args: Vec<String>,
82
83 pub command: String,
84
85 #[serde(skip_serializing_if = "Vec::is_empty")]
86 pub deps: Vec<TaskDependencyConfig>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub description: Option<String>,
90
91 #[serde(skip_serializing_if = "EnvMap::is_empty")]
92 pub env: EnvMap,
93
94 pub id: Id,
95
96 #[serde(skip_serializing_if = "Vec::is_empty")]
97 pub inputs: Vec<Input>,
98
99 #[serde(skip_serializing_if = "FxHashSet::is_empty")]
100 pub input_env: FxHashSet<String>,
101
102 #[serde(skip_serializing_if = "FxHashMap::is_empty")]
103 pub input_files: FxHashMap<WorkspaceRelativePathBuf, TaskFileInput>,
104
105 #[serde(skip_serializing_if = "FxHashMap::is_empty")]
106 pub input_globs: FxHashMap<WorkspaceRelativePathBuf, TaskGlobInput>,
107
108 pub options: TaskOptions,
109
110 #[serde(skip_serializing_if = "Vec::is_empty")]
111 pub outputs: Vec<Output>,
112
113 #[serde(skip_serializing_if = "FxHashMap::is_empty")]
114 pub output_files: FxHashMap<WorkspaceRelativePathBuf, TaskFileOutput>,
115
116 #[serde(skip_serializing_if = "FxHashMap::is_empty")]
117 pub output_globs: FxHashMap<WorkspaceRelativePathBuf, TaskGlobOutput>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub preset: Option<TaskPreset>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub script: Option<String>,
124
125 pub state: TaskState,
126
127 pub target: Target,
128
129 #[serde(skip_serializing_if = "Vec::is_empty")]
130 pub toolchains: Vec<Id>,
131
132 #[serde(rename = "type")]
133 pub type_of: TaskType,
134 }
135);
136
137impl Task {
138 pub fn create_globset(&self) -> miette::Result<glob::GlobSet<'_>> {
140 let (gi, ni) = split_patterns(self.input_globs.keys());
143 let (go, no) = split_patterns(self.output_globs.keys());
144
145 let g = gi;
147
148 let mut n = vec![];
152 n.extend(go);
153 n.extend(ni);
154 n.extend(no);
155
156 Ok(glob::GlobSet::new_split(g, n)?)
157 }
158
159 pub fn get_affected_files<S: AsRef<str>>(
162 &self,
163 workspace_root: &Path,
164 changed_files: &FxHashSet<WorkspaceRelativePathBuf>,
165 project_source: S,
166 ) -> miette::Result<Vec<PathBuf>> {
167 let mut files = vec![];
168 let globset = self.create_globset()?;
169 let project_source = project_source.as_ref();
170
171 for file in changed_files {
172 if file.starts_with(project_source)
174 && (self.input_files.contains_key(file) || globset.matches(file.as_str()))
175 {
176 files.push(file.to_logical_path(workspace_root));
177 }
178 }
179
180 Ok(files)
181 }
182
183 pub fn get_command_line(&self) -> String {
186 self.script
187 .clone()
188 .unwrap_or_else(|| format!("{} {}", self.command, self.args.join(" ")))
189 }
190
191 pub fn get_input_files(&self, workspace_root: &Path) -> miette::Result<Vec<PathBuf>> {
193 let mut list = FxHashSet::default();
194
195 for path in self.input_files.keys() {
196 let file = path.to_logical_path(workspace_root);
197
198 if file.is_file() && file.exists() {
200 list.insert(file);
201 }
202 }
203
204 list.extend(
205 self.get_input_files_with_globs(workspace_root, self.input_globs.iter().collect())?,
206 );
207
208 Ok(list.into_iter().collect())
209 }
210
211 pub fn get_input_files_with_globs(
214 &self,
215 workspace_root: &Path,
216 globs: FxHashMap<&WorkspaceRelativePathBuf, &TaskGlobInput>,
217 ) -> miette::Result<Vec<PathBuf>> {
218 let mut list = FxHashSet::default();
219 let mut cached_globs = vec![];
220 let mut non_cached_globs = vec![];
221
222 for (glob, params) in globs {
223 if params.cache {
224 cached_globs.push(glob);
225 } else {
226 non_cached_globs.push(glob);
227 }
228 }
229
230 if !cached_globs.is_empty() {
231 list.extend(glob::walk_fast_with_options(
232 workspace_root,
233 cached_globs,
234 GlobWalkOptions::default().cache().files(),
235 )?);
236 }
237
238 if !non_cached_globs.is_empty() {
239 list.extend(glob::walk_fast_with_options(
240 workspace_root,
241 non_cached_globs,
242 GlobWalkOptions::default().files(),
243 )?);
244 }
245
246 Ok(list.into_iter().collect())
247 }
248
249 pub fn get_output_files(
251 &self,
252 workspace_root: &Path,
253 include_non_globs: bool,
254 ) -> miette::Result<Vec<PathBuf>> {
255 let mut list = FxHashSet::default();
256
257 if include_non_globs {
258 for file in self.output_files.keys() {
259 list.insert(file.to_logical_path(workspace_root));
260 }
261 }
262
263 if !self.output_globs.is_empty() {
264 list.extend(glob::walk_fast_with_options(
265 workspace_root,
266 self.output_globs.keys(),
267 GlobWalkOptions::default().files(),
268 )?);
269 }
270
271 Ok(list.into_iter().collect())
272 }
273
274 pub fn is_build_type(&self) -> bool {
276 matches!(self.type_of, TaskType::Build) || !self.outputs.is_empty()
277 }
278
279 pub fn is_expanded(&self) -> bool {
281 self.state.expanded
282 }
283
284 pub fn is_internal(&self) -> bool {
286 self.options.internal
287 }
288
289 pub fn is_interactive(&self) -> bool {
291 self.options.interactive
292 }
293
294 pub fn is_no_op(&self) -> bool {
296 (self.command == "nop" || self.command == "noop" || self.command == "no-op")
297 && self.script.is_none()
298 }
299
300 pub fn is_run_type(&self) -> bool {
302 matches!(self.type_of, TaskType::Run)
303 }
304
305 pub fn is_system_toolchain(&self) -> bool {
307 self.toolchains.is_empty() || self.toolchains.len() == 1 && self.toolchains[0] == "system"
308 }
309
310 pub fn is_test_type(&self) -> bool {
312 matches!(self.type_of, TaskType::Test)
313 }
314
315 pub fn is_persistent(&self) -> bool {
317 self.options.persistent
318 }
319
320 pub fn should_run_in_ci(&self) -> bool {
322 if !self.options.run_in_ci.is_enabled() || self.options.run_in_ci == TaskOptionRunInCI::Skip
323 {
324 return false;
325 }
326
327 self.is_build_type() || self.is_test_type()
328 }
329
330 pub fn to_fragment(&self) -> TaskFragment {
332 TaskFragment {
333 target: self.target.clone(),
334 toolchains: self.toolchains.clone(),
335 }
336 }
337}
338
339impl Default for Task {
340 fn default() -> Self {
341 Self {
342 args: vec![],
343 command: String::from("noop"),
344 deps: vec![],
345 description: None,
346 env: EnvMap::default(),
347 id: Id::default(),
348 inputs: vec![],
349 input_env: FxHashSet::default(),
350 input_files: FxHashMap::default(),
351 input_globs: FxHashMap::default(),
352 options: TaskOptions::default(),
353 outputs: vec![],
354 output_files: FxHashMap::default(),
355 output_globs: FxHashMap::default(),
356 preset: None,
357 script: None,
358 state: TaskState::default(),
359 target: Target::default(),
360 toolchains: vec![Id::raw("system")],
361 type_of: TaskType::default(),
362 }
363 }
364}
365
366impl fmt::Display for Task {
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 write!(f, "{}", self.target)
369 }
370}
371
372cacheable!(
373 #[derive(Clone, Debug, Default, PartialEq)]
375 pub struct TaskFragment {
376 pub target: Target,
378
379 #[serde(default, skip_serializing_if = "Vec::is_empty")]
381 pub toolchains: Vec<Id>,
382 }
383);