add_determinism/add_det/
config.rs

1/* SPDX-License-Identifier: GPL-3.0-or-later */
2
3use anyhow::{anyhow, Result};
4use clap::Parser;
5use log::{debug, info, LevelFilter};
6use std::path::PathBuf;
7use std::thread;
8
9use crate::setup;
10use crate::simplelog;
11use super::handlers;
12
13#[derive(Debug, Parser)]
14#[command(version, about, long_about = None)]
15struct Options {
16    /// Paths to operate on
17    #[arg(value_name = "path")]
18    pub inputs: Vec<PathBuf>,
19
20    /// Handlers to enable/disable;
21    /// use --handler=list to list
22    #[arg(long = "handler")]
23    pub handlers: Vec<String>,
24
25    /// Adjust behaviour as appropriate for a build root program
26    #[arg(long)]
27    pub brp: bool,
28
29    /// Turn on debugging output
30    #[arg(short, long)]
31    pub verbose: bool,
32
33    /// Fail if any modifications would have been made
34    #[arg(long)]
35    pub check: bool,
36
37    /// Ignore file name extensions, always run handlers.
38    #[arg(long)]
39    pub ignore_extension: bool,
40
41    /// Print contents of files
42    #[arg(short, long,
43          conflicts_with = "brp",
44          conflicts_with = "check",
45          conflicts_with = "jobs")]
46    pub print: bool,
47
48    /// Use N worker processes
49    #[arg(short = 'j',
50          value_name = "N",
51          num_args = 0..=1,
52          default_missing_value = "0")]
53    pub jobs: Option<u32>,
54}
55
56pub struct Config {
57    pub inputs: Vec<PathBuf>,
58    pub brp: bool,
59    pub print: bool,
60    pub check: bool,
61    pub ignore_extension: bool,
62    pub jobs: Option<u32>,
63    pub source_date_epoch: Option<i64>,
64    pub handler_names: Vec<&'static str>,
65    pub strict_handlers: bool,
66}
67
68fn filter_by_name(name: &str, enabled_by_default: bool, filter: &[&str]) -> bool {
69    let mut negative_filter = true;
70
71    for f in filter.iter().rev() {
72        if let Some(f) = f.strip_prefix('-') {
73            if name == f {
74                return false;
75            }
76        } else {
77            negative_filter = false;
78
79            if name == *f {
80                return true;
81            }
82        }
83    }
84
85    enabled_by_default && negative_filter
86}
87
88pub fn requested_handlers(filter: &[&str]) -> Result<(Vec<&'static str>, bool)> {
89    if filter.iter().any(|x|  x.starts_with('-')) &&
90       filter.iter().any(|x| !x.starts_with('-')) {
91            return Err(anyhow!("Cannot mix --handler options with '-' and without"));
92        }
93
94    if let Some(name) = filter
95        .iter()
96        .map(|x| x.strip_prefix('-').unwrap_or(x))
97        .find(|x| !handlers::handler_names().contains(x))
98    {
99        return Err(anyhow!("Unknown handler name: {:?}", name));
100    }
101
102    let list: Vec<&'static str> = handlers::HANDLERS
103        .iter()
104        .filter(|(name, enabled_by_default, _)| filter_by_name(name, *enabled_by_default, filter))
105        .map(|(name, _, _)| *name)
106        .collect();
107
108    if list.is_empty() {
109        return Err(anyhow!("Requested handler list is empty, will have nothing to do"));
110    }
111
112    let strict = !filter.is_empty();
113    debug!("Requested handlers: {} (strict={})", list.join(", "), strict);
114    Ok((list, strict))
115}
116
117impl Config {
118    pub fn make() -> Result<Option<Self>> {
119        let options = Options::parse();
120
121        // handlers
122
123        let handlers: Vec<&str> = options.handlers.iter().flat_map(|s| s.split(',')).collect();
124
125        if handlers.contains(&"list") {
126            println!("{}", handlers::handler_names().join("\n"));
127            return Ok(None);
128        }
129
130        // log level
131
132        let log_level = if options.verbose { LevelFilter::Debug } else { LevelFilter::Info };
133        // Prefix logs with [pid] if we have multiple workers and -v was used,
134        // which means that we expect output from various workers.
135        let show_pid = options.verbose && options.jobs.is_some();
136        simplelog::init(log_level, show_pid)?;
137
138        let (handler_names, strict_handlers) = requested_handlers(&handlers)?;
139
140        // positional args
141
142        if options.inputs.is_empty() && !options.brp {
143            info!("No arguments specified, nothing to do. 😎");
144        }
145
146        // $SOURCE_DATE_EPOCH
147        let source_date_epoch = setup::source_date_epoch()?;
148
149        let jobs = options.jobs.map(
150            |n| if n > 0 { n } else {
151                let j = thread::available_parallelism().unwrap().get();
152                debug!("Will use {j} workers");
153                j as u32
154            }
155        );
156
157        Ok(Some(Self {
158            inputs: options.inputs,
159            brp: options.brp,
160            print: options.print,
161            check: options.check,
162            ignore_extension: options.ignore_extension,
163            jobs,
164            source_date_epoch,
165            handler_names,
166            strict_handlers,
167        }))
168    }
169
170    #[allow(dead_code)]
171    // FIXME: should this be marked as #[cfg(test)]? But then the tests don't compile.
172    pub const fn empty(source_date_epoch: i64, check: bool) -> Self {
173        Self {
174            inputs: vec![],
175            brp: false,
176            print: false,
177            check,
178            ignore_extension: false,
179            jobs: None,
180            source_date_epoch: Some(source_date_epoch),
181            handler_names: vec![],
182            strict_handlers: false,
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_filter_by_name() {
193        assert_eq!(filter_by_name("x", true, &vec!["x", "y"]), true);
194        assert_eq!(filter_by_name("x", true, &vec!["x"]), true);
195        assert_eq!(filter_by_name("x", true, &vec![]), true);
196        assert_eq!(filter_by_name("x", true, &vec!["-x"]), false);
197        assert_eq!(filter_by_name("x", true, &vec!["-y"]), true);
198
199        assert_eq!(filter_by_name("x", false, &vec!["x", "y"]), true);
200        assert_eq!(filter_by_name("x", false, &vec!["x"]), true);
201        assert_eq!(filter_by_name("x", false, &vec![]), false);
202        assert_eq!(filter_by_name("x", false, &vec!["-x"]), false);
203        assert_eq!(filter_by_name("x", false, &vec!["-y"]), false);
204    }
205
206    #[test]
207    fn test_requested_handlers() {
208        let (list, strict) = requested_handlers(&vec![]).unwrap();
209        assert_eq!(list, vec!["ar", "jar", "javadoc", "gzip", "pyc", "zip"]);
210        assert_eq!(strict, false);
211
212        let (list, strict) = requested_handlers(&vec!["ar", "pyc-zero-mtime"]).unwrap();
213        assert_eq!(list, vec!["ar", "pyc-zero-mtime"]);
214        assert_eq!(strict, true);
215
216        let (list, strict) = requested_handlers(&vec!["-pyc-zero-mtime"]).unwrap();
217        assert_eq!(list, vec!["ar", "jar", "javadoc", "gzip", "pyc", "zip"]);
218        assert_eq!(strict, true);
219    }
220}