1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Copyright (C) 2022, Alex Badics
// This file is part of peginator
// Licensed under the MIT license. See LICENSE file in the project root for details.

//! Buildscript helpers for peginator
//!
//! See [Compile] for examples.

use std::{
    ffi::OsStr,
    fs::{self, File},
    io::Read,
    path::PathBuf,
    process::Command,
};

use anyhow::Result;
use colored::*;

use crate::codegen::{generate_source_header, CodegenGrammar, CodegenSettings};
use crate::grammar::Grammar;
use crate::{PegParser, PrettyParseError};

/// Compiles peginator grammars into rust code with a builder interface.
///
/// It only recompiles files if it detects (based on the generated file header in the `.rs` file)
/// change in either the peginator library, or the grammar file.
///
/// It is meant to be used as `peginator::buildscript::Compile`, hence the generic name.
///
/// Example `build.rs` for using a single grammar file and putting the result in the target directory:
/// ```no_run
///# #[allow(clippy::needless_doctest_main)]
/// fn main() {
///     peginator::buildscript::Compile::file("my_grammar.ebnf")
///        .destination(format!("{}/my_grammar.rs", std::env::var("OUT_DIR").unwrap()))
///        .format()
///        .run_exit_on_error();
///     println!("cargo:rerun-if-changed=my_grammar.ebnf");
/// }
/// ```
///
/// Importing this grammar:
/// ```ignore
/// mod grammar { include!(concat!(env!("OUT_DIR"), "/my_grammar.rs")); }
/// ```
///
/// Example `build.rs` for multiple grammar files in the src directory, putting compiled files next to
/// their grammar definitions:
/// ```no_run
///# #[allow(clippy::needless_doctest_main)]
/// fn main() {
///     peginator::buildscript::Compile::directory("src")
///        .format()
///        .run_exit_on_error()
/// }
/// ```
///
#[derive(Debug)]
#[must_use]
pub struct Compile {
    source_path: PathBuf,
    destination_path: Option<PathBuf>,
    format: bool,
    recursive: bool,
    use_peginator_build_time: bool,
    settings: CodegenSettings,
    prefix: String,
}

impl Compile {
    fn default() -> Self {
        Compile {
            source_path: PathBuf::new(),
            destination_path: None,
            format: false,
            recursive: false,
            use_peginator_build_time: false,
            settings: Default::default(),
            prefix: String::new(),
        }
    }
    /// Run compilation on a whole directory run.
    ///
    /// The whole directory is recursively searched for files with the `.ebnf` extension, and
    /// compiled to rust code with the same filename but with `.rs` extension.
    pub fn directory<T: Into<PathBuf>>(filename: T) -> Self {
        Compile {
            source_path: filename.into(),
            recursive: true,
            ..Self::default()
        }
    }

    /// Run compilation on a single file.
    ///
    /// The file will be compiled. If destination is not given, it will be the same filename in the
    /// same directory, but with `.rs` extension.
    pub fn file<T: Into<PathBuf>>(filename: T) -> Self {
        Compile {
            source_path: filename.into(),
            ..Self::default()
        }
    }

    /// Destination file name.
    ///
    /// This option only used if running on a single file.
    pub fn destination<T: Into<PathBuf>>(self, filename: T) -> Self {
        Compile {
            destination_path: Some(filename.into()),
            ..self
        }
    }

    /// Format .rs files with rustfmt after grammar compilation.
    pub fn format(self) -> Self {
        Compile {
            format: true,
            ..self
        }
    }

    /// Include the build time of the peginator library in the peginator version part of the
    /// generated header.
    ///
    /// In effect, this will recompile grammar files if the peginator library changed without a
    /// version bump. This is mostly only useful during the development of peginator itself, and is
    /// only used in the peginator_tests package.
    pub fn use_peginator_build_time(self) -> Self {
        Compile {
            use_peginator_build_time: true,
            ..self
        }
    }

    /// Use a specific set of derives when declaring structs.
    ///
    /// The default set is `#[derive(Debug, Clone)]`
    pub fn derives(mut self, derives: Vec<String>) -> Self {
        self.settings.derives = derives;
        self
    }

    /// Set a prefix (code) that will be pasted into the file between the header and the generated code
    ///
    /// Useful for `use` declarations or maybe custom structs.
    pub fn prefix(self, prefix: String) -> Self {
        Compile { prefix, ..self }
    }

    fn run_on_single_file(&self, source: &PathBuf, destination: &PathBuf) -> Result<()> {
        let grammar = fs::read_to_string(source)?;
        let source_header = format!(
            "{}\n{}",
            generate_source_header(&grammar, self.use_peginator_build_time),
            self.prefix
        );
        if let Ok(f) = File::open(destination) {
            let mut existing_header = String::new();
            if f.take(source_header.bytes().count() as u64)
                .read_to_string(&mut existing_header)
                .is_ok()
                && source_header == existing_header
            {
                return Ok(());
            }
        };

        let parsed_grammar = Grammar::parse(&grammar)
            .map_err(|err| PrettyParseError::from_parse_error(&err, &grammar, source.to_str()))?;
        let generated_code = format!(
            "{}\n{}",
            source_header,
            parsed_grammar.generate_code(&self.settings)?
        );
        fs::write(destination, &generated_code)?;
        if self.format {
            Command::new("rustfmt").arg(destination).status()?;
        };
        Ok(())
    }

    fn run_recursively(&self, source: &PathBuf) -> Result<()> {
        if source.is_dir() {
            source
                .read_dir()?
                .try_for_each(|c| self.run_recursively(&c?.path()))
        } else if source.extension().and_then(OsStr::to_str) == Some("ebnf") {
            self.run_on_single_file(source, &source.with_extension("rs"))
        } else {
            Ok(())
        }
    }

    /// Run the compilation, returning an error.
    ///
    /// In case of a parse error, [crate::PrettyParseError] is thrown, which will print a pretty
    /// error with `format!` or `print!`.
    pub fn run(self) -> Result<()> {
        if self.recursive {
            self.run_recursively(&self.source_path)
        } else {
            self.run_on_single_file(
                &self.source_path.clone(),
                &self
                    .destination_path
                    .clone()
                    .unwrap_or_else(|| self.source_path.with_extension("rs")),
            )
        }
    }

    /// Run the compilation, and exit with an exit code in case of an error.
    ///
    /// It also makes sure to pretty-print the error, should one occur.
    pub fn run_exit_on_error(self) {
        colored::control::set_override(true);
        let result = self.run();
        if let Err(error) = result {
            // I absolutely hate how this is a global, because there is no way to know if it was forced
            // already. Thankfully we are exiting right after this.
            eprintln!(
                "{red_error}{colon} {error}",
                red_error = "error".red().bold(),
                colon = ":".bold().white(),
                error = error
            );
            std::process::exit(1);
        }
    }
}