Skip to main content

waterui_cli/platforming/
backend.rs

1//! Backend configuration and initialization for `WaterUI` projects.
2
3use 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/// Configuration for all backends in a `WaterUI` project.
20///
21/// `[backend]` in `Water.toml`
22#[derive(Debug, Serialize, Deserialize, Clone, Default)]
23pub struct Backends {
24    /// Base path for all backends, relative to project root.
25    /// Empty string means project root for app manifests.
26    /// Playground projects do not persist managed backend paths in `Water.toml`.
27    #[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    /// Check if no backends are configured.
39    #[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    /// Whether any backend-project scaffolding is configured.
55    ///
56    /// `[backends.esp32]` is deliberately excluded: it carries device
57    /// configuration — chip, panel geometry, bundled fonts — that only the
58    /// app author can know, while the other entries describe backend
59    /// projects that playground mode delegates to the CLI.
60    #[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    /// Get the base path for backends, relative to project root.
70    #[must_use]
71    pub fn path(&self) -> &Path {
72        Path::new(&self.path)
73    }
74
75    /// Set the base path for backends.
76    pub fn set_path(&mut self, path: impl Into<String>) {
77        self.path = path.into();
78    }
79
80    /// Get the Android backend configuration, if any.
81    #[must_use]
82    pub const fn android(&self) -> Option<&AndroidBackend> {
83        self.android.as_ref()
84    }
85
86    /// Get the Apple backend configuration, if any.
87    #[must_use]
88    pub const fn apple(&self) -> Option<&AppleBackend> {
89        self.apple.as_ref()
90    }
91
92    /// Set the Apple backend configuration.
93    pub fn set_apple(&mut self, backend: AppleBackend) {
94        self.apple = Some(backend);
95    }
96
97    /// Remove Apple backend configuration.
98    pub fn clear_apple(&mut self) {
99        self.apple = None;
100    }
101
102    /// Set the Android backend configuration.
103    pub fn set_android(&mut self, backend: AndroidBackend) {
104        self.android = Some(backend);
105    }
106
107    /// Remove Android backend configuration.
108    pub fn clear_android(&mut self) {
109        self.android = None;
110    }
111
112    /// Get the GTK4 backend configuration, if any.
113    #[must_use]
114    pub const fn gtk4(&self) -> Option<&Gtk4Backend> {
115        self.gtk4.as_ref()
116    }
117
118    /// Set the GTK4 backend configuration.
119    pub fn set_gtk4(&mut self, backend: Gtk4Backend) {
120        self.gtk4 = Some(backend);
121    }
122
123    /// Remove GTK4 backend configuration.
124    pub fn clear_gtk4(&mut self) {
125        self.gtk4 = None;
126    }
127
128    /// Get the hydrolysis backend configuration, if any.
129    #[must_use]
130    pub const fn hydrolysis(&self) -> Option<&HydrolysisBackend> {
131        self.hydrolysis.as_ref()
132    }
133
134    /// Set the hydrolysis backend configuration.
135    pub fn set_hydrolysis(&mut self, backend: HydrolysisBackend) {
136        self.hydrolysis = Some(backend);
137    }
138
139    /// Remove hydrolysis backend configuration.
140    pub fn clear_hydrolysis(&mut self) {
141        self.hydrolysis = None;
142    }
143
144    /// Get the `WinUI` backend configuration, if any.
145    #[must_use]
146    pub const fn winui(&self) -> Option<&crate::winui::backend::WinUiBackend> {
147        self.winui.as_ref()
148    }
149
150    /// Set the `WinUI` backend configuration.
151    pub fn set_winui(&mut self, backend: crate::winui::backend::WinUiBackend) {
152        self.winui = Some(backend);
153    }
154
155    /// Remove `WinUI` backend configuration.
156    pub fn clear_winui(&mut self) {
157        self.winui = None;
158    }
159
160    /// Get the ESP32 backend configuration, if any.
161    #[must_use]
162    pub const fn esp32(&self) -> Option<&Esp32Backend> {
163        self.esp32.as_ref()
164    }
165
166    /// Set the ESP32 backend configuration.
167    pub fn set_esp32(&mut self, backend: Esp32Backend) {
168        self.esp32 = Some(backend);
169    }
170
171    /// Remove ESP32 backend configuration.
172    pub fn clear_esp32(&mut self) {
173        self.esp32 = None;
174    }
175}
176
177/// Error type for failing to initialize a backend.
178#[derive(Debug, thiserror::Error)]
179pub enum FailToInitBackend {
180    /// I/O error while scaffolding templates.
181    #[error("Failed to write template files: {0}")]
182    Io(#[from] std::io::Error),
183    /// Invalid backend configuration prevented scaffolding (e.g. an
184    /// unsupported chip in `[backends.esp32]`).
185    #[error("Invalid backend configuration: {0}")]
186    Config(#[source] eyre::Error),
187}
188
189/// Trait for backends in a `WaterUI` project.
190///
191/// A backend handles building and packaging for specific platforms.
192/// Each backend knows:
193/// - Which platforms it supports
194/// - How to build Rust code for those platforms
195/// - How to package artifacts for distribution
196pub trait Backend: Sized + Send + Sync {
197    /// The default relative path for this backend (e.g., "android", "apple").
198    const DEFAULT_PATH: &'static str;
199
200    /// Paths relative to the backend directory that should be preserved during re-scaffolding.
201    ///
202    /// These typically contain build caches that are expensive to regenerate.
203    /// During `reinit_backend()`, only items NOT in this list are deleted before calling `init()`.
204    const CACHE_PATHS: &'static [&'static str];
205
206    /// Get the relative path for this backend instance.
207    ///
208    /// This is relative to `Backends::path()`.
209    fn path(&self) -> &Path;
210
211    /// Initialize the backend for the given project.
212    ///
213    /// Creates necessary files/folders for the backend at `project.backend_path::<Self>()`.
214    /// Returns the initialized backend configuration.
215    fn init(project: &Project) -> impl Future<Output = Result<Self, FailToInitBackend>> + Send;
216
217    // =========================================================================
218    // New methods for build/package (migrated from Platform trait)
219    // =========================================================================
220
221    /// Check if this backend supports the given platform.
222    fn supports(&self, platform: TargetPlatform) -> bool;
223
224    /// Build the Rust library for the target platform.
225    ///
226    /// Returns the target directory path where the built library is located.
227    fn build(
228        &self,
229        project: &Project,
230        platform: TargetPlatform,
231        options: BuildOptions,
232    ) -> impl Future<Output = eyre::Result<PathBuf>> + Send;
233
234    /// Package the project for the target platform.
235    ///
236    /// Returns the artifact (e.g., .app, .apk, binary).
237    fn package(
238        &self,
239        project: &Project,
240        platform: TargetPlatform,
241        options: PackageOptions,
242    ) -> impl Future<Output = eyre::Result<Artifact>> + Send;
243
244    /// Clean build artifacts for the platform.
245    fn clean(
246        &self,
247        project: &Project,
248        platform: TargetPlatform,
249    ) -> impl Future<Output = eyre::Result<()>> + Send;
250}
251
252/// Re-initialize a backend, preserving cache directories.
253///
254/// This function:
255/// 1. Identifies cache paths that should be preserved (from `Backend::CACHE_PATHS`)
256/// 2. Deletes all non-cache items in the backend directory
257/// 3. Calls `Backend::init()` to re-scaffold the backend
258///
259/// This allows template updates to be applied while keeping expensive build caches.
260///
261/// # Errors
262/// Returns an error if the backend directory cannot be read, cleaned, or re-initialized.
263pub 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        // Get cache paths to preserve
268        let cache_paths: std::collections::HashSet<&str> = B::CACHE_PATHS.iter().copied().collect();
269
270        // Delete only non-cache items
271        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    // Re-scaffold templates (cache dirs untouched)
289    B::init(project).await
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    /// `[backends.esp32]` is device configuration, not backend-project
297    /// scaffolding, so it alone must not trip the playground restriction.
298    #[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}