bebop_tools/
lib.rs

1use std::collections::LinkedList;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6#[cfg(feature = "downloader")]
7pub use downloader::*;
8#[cfg(feature = "format")]
9use format::*;
10
11/// Configurable compiler path. By default it will use the downloaded executeable or assume it is in PATH.
12pub static mut COMPILER_PATH: Option<PathBuf> = None;
13pub static mut GENERATED_PREFIX: Option<String> = None;
14
15#[cfg(feature = "downloader")]
16mod downloader;
17#[cfg(feature = "format")]
18mod format;
19
20#[derive(Debug)]
21pub struct BuildConfig {
22    /// Whether to skip generating the autogen module doc header in the files. Default: `false`.
23    pub skip_generated_notice: bool,
24    /// Whether to generate a `mod.rs` file of all the source. Default: `true`.
25    pub generate_module_file: bool,
26    /// Whether files should be automatically formatted after being generated.
27    /// Does nothing if feature `format` is not enabled. Default: `true`.
28    pub format_files: bool,
29}
30
31impl Default for BuildConfig {
32    fn default() -> Self {
33        Self {
34            skip_generated_notice: false,
35            generate_module_file: true,
36            format_files: true,
37        }
38    }
39}
40
41/// Build all schemas in a given directory and write them to the destination directory including a
42/// `mod.rs` file.
43///
44/// **WARNING: THIS DELETES DATA IN THE DESTINATION DIRECTORY** use something like `src/bebop` or `src/generated`.
45pub fn build_schema_dir(
46    source: impl AsRef<Path>,
47    destination: impl AsRef<Path>,
48    config: &BuildConfig,
49) {
50    if !destination.as_ref().exists() {
51        fs::create_dir_all(destination.as_ref()).unwrap();
52    }
53
54    // clean all previously built files
55    fs::read_dir(destination.as_ref())
56        .unwrap()
57        .filter_map(|entry| {
58            let entry = entry.unwrap();
59            let name = entry.file_name().to_str().unwrap().to_string();
60            if entry.file_type().unwrap().is_file() && name != "mod.rs" {
61                Some(name)
62            } else {
63                None
64            }
65        })
66        .for_each(|file| fs::remove_file(PathBuf::from(destination.as_ref()).join(file)).unwrap());
67
68    // build all files and update lib.rs
69    let files = recurse_schema_dir(source, destination.as_ref(), config);
70
71    // update the mod file
72    if config.generate_module_file {
73        let mod_file_path = PathBuf::from(destination.as_ref()).join("mod.rs");
74        fs::write(
75            &mod_file_path,
76            &files
77                .into_iter()
78                .map(|mut schema_name| {
79                    schema_name.insert_str(0, "pub mod ");
80                    schema_name.push(';');
81                    schema_name.push('\n');
82                    schema_name
83                })
84                .collect::<String>(),
85        )
86        .unwrap();
87
88        #[cfg(feature = "format")]
89        if config.format_files {
90            fmt_file(mod_file_path);
91        }
92    }
93}
94
95/// Build a single schema file and write it to the destination file.
96///
97/// **WARNING: THIS OVERWRITES THE DESTINATION FILE.**
98pub fn build_schema(schema: impl AsRef<Path>, destination: impl AsRef<Path>, config: &BuildConfig) {
99    let (schema, destination) = (schema.as_ref(), destination.as_ref());
100    let compiler_path = compiler_path();
101    println!("cargo:rerun-if-changed={}", compiler_path.to_str().unwrap());
102    println!("cargo:rerun-if-changed={}", schema.to_str().unwrap());
103
104    let mut cmd = Command::new(compiler_path);
105    let output = cmd
106        .arg("-i")
107        .arg(schema)
108        .arg("build")
109        .arg("--generator")
110        .arg(format!(
111            "rust:{},noEmitNotice={}",
112            destination.to_str().unwrap(),
113            config.skip_generated_notice
114        ))
115        .output()
116        .expect("Could not run bebopc");
117
118    if !(output.status.success()) {
119        println!(
120            "cargo:warning=Failed to build schema {}",
121            schema.to_str().unwrap()
122        );
123        for line in String::from_utf8(output.stdout).unwrap().lines() {
124            println!("cargo:warning=STDOUT: {}", line);
125        }
126        for line in String::from_utf8(output.stderr).unwrap().lines() {
127            println!("cargo:warning=STDERR: {}", line);
128        }
129        panic!("Failed to build schema!");
130    }
131
132    #[cfg(feature = "format")]
133    if config.format_files {
134        fmt_file(destination);
135    }
136}
137
138fn recurse_schema_dir(
139    dir: impl AsRef<Path>,
140    dest: impl AsRef<Path>,
141    config: &BuildConfig,
142) -> LinkedList<String> {
143    let mut list = LinkedList::new();
144    for dir_entry in fs::read_dir(&dir).unwrap() {
145        let dir_entry = dir_entry.unwrap();
146        let file_type = dir_entry.file_type().unwrap();
147        let file_path = PathBuf::from(dir.as_ref()).join(dir_entry.file_name());
148        if file_type.is_dir() {
149            if dir_entry.file_name() == "ShouldFail" {
150                // do nothing
151            } else {
152                list.append(&mut recurse_schema_dir(&file_path, dest.as_ref(), config));
153            }
154        } else if file_type.is_file()
155            && file_path
156                .extension()
157                .map(|s| s.to_str().unwrap())
158                .unwrap_or("")
159                == "bop"
160        {
161            let fname = format!(
162                "{}{}",
163                unsafe { GENERATED_PREFIX.as_deref().unwrap_or_else(|| "".into()) },
164                file_stem(file_path.as_path())
165            );
166            build_schema(
167                canonicalize(file_path.to_str().unwrap()),
168                canonicalize(&dest).join(fname.clone() + ".rs"),
169                config,
170            );
171            list.push_back(fname);
172        } else {
173            // do nothing
174        }
175    }
176    list
177}
178
179fn file_stem(path: impl AsRef<Path>) -> String {
180    path.as_ref()
181        .file_stem()
182        .unwrap()
183        .to_str()
184        .unwrap()
185        .to_string()
186}
187
188fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
189    let p = path
190        .as_ref()
191        .canonicalize()
192        .unwrap()
193        .to_str()
194        .unwrap()
195        .to_string();
196    if p.starts_with(r"\\?\") {
197        p.strip_prefix(r"\\?\").unwrap()
198    } else {
199        &p
200    }
201    .into()
202}
203
204fn compiler_path() -> PathBuf {
205    (unsafe { COMPILER_PATH.clone() }).unwrap_or_else(|| canonicalize("bebopc"))
206}