waterui_cli/platforming/
backend.rs1use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 android::backend::AndroidBackend,
9 apple::backend::AppleBackend,
10 build::BuildOptions,
11 device::Artifact,
12 esp32::backend::Esp32Backend,
13 gtk4::backend::Gtk4Backend,
14 hydrolysis::backend::HydrolysisBackend,
15 platform::{PackageOptions, TargetPlatform},
16 project::Project,
17};
18
19#[derive(Debug, Serialize, Deserialize, Clone, Default)]
23pub struct Backends {
24 #[serde(default, skip_serializing_if = "String::is_empty")]
28 path: String,
29 android: Option<AndroidBackend>,
30 apple: Option<AppleBackend>,
31 gtk4: Option<Gtk4Backend>,
32 hydrolysis: Option<HydrolysisBackend>,
33 winui: Option<crate::winui::backend::WinUiBackend>,
34 esp32: Option<Esp32Backend>,
35}
36
37impl Backends {
38 #[must_use]
40 pub const fn is_empty(&self) -> bool {
41 self.android.is_none()
42 && self.apple.is_none()
43 && self.gtk4.is_none()
44 && self.hydrolysis.is_none()
45 && self.winui.is_none()
46 && self.esp32.is_none()
47 }
48
49 #[cfg(test)]
50 pub(crate) fn set_esp32_for_tests(&mut self, backend: Esp32Backend) {
51 self.esp32 = Some(backend);
52 }
53
54 #[must_use]
61 pub const fn configures_backend_projects(&self) -> bool {
62 self.android.is_some()
63 || self.apple.is_some()
64 || self.gtk4.is_some()
65 || self.hydrolysis.is_some()
66 || self.winui.is_some()
67 }
68
69 #[must_use]
71 pub fn path(&self) -> &Path {
72 Path::new(&self.path)
73 }
74
75 pub fn set_path(&mut self, path: impl Into<String>) {
77 self.path = path.into();
78 }
79
80 #[must_use]
82 pub const fn android(&self) -> Option<&AndroidBackend> {
83 self.android.as_ref()
84 }
85
86 #[must_use]
88 pub const fn apple(&self) -> Option<&AppleBackend> {
89 self.apple.as_ref()
90 }
91
92 pub fn set_apple(&mut self, backend: AppleBackend) {
94 self.apple = Some(backend);
95 }
96
97 pub fn clear_apple(&mut self) {
99 self.apple = None;
100 }
101
102 pub fn set_android(&mut self, backend: AndroidBackend) {
104 self.android = Some(backend);
105 }
106
107 pub fn clear_android(&mut self) {
109 self.android = None;
110 }
111
112 #[must_use]
114 pub const fn gtk4(&self) -> Option<&Gtk4Backend> {
115 self.gtk4.as_ref()
116 }
117
118 pub fn set_gtk4(&mut self, backend: Gtk4Backend) {
120 self.gtk4 = Some(backend);
121 }
122
123 pub fn clear_gtk4(&mut self) {
125 self.gtk4 = None;
126 }
127
128 #[must_use]
130 pub const fn hydrolysis(&self) -> Option<&HydrolysisBackend> {
131 self.hydrolysis.as_ref()
132 }
133
134 pub fn set_hydrolysis(&mut self, backend: HydrolysisBackend) {
136 self.hydrolysis = Some(backend);
137 }
138
139 pub fn clear_hydrolysis(&mut self) {
141 self.hydrolysis = None;
142 }
143
144 #[must_use]
146 pub const fn winui(&self) -> Option<&crate::winui::backend::WinUiBackend> {
147 self.winui.as_ref()
148 }
149
150 pub fn set_winui(&mut self, backend: crate::winui::backend::WinUiBackend) {
152 self.winui = Some(backend);
153 }
154
155 pub fn clear_winui(&mut self) {
157 self.winui = None;
158 }
159
160 #[must_use]
162 pub const fn esp32(&self) -> Option<&Esp32Backend> {
163 self.esp32.as_ref()
164 }
165
166 pub fn set_esp32(&mut self, backend: Esp32Backend) {
168 self.esp32 = Some(backend);
169 }
170
171 pub fn clear_esp32(&mut self) {
173 self.esp32 = None;
174 }
175}
176
177#[derive(Debug, thiserror::Error)]
179pub enum FailToInitBackend {
180 #[error("Failed to write template files: {0}")]
182 Io(#[from] std::io::Error),
183 #[error("Invalid backend configuration: {0}")]
186 Config(#[source] eyre::Error),
187}
188
189pub trait Backend: Sized + Send + Sync {
197 const DEFAULT_PATH: &'static str;
199
200 const CACHE_PATHS: &'static [&'static str];
205
206 fn path(&self) -> &Path;
210
211 fn init(project: &Project) -> impl Future<Output = Result<Self, FailToInitBackend>> + Send;
216
217 fn supports(&self, platform: TargetPlatform) -> bool;
223
224 fn build(
228 &self,
229 project: &Project,
230 platform: TargetPlatform,
231 options: BuildOptions,
232 ) -> impl Future<Output = eyre::Result<PathBuf>> + Send;
233
234 fn package(
238 &self,
239 project: &Project,
240 platform: TargetPlatform,
241 options: PackageOptions,
242 ) -> impl Future<Output = eyre::Result<Artifact>> + Send;
243
244 fn clean(
246 &self,
247 project: &Project,
248 platform: TargetPlatform,
249 ) -> impl Future<Output = eyre::Result<()>> + Send;
250}
251
252pub async fn reinit_backend<B: Backend>(project: &Project) -> Result<B, FailToInitBackend> {
264 let backend_path = project.backend_path::<B>();
265
266 if backend_path.exists() {
267 let cache_paths: std::collections::HashSet<&str> = B::CACHE_PATHS.iter().copied().collect();
269
270 let entries = std::fs::read_dir(&backend_path)?;
272 for entry in entries {
273 let entry = entry?;
274 let name = entry.file_name();
275 let name_str = name.to_string_lossy();
276
277 if !cache_paths.contains(&*name_str) {
278 let path = entry.path();
279 if path.is_dir() {
280 std::fs::remove_dir_all(&path)?;
281 } else {
282 std::fs::remove_file(&path)?;
283 }
284 }
285 }
286 }
287
288 B::init(project).await
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
299 fn esp32_device_config_is_not_backend_project_configuration() {
300 let mut backends = Backends::default();
301 assert!(!backends.configures_backend_projects());
302
303 backends.set_esp32_for_tests(Esp32Backend::new());
304 assert!(!backends.configures_backend_projects());
305 assert!(!backends.is_empty());
306
307 backends.set_gtk4(Gtk4Backend::default());
308 assert!(backends.configures_backend_projects());
309 }
310}