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