bacon/
mission.rs

1use {
2    crate::*,
3    lazy_regex::regex_replace_all,
4    rustc_hash::FxHashSet,
5    std::{
6        collections::HashMap,
7        path::PathBuf,
8    },
9};
10
11/// the description of the mission of bacon
12/// after analysis of the args, env, and surroundings
13#[derive(Debug)]
14pub struct Mission<'s> {
15    pub location_name: String,
16    pub concrete_job_ref: ConcreteJobRef,
17    pub execution_directory: PathBuf,
18    pub package_directory: PathBuf,
19    pub workspace_directory: Option<PathBuf>,
20    pub job: Job,
21    pub paths_to_watch: Vec<PathBuf>,
22    pub settings: &'s Settings,
23}
24
25impl Mission<'_> {
26    /// Return an Ignorer according to the job's settings
27    pub fn ignorer(&self) -> IgnorerSet {
28        let mut set = IgnorerSet::default();
29        if self.job.apply_gitignore != Some(false) {
30            match GitIgnorer::new(&self.package_directory) {
31                Ok(git_ignorer) => {
32                    set.add(Box::new(git_ignorer));
33                }
34                Err(e) => {
35                    // might be normal, eg not in a git repo
36                    debug!("Failed to initialise git ignorer: {e}");
37                }
38            }
39        }
40        if !self.job.ignore.is_empty() {
41            let mut glob_ignorer = GlobIgnorer::default();
42            for pattern in &self.job.ignore {
43                if let Some(override_pattern) = pattern.strip_prefix('!') {
44                    // Negative pattern: force-include matching paths, overriding
45                    // any ignore rules (including .gitignore)
46                    if let Err(e) = set.add_override(override_pattern, &self.package_directory) {
47                        warn!("Failed to add override pattern {pattern}: {e}");
48                    }
49                } else if let Err(e) = glob_ignorer.add(pattern, &self.package_directory) {
50                    warn!("Failed to add ignore pattern {pattern}: {e}");
51                }
52            }
53            set.add(Box::new(glob_ignorer));
54        }
55        set
56    }
57
58    pub fn is_success(
59        &self,
60        report: &Report,
61    ) -> bool {
62        report.is_success(self.job.allow_warnings(), self.job.allow_failures())
63    }
64
65    pub fn make_absolute(
66        &self,
67        path: PathBuf,
68    ) -> PathBuf {
69        if path.is_absolute() {
70            return path;
71        }
72        // There's a small mess here. Cargo tends to make paths relative
73        // not to the package or work directory but to the workspace, contrary
74        // to any sane tool. We have to guess.
75        if let Some(workspace) = &self.workspace_directory {
76            let workspace_joined = workspace.join(&path);
77            if workspace_joined.exists() {
78                return workspace_joined;
79            }
80        }
81        self.package_directory.join(&path)
82    }
83
84    /// build (and doesn't call) the external cargo command
85    pub fn get_command(&self) -> anyhow::Result<CommandBuilder> {
86        let mut command = if self.job.expand_env_vars() {
87            self.job
88                .command
89                .iter()
90                .map(|token| {
91                    regex_replace_all!(r"\$([A-Z0-9a-z_]+)", token, |whole: &str, name| {
92                        match std::env::var(name) {
93                            Ok(value) => value,
94                            Err(_) => {
95                                warn!("variable {whole} not found in env");
96                                whole.to_string()
97                            }
98                        }
99                    })
100                    .to_string()
101                })
102                .collect()
103        } else {
104            self.job.command.clone()
105        };
106
107        if command.is_empty() {
108            anyhow::bail!(
109                "Empty command in job {}",
110                self.concrete_job_ref.badge_label()
111            );
112        }
113
114        let scope = &self.concrete_job_ref.scope;
115        if scope.has_tests() && command.len() > 2 {
116            let tests = if command[0] == "cargo" && command[1] == "test" {
117                // Here we're going around a limitation of the vanilla cargo test:
118                // it can only be scoped to one test
119                &scope.tests[..1]
120            } else {
121                &scope.tests
122            };
123            for test in tests {
124                command.push(test.clone());
125            }
126        }
127
128        let mut tokens = command.iter();
129        let mut command = CommandBuilder::new(
130            tokens.next().unwrap(), // implies a check in the job
131        );
132        command.with_stdout(self.job.need_stdout());
133        let envs: HashMap<&String, &String> = self
134            .settings
135            .all_jobs
136            .env
137            .iter()
138            .chain(self.job.env.iter())
139            .collect();
140        if !self.job.extraneous_args() {
141            command.args(tokens);
142            command.current_dir(&self.execution_directory);
143            command.envs(envs);
144            debug!("command: {:#?}", &command);
145            return Ok(command);
146        }
147
148        let mut no_default_features_done = false;
149        let mut features_done = false;
150        let mut last_is_features = false;
151        let mut tokens = tokens.chain(&self.settings.additional_job_args);
152        let mut has_double_dash = false;
153        for arg in tokens.by_ref() {
154            if arg == "--" {
155                // we'll defer addition of the following arguments to after
156                // the addition of the features stuff, so that the features
157                // arguments are given to the cargo command.
158                has_double_dash = true;
159                break;
160            }
161            if last_is_features {
162                if self.settings.all_features {
163                    debug!("ignoring features given along --all-features");
164                } else {
165                    features_done = true;
166                    // arg is expected there to be the list of features
167                    match (&self.settings.features, self.settings.no_default_features) {
168                        (Some(features), false) => {
169                            // we take the features of both the job and the args
170                            command.arg("--features");
171                            command.arg(merge_features(arg, features));
172                        }
173                        (Some(features), true) => {
174                            // arg add features and remove the job ones
175                            command.arg("--features");
176                            command.arg(features);
177                        }
178                        (None, true) => {
179                            // we pass no feature
180                        }
181                        (None, false) => {
182                            // nothing to change
183                            command.arg("--features");
184                            command.arg(arg);
185                        }
186                    }
187                }
188                last_is_features = false;
189            } else if arg == "--no-default-features" {
190                no_default_features_done = true;
191                last_is_features = false;
192                command.arg(arg);
193            } else if arg == "--features" {
194                last_is_features = true;
195            } else {
196                command.arg(arg);
197            }
198        }
199        if self.settings.no_default_features && !no_default_features_done {
200            command.arg("--no-default-features");
201        }
202        if self.settings.all_features {
203            command.arg("--all-features");
204        }
205        if !features_done {
206            if let Some(features) = &self.settings.features {
207                if self.settings.all_features {
208                    debug!("not using features because of --all-features");
209                } else {
210                    command.arg("--features");
211                    command.arg(features);
212                }
213            }
214        }
215        if has_double_dash {
216            command.arg("--");
217            for arg in tokens {
218                command.arg(arg);
219            }
220        }
221        command.current_dir(&self.execution_directory);
222        command.envs(envs);
223        debug!("command builder: {:#?}", &command);
224        Ok(command)
225    }
226
227    pub fn kill_command(&self) -> Option<Vec<String>> {
228        self.job.kill.clone()
229    }
230
231    /// whether we need stdout and not just stderr
232    pub fn need_stdout(&self) -> bool {
233        self.job
234            .need_stdout
235            .or(self.settings.all_jobs.need_stdout)
236            .unwrap_or(false)
237    }
238
239    pub fn analyzer(&self) -> AnalyzerRef {
240        self.job.analyzer.unwrap_or_default()
241    }
242
243    pub fn ignored_lines_patterns(&self) -> Option<&Vec<LinePattern>> {
244        self.job
245            .ignored_lines
246            .as_ref()
247            .or(self.settings.all_jobs.ignored_lines.as_ref())
248            .filter(|p| !p.is_empty())
249    }
250
251    pub fn sound_player_if_needed(&self) -> Option<SoundPlayer> {
252        if self.job.sound.is_enabled() {
253            match SoundPlayer::new(self.job.sound.get_base_volume()) {
254                Ok(sound_player) => Some(sound_player),
255                Err(e) => {
256                    warn!("Failed to initialise sound player: {e}");
257                    None
258                }
259            }
260        } else {
261            None
262        }
263    }
264}
265
266fn merge_features(
267    a: &str,
268    b: &str,
269) -> String {
270    let mut features = FxHashSet::default();
271    for feature in a.split(',') {
272        features.insert(feature);
273    }
274    for feature in b.split(',') {
275        features.insert(feature);
276    }
277    features.iter().copied().collect::<Vec<&str>>().join(",")
278}