Skip to main content

waterui_cli/platforming/
platform.rs

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