moon_task/
task.rs

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        // Inputs are using defaults `**/*`
18        #[serde(skip_serializing_if = "is_false")]
19        pub default_inputs: bool,
20
21        // Inputs were configured explicitly as `[]`
22        #[serde(skip_serializing_if = "is_false")]
23        pub empty_inputs: bool,
24
25        // Has the task (and parent project) been expanded
26        #[serde(skip_serializing_if = "is_false")]
27        pub expanded: bool,
28
29        // Is task defined in a root-level project
30        #[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    /// Create a globset of all input globs to match with.
139    pub fn create_globset(&self) -> miette::Result<glob::GlobSet<'_>> {
140        // Both inputs/outputs may have a mix of negated and
141        // non-negated globs, so we must split them into groups
142        let (gi, ni) = split_patterns(self.input_globs.keys());
143        let (go, no) = split_patterns(self.output_globs.keys());
144
145        // We then only match against non-negated inputs
146        let g = gi;
147
148        // While output non-negated/negated and negated inputs
149        // are all considered negations (inputs and outputs
150        // shouldn't overlay)
151        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    /// Return a list of project-relative affected files filtered down from
160    /// the provided changed files list.
161    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            // Don't run on files outside of the project
173            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    /// Return the task command/args/script as a full command line for
184    /// use within logs and debugs.
185    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    /// Return a list of all workspace-relative input files.
192    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            // Detect if file actually exists
199            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    /// Return a list of all workspace-relative input files based
212    /// on the provided globs and their params.
213    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    /// Return a list of all workspace-relative output files.
250    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    /// Return true if the task is a "build" type.
275    pub fn is_build_type(&self) -> bool {
276        matches!(self.type_of, TaskType::Build) || !self.outputs.is_empty()
277    }
278
279    /// Return true if the task has been expanded.
280    pub fn is_expanded(&self) -> bool {
281        self.state.expanded
282    }
283
284    /// Return true if an internal task.
285    pub fn is_internal(&self) -> bool {
286        self.options.internal
287    }
288
289    /// Return true if an interactive task.
290    pub fn is_interactive(&self) -> bool {
291        self.options.interactive
292    }
293
294    /// Return true if the task is a "no operation" and does nothing.
295    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    /// Return true if the task is a "run" type.
301    pub fn is_run_type(&self) -> bool {
302        matches!(self.type_of, TaskType::Run)
303    }
304
305    /// Return true of the task will run in the system toolchain.
306    pub fn is_system_toolchain(&self) -> bool {
307        self.toolchains.is_empty() || self.toolchains.len() == 1 && self.toolchains[0] == "system"
308    }
309
310    /// Return true if the task is a "test" type.
311    pub fn is_test_type(&self) -> bool {
312        matches!(self.type_of, TaskType::Test)
313    }
314
315    /// Return true if a persistently running task.
316    pub fn is_persistent(&self) -> bool {
317        self.options.persistent
318    }
319
320    /// Return true if the task should run in a CI environment.
321    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    /// Convert the task into a fragment.
331    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    /// Fragment of a task including important fields.
374    #[derive(Clone, Debug, Default, PartialEq)]
375    pub struct TaskFragment {
376        /// Target of the task.
377        pub target: Target,
378
379        /// Toolchains the task belongs to.
380        #[serde(default, skip_serializing_if = "Vec::is_empty")]
381        pub toolchains: Vec<Id>,
382    }
383);