wasm_opt/
integration.rs

1//! Easy integration with tools that already use `wasm-opt` via CLI.
2//!
3//! The [`run_from_command_args`] function interprets the arguments to
4//! a [`Command`], typically used for executing a subprocess, to construct
5//! an [`OptimizationOptions`], then runs the optimizer.
6//!
7//! Note that integrators must used the provided `Command` type, _not_
8//! `std::process::Command`. The provided type is a thin wrapper around the
9//! standard type that is needed for backwards compatibility with older versions
10//! of Rust.
11
12use crate::api::{Feature, FileType, OptimizationOptions, OptimizeLevel, Pass, ShrinkLevel};
13use crate::profiles::Profile;
14use crate::run::OptimizationError;
15use std::ffi::{OsStr, OsString};
16use std::iter::Iterator;
17use std::num::ParseIntError;
18use std::path::PathBuf;
19use std::result::Result;
20use std::str::FromStr;
21use strum::IntoEnumIterator;
22use thiserror::Error;
23
24pub use crate::fake_command::Command;
25
26/// Interpret a pre-built [`Command`] as an [`OptimizationOptions`],
27/// then call [`OptimizationOptions::run`] on it.
28///
29/// This function is meant for easy integration with tools that already
30/// call `wasm-opt` via the command line, allowing them to use either
31/// the command-line tool or the integrated API from a single `Command` builder.
32/// New programs that just need to optimize wasm should use `OptimizationOptions` directly.
33///
34/// This function is provided on a best-effort basis to support programs
35/// trying to integrate with the crate.
36/// In general, it should support any command line options that are also supported
37/// by the `OptimizationOptions` API,
38/// but it may not parse — and in some cases may not interpret —
39/// those commands in exactly the same way.
40/// It is meant to make it _possible_ to produce a single command-line that works
41/// with both the CLI and the API,
42/// not to reproduce the behavior of the CLI perfectly.
43///
44/// The `-o` argument is required, followed by a path —
45/// the `wasm-opt` tool writes the optimized module to stdout by default,
46/// but this library is not currently capable of that.
47/// `-o` specifies a file in which to write the module.
48/// If `-o` is not provided, [`Error::OutputFileRequired`] is returned.
49///
50/// Only the arguments to `command` are interpreted;
51/// environment variables and other settings are ignored.
52///
53/// # Errors
54///
55/// - Returns [`Error::Unsupported`] if any argument is not understood.
56/// - Returns [`Error::OutputFileRequired`] if the `-o` argument and subsequent path
57///   are not provided.
58pub fn run_from_command_args(command: Command) -> Result<(), Error> {
59    let parsed = parse_command_args(command)?;
60
61    parsed.opts.run_with_sourcemaps(
62        parsed.input_file,
63        parsed.input_sourcemap,
64        parsed.output_file,
65        parsed.output_sourcemap,
66        parsed.sourcemap_url,
67    )?;
68
69    Ok(())
70}
71
72/// An error resulting from [`run_from_command_args`].
73#[derive(Error, Debug)]
74pub enum Error {
75    /// No input file specified.
76    #[error("An input file is required")]
77    InputFileRequired,
78    /// No output file specified.
79    #[error("The `-o` option to `wasm-opt` is required")]
80    OutputFileRequired,
81    /// Expected another argument.
82    #[error("The `wasm-opt` argument list ended while expecting another argument")]
83    UnexpectedEndOfArgs,
84    /// Expected to parse unicode.
85    #[error("Argument must be unicode: {arg:?}")]
86    NeedUnicode { arg: OsString },
87    /// Expected to parse a number.
88    #[error("Argument must be a number: {arg:?}")]
89    NeedNumber {
90        arg: OsString,
91        #[source]
92        source: ParseIntError,
93    },
94    /// Unsupported or unrecognized command-line option.
95    #[error("Unsupported `wasm-opt` command-line arguments: {args:?}")]
96    Unsupported { args: Vec<OsString> },
97    /// An error occurred while executing [`OptimizationOptions::run`].
98    #[error("Error while optimization wasm modules")]
99    ExecutionError(
100        #[from]
101        #[source]
102        OptimizationError,
103    ),
104}
105
106struct ParsedCliArgs {
107    opts: OptimizationOptions,
108    input_file: PathBuf,
109    input_sourcemap: Option<PathBuf>,
110    output_file: PathBuf,
111    output_sourcemap: Option<PathBuf>,
112    sourcemap_url: Option<String>,
113}
114
115#[rustfmt::skip]
116fn parse_command_args(command: Command) -> Result<ParsedCliArgs, Error> {
117    let mut opts = OptimizationOptions::new_opt_level_0();
118
119    let mut args = command.get_args();
120
121    let mut input_file: Option<PathBuf> = None;
122    let mut input_sourcemap: Option<PathBuf> = None;
123    let mut output_file: Option<PathBuf> = None;
124    let mut output_sourcemap: Option<PathBuf> = None;
125    let mut sourcemap_url: Option<String> = None;
126
127    let mut unsupported: Vec<OsString> = vec![];
128
129    while let Some(arg) = args.next() {
130        let arg = if let Some(arg) = arg.to_str() {
131            arg
132        } else {
133            // Not unicode. Might still be the infile.
134            parse_infile_path(arg, &mut input_file, &mut unsupported);
135            continue;
136        };
137
138        // Keep these cases in order they are listed in the original cpp files.
139        match arg {
140            /* from wasm-opt.cpp */
141
142            "--output" | "-o" => {
143                parse_path_into(&mut args, &mut output_file, &mut unsupported)?;
144            }
145            "--emit-text" | "-S" => {
146                opts.writer_file_type(FileType::Wat);
147            }
148            "--converge" | "-c" => {
149                opts.set_converge();
150            }
151            "--input-source-map" | "-ism" => {
152                parse_path_into(&mut args, &mut input_sourcemap, &mut unsupported)?;
153            }
154            "--output-source-map" | "-osm" => {
155                parse_path_into(&mut args, &mut output_sourcemap, &mut unsupported)?;
156            }
157            "--output-source-map-url" | "-osu" => {
158                sourcemap_url = Some(parse_unicode(&mut args)?);
159            }
160
161            /* from optimization-options.h */
162
163            "-O" => {
164                Profile::optimize_for_size().apply_to_opts(&mut opts);
165            }
166            "-O0" => {
167                Profile::opt_level_0().apply_to_opts(&mut opts);
168            }
169            "-O1" => {
170                Profile::opt_level_1().apply_to_opts(&mut opts);
171            }
172            "-O2" => {
173                Profile::opt_level_2().apply_to_opts(&mut opts);
174            }
175            "-O3" => {
176                Profile::opt_level_3().apply_to_opts(&mut opts);
177            }
178            "-O4" => {
179                Profile::opt_level_4().apply_to_opts(&mut opts);
180            }
181            "-Os" => {
182                Profile::optimize_for_size().apply_to_opts(&mut opts);
183            }
184            "-Oz" => {
185                Profile::optimize_for_size_aggressively().apply_to_opts(&mut opts);
186            }
187            "--optimize-level" | "-ol" => {
188                match parse_unicode(&mut args)?.as_str() {
189                    "0" => { opts.optimize_level(OptimizeLevel::Level0); },
190                    "1" => { opts.optimize_level(OptimizeLevel::Level1); },
191                    "2" => { opts.optimize_level(OptimizeLevel::Level2); },
192                    "3" => { opts.optimize_level(OptimizeLevel::Level3); },
193                    "4" => { opts.optimize_level(OptimizeLevel::Level4); },
194                    arg => {
195                        unsupported.push(OsString::from(arg));
196                    }
197                }
198            }
199            "--shrink-level" | "-s" => {
200                match parse_unicode(&mut args)?.as_str() {
201                    "0" => { opts.shrink_level(ShrinkLevel::Level0); },
202                    "1" => { opts.shrink_level(ShrinkLevel::Level1); },
203                    "2" => { opts.shrink_level(ShrinkLevel::Level2); },
204                    arg => {
205                        unsupported.push(OsString::from(arg));
206                    }
207                }
208            }
209            "--debuginfo" | "-g" => {
210                opts.debug_info(true);
211            }
212            "--always-inline-max-function-size" | "-aimfs" => {
213                opts.always_inline_max_size(parse_u32(&mut args)?);
214            }
215            "--flexible-inline-max-function-size" | "-fimfs" => {
216                opts.flexible_inline_max_size(parse_u32(&mut args)?);
217            }
218            "--one-caller-inline-max-function-size" | "-ocifms" => {
219                opts.one_caller_inline_max_size(parse_u32(&mut args)?);
220            }
221            "--inline-functions-with-loops" | "-ifwl" => {
222                opts.allow_functions_with_loops(true);
223            }
224            "--partial-inlining-ifs" | "-pii" => {
225                opts.partial_inlining_ifs(parse_u32(&mut args)?);
226            }
227            "--traps-never-happen" | "-tnh" => {
228                opts.traps_never_happen(true);
229            }
230            "--low-memory-unused" | "-lmu" => {
231                opts.low_memory_unused(true);
232            }
233            "--fast-math" | "-ffm" => {
234                opts.fast_math(true);
235            }
236            "--zero-filled-memory" | "-uim" => {
237                opts.zero_filled_memory(true);
238            }
239
240            /* from tool-options.h */
241
242            "--mvp-features" | "-mvp" => {
243                opts.mvp_features_only();
244            }
245            "--all-features" | "-all" => {
246                opts.all_features();
247            }
248            "--quiet" | "-q" => {
249                /* pass */
250            }
251            "--no-validation" | "-n" => {
252                opts.validate(false);
253            }
254            "--pass-arg" | "-pa" => {
255                let args = parse_unicode(&mut args)?;
256                if args.contains("@") {
257                    let args: Vec<&str> = args.split("@").collect();
258                    opts.set_pass_arg(args[0], args[1]);
259                } else {
260                    opts.set_pass_arg(&args, "1");
261                }
262            }
263
264            /* fallthrough */
265
266            _ => {
267                // todo parse pass names w/ pass args (--pass-name=value).
268
269                if arg.starts_with("--enable-") {
270                    let feature = &arg[9..];
271                    if let Ok(feature) = Feature::from_str(feature) {
272                        opts.enable_feature(feature);
273                    } else {
274                        unsupported.push(OsString::from(arg));
275                    }
276                } else if arg.starts_with("--disable-") {
277                    let feature = &arg[10..];
278                    if let Ok(feature) = Feature::from_str(feature) {
279                        opts.disable_feature(feature);
280                    } else {
281                        unsupported.push(OsString::from(arg));
282                    }
283                } else {
284                    let mut is_pass = false;
285                    for pass in Pass::iter() {
286                        if is_pass_argument(arg, &pass) {
287                            opts.add_pass(pass);
288                            is_pass = true;
289                        }
290                    }
291
292                    if !is_pass {
293                        if arg.starts_with("-") && arg.len() > 1 {
294                            // Reject args that look like flags that we don't support.
295                            unsupported.push(OsString::from(arg));
296                        } else {
297                            parse_infile_path(OsStr::new(arg), &mut input_file, &mut unsupported);
298                        }
299                    }
300                }
301            }
302        }
303    }
304
305    let input_file = if let Some(input_file) = input_file {
306        input_file
307    } else {
308        return Err(Error::InputFileRequired);
309    };
310    let output_file = if let Some(output_file) = output_file {
311        output_file
312    } else {
313        return Err(Error::OutputFileRequired);
314    };
315
316    if unsupported.len() > 0 {
317        return Err(Error::Unsupported {
318            args: unsupported,
319        });
320    }
321
322    Ok(ParsedCliArgs {
323        opts,
324        input_file,
325        input_sourcemap,
326        output_file,
327        output_sourcemap,
328        sourcemap_url,
329    })
330}
331
332fn is_pass_argument(arg: &str, pass: &Pass) -> bool {
333    let pass_name = pass.name();
334    arg.starts_with("--") && arg.contains(pass_name) && arg.len() == 2 + pass_name.len()
335}
336
337fn parse_infile_path(
338    arg: &OsStr,
339    maybe_input_file: &mut Option<PathBuf>,
340    unsupported: &mut Vec<OsString>,
341) {
342    parse_path_into(&mut Some(arg).into_iter(), maybe_input_file, unsupported).expect("impossible")
343}
344
345fn parse_path_into<'item>(
346    args: &mut impl Iterator<Item = &'item OsStr>,
347    maybe_path: &mut Option<PathBuf>,
348    unsupported: &mut Vec<OsString>,
349) -> Result<(), Error> {
350    if let Some(arg) = args.next() {
351        if maybe_path.is_none() {
352            *maybe_path = Some(PathBuf::from(arg));
353        } else {
354            unsupported.push(OsString::from(arg));
355        }
356
357        Ok(())
358    } else {
359        Err(Error::UnexpectedEndOfArgs)
360    }
361}
362
363fn parse_unicode<'item>(args: &mut impl Iterator<Item = &'item OsStr>) -> Result<String, Error> {
364    if let Some(arg) = args.next() {
365        if let Some(arg) = arg.to_str() {
366            Ok(arg.to_owned())
367        } else {
368            Err(Error::NeedUnicode {
369                arg: arg.to_owned(),
370            })
371        }
372    } else {
373        Err(Error::UnexpectedEndOfArgs)
374    }
375}
376
377fn parse_u32<'item>(args: &mut impl Iterator<Item = &'item OsStr>) -> Result<u32, Error> {
378    let arg = parse_unicode(args)?;
379    let number: u32 = arg.parse().map_err(|e| Error::NeedNumber {
380        arg: OsString::from(arg),
381        source: e,
382    })?;
383    Ok(number)
384}