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]
63 pub fn configures_backend_projects(&self) -> bool {
64 self.android
65 .as_ref()
66 .is_some_and(AndroidBackend::configures_project)
67 || self
68 .apple
69 .as_ref()
70 .is_some_and(AppleBackend::configures_project)
71 || self.gtk4.is_some()
72 || self.hydrolysis.is_some()
73 || self.winui.is_some()
74 }
75
76 #[must_use]
78 pub fn path(&self) -> &Path {
79 Path::new(&self.path)
80 }
81
82 pub fn set_path(&mut self, path: impl Into<String>) {
84 self.path = path.into();
85 }
86
87 #[must_use]
89 pub const fn android(&self) -> Option<&AndroidBackend> {
90 self.android.as_ref()
91 }
92
93 #[must_use]
95 pub const fn apple(&self) -> Option<&AppleBackend> {
96 self.apple.as_ref()
97 }
98
99 pub fn set_apple(&mut self, backend: AppleBackend) {
101 self.apple = Some(backend);
102 }
103
104 pub fn clear_apple(&mut self) {
106 self.apple = None;
107 }
108
109 pub fn set_android(&mut self, backend: AndroidBackend) {
111 self.android = Some(backend);
112 }
113
114 pub fn clear_android(&mut self) {
116 self.android = None;
117 }
118
119 #[must_use]
121 pub const fn gtk4(&self) -> Option<&Gtk4Backend> {
122 self.gtk4.as_ref()
123 }
124
125 pub fn set_gtk4(&mut self, backend: Gtk4Backend) {
127 self.gtk4 = Some(backend);
128 }
129
130 pub fn clear_gtk4(&mut self) {
132 self.gtk4 = None;
133 }
134
135 #[must_use]
137 pub const fn hydrolysis(&self) -> Option<&HydrolysisBackend> {
138 self.hydrolysis.as_ref()
139 }
140
141 pub fn set_hydrolysis(&mut self, backend: HydrolysisBackend) {
143 self.hydrolysis = Some(backend);
144 }
145
146 pub fn clear_hydrolysis(&mut self) {
148 self.hydrolysis = None;
149 }
150
151 #[must_use]
153 pub const fn winui(&self) -> Option<&crate::winui::backend::WinUiBackend> {
154 self.winui.as_ref()
155 }
156
157 pub fn set_winui(&mut self, backend: crate::winui::backend::WinUiBackend) {
159 self.winui = Some(backend);
160 }
161
162 pub fn clear_winui(&mut self) {
164 self.winui = None;
165 }
166
167 #[must_use]
169 pub const fn esp32(&self) -> Option<&Esp32Backend> {
170 self.esp32.as_ref()
171 }
172
173 pub fn set_esp32(&mut self, backend: Esp32Backend) {
175 self.esp32 = Some(backend);
176 }
177
178 pub fn clear_esp32(&mut self) {
180 self.esp32 = None;
181 }
182}
183
184#[derive(Debug, thiserror::Error)]
186pub enum FailToInitBackend {
187 #[error("Failed to write template files: {0}")]
189 Io(#[from] std::io::Error),
190 #[error("Invalid backend configuration: {0}")]
193 Config(#[source] eyre::Error),
194}
195
196pub trait Backend: Sized + Send + Sync {
204 const DEFAULT_PATH: &'static str;
206
207 const CACHE_PATHS: &'static [&'static str];
212
213 fn path(&self) -> &Path;
217
218 fn init(project: &Project) -> impl Future<Output = Result<Self, FailToInitBackend>> + Send;
223
224 fn supports(&self, platform: TargetPlatform) -> bool;
230
231 fn build(
235 &self,
236 project: &Project,
237 platform: TargetPlatform,
238 options: BuildOptions,
239 ) -> impl Future<Output = eyre::Result<PathBuf>> + Send;
240
241 fn package(
245 &self,
246 project: &Project,
247 platform: TargetPlatform,
248 options: PackageOptions,
249 ) -> impl Future<Output = eyre::Result<Artifact>> + Send;
250
251 fn clean(
253 &self,
254 project: &Project,
255 platform: TargetPlatform,
256 ) -> impl Future<Output = eyre::Result<()>> + Send;
257}
258
259pub async fn reinit_backend<B: Backend>(project: &Project) -> Result<B, FailToInitBackend> {
271 let backend_path = project.backend_path::<B>();
272
273 if backend_path.exists() {
274 let cache_paths: std::collections::HashSet<&str> = B::CACHE_PATHS.iter().copied().collect();
276
277 let entries = std::fs::read_dir(&backend_path)?;
279 for entry in entries {
280 let entry = entry?;
281 let name = entry.file_name();
282 let name_str = name.to_string_lossy();
283
284 if !cache_paths.contains(&*name_str) {
285 let path = entry.path();
286 if path.is_dir() {
287 std::fs::remove_dir_all(&path)?;
288 } else {
289 std::fs::remove_file(&path)?;
290 }
291 }
292 }
293 }
294
295 B::init(project).await
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
306 fn esp32_device_config_is_not_backend_project_configuration() {
307 let mut backends = Backends::default();
308 assert!(!backends.configures_backend_projects());
309
310 backends.set_esp32_for_tests(Esp32Backend::new());
311 assert!(!backends.configures_backend_projects());
312 assert!(!backends.is_empty());
313
314 backends.set_gtk4(Gtk4Backend::default());
315 assert!(backends.configures_backend_projects());
316 }
317
318 #[test]
322 fn android_backend_path_alone_is_not_project_configuration() {
323 let mut backends = Backends::default();
324 backends.set_android(AndroidBackend::new().with_backend_path("/opt/android-backend"));
325 assert!(!backends.configures_backend_projects());
326
327 backends.set_android(
328 AndroidBackend::new()
329 .with_backend_path("/opt/android-backend")
330 .with_project_path("droid"),
331 );
332 assert!(backends.configures_backend_projects());
333 }
334
335 #[test]
340 fn playground_manifest_may_select_the_android_runtime_source() {
341 let manifest: crate::project::Manifest = toml::from_str(
342 r#"
343 [package]
344 type = "playground"
345 name = "Demo"
346 bundle_identifier = "dev.waterui.demo"
347
348 [backends.android]
349 backend_path = "/opt/android-backend"
350 "#,
351 )
352 .expect("manifest parses");
353 assert_eq!(
354 manifest.package.package_type,
355 crate::project::PackageType::Playground
356 );
357 assert!(!manifest.backends.configures_backend_projects());
358 assert_eq!(
359 manifest
360 .backends
361 .android()
362 .and_then(|b| b.backend_path().map(str::to_string)),
363 Some("/opt/android-backend".to_string())
364 );
365
366 let scaffolded: crate::project::Manifest = toml::from_str(
367 r#"
368 [package]
369 type = "playground"
370 name = "Demo"
371 bundle_identifier = "dev.waterui.demo"
372
373 [backends.android]
374 project_path = "droid"
375 "#,
376 )
377 .expect("manifest parses");
378 assert!(scaffolded.backends.configures_backend_projects());
379 }
380}