waterui_cli/workflows/inspector/
launcher.rs1use std::path::{Path, PathBuf};
4use std::pin::Pin;
5
6use eyre::{Context as _, Result, bail};
7use tracing::info;
8
9use crate::build::{BuildOptions, BuildProfile, BuildProgress};
10use crate::device::{Device, Local, RunOptions, Running};
11use crate::platform::TargetPlatform;
12use crate::project::{ManagedBackends, Project};
13use crate::runtime_compat::runtime_profile_tag;
14use crate::runtime_fingerprint::{compute_runtime_fingerprint, runtime_package_identity};
15use crate::support_app;
16use crate::templates::TemplateContext;
17
18const INSPECTOR_TEMPLATE_COMMIT: &str = env!("WATERUI_CLI_COMMIT");
19const INSPECTOR_METADATA_FILE: &str = ".waterui-inspector-signature";
20
21#[derive(Debug, Clone)]
22struct InspectorRequirements {
23 waterui_path: Option<PathBuf>,
24 runtime_fingerprint: String,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum InspectorPlatform {
30 IosSimulator,
32 Macos,
34 Android,
36}
37
38#[derive(Debug, Clone)]
40pub struct InspectorLaunchOptions {
41 pub target_addr: String,
43 pub token: String,
45}
46
47#[derive(Debug)]
49pub struct InspectorSession {
50 pub platform: InspectorPlatform,
52 running: Option<Pin<Box<Running>>>,
54 owns_app: bool,
56}
57
58impl InspectorSession {
59 pub fn shutdown(&mut self) -> Result<()> {
64 if self.owns_app {
65 self.running.take();
66 }
67 Ok(())
68 }
69
70 pub fn detach(&mut self) {
72 if let Some(running) = self.running.take() {
73 std::mem::forget(running);
74 self.owns_app = false;
75 }
76 }
77}
78
79const fn inspector_target_platform(platform: InspectorPlatform) -> TargetPlatform {
81 match platform {
82 InspectorPlatform::Macos => TargetPlatform::MacOS,
83 InspectorPlatform::IosSimulator => TargetPlatform::IOSSimulator,
84 InspectorPlatform::Android => TargetPlatform::Android,
85 }
86}
87
88async fn open_inspector_project(
89 inspector_app_path: &Path,
90 platform: InspectorPlatform,
91) -> Result<Project> {
92 Project::open(
93 inspector_app_path,
94 ManagedBackends::for_platform(inspector_target_platform(platform)),
95 )
96 .await
97 .wrap_err("Failed to open inspector support app project")
98}
99
100pub async fn launch_inspector_session(
105 project_path: &Path,
106 platform: InspectorPlatform,
107 options: InspectorLaunchOptions,
108 progress: Option<BuildProgress>,
109) -> Result<InspectorSession> {
110 let requirements = resolve_inspector_requirements(project_path).await?;
111
112 let inspector_app_path = inspector_support_path()?;
113 ensure_inspector_support_app(&inspector_app_path, &requirements).await?;
114
115 let project = open_inspector_project(&inspector_app_path, platform).await?;
116
117 let mut run_options = RunOptions::new();
118 run_options.insert_env_var(
119 "WATERUI_INSPECTOR_TARGET_ADDR".to_string(),
120 options.target_addr.clone(),
121 );
122 run_options.insert_env_var("WATERUI_INSPECTOR_TOKEN".to_string(), options.token.clone());
123
124 let host = crate::toolchain::Host::current();
125 let running = match platform {
126 InspectorPlatform::Macos => {
127 let backend = project
128 .apple_backend()
129 .ok_or_else(|| eyre::eyre!("Apple backend not configured"))?;
130 let device = Local;
131 device.launch(&host).await?;
132 info!("Building and running inspector app on macOS...");
133 project
134 .run_with_options(
135 backend,
136 TargetPlatform::MacOS,
137 device,
138 run_options,
139 progress.clone(),
140 )
141 .await
142 .map_err(|e| eyre::eyre!("Failed to run inspector app: {e}"))?
143 }
144 InspectorPlatform::IosSimulator => {
145 let backend = project
146 .apple_backend()
147 .ok_or_else(|| eyre::eyre!("Apple backend not configured"))?;
148 let simulator =
149 crate::apple::device::AppleSimulator::select_ios(&host, &project, None).await?;
150
151 simulator.launch(&host).await?;
152 info!("Building and running inspector app on iOS Simulator...");
153 project
154 .run_with_options(
155 backend,
156 TargetPlatform::IOSSimulator,
157 simulator,
158 run_options,
159 progress.clone(),
160 )
161 .await
162 .map_err(|e| eyre::eyre!("Failed to run inspector app: {e}"))?
163 }
164 InspectorPlatform::Android => {
165 let backend = project
166 .android_backend()
167 .ok_or_else(|| eyre::eyre!("Android backend not configured"))?;
168
169 let devices = crate::android::device::AndroidDevice::scan(&host).await?;
170 if let Some(device) = devices.into_iter().next() {
171 device.launch(&host).await?;
172 info!("Building and running inspector app on Android device...");
173 project
174 .run_android_with_options(
175 backend,
176 device,
177 run_options,
178 BuildOptions::development(BuildProfile::Debug),
179 progress.clone(),
180 )
181 .await
182 .map_err(|e| eyre::eyre!("Failed to run inspector app: {e}"))?
183 } else {
184 let avds = crate::android::platform::AndroidPlatform::list_avds(&host).await?;
185 let avd_name = avds
186 .into_iter()
187 .next()
188 .ok_or_else(|| eyre::eyre!("No Android devices or emulators available."))?;
189 let emulator =
190 crate::android::device::AndroidEmulator::open(&host, avd_name).await?;
191 emulator.launch(&host).await?;
192 info!("Building and running inspector app on Android emulator...");
193 project
194 .run_android_with_options(
195 backend,
196 emulator,
197 run_options,
198 BuildOptions::development(BuildProfile::Debug),
199 progress.clone(),
200 )
201 .await
202 .map_err(|e| eyre::eyre!("Failed to run inspector app: {e}"))?
203 }
204 }
205 };
206
207 Ok(InspectorSession {
208 platform,
209 running: Some(Box::pin(running)),
210 owns_app: true,
211 })
212}
213
214fn inspector_support_path() -> Result<PathBuf> {
215 support_app::support_app_path("inspector_support")
216}
217
218async fn ensure_inspector_support_app(
219 path: &Path,
220 requirements: &InspectorRequirements,
221) -> Result<()> {
222 let desired_signature = inspector_signature(requirements);
223 let scaffold_path = path.to_path_buf();
224 let scaffold_requirements = requirements.clone();
225 support_app::ensure_support_app(
226 path,
227 INSPECTOR_METADATA_FILE,
228 &desired_signature,
229 "inspector support",
230 move || async move { scaffold_inspector_app(&scaffold_path, &scaffold_requirements).await },
231 )
232 .await
233}
234
235async fn scaffold_inspector_app(path: &Path, requirements: &InspectorRequirements) -> Result<()> {
236 use crate::project::{CreateOptions, Manifest as WaterManifest, PackageType};
237
238 let waterui_path = requirements.waterui_path.clone();
239
240 let options = CreateOptions {
241 name: "WaterUI Inspector".to_string(),
242 bundle_identifier: crate::project_types::BundleIdentifier::try_from(
243 "dev.waterui.inspector",
244 )
245 .expect("inspector support bundle identifier must be valid"),
246 package_type: PackageType::Playground,
247 waterui_path: waterui_path.clone(),
248 channel: None,
249 framework_manifest: None,
250 framework: None,
251 author: String::new(),
252 backends: Vec::new(),
253 web: None,
254 };
255
256 let project = Project::create(path, options)
257 .await
258 .map_err(|e| eyre::eyre!("Failed to create inspector app: {e}"))?;
259
260 let mut manifest = WaterManifest::open(project.root().join("Water.toml")).await?;
261 manifest.package.accessory = false;
262 manifest.save(project.root()).await?;
263
264 let ctx = TemplateContext::for_support_playground(
265 "WaterUI Inspector",
266 project.crate_name().clone(),
267 crate::project_types::BundleIdentifier::try_from("dev.waterui.inspector")
268 .expect("inspector support bundle identifier must be valid"),
269 waterui_path,
270 &project.resolved_framework().await?,
271 false,
272 None,
273 );
274
275 crate::templates::inspector::scaffold(project.root(), &ctx)
276 .await
277 .wrap_err("Failed to scaffold embedded inspector app template")?;
278
279 info!("Inspector app scaffolded at {}", path.display());
280 Ok(())
281}
282
283fn inspector_signature(requirements: &InspectorRequirements) -> String {
284 format!(
285 "template_commit={INSPECTOR_TEMPLATE_COMMIT}\nwaterui_dependency={}\nruntime_fingerprint={}\ntemplate_fingerprint={}",
286 requirements.waterui_path.as_ref().map_or_else(
287 || String::from("registry"),
288 |path| path.display().to_string()
289 ),
290 requirements.runtime_fingerprint,
291 crate::templates::inspector::template_fingerprint(),
292 )
293}
294
295async fn resolve_inspector_requirements(project_path: &Path) -> Result<InspectorRequirements> {
296 let current_dir = project_path.to_path_buf();
297 let metadata = smol::unblock(move || {
298 cargo_metadata::MetadataCommand::new()
299 .current_dir(current_dir)
300 .exec()
301 })
302 .await
303 .wrap_err("Failed to resolve user project Cargo metadata for inspector compatibility")?;
304
305 let waterui = select_unique_package(&metadata, "waterui")?;
306 let waterui_core = select_unique_package(&metadata, "waterui-core")?;
307 let runtime_identity = runtime_package_identity(waterui_core);
308
309 let runtime_fingerprint_base = if waterui.source.is_none() {
310 let waterui_root = waterui
311 .manifest_path
312 .as_std_path()
313 .parent()
314 .map(Path::to_path_buf)
315 .ok_or_else(|| eyre::eyre!("Failed to derive waterui package root path"))?;
316 let fingerprint = compute_runtime_fingerprint(&waterui_root, &runtime_identity).await?;
317 return Ok(InspectorRequirements {
318 waterui_path: Some(waterui_root),
319 runtime_fingerprint: format!("{fingerprint}|profile={}", runtime_profile_tag()),
320 });
321 } else {
322 let source = waterui
323 .source
324 .as_ref()
325 .map(ToString::to_string)
326 .expect("registry dependency must have a source");
327 format!("{runtime_identity}:source:{source}")
328 };
329
330 Ok(InspectorRequirements {
331 waterui_path: None,
332 runtime_fingerprint: format!(
333 "{runtime_fingerprint_base}|profile={}",
334 runtime_profile_tag()
335 ),
336 })
337}
338
339fn select_unique_package<'a>(
340 metadata: &'a cargo_metadata::Metadata,
341 name: &str,
342) -> Result<&'a cargo_metadata::Package> {
343 let mut matches = metadata.packages.iter().filter(|p| p.name == name);
344 let first = matches
345 .next()
346 .ok_or_else(|| eyre::eyre!("Could not resolve package `{name}` from metadata"))?;
347 if matches.next().is_some() {
348 bail!(
349 "Multiple `{name}` packages were resolved. Inspector requires a single resolved `{name}` package."
350 );
351 }
352 Ok(first)
353}