wl_client_builder/
builder.rs

1use {
2    crate::{
3        formatter::{format_interface_file, format_mod_file, format_protocol_file},
4        parser::{ParserError, parse},
5    },
6    std::{
7        env::VarError,
8        fs::File,
9        io::{self, BufWriter, Write},
10        path::{Path, PathBuf},
11    },
12    thiserror::Error,
13};
14
15#[derive(Debug, Error)]
16enum BuilderError {
17    #[error("Could not read {}", .0.display())]
18    ReadFile(PathBuf, #[source] io::Error),
19    #[error("Could not open {} for reading", .0.display())]
20    OpenDir(PathBuf, #[source] io::Error),
21    #[error("Could not read from {}", .0.display())]
22    ReadDir(PathBuf, #[source] io::Error),
23    #[error("Could not parse {}", .0.display())]
24    ParseFile(PathBuf, #[source] ParserError),
25    #[error("Could not format {}", .0.display())]
26    FormatFile(PathBuf, #[source] io::Error),
27    #[error("Could not determine OUT_DIR")]
28    OutDir(#[source] VarError),
29    #[error("Could not create {}", .0.display())]
30    CreateDir(PathBuf, #[source] io::Error),
31    #[error("Could not open {} for writing", .0.display())]
32    OpenFile(PathBuf, #[source] io::Error),
33}
34
35/// A builder for `wl-client` wrappers.
36pub struct Builder {
37    build_script: bool,
38    add_default_dir: bool,
39    target_dir: Option<PathBuf>,
40    files: Vec<PathBuf>,
41    dirs: Vec<PathBuf>,
42    wl_client_path: Option<String>,
43}
44
45impl Default for Builder {
46    fn default() -> Self {
47        Self {
48            build_script: true,
49            add_default_dir: true,
50            target_dir: Default::default(),
51            files: Default::default(),
52            dirs: Default::default(),
53            wl_client_path: None,
54        }
55    }
56}
57
58impl Builder {
59    /// Sets the rust module path to the `wl-client` crate.
60    ///
61    /// By default, the generated code assumes that the `wl-client` crate is accessible
62    /// via `::wl_client`.
63    pub fn wl_client_path(mut self, path: &str) -> Self {
64        self.wl_client_path = Some(path.into());
65        self
66    }
67
68    /// Adds a protocol XML file.
69    pub fn xml_file(mut self, path: impl AsRef<Path>) -> Self {
70        self.files.push(path.as_ref().to_path_buf());
71        self
72    }
73
74    /// Adds a protocol XML dir.
75    ///
76    /// This behaves as if all XML files in this directory (but not in any
77    /// sub-directories) had been added with [`Builder::xml_file`].
78    pub fn xml_dir(mut self, path: impl AsRef<Path>) -> Self {
79        self.dirs.push(path.as_ref().to_path_buf());
80        self
81    }
82
83    /// Enables or disables the default `wayland-protocols` dir.
84    ///
85    /// By default, the builder will try to load XML files from the `wayland-protocols`
86    /// directory relative to the current working directory.
87    pub fn with_default_dir(mut self, default_dir: bool) -> Self {
88        self.add_default_dir = default_dir;
89        self
90    }
91
92    /// Enables or disables `build.rs` logic.
93    ///
94    /// By default, the builder assumes that it is being used from `build.rs`. It will
95    /// emit `cargo::` messages and treats a relative [`Builder::target_dir`] as relative
96    /// to `$OUT_DIR`.
97    pub fn for_build_rs(mut self, build_rs: bool) -> Self {
98        self.build_script = build_rs;
99        self
100    }
101
102    /// The target directory into which to generate the `mod.rs`.
103    ///
104    /// By default, the target directory is `wayland-protocols`.
105    ///
106    /// If [`Builder::for_build_rs`] is enabled, then a relative target directory will be
107    /// interpreted relative to `$OUT_DIR`.
108    pub fn target_dir(mut self, target_dir: impl AsRef<Path>) -> Self {
109        self.target_dir = Some(target_dir.as_ref().to_path_buf());
110        self
111    }
112
113    /// Generates the code.
114    pub fn build(self) -> Result<(), crate::Error> {
115        self.build_().map_err(|e| crate::Error(Box::new(e)))
116    }
117
118    fn build_(mut self) -> Result<(), BuilderError> {
119        let mut target_dir = PathBuf::new();
120        if self.build_script {
121            let out_dir = std::env::var("OUT_DIR").map_err(BuilderError::OutDir)?;
122            target_dir.push(out_dir);
123        }
124        if let Some(d) = &self.target_dir {
125            target_dir.push(d);
126        } else {
127            target_dir.push("wayland-protocols");
128        }
129        create_dir(&target_dir)?;
130
131        let mut protocol_objects = vec![];
132
133        if self.add_default_dir {
134            self.dirs.push(PathBuf::from("wayland-protocols"));
135        }
136        for dir in self.dirs {
137            if self.build_script {
138                println!("cargo::rerun-if-changed={}", dir.display());
139            }
140            let iter = match std::fs::read_dir(&dir) {
141                Ok(c) => c,
142                Err(e) => return Err(BuilderError::OpenDir(dir, e)),
143            };
144            for file in iter {
145                let file = match file {
146                    Ok(f) => f,
147                    Err(e) => return Err(BuilderError::ReadDir(dir, e)),
148                };
149                if !file.file_name().as_encoded_bytes().ends_with(b".xml") {
150                    continue;
151                }
152                self.files.push(file.path());
153            }
154        }
155        for file in self.files {
156            if self.build_script {
157                println!("cargo::rerun-if-changed={}", file.display());
158            }
159            let contents = match std::fs::read(&file) {
160                Ok(c) => c,
161                Err(e) => return Err(BuilderError::ReadFile(file, e)),
162            };
163            let protocols = match parse(&contents) {
164                Ok(c) => c,
165                Err(e) => return Err(BuilderError::ParseFile(file, e)),
166            };
167            for protocol in protocols {
168                let protocol_file = format!("{}.rs", protocol.name);
169                format_file(&target_dir.join(&protocol_file), |f| {
170                    format_protocol_file(f, &protocol)
171                })?;
172                let dir = target_dir.join(&protocol.name);
173                create_dir(&dir)?;
174                let mut interfaces = vec![];
175                for interface in protocol.interfaces {
176                    let file_name = format!("{}.rs", interface.name);
177                    format_file(&dir.join(&file_name), |f| {
178                        format_interface_file(
179                            f,
180                            self.wl_client_path.as_deref().unwrap_or("::wl_client"),
181                            &interface,
182                        )
183                    })?;
184                    let mut enums = vec![];
185                    for enum_ in interface.enums {
186                        enums.push(enum_.name);
187                    }
188                    interfaces.push((interface.name, enums));
189                }
190                protocol_objects.push((protocol.name, interfaces));
191            }
192        }
193
194        format_file(&target_dir.join("mod.rs"), |f| {
195            format_mod_file(f, &protocol_objects)
196        })?;
197        Ok(())
198    }
199}
200
201fn create_dir(path: &Path) -> Result<(), BuilderError> {
202    if let Err(e) = std::fs::create_dir_all(path) {
203        return Err(BuilderError::CreateDir(path.to_owned(), e));
204    }
205    Ok(())
206}
207
208fn open_file(path: &Path) -> Result<BufWriter<File>, BuilderError> {
209    let file = File::options()
210        .write(true)
211        .create(true)
212        .truncate(true)
213        .open(path);
214    match file {
215        Ok(f) => Ok(BufWriter::new(f)),
216        Err(e) => Err(BuilderError::OpenFile(path.to_owned(), e)),
217    }
218}
219
220fn format_file(
221    path: &Path,
222    f: impl FnOnce(&mut BufWriter<File>) -> io::Result<()>,
223) -> Result<(), BuilderError> {
224    let mut file = open_file(path)?;
225    let mut res = f(&mut file);
226    if res.is_ok() {
227        res = file.flush();
228    }
229    if let Err(e) = res {
230        return Err(BuilderError::FormatFile(path.to_owned(), e));
231    }
232    Ok(())
233}