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