Skip to main content

moon_task/
task.rs

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        // Inputs are using defaults `**/*`
19        #[serde(skip_serializing_if = "is_false")]
20        pub default_inputs: bool,
21
22        // Inputs were configured explicitly as `[]`
23        #[serde(skip_serializing_if = "is_false")]
24        pub empty_inputs: bool,
25
26        // Has the task (and parent project) been expanded
27        #[serde(skip_serializing_if = "is_false")]
28        pub expanded: bool,
29
30        // Is task defined in a root-level project
31        #[serde(skip_serializing_if = "is_false")]
32        pub root_level: bool,
33
34        // The `runInCI` option was configured explicitly
35        #[serde(skip_serializing_if = "is_false")]
36        pub set_run_in_ci: bool,
37
38        // Has shell been explicitly disabled
39        #[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    /// Create a globset of all input globs to match with.
148    pub fn create_globset(&self) -> miette::Result<glob::GlobSet<'_>> {
149        // Both inputs/outputs may have a mix of negated and
150        // non-negated globs, so we must split them into groups
151        let (gi, ni) = split_patterns(self.input_globs.keys());
152        let (go, no) = split_patterns(self.output_globs.keys());
153
154        // We then only match against non-negated inputs
155        let g = gi;
156
157        // While output non-negated/negated and negated inputs
158        // are all considered negations (inputs and outputs
159        // shouldn't overlay)
160        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    /// Return a list of affected files filtered down from
169    /// the provided changed files list.
170    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    /// Return the task command/args/script as a full command line for
188    /// use within logs and debugs.
189    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    /// Return a list of all workspace-relative input files.
204    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            // Detect if file actually exists
211            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    /// Return a list of all workspace-relative input files based
224    /// on the provided globs and their params.
225    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    /// Return a list of all workspace-relative output files.
262    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    /// Return true if the task is a "build" type.
287    pub fn is_build_type(&self) -> bool {
288        matches!(self.type_of, TaskType::Build) || !self.outputs.is_empty()
289    }
290
291    /// Return true if the task has been expanded.
292    pub fn is_expanded(&self) -> bool {
293        self.state.expanded
294    }
295
296    /// Return true if an internal task.
297    pub fn is_internal(&self) -> bool {
298        self.options.internal
299    }
300
301    /// Return true if an interactive task.
302    pub fn is_interactive(&self) -> bool {
303        self.options.interactive
304    }
305
306    /// Return true if the task is a "no operation" and does nothing.
307    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    /// Return true if the task is a "run" type.
313    pub fn is_run_type(&self) -> bool {
314        matches!(self.type_of, TaskType::Run)
315    }
316
317    /// Return true of the task will run in the system toolchain.
318    pub fn is_system_toolchain(&self) -> bool {
319        self.toolchains.is_empty() || self.toolchains.len() == 1 && self.toolchains[0] == "system"
320    }
321
322    /// Return true if the task is a "test" type.
323    pub fn is_test_type(&self) -> bool {
324        matches!(self.type_of, TaskType::Test)
325    }
326
327    /// Return true if a persistently running task.
328    pub fn is_persistent(&self) -> bool {
329        self.options.persistent
330    }
331
332    /// Return true if the task should run, whether in a CI environment or not.
333    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    /// Convert the task into a fragment.
346    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    /// Fragment of a task including important fields.
389    #[derive(Clone, Debug, Default, PartialEq)]
390    pub struct TaskFragment {
391        /// Target of the task.
392        pub target: Target,
393
394        /// Toolchains the task belongs to.
395        #[serde(default, skip_serializing_if = "Vec::is_empty")]
396        pub toolchains: Vec<Id>,
397    }
398);