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