peg_pack/
cli.rs

1use std::fs::File;
2use std::io::{ErrorKind, Write};
3use std::panic::PanicInfo;
4use std::path::{Path, PathBuf};
5use std::process::{exit, Command, Output};
6use std::time::Instant;
7use std::{fs, io, panic};
8
9use atty::Stream;
10use clap::CommandFactory;
11use clap::FromArgMatches;
12use clap::Parser as CliParser;
13use regex::bytes::Regex;
14use termcolor::{Color, ColorSpec, StandardStream, WriteColor};
15
16use crate::core::{CompilerSettings, Error, Parser};
17
18/// A list of paths and contents to copy into the build directory
19const OUT_DIR_FILES: &[(&str, &[u8])] = &[
20    ("build/runtime/mod.rs", include_bytes!("runtime/mod.rs")),
21    (
22        "build/runtime/context.rs",
23        include_bytes!("runtime/context.rs"),
24    ),
25    (
26        "build/runtime/grammar.rs",
27        include_bytes!("runtime/grammar.rs"),
28    ),
29    ("build/runtime/input.rs", include_bytes!("runtime/input.rs")),
30    (
31        "build/runtime/result.rs",
32        include_bytes!("runtime/result.rs"),
33    ),
34    (
35        "build/runtime/buffered_iter.rs",
36        include_bytes!("runtime/buffered_iter.rs"),
37    ),
38    (
39        "build/runtime/array_vec.rs",
40        include_bytes!("runtime/array_vec.rs"),
41    ),
42    (
43        "build/runtime/small_vec.rs",
44        include_bytes!("runtime/small_vec.rs"),
45    ),
46    ("build/runtime/stack.rs", include_bytes!("runtime/stack.rs")),
47    ("build/runtime/cache.rs", include_bytes!("runtime/cache.rs")),
48    ("build/runtime/refc.rs", include_bytes!("runtime/refc.rs")),
49    ("build/harness.rs", include_bytes!("include/harness.rs")),
50    ("build/loader.js", include_bytes!("include/loader.js")),
51    ("loader.d.ts", include_bytes!("include/loader.d.ts")),
52    (".gitignore", include_bytes!("include/gitignore")),
53];
54
55pub fn parse_args() -> Cli {
56    let command = (Cli::command() as clap::Command).color(clap::ColorChoice::Auto);
57    Cli::from_arg_matches(&command.get_matches()).unwrap()
58}
59
60pub fn run(args: Cli) {
61    let context = Context::new(args);
62    context.run();
63}
64
65/// Installs a nicer panic that tells the user about the crash before printing
66/// the usual backtrace
67pub fn setup_panic_hook() {
68    let default_hook = panic::take_hook();
69    panic::set_hook(Box::new(move |info| panic_hook(info, &default_hook)));
70}
71
72fn panic_hook(info: &PanicInfo, default_hook: &dyn Fn(&PanicInfo)) {
73    let color = if atty::is(Stream::Stderr) {
74        termcolor::ColorChoice::Auto
75    } else {
76        termcolor::ColorChoice::Never
77    };
78
79    let mut red = ColorSpec::new();
80    red.set_fg(Some(Color::Red));
81
82    let mut reset = ColorSpec::new();
83    reset.set_reset(true);
84
85    let mut stderr = StandardStream::stderr(color);
86    let _ = stderr.set_color(&red);
87    let _ = writeln!(
88        stderr,
89        "Fatal internal error, this is a bug. Please report this to the developers"
90    );
91    let _ = stderr.set_color(&reset);
92
93    default_hook(info);
94}
95
96#[derive(CliParser)]
97#[clap(author, version, about)]
98pub struct Cli {
99    /// The grammar file to generate from
100    pub grammar: PathBuf,
101
102    /// The output directory for build artifacts
103    #[clap(short, long)]
104    pub out_dir: Option<PathBuf>,
105
106    /// Parse stdin using the generated parser
107    #[clap(short, long)]
108    pub interactive: bool,
109
110    /// Enable slow state analysis optimizations
111    #[clap(long)]
112    pub state_opt: bool,
113}
114
115struct Context {
116    opts: Cli,
117    stderr: StandardStream,
118    /// Whether or not the last line of stderr is a progress indicator
119    active_indicator: bool,
120    /// The time peg-pack started
121    start: Instant,
122}
123
124impl Context {
125    fn new(cli: Cli) -> Self {
126        // Matches clap's semantics
127        let stderr_color = if atty::is(Stream::Stderr) {
128            termcolor::ColorChoice::Auto
129        } else {
130            termcolor::ColorChoice::Never
131        };
132
133        let stderr = StandardStream::stderr(stderr_color);
134
135        Self {
136            stderr,
137            opts: cli,
138            active_indicator: false,
139            start: Instant::now(),
140        }
141    }
142
143    fn run(mut self) {
144        self.set_indicator("Checking environment");
145        self.check_node();
146        self.check_grammar();
147
148        if self.opts.interactive {
149            self.check_rust();
150        }
151
152        self.set_indicator("Setting up output");
153        self.create_out_dir();
154        self.populate_out_dir();
155
156        self.clear_indicator();
157        self.execute_grammar();
158
159        self.set_indicator("Generating parser");
160        let parser = self.load_parser();
161        self.generate_code(parser);
162
163        if self.opts.interactive {
164            self.set_indicator("Compiling");
165            self.compile();
166
167            self.print_ready();
168            self.execute();
169        } else {
170            self.print_ready();
171        }
172    }
173
174    fn print_ready(&mut self) {
175        self.println(format!("Parser built in {:.1?}", self.start.elapsed()));
176    }
177
178    /// Load the generated IR file into a parser
179    fn load_parser(&mut self) -> Parser {
180        let ir = match fs::read(self.ir_file()) {
181            Ok(ir) => ir,
182            Err(err) => {
183                self.exit_with_error(format!("Could not read IR: {}", err));
184            }
185        };
186
187        let mut settings = CompilerSettings::normal();
188        settings.state_optimization = self.opts.state_opt;
189
190        match Parser::load(&ir, settings) {
191            Ok(parser) => parser,
192            Err(Error::Load(message)) => self.exit_with_error(message),
193            Err(Error::LeftRecursive(left_recursive)) => {
194                self.print_error_heading();
195
196                if left_recursive.len() == 1 {
197                    self.print("Ill-formed grammar, ");
198                    self.print_color(Color::Yellow, false);
199                    self.print(left_recursive.iter().next().unwrap());
200                    self.print_reset();
201                    self.println(" is left-recursive");
202                } else {
203                    self.print("Ill-formed grammar, the following rules are left-recursive: ");
204
205                    for (i, rule) in left_recursive.iter().enumerate() {
206                        if i != 0 {
207                            self.print(", ");
208                        }
209
210                        self.print_color(Color::Yellow, false);
211                        self.print(rule);
212                        self.print_reset();
213                    }
214
215                    self.println("");
216                }
217
218                exit(1);
219            }
220        }
221    }
222
223    /// Generate the Rust code for the parser
224    fn generate_code(&mut self, parser: Parser) {
225        let code = parser.generate();
226
227        if let Err(err) = fs::write(self.parser_file(), code) {
228            self.exit_with_error(format!("Could not write generated code: {}", err));
229        }
230    }
231
232    /// Compile the parser into an executable
233    fn compile(&mut self) {
234        let result = Command::new("rustc")
235            .args(["--edition", "2021"])
236            .args(["-C", "opt-level=3"])
237            .arg("-o")
238            .arg(self.executable_file())
239            .arg(self.harness_file())
240            .output();
241
242        let result = match result {
243            Ok(result) => result,
244            Err(err) => {
245                self.exit_with_error(format!("Could not compile parser: {}", err));
246            }
247        };
248
249        if !result.status.success() {
250            self.exit_with_error_and_output("Could not compile parser", &result);
251        }
252    }
253
254    /// Run the parser executable
255    fn execute(&mut self) {
256        let result = Command::new(self.executable_file()).status();
257
258        let status = match result {
259            Ok(result) => result,
260            Err(err) => {
261                self.exit_with_error(format!("Could not launch parser: {}", err));
262            }
263        };
264
265        if !status.success() {
266            if let Some(status) = status.code() {
267                self.exit_with_error(format!("Parser exited with status {}", status));
268            } else {
269                self.exit_with_error("Parser exited with unknown status");
270            }
271        }
272    }
273
274    /// Execute the grammar script and generator IR
275    fn execute_grammar(&mut self) {
276        if let Err(err) = self.execute_grammar_unhandled() {
277            self.exit_with_error(format!("Could not run grammar script: {}", err));
278        }
279    }
280
281    fn execute_grammar_unhandled(&mut self) -> io::Result<()> {
282        let grammar_path = self.opts.grammar.canonicalize()?;
283        let loader_path = self.loader_file();
284        let ir_path = self.ir_file();
285
286        let status = Command::new("node")
287            .env("PEG_PACK_GRAMMAR", grammar_path)
288            .env("PEG_PACK_IR", ir_path)
289            .arg(loader_path)
290            .status()?;
291
292        if !status.success() {
293            if let Some(status) = status.code() {
294                self.exit_with_error(format!("Grammar script exited with status {}", status));
295            } else {
296                self.exit_with_error("Grammar script exited with unknown status");
297            }
298        }
299
300        Ok(())
301    }
302
303    /// Check that the grammar script is an accessible file
304    fn check_grammar(&mut self) {
305        let grammar = &self.opts.grammar;
306        let display = grammar.display();
307
308        if let Err(err) = File::open(grammar) {
309            if err.kind() == ErrorKind::NotFound {
310                self.exit_with_error(format!("Grammar file does not exist ({})", display));
311            } else if err.kind() == ErrorKind::PermissionDenied {
312                self.exit_with_error(format!(
313                    "Insufficient permissions to access grammar file ({})",
314                    display
315                ));
316            } else {
317                self.exit_with_error(format!(
318                    "Could not open grammar file ({}): {}",
319                    display, err
320                ));
321            }
322        }
323
324        if !grammar.is_file() {
325            self.exit_with_error(format!("Grammar was not a file ({})", display));
326        }
327    }
328
329    /// Remove the old output directory and create a new one
330    fn create_out_dir(&mut self) {
331        let out_dir = self.out_dir();
332        let display = out_dir.display();
333
334        if out_dir.exists() && !out_dir.is_dir() {
335            self.exit_with_error(format!("Output directory is not a directory ({})", display));
336        }
337
338        if out_dir.exists() {
339            if let Err(err) = fs::remove_dir_all(out_dir) {
340                self.exit_with_error(format!("Could not remove old output directory: {}", err));
341            }
342        }
343
344        if let Err(err) = fs::create_dir(out_dir) {
345            if err.kind() == ErrorKind::NotFound {
346                self.exit_with_error(format!(
347                    "Parent of output directory does not exist ({})",
348                    display
349                ));
350            } else if err.kind() == ErrorKind::PermissionDenied {
351                self.exit_with_error(format!(
352                    "Insufficient permissions to create output directory ({})",
353                    display
354                ));
355            } else {
356                self.exit_with_error(format!(
357                    "Could not create output directory ({}): {}",
358                    display, err
359                ));
360            }
361        }
362    }
363
364    /// Populate the output directory with the required build files
365    fn populate_out_dir(&mut self) {
366        if let Err(err) = self.populate_out_dir_unhandled() {
367            self.exit_with_error(format!("Error populating output directory: {}", err));
368        }
369    }
370
371    fn populate_out_dir_unhandled(&mut self) -> io::Result<()> {
372        let out_dir = self.out_dir();
373
374        for (name, data) in OUT_DIR_FILES {
375            let path = out_dir.join(name);
376            assert!(path.starts_with(out_dir));
377
378            let parent = path.parent().unwrap();
379            fs::create_dir_all(parent)?;
380
381            fs::write(path, data)?;
382        }
383
384        Ok(())
385    }
386
387    /// Check that a recent version of NodeJS is installed
388    fn check_node(&mut self) {
389        let command = Command::new("node").arg("--version").output();
390        let version_regex = Regex::new(r"^v(\d+)\.").unwrap();
391
392        self.check_command_installation(
393            command,
394            "NodeJS",
395            "https://nodejs.org",
396            version_regex,
397            16,
398            ">=16.0.0",
399        );
400    }
401
402    /// Check that a recent version of Rust is installed
403    fn check_rust(&mut self) {
404        let command = Command::new("rustc")
405            .args(["+stable", "--version"])
406            .output();
407        let version_regex = Regex::new(r"^rustc 1\.(\d+)\.").unwrap();
408
409        self.check_command_installation(
410            command,
411            "Rust",
412            "https://rustup.rs",
413            version_regex,
414            61,
415            "^1.61.0",
416        );
417    }
418
419    /// Run version command and use a regex to check its output
420    fn check_command_installation(
421        &mut self,
422        result: io::Result<Output>,
423        name: &str,
424        download_url: &str,
425        version_regex: Regex,
426        expected_version: u32,
427        expected_version_spec: &str,
428    ) {
429        let result = match result {
430            Ok(result) => result,
431            Err(err) => {
432                if err.kind() == ErrorKind::NotFound {
433                    self.exit_with_error(format!(
434                        "{} was not found. Check that it is installed and added to PATH ({})",
435                        name, download_url
436                    ));
437                } else if err.kind() == ErrorKind::PermissionDenied {
438                    self.exit_with_error(format!(
439                        "Insufficient permission to run {}. Reconfigure your installation, or run with more permissions",
440                        name
441                    ));
442                } else {
443                    self.exit_with_error(format!("Could not run {}: {}", name, err));
444                }
445            }
446        };
447
448        if !result.status.success() {
449            self.exit_with_error_and_output(
450                format!("{} version check returned non-zero exit status", name),
451                &result,
452            );
453        }
454
455        let version = version_regex.captures(&result.stdout).and_then(|captures| {
456            let version_match = captures.get(1).unwrap();
457            let version_str = String::from_utf8_lossy(version_match.as_bytes());
458            version_str.parse::<u32>().ok()
459        });
460
461        match version {
462            Some(version) if version < expected_version => {
463                self.print_warn(format!(
464                    "{} version ({}) out of date, expected {}",
465                    name, version, expected_version_spec
466                ));
467            }
468            None => {
469                let version = String::from_utf8_lossy(&result.stdout);
470                self.print_warn(format!(
471                    "Could not parse {} version ({})",
472                    name,
473                    version.trim()
474                ));
475            }
476            Some(_) => {}
477        }
478    }
479
480    fn executable_file(&self) -> PathBuf {
481        if cfg!(windows) {
482            self.out_dir().join("build/parser.exe")
483        } else {
484            self.out_dir().join("build/parser")
485        }
486    }
487
488    fn parser_file(&self) -> PathBuf {
489        self.out_dir().join("parser.rs")
490    }
491
492    fn harness_file(&self) -> PathBuf {
493        self.out_dir().join("build/harness.rs")
494    }
495
496    fn loader_file(&self) -> PathBuf {
497        self.out_dir().join("build/loader.js")
498    }
499
500    fn ir_file(&self) -> PathBuf {
501        self.out_dir().join("build/ir.json")
502    }
503
504    fn out_dir(&self) -> &Path {
505        self.opts
506            .out_dir
507            .as_ref()
508            .map(|buf| buf as &Path)
509            .unwrap_or_else(|| Path::new("peg-pack-out"))
510    }
511
512    /// Prints a progress indicator, replacing the previous if possible
513    fn set_indicator(&mut self, indicator: impl AsRef<str>) {
514        self.clear_indicator();
515
516        let mut color = ColorSpec::new();
517        color.set_italic(true);
518        color.set_fg(Some(Color::Blue));
519
520        let mut reset = ColorSpec::new();
521        reset.set_reset(true);
522
523        let _ = self.stderr.set_color(&color);
524        let _ = writeln!(self.stderr, "{}", indicator.as_ref());
525        let _ = self.stderr.set_color(&reset);
526
527        self.active_indicator = true;
528    }
529
530    /// Remove the last progress indicator if possible
531    fn clear_indicator(&mut self) {
532        if self.active_indicator && self.stderr.supports_color() {
533            let _ = write!(self.stderr, "\x1b[F\x1b[K");
534        }
535
536        self.active_indicator = false;
537    }
538
539    /// Prints the output of a command
540    fn print_output(&mut self, output: &Output) {
541        self.print_output_stream("stdout", &output.stdout);
542        self.print_output_stream("stderr", &output.stderr);
543    }
544
545    fn print_output_stream(&mut self, name: &str, stream: &[u8]) {
546        if stream.is_empty() {
547            return;
548        }
549
550        let content = String::from_utf8_lossy(stream);
551
552        for line in content.lines() {
553            self.print_color(Color::Cyan, true);
554            self.print(name);
555            self.print(": ");
556            self.print_reset();
557            self.println(line);
558        }
559    }
560
561    /// Print an error message and exit
562    fn exit_with_error(&mut self, message: impl AsRef<str>) -> ! {
563        self.print_error(message);
564        exit(1);
565    }
566
567    /// Print an error message and command output, then exit
568    fn exit_with_error_and_output(&mut self, message: impl AsRef<str>, output: &Output) -> ! {
569        self.print_error(message);
570        self.print_output(output);
571        exit(1);
572    }
573
574    fn print_error(&mut self, message: impl AsRef<str>) {
575        self.print_error_heading();
576        self.println(message);
577    }
578
579    fn print_warn(&mut self, message: impl AsRef<str>) {
580        self.print_warn_heading();
581        self.println(message);
582    }
583
584    fn print_error_heading(&mut self) {
585        self.print_color(Color::Red, true);
586        self.print("error: ");
587        self.print_reset();
588    }
589
590    fn print_warn_heading(&mut self) {
591        self.print_color(Color::Yellow, true);
592        self.print("warn: ");
593        self.print_reset();
594    }
595
596    fn print(&mut self, message: impl AsRef<str>) {
597        self.clear_indicator();
598        let _ = write!(self.stderr, "{}", message.as_ref());
599    }
600
601    fn println(&mut self, message: impl AsRef<str>) {
602        self.clear_indicator();
603        let _ = writeln!(self.stderr, "{}", message.as_ref());
604    }
605
606    fn print_color(&mut self, color: Color, bold: bool) {
607        self.clear_indicator();
608        let mut color_spec = ColorSpec::new();
609        color_spec.set_fg(Some(color));
610        color_spec.set_bold(bold);
611        let _ = self.stderr.set_color(&color_spec);
612    }
613
614    fn print_reset(&mut self) {
615        self.clear_indicator();
616        let mut reset_color = ColorSpec::new();
617        reset_color.set_reset(true);
618        let _ = self.stderr.set_color(&reset_color);
619    }
620}