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