|
8 | 8 | //! Module segments (`aw-tauri`, …) do not change. |
9 | 9 |
|
10 | 10 | use std::fs; |
11 | | -use std::path::PathBuf; |
| 11 | +use std::path::{Path, PathBuf}; |
12 | 12 |
|
13 | 13 | use crate::profile::{current_profile, DEFAULT_PROFILE, TESTING_PROFILE}; |
14 | 14 |
|
@@ -170,64 +170,83 @@ pub fn get_runtime_dir() -> PathBuf { |
170 | 170 | /// - Linux AppImage: `$APPDIR/usr/lib/aw-tauri/modules/` |
171 | 171 | /// - macOS: `Contents/Resources/modules/` (and legacy `Contents/Resources/`) |
172 | 172 | pub fn get_install_discovery_paths() -> Vec<PathBuf> { |
173 | | - let mut paths = Vec::new(); |
174 | | - |
175 | 173 | #[cfg(target_os = "linux")] |
176 | 174 | { |
177 | | - if let Ok(exe_path) = std::env::current_exe() { |
178 | | - if let Some(exe_dir) = exe_path.parent() { |
179 | | - // externalBin / same-directory layout |
180 | | - paths.push(exe_dir.to_path_buf()); |
181 | | - |
182 | | - // Tauri resources: ../lib/<productName>/ relative to the binary |
183 | | - if let Some(prefix) = exe_dir.parent() { |
184 | | - let resource = prefix.join("lib").join("aw-tauri"); |
185 | | - if resource.exists() { |
186 | | - paths.push(resource.join("modules")); |
187 | | - paths.push(resource); |
188 | | - } |
189 | | - } |
190 | | - } |
191 | | - } |
| 175 | + linux_install_discovery_paths( |
| 176 | + std::env::current_exe().ok().as_deref(), |
| 177 | + std::env::var_os("APPDIR").map(PathBuf::from).as_deref(), |
| 178 | + ) |
| 179 | + } |
| 180 | + |
| 181 | + #[cfg(target_os = "macos")] |
| 182 | + { |
| 183 | + macos_install_discovery_paths(std::env::current_exe().ok().as_deref()) |
| 184 | + } |
| 185 | + |
| 186 | + #[cfg(not(any(target_os = "linux", target_os = "macos")))] |
| 187 | + { |
| 188 | + Vec::new() |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +/// Linux layout, split out from `get_install_discovery_paths` so it can be |
| 193 | +/// tested against a fake install tree. |
| 194 | +#[cfg(any(target_os = "linux", test))] |
| 195 | +fn linux_install_discovery_paths(exe_path: Option<&Path>, appdir: Option<&Path>) -> Vec<PathBuf> { |
| 196 | + let mut paths = Vec::new(); |
| 197 | + |
| 198 | + if let Some(exe_dir) = exe_path.and_then(Path::parent) { |
| 199 | + // externalBin / same-directory layout |
| 200 | + paths.push(exe_dir.to_path_buf()); |
192 | 201 |
|
193 | | - // AppImage runtime sets APPDIR to the mounted squashfs root |
194 | | - if let Ok(appdir) = std::env::var("APPDIR") { |
195 | | - let resource = PathBuf::from(appdir) |
196 | | - .join("usr") |
197 | | - .join("lib") |
198 | | - .join("aw-tauri"); |
| 202 | + // Tauri resources: ../lib/<productName>/ relative to the binary |
| 203 | + if let Some(prefix) = exe_dir.parent() { |
| 204 | + let resource = prefix.join("lib").join("aw-tauri"); |
199 | 205 | if resource.exists() { |
200 | | - let modules = resource.join("modules"); |
201 | | - if !paths.contains(&modules) { |
202 | | - paths.push(modules); |
203 | | - } |
204 | | - if !paths.contains(&resource) { |
205 | | - paths.push(resource); |
206 | | - } |
| 206 | + paths.push(resource.join("modules")); |
| 207 | + paths.push(resource); |
207 | 208 | } |
208 | 209 | } |
209 | 210 | } |
210 | 211 |
|
211 | | - #[cfg(target_os = "macos")] |
212 | | - { |
213 | | - // Structure: Contents/MacOS/aw-tauri -> go up two levels -> Contents/Resources |
214 | | - if let Ok(exe_path) = std::env::current_exe() { |
215 | | - if let Some(contents_dir) = exe_path.parent().and_then(|p| p.parent()) { |
216 | | - let resources_dir = contents_dir.join("Resources"); |
217 | | - if resources_dir.exists() { |
218 | | - // Modules bundled via tauri.conf.json `bundle.resources` land in Resources/modules/. |
219 | | - paths.push(resources_dir.join("modules")); |
220 | | - // Also include Resources/ directly for compatibility with modules placed |
221 | | - // at the root (e.g. legacy build_app_tauri.sh layout). |
222 | | - paths.push(resources_dir); |
223 | | - } |
| 212 | + // AppImage runtime sets APPDIR to the mounted squashfs root |
| 213 | + if let Some(appdir) = appdir { |
| 214 | + let resource = appdir.join("usr").join("lib").join("aw-tauri"); |
| 215 | + if resource.exists() { |
| 216 | + let modules = resource.join("modules"); |
| 217 | + if !paths.contains(&modules) { |
| 218 | + paths.push(modules); |
| 219 | + } |
| 220 | + if !paths.contains(&resource) { |
| 221 | + paths.push(resource); |
224 | 222 | } |
225 | 223 | } |
226 | 224 | } |
227 | 225 |
|
228 | 226 | paths |
229 | 227 | } |
230 | 228 |
|
| 229 | +/// macOS layout, split out from `get_install_discovery_paths` so it can be |
| 230 | +/// tested against a fake app bundle. |
| 231 | +#[cfg(any(target_os = "macos", test))] |
| 232 | +fn macos_install_discovery_paths(exe_path: Option<&Path>) -> Vec<PathBuf> { |
| 233 | + let mut paths = Vec::new(); |
| 234 | + |
| 235 | + // Structure: Contents/MacOS/aw-tauri -> go up two levels -> Contents/Resources |
| 236 | + if let Some(contents_dir) = exe_path.and_then(Path::parent).and_then(Path::parent) { |
| 237 | + let resources_dir = contents_dir.join("Resources"); |
| 238 | + if resources_dir.exists() { |
| 239 | + // Modules bundled via tauri.conf.json `bundle.resources` land in Resources/modules/. |
| 240 | + paths.push(resources_dir.join("modules")); |
| 241 | + // Also include Resources/ directly for compatibility with modules placed |
| 242 | + // at the root (e.g. legacy build_app_tauri.sh layout). |
| 243 | + paths.push(resources_dir); |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + paths |
| 248 | +} |
| 249 | + |
231 | 250 | pub fn get_discovery_paths() -> Vec<PathBuf> { |
232 | 251 | let mut discovery_paths = Vec::new(); |
233 | 252 |
|
@@ -353,10 +372,102 @@ mod tests { |
353 | 372 | assert_eq!(appname_for("my-profile"), "activitywatch-my-profile"); |
354 | 373 | } |
355 | 374 |
|
| 375 | + /// Fresh, empty scratch dir for building fake install trees. |
| 376 | + fn scratch_dir(name: &str) -> PathBuf { |
| 377 | + let dir = |
| 378 | + std::env::temp_dir().join(format!("aw-tauri-test-{}-{}", name, std::process::id())); |
| 379 | + let _ = fs::remove_dir_all(&dir); |
| 380 | + fs::create_dir_all(&dir).unwrap(); |
| 381 | + dir |
| 382 | + } |
| 383 | + |
| 384 | + #[test] |
| 385 | + fn test_linux_install_paths_deb_rpm_layout() { |
| 386 | + // /usr/bin/aw-tauri with resources in /usr/lib/aw-tauri/ |
| 387 | + let root = scratch_dir("deb"); |
| 388 | + let usr = root.join("usr"); |
| 389 | + fs::create_dir_all(usr.join("bin")).unwrap(); |
| 390 | + fs::create_dir_all(usr.join("lib").join("aw-tauri").join("modules")).unwrap(); |
| 391 | + |
| 392 | + let paths = linux_install_discovery_paths(Some(&usr.join("bin").join("aw-tauri")), None); |
| 393 | + assert_eq!( |
| 394 | + paths, |
| 395 | + vec![ |
| 396 | + usr.join("bin"), |
| 397 | + usr.join("lib").join("aw-tauri").join("modules"), |
| 398 | + usr.join("lib").join("aw-tauri"), |
| 399 | + ] |
| 400 | + ); |
| 401 | + |
| 402 | + let _ = fs::remove_dir_all(&root); |
| 403 | + } |
| 404 | + |
| 405 | + #[test] |
| 406 | + fn test_linux_install_paths_without_resources() { |
| 407 | + // No ../lib/aw-tauri next to the binary: only the binary's own dir. |
| 408 | + let root = scratch_dir("bare"); |
| 409 | + fs::create_dir_all(root.join("bin")).unwrap(); |
| 410 | + |
| 411 | + let paths = linux_install_discovery_paths(Some(&root.join("bin").join("aw-tauri")), None); |
| 412 | + assert_eq!(paths, vec![root.join("bin")]); |
| 413 | + |
| 414 | + let _ = fs::remove_dir_all(&root); |
| 415 | + } |
| 416 | + |
| 417 | + #[test] |
| 418 | + fn test_linux_install_paths_appimage_layout() { |
| 419 | + // AppImage: binary runs from a different dir, resources under $APPDIR. |
| 420 | + let root = scratch_dir("appimage"); |
| 421 | + let appdir = root.join("squashfs-root"); |
| 422 | + let resource = appdir.join("usr").join("lib").join("aw-tauri"); |
| 423 | + fs::create_dir_all(resource.join("modules")).unwrap(); |
| 424 | + let exe_dir = root.join("elsewhere").join("bin"); |
| 425 | + fs::create_dir_all(&exe_dir).unwrap(); |
| 426 | + |
| 427 | + let paths = linux_install_discovery_paths(Some(&exe_dir.join("aw-tauri")), Some(&appdir)); |
| 428 | + assert_eq!( |
| 429 | + paths, |
| 430 | + vec![exe_dir.clone(), resource.join("modules"), resource.clone()] |
| 431 | + ); |
| 432 | + |
| 433 | + // When the binary itself lives in $APPDIR/usr/bin, paths aren't duplicated. |
| 434 | + let exe = appdir.join("usr").join("bin").join("aw-tauri"); |
| 435 | + let paths = linux_install_discovery_paths(Some(&exe), Some(&appdir)); |
| 436 | + assert_eq!( |
| 437 | + paths, |
| 438 | + vec![ |
| 439 | + appdir.join("usr").join("bin"), |
| 440 | + resource.join("modules"), |
| 441 | + resource, |
| 442 | + ] |
| 443 | + ); |
| 444 | + |
| 445 | + let _ = fs::remove_dir_all(&root); |
| 446 | + } |
| 447 | + |
| 448 | + #[test] |
| 449 | + fn test_macos_install_paths_bundle_layout() { |
| 450 | + let root = scratch_dir("macos"); |
| 451 | + let contents = root.join("ActivityWatch.app").join("Contents"); |
| 452 | + fs::create_dir_all(contents.join("MacOS")).unwrap(); |
| 453 | + fs::create_dir_all(contents.join("Resources").join("modules")).unwrap(); |
| 454 | + |
| 455 | + let paths = macos_install_discovery_paths(Some(&contents.join("MacOS").join("aw-tauri"))); |
| 456 | + assert_eq!( |
| 457 | + paths, |
| 458 | + vec![ |
| 459 | + contents.join("Resources").join("modules"), |
| 460 | + contents.join("Resources"), |
| 461 | + ] |
| 462 | + ); |
| 463 | + |
| 464 | + let _ = fs::remove_dir_all(&root); |
| 465 | + } |
| 466 | + |
356 | 467 | #[test] |
357 | | - fn test_install_discovery_paths_is_callable() { |
358 | | - // Does not require a real install layout; just ensures the helper runs. |
359 | | - let _ = get_install_discovery_paths(); |
| 468 | + fn test_install_paths_without_exe() { |
| 469 | + assert!(linux_install_discovery_paths(None, None).is_empty()); |
| 470 | + assert!(macos_install_discovery_paths(None).is_empty()); |
360 | 471 | } |
361 | 472 |
|
362 | 473 | #[cfg(target_os = "linux")] |
|
0 commit comments