xref: /wasmtime-44.0.1/crates/cli-flags/src/opt.rs (revision d74b34ff)
1 //! Support for parsing Wasmtime's `-O`, `-W`, etc "option groups"
2 //!
3 //! This builds up a clap-derive-like system where there's ideally a single
4 //! macro `wasmtime_option_group!` which is invoked per-option which enables
5 //! specifying options in a struct-like syntax where all other boilerplate about
6 //! option parsing is contained exclusively within this module.
7 
8 use crate::WasiNnGraph;
9 use anyhow::{bail, Result};
10 use clap::builder::{StringValueParser, TypedValueParser, ValueParserFactory};
11 use clap::error::{Error, ErrorKind};
12 use std::marker;
13 use std::time::Duration;
14 
15 #[macro_export]
16 macro_rules! wasmtime_option_group {
17     (
18         $(#[$attr:meta])*
19         pub struct $opts:ident {
20             $(
21                 $(#[doc = $doc:tt])*
22                 pub $opt:ident: $container:ident<$payload:ty>,
23             )+
24 
25             $(
26                 #[prefixed = $prefix:tt]
27                 $(#[doc = $prefixed_doc:tt])*
28                 pub $prefixed:ident: Vec<(String, Option<String>)>,
29             )?
30         }
31         enum $option:ident {
32             ...
33         }
34     ) => {
35         #[derive(Default, Debug)]
36         $(#[$attr])*
37         pub struct $opts {
38             $(
39                 pub $opt: $container<$payload>,
40             )+
41             $(
42                 pub $prefixed: Vec<(String, Option<String>)>,
43             )?
44         }
45 
46         #[derive(Clone, Debug,PartialEq)]
47         #[allow(non_camel_case_types)]
48         enum $option {
49             $(
50                 $opt($payload),
51             )+
52             $(
53                 $prefixed(String, Option<String>),
54             )?
55         }
56 
57         impl $crate::opt::WasmtimeOption for $option {
58             const OPTIONS: &'static [$crate::opt::OptionDesc<$option>] = &[
59                 $(
60                     $crate::opt::OptionDesc {
61                         name: $crate::opt::OptName::Name(stringify!($opt)),
62                         parse: |_, s| {
63                             Ok($option::$opt(
64                                 $crate::opt::WasmtimeOptionValue::parse(s)?
65                             ))
66                         },
67                         val_help: <$payload as $crate::opt::WasmtimeOptionValue>::VAL_HELP,
68                         docs: concat!($($doc, "\n",)*),
69                     },
70                  )+
71                 $(
72                     $crate::opt::OptionDesc {
73                         name: $crate::opt::OptName::Prefix($prefix),
74                         parse: |name, val| {
75                             Ok($option::$prefixed(
76                                 name.to_string(),
77                                 val.map(|v| v.to_string()),
78                             ))
79                         },
80                         val_help: "[=val]",
81                         docs: concat!($($prefixed_doc, "\n",)*),
82                     },
83                  )?
84             ];
85         }
86 
87         impl $opts {
88             fn configure_with(&mut self, opts: &[$crate::opt::CommaSeparated<$option>]) {
89                 for opt in opts.iter().flat_map(|o| o.0.iter()) {
90                     match opt {
91                         $(
92                             $option::$opt(val) => {
93                                 let dst = &mut self.$opt;
94                                 wasmtime_option_group!(@push $container dst val);
95                             }
96                         )+
97                         $(
98                             $option::$prefixed(key, val) => self.$prefixed.push((key.clone(), val.clone())),
99                         )?
100                     }
101                 }
102             }
103         }
104     };
105 
106     (@push Option $dst:ident $val:ident) => (*$dst = Some($val.clone()));
107     (@push Vec $dst:ident $val:ident) => ($dst.push($val.clone()));
108 }
109 
110 /// Parser registered with clap which handles parsing the `...` in `-O ...`.
111 #[derive(Clone, Debug, PartialEq)]
112 pub struct CommaSeparated<T>(pub Vec<T>);
113 
114 impl<T> ValueParserFactory for CommaSeparated<T>
115 where
116     T: WasmtimeOption,
117 {
118     type Parser = CommaSeparatedParser<T>;
119 
120     fn value_parser() -> CommaSeparatedParser<T> {
121         CommaSeparatedParser(marker::PhantomData)
122     }
123 }
124 
125 #[derive(Clone)]
126 pub struct CommaSeparatedParser<T>(marker::PhantomData<T>);
127 
128 impl<T> TypedValueParser for CommaSeparatedParser<T>
129 where
130     T: WasmtimeOption,
131 {
132     type Value = CommaSeparated<T>;
133 
134     fn parse_ref(
135         &self,
136         cmd: &clap::Command,
137         arg: Option<&clap::Arg>,
138         value: &std::ffi::OsStr,
139     ) -> Result<Self::Value, Error> {
140         let val = StringValueParser::new().parse_ref(cmd, arg, value)?;
141 
142         let options = T::OPTIONS;
143         let arg = arg.expect("should always have an argument");
144         let arg_long = arg.get_long().expect("should have a long name specified");
145         let arg_short = arg.get_short().expect("should have a short name specified");
146 
147         // Handle `-O help` which dumps all the `-O` options, their messages,
148         // and then exits.
149         if val == "help" {
150             let mut max = 0;
151             for d in options {
152                 max = max.max(d.name.display_string().len() + d.val_help.len());
153             }
154             println!("Available {arg_long} options:\n");
155             for d in options {
156                 print!(
157                     "  -{arg_short} {:>1$}",
158                     d.name.display_string(),
159                     max - d.val_help.len()
160                 );
161                 print!("{}", d.val_help);
162                 print!(" --");
163                 if val == "help" {
164                     for line in d.docs.lines().map(|s| s.trim()) {
165                         if line.is_empty() {
166                             break;
167                         }
168                         print!(" {line}");
169                     }
170                     println!();
171                 } else {
172                     println!();
173                     for line in d.docs.lines().map(|s| s.trim()) {
174                         let line = line.trim();
175                         println!("        {line}");
176                     }
177                 }
178             }
179             println!("\npass `-{arg_short} help-long` to see longer-form explanations");
180             std::process::exit(0);
181         }
182         if val == "help-long" {
183             println!("Available {arg_long} options:\n");
184             for d in options {
185                 println!(
186                     "  -{arg_short} {}{} --",
187                     d.name.display_string(),
188                     d.val_help
189                 );
190                 println!();
191                 for line in d.docs.lines().map(|s| s.trim()) {
192                     let line = line.trim();
193                     println!("        {line}");
194                 }
195             }
196             std::process::exit(0);
197         }
198 
199         let mut result = Vec::new();
200         for val in val.split(',') {
201             // Split `k=v` into `k` and `v` where `v` is optional
202             let mut iter = val.splitn(2, '=');
203             let key = iter.next().unwrap();
204             let key_val = iter.next();
205 
206             // Find `key` within `T::OPTIONS`
207             let option = options
208                 .iter()
209                 .filter_map(|d| match d.name {
210                     OptName::Name(s) => {
211                         let s = s.replace('_', "-");
212                         if s == key {
213                             Some((d, s))
214                         } else {
215                             None
216                         }
217                     }
218                     OptName::Prefix(s) => {
219                         let name = key.strip_prefix(s)?.strip_prefix("-")?;
220                         Some((d, name.to_string()))
221                     }
222                 })
223                 .next();
224 
225             let (desc, key) = match option {
226                 Some(pair) => pair,
227                 None => {
228                     let err = Error::raw(
229                         ErrorKind::InvalidValue,
230                         format!("unknown -{arg_short} / --{arg_long} option: {key}\n"),
231                     );
232                     return Err(err.with_cmd(cmd));
233                 }
234             };
235 
236             result.push((desc.parse)(&key, key_val).map_err(|e| {
237                 Error::raw(
238                     ErrorKind::InvalidValue,
239                     format!("failed to parse -{arg_short} option `{val}`: {e:?}\n"),
240                 )
241                 .with_cmd(cmd)
242             })?)
243         }
244 
245         Ok(CommaSeparated(result))
246     }
247 }
248 
249 /// Helper trait used by `CommaSeparated` which contains a list of all options
250 /// supported by the option group.
251 pub trait WasmtimeOption: Sized + Send + Sync + Clone + 'static {
252     const OPTIONS: &'static [OptionDesc<Self>];
253 }
254 
255 pub struct OptionDesc<T> {
256     pub name: OptName,
257     pub docs: &'static str,
258     pub parse: fn(&str, Option<&str>) -> Result<T>,
259     pub val_help: &'static str,
260 }
261 
262 pub enum OptName {
263     /// A named option. Note that the `str` here uses `_` instead of `-` because
264     /// it's derived from Rust syntax.
265     Name(&'static str),
266 
267     /// A prefixed option which strips the specified `name`, then `-`.
268     Prefix(&'static str),
269 }
270 
271 impl OptName {
272     fn display_string(&self) -> String {
273         match self {
274             OptName::Name(s) => s.replace('_', "-"),
275             OptName::Prefix(s) => format!("{s}-<KEY>"),
276         }
277     }
278 }
279 
280 /// A helper trait for all types of options that can be parsed. This is what
281 /// actually parses the `=val` in `key=val`
282 pub trait WasmtimeOptionValue: Sized {
283     /// Help text for the value to be specified.
284     const VAL_HELP: &'static str;
285 
286     /// Parses the provided value, if given, returning an error on failure.
287     fn parse(val: Option<&str>) -> Result<Self>;
288 }
289 
290 impl WasmtimeOptionValue for String {
291     const VAL_HELP: &'static str = "=val";
292     fn parse(val: Option<&str>) -> Result<Self> {
293         match val {
294             Some(val) => Ok(val.to_string()),
295             None => bail!("value must be specified with `key=val` syntax"),
296         }
297     }
298 }
299 
300 impl WasmtimeOptionValue for u32 {
301     const VAL_HELP: &'static str = "=N";
302     fn parse(val: Option<&str>) -> Result<Self> {
303         let val = String::parse(val)?;
304         match val.strip_prefix("0x") {
305             Some(hex) => Ok(u32::from_str_radix(hex, 16)?),
306             None => Ok(val.parse()?),
307         }
308     }
309 }
310 
311 impl WasmtimeOptionValue for u64 {
312     const VAL_HELP: &'static str = "=N";
313     fn parse(val: Option<&str>) -> Result<Self> {
314         let val = String::parse(val)?;
315         match val.strip_prefix("0x") {
316             Some(hex) => Ok(u64::from_str_radix(hex, 16)?),
317             None => Ok(val.parse()?),
318         }
319     }
320 }
321 
322 impl WasmtimeOptionValue for usize {
323     const VAL_HELP: &'static str = "=N";
324     fn parse(val: Option<&str>) -> Result<Self> {
325         let val = String::parse(val)?;
326         match val.strip_prefix("0x") {
327             Some(hex) => Ok(usize::from_str_radix(hex, 16)?),
328             None => Ok(val.parse()?),
329         }
330     }
331 }
332 
333 impl WasmtimeOptionValue for bool {
334     const VAL_HELP: &'static str = "[=y|n]";
335     fn parse(val: Option<&str>) -> Result<Self> {
336         match val {
337             None | Some("y") | Some("yes") | Some("true") => Ok(true),
338             Some("n") | Some("no") | Some("false") => Ok(false),
339             Some(s) => bail!("unknown boolean flag `{s}`, only yes,no,<nothing> accepted"),
340         }
341     }
342 }
343 
344 impl WasmtimeOptionValue for Duration {
345     const VAL_HELP: &'static str = "=N|Ns|Nms|..";
346     fn parse(val: Option<&str>) -> Result<Duration> {
347         let s = String::parse(val)?;
348         // assume an integer without a unit specified is a number of seconds ...
349         if let Ok(val) = s.parse() {
350             return Ok(Duration::from_secs(val));
351         }
352         // ... otherwise try to parse it with units such as `3s` or `300ms`
353         let dur = humantime::parse_duration(&s)?;
354         Ok(dur)
355     }
356 }
357 
358 impl WasmtimeOptionValue for wasmtime::OptLevel {
359     const VAL_HELP: &'static str = "=0|1|2|s";
360     fn parse(val: Option<&str>) -> Result<Self> {
361         match String::parse(val)?.as_str() {
362             "0" => Ok(wasmtime::OptLevel::None),
363             "1" => Ok(wasmtime::OptLevel::Speed),
364             "2" => Ok(wasmtime::OptLevel::Speed),
365             "s" => Ok(wasmtime::OptLevel::SpeedAndSize),
366             other => bail!(
367                 "unknown optimization level `{}`, only 0,1,2,s accepted",
368                 other
369             ),
370         }
371     }
372 }
373 
374 impl WasmtimeOptionValue for wasmtime::Strategy {
375     const VAL_HELP: &'static str = "=winch|cranelift";
376     fn parse(val: Option<&str>) -> Result<Self> {
377         match String::parse(val)?.as_str() {
378             "cranelift" => Ok(wasmtime::Strategy::Cranelift),
379             "winch" => Ok(wasmtime::Strategy::Winch),
380             other => bail!("unknown compiler `{other}` only `cranelift` and `winch` accepted",),
381         }
382     }
383 }
384 
385 impl WasmtimeOptionValue for WasiNnGraph {
386     const VAL_HELP: &'static str = "=<format>::<dir>";
387     fn parse(val: Option<&str>) -> Result<Self> {
388         let val = String::parse(val)?;
389         let mut parts = val.splitn(2, "::");
390         Ok(WasiNnGraph {
391             format: parts.next().unwrap().to_string(),
392             dir: match parts.next() {
393                 Some(part) => part.into(),
394                 None => bail!("graph does not contain `::` separator for directory"),
395             },
396         })
397     }
398 }
399