Skip to main content

waterui_cli/platforming/
platform.rs

1//! Platform abstraction for `WaterUI` CLI.
2
3use std::str::FromStr;
4
5use target_lexicon::{
6    Aarch64Architecture, Architecture, DefaultToHost, Environment, OperatingSystem,
7    Riscv32Architecture, Triple, Vendor,
8};
9
10// ============================================================================
11// Target Platform Enum (New Architecture)
12// ============================================================================
13
14/// Target platform for building and running `WaterUI` apps.
15///
16/// This enum replaces the old `Platform` trait with a simpler, more explicit model.
17/// Each variant represents a specific target platform that `WaterUI` can build for.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum TargetPlatform {
20    // Apple platforms
21    /// macOS (current machine architecture)
22    MacOS,
23    /// iOS (physical device, ARM64)
24    IOS,
25    /// iOS Simulator (host machine architecture)
26    IOSSimulator,
27    /// tvOS (physical device)
28    TvOS,
29    /// tvOS Simulator
30    TvOSSimulator,
31    /// watchOS (physical device)
32    WatchOS,
33    /// watchOS Simulator
34    WatchOSSimulator,
35    /// visionOS (physical device)
36    VisionOS,
37    /// visionOS Simulator
38    VisionOSSimulator,
39
40    // Other platforms
41    /// Android
42    Android,
43    /// Linux (GTK4)
44    Linux,
45    /// Windows (Hydrolysis)
46    Windows,
47    /// Web (WASM + WebGPU)
48    Web,
49    /// ESP32-S3 (Xtensa, ESP-IDF firmware via the Dew backend)
50    Esp32S3,
51    /// ESP32-C3 (RISC-V, ESP-IDF firmware via the Dew backend)
52    Esp32C3,
53    /// ESP32-P4 (RISC-V with FPU, ESP-IDF firmware via the Dew backend)
54    Esp32P4,
55}
56
57/// Backend types available for building.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum TargetBackend {
60    /// Apple backend (Xcode, UIKit/AppKit)
61    Apple,
62    /// Android backend (Gradle, Android Views)
63    Android,
64    /// GTK4 backend (pure Rust binary)
65    Gtk4,
66    /// Hydrolysis backend (self-drawn renderer)
67    Hydrolysis,
68    /// Dew backend (embedded-first CPU renderer for ESP32-class chips)
69    Dew,
70}
71
72impl TargetPlatform {
73    /// Get the target triple for this platform.
74    ///
75    /// # Panics
76    /// May panic if `target_lexicon` cannot resolve the current host triple for simulator or host-native targets.
77    #[must_use]
78    pub fn triple(&self) -> Triple {
79        match self {
80            Self::MacOS => Triple {
81                architecture: DefaultToHost::default().0.architecture,
82                vendor: Vendor::Apple,
83                operating_system: OperatingSystem::Darwin(None),
84                environment: Environment::Unknown,
85                binary_format: target_lexicon::BinaryFormat::Macho,
86            },
87            Self::IOS => Triple {
88                architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64),
89                vendor: Vendor::Apple,
90                operating_system: OperatingSystem::IOS(None),
91                environment: Environment::Unknown,
92                binary_format: target_lexicon::BinaryFormat::Macho,
93            },
94            Self::IOSSimulator => {
95                let arch = DefaultToHost::default().0.architecture;
96                let env = match arch {
97                    Architecture::X86_64 => Environment::Unknown,
98                    _ => Environment::Sim,
99                };
100                Triple {
101                    architecture: arch,
102                    vendor: Vendor::Apple,
103                    operating_system: OperatingSystem::IOS(None),
104                    environment: env,
105                    binary_format: target_lexicon::BinaryFormat::Macho,
106                }
107            }
108            Self::TvOS => Triple {
109                architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64),
110                vendor: Vendor::Apple,
111                operating_system: OperatingSystem::TvOS(None),
112                environment: Environment::Unknown,
113                binary_format: target_lexicon::BinaryFormat::Macho,
114            },
115            Self::TvOSSimulator => Triple {
116                architecture: DefaultToHost::default().0.architecture,
117                vendor: Vendor::Apple,
118                operating_system: OperatingSystem::TvOS(None),
119                environment: Environment::Sim,
120                binary_format: target_lexicon::BinaryFormat::Macho,
121            },
122            Self::WatchOS => Triple {
123                architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64),
124                vendor: Vendor::Apple,
125                operating_system: OperatingSystem::WatchOS(None),
126                environment: Environment::Unknown,
127                binary_format: target_lexicon::BinaryFormat::Macho,
128            },
129            Self::WatchOSSimulator => Triple {
130                architecture: DefaultToHost::default().0.architecture,
131                vendor: Vendor::Apple,
132                operating_system: OperatingSystem::WatchOS(None),
133                environment: Environment::Sim,
134                binary_format: target_lexicon::BinaryFormat::Macho,
135            },
136            Self::VisionOS => Triple {
137                architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64),
138                vendor: Vendor::Apple,
139                operating_system: OperatingSystem::VisionOS(None),
140                environment: Environment::Unknown,
141                binary_format: target_lexicon::BinaryFormat::Macho,
142            },
143            Self::VisionOSSimulator => Triple {
144                architecture: DefaultToHost::default().0.architecture,
145                vendor: Vendor::Apple,
146                operating_system: OperatingSystem::VisionOS(None),
147                environment: Environment::Sim,
148                binary_format: target_lexicon::BinaryFormat::Macho,
149            },
150            Self::Android => Triple {
151                architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64),
152                vendor: Vendor::Unknown,
153                operating_system: OperatingSystem::Linux,
154                environment: Environment::Android,
155                binary_format: target_lexicon::BinaryFormat::Elf,
156            },
157            Self::Linux | Self::Windows => Triple::host(),
158            Self::Web => Triple::from_str("wasm32-unknown-unknown")
159                .expect("web target triple must remain valid"),
160            Self::Esp32S3 => Triple::from_str("xtensa-esp32s3-espidf")
161                .expect("esp32s3 target triple must remain valid"),
162            Self::Esp32C3 => Triple::from_str("riscv32imc-esp-espidf")
163                .expect("esp32c3 target triple must remain valid"),
164            Self::Esp32P4 => Triple::from_str("riscv32imafc-esp-espidf")
165                .expect("esp32p4 target triple must remain valid"),
166        }
167    }
168
169    /// Get available backends for this platform.
170    #[must_use]
171    pub const fn available_backends(&self) -> &[TargetBackend] {
172        match self {
173            Self::MacOS => &[TargetBackend::Apple, TargetBackend::Hydrolysis],
174            Self::IOS
175            | Self::IOSSimulator
176            | Self::TvOS
177            | Self::TvOSSimulator
178            | Self::WatchOS
179            | Self::WatchOSSimulator
180            | Self::VisionOS
181            | Self::VisionOSSimulator => &[TargetBackend::Apple],
182            Self::Android => &[TargetBackend::Android],
183            Self::Linux => &[TargetBackend::Gtk4, TargetBackend::Hydrolysis],
184            Self::Windows | Self::Web => &[TargetBackend::Hydrolysis],
185            Self::Esp32S3 | Self::Esp32C3 | Self::Esp32P4 => &[TargetBackend::Dew],
186        }
187    }
188
189    /// Get the default backend for this platform.
190    #[must_use]
191    pub const fn default_backend(&self) -> TargetBackend {
192        match self {
193            Self::MacOS
194            | Self::IOS
195            | Self::IOSSimulator
196            | Self::TvOS
197            | Self::TvOSSimulator
198            | Self::WatchOS
199            | Self::WatchOSSimulator
200            | Self::VisionOS
201            | Self::VisionOSSimulator => TargetBackend::Apple,
202            Self::Android => TargetBackend::Android,
203            Self::Linux => TargetBackend::Gtk4,
204            Self::Windows | Self::Web => TargetBackend::Hydrolysis,
205            Self::Esp32S3 | Self::Esp32C3 | Self::Esp32P4 => TargetBackend::Dew,
206        }
207    }
208
209    /// Check if this platform is a simulator/emulator.
210    #[must_use]
211    pub const fn is_simulator(&self) -> bool {
212        matches!(
213            self,
214            Self::IOSSimulator
215                | Self::TvOSSimulator
216                | Self::WatchOSSimulator
217                | Self::VisionOSSimulator
218        )
219    }
220
221    /// Get the SDK name for Apple platforms.
222    #[must_use]
223    pub const fn sdk_name(&self) -> Option<&'static str> {
224        match self {
225            Self::MacOS => Some("macosx"),
226            Self::IOS => Some("iphoneos"),
227            Self::IOSSimulator => Some("iphonesimulator"),
228            Self::TvOS => Some("appletvos"),
229            Self::TvOSSimulator => Some("appletvsimulator"),
230            Self::WatchOS => Some("watchos"),
231            Self::WatchOSSimulator => Some("watchsimulator"),
232            Self::VisionOS => Some("xros"),
233            Self::VisionOSSimulator => Some("xrsimulator"),
234            Self::Android
235            | Self::Linux
236            | Self::Windows
237            | Self::Web
238            | Self::Esp32S3
239            | Self::Esp32C3
240            | Self::Esp32P4 => None,
241        }
242    }
243
244    /// Get the architecture for this platform.
245    #[must_use]
246    pub fn arch(&self) -> Architecture {
247        match self {
248            Self::MacOS
249            | Self::IOSSimulator
250            | Self::TvOSSimulator
251            | Self::WatchOSSimulator
252            | Self::VisionOSSimulator
253            | Self::Linux
254            | Self::Windows => DefaultToHost::default().0.architecture,
255            Self::IOS | Self::TvOS | Self::WatchOS | Self::VisionOS | Self::Android => {
256                Architecture::Aarch64(Aarch64Architecture::Aarch64)
257            }
258            Self::Web => Architecture::Wasm32,
259            Self::Esp32S3 => Architecture::XTensa,
260            Self::Esp32C3 => Architecture::Riscv32(Riscv32Architecture::Riscv32imc),
261            Self::Esp32P4 => Architecture::Riscv32(Riscv32Architecture::Riscv32imafc),
262        }
263    }
264}
265
266// ============================================================================
267// Package Options
268// ============================================================================
269
270/// Configuration options for packaging the application.
271///
272/// This struct contains settings that control how the application
273/// is packaged for distribution across different platforms.
274#[derive(Debug, Clone)]
275pub struct PackageOptions {
276    /// Whether to prepare the package for store distribution.
277    ///
278    /// When `true`, the package will be configured for submission to
279    /// official app stores (App Store for iOS/macOS or Play Store for Android).
280    ///
281    /// When `false`, the package will be prepared for direct distribution
282    /// or development purposes.
283    ///
284    /// # Warning
285    ///
286    /// Enable this option only change your packaging format, it does not change your build configuration.
287    /// For a real world distribution build, you may also want to disable `debug` in `BuildOptions`.
288    distribution: bool,
289
290    /// Whether to enable debug mode in the packaged application.
291    ///
292    /// When `true`, the application will include additional debug information
293    /// and logging capabilities to facilitate troubleshooting during development.
294    ///
295    /// When `false`, the application will be optimized for release with
296    /// minimal debug information.
297    ///
298    /// This flag is not conflict with `distribution`, since `distribution` decide the package format,
299    /// while `debug` decide the build configuration.
300    debug: bool,
301
302    /// Whether the package embeds the shared `WaterUI` Rust runtime.
303    shared_rust_runtime: bool,
304
305    /// How `include_web!` mounts reach the packaged app.
306    web_frontend: WebFrontendMode,
307}
308
309/// Whether an `include_web!` mount is staged from a frontend build or served
310/// by a running dev server.
311///
312/// In dev-server mode staging skips a web mount entirely — no frontend build
313/// and no copied output — since the app opens the bundler's dev-server URL
314/// instead of the staged bundle. `water run` selects it in debug mode;
315/// packaging and `--release` runs never do.
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
317pub enum WebFrontendMode {
318    /// Build the frontend and stage its output into the bundle.
319    #[default]
320    Stage,
321    /// A running dev server serves the mount; nothing is staged for it.
322    DevServer,
323}
324
325impl PackageOptions {
326    /// Create options for installing and running a development build.
327    #[must_use]
328    pub const fn development() -> Self {
329        Self {
330            distribution: false,
331            debug: true,
332            shared_rust_runtime: true,
333            web_frontend: WebFrontendMode::Stage,
334        }
335    }
336
337    /// Create options for a self-contained package artifact.
338    #[must_use]
339    pub const fn packaging(distribution: bool, debug: bool) -> Self {
340        Self {
341            distribution,
342            debug,
343            shared_rust_runtime: false,
344            web_frontend: WebFrontendMode::Stage,
345        }
346    }
347
348    /// Override the debug flag without changing the runtime linkage.
349    #[must_use]
350    pub const fn with_debug(mut self, debug: bool) -> Self {
351        self.debug = debug;
352        self
353    }
354
355    /// Mark web mounts as dev-server-served for this packaging pass.
356    #[must_use]
357    pub const fn with_dev_server(mut self, dev_server: bool) -> Self {
358        self.web_frontend = if dev_server {
359            WebFrontendMode::DevServer
360        } else {
361            WebFrontendMode::Stage
362        };
363        self
364    }
365
366    /// Whether to package in distribution mode
367    #[must_use]
368    pub const fn is_distribution(&self) -> bool {
369        self.distribution
370    }
371
372    /// Whether to package in debug mode
373    #[must_use]
374    pub const fn is_debug(&self) -> bool {
375        self.debug
376    }
377
378    /// Whether the package must embed the shared `WaterUI` Rust runtime.
379    #[must_use]
380    pub const fn uses_shared_rust_runtime(&self) -> bool {
381        self.shared_rust_runtime
382    }
383
384    /// Whether web mounts are dev-server-served and skipped during staging.
385    #[must_use]
386    pub const fn uses_dev_server(&self) -> bool {
387        matches!(self.web_frontend, WebFrontendMode::DevServer)
388    }
389}
390
391#[cfg(test)]
392mod package_options_tests {
393    use super::PackageOptions;
394
395    #[test]
396    fn development_embeds_shared_runtime_and_packaging_does_not() {
397        let development = PackageOptions::development();
398        assert!(development.is_debug());
399        assert!(!development.is_distribution());
400        assert!(development.uses_shared_rust_runtime());
401
402        for options in [
403            PackageOptions::packaging(false, true),
404            PackageOptions::packaging(true, false),
405        ] {
406            assert!(!options.uses_shared_rust_runtime());
407        }
408    }
409}