Skip to main content

Mountain/Binary/Extension/
ScanPathConfigure.rs

1//! # Extension Scan Path Configure Module
2//!
3//! Configures extension scan paths from the executable directory.
4
5use std::path::PathBuf;
6
7use crate::{
8	ApplicationState::State::ApplicationState::{ApplicationState, MapLockError},
9	dev_log,
10};
11
12/// Configures extension scan paths by resolving paths from the executable
13/// directory.
14///
15/// # Arguments
16///
17/// * `AppState` - The application state containing ExtensionScanPaths
18///
19/// # Returns
20///
21/// A `Result` indicating success or failure.
22///
23/// # Scan Path Configuration
24///
25/// This function adds the following default scan paths:
26/// - `../Resources/extensions` - Bundled extensions in app resources directory
27/// - `extensions` - Local extensions directory relative to executable
28///
29/// # Errors
30///
31/// Returns an error if ExtensionScanPaths mutex lock fails.
32pub fn ScanPathConfigure(AppState:&std::sync::Arc<ApplicationState>) -> Result<Vec<PathBuf>, String> {
33	dev_log!("extensions", "[Extensions] [ScanPaths] Locking ExtensionScanPaths...");
34
35	let mut ScanPathsGuard = AppState
36		.Extension
37		.Registry
38		.ExtensionScanPaths
39		.lock()
40		.map_err(MapLockError)
41		.map_err(|e| format!("Failed to lock ExtensionScanPaths: {}", e))?;
42
43	// Skip all built-in extensions when either the legacy
44	// `Skip` or the `.env.Land.Extensions` flag
45	// `Skip` is set. Both accepted so kernel /
46	// minimal profiles and the skill-file env stay in sync. User scan path
47	// still runs so VSIX-installed extensions remain visible.
48	let SkipBuiltins = matches!(std::env::var("Skip").as_deref(), Ok("1") | Ok("true"))
49		|| matches!(std::env::var("Skip").as_deref(), Ok("1") | Ok("true"));
50
51	if SkipBuiltins {
52		dev_log!(
53			"extensions",
54			"[Extensions] [ScanPaths] Skip=true - skipping all built-in paths, keeping user path"
55		);
56	} else {
57		dev_log!("extensions", "[Extensions] [ScanPaths] Adding default scan paths...");
58	}
59
60	// `Ship` takes precedence over the executable-
61	// relative probing chain. Useful for CI builds where the bundle layout
62	// differs from both the `.app` convention and the repo layout.
63	if !SkipBuiltins {
64		if let Ok(Override) = std::env::var("Ship") {
65			let OverridePath = ExpandUserPath(&Override);
66
67			if OverridePath.exists() {
68				dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Ship)", OverridePath.display());
69
70				ScanPathsGuard.push(OverridePath);
71			} else {
72				dev_log!(
73					"extensions",
74					"warn: [Extensions] [ScanPaths] Ship={} does not exist; ignoring",
75					Override
76				);
77			}
78		}
79	}
80
81	// Resolve paths from executable directory
82	if !SkipBuiltins {
83		if let Ok(ExecutableDirectory) = std::env::current_exe() {
84			if let Some(Parent) = ExecutableDirectory.parent() {
85				// Resolve `..` segments lexically (no filesystem access needed)
86				// so log output is always clean even for non-existent paths.
87				// Falls back to `std::fs::canonicalize` for existing paths to
88				// also resolve symlinks.
89				let Normalize = |P:std::path::PathBuf| -> std::path::PathBuf {
90					if P.exists() {
91						return P.canonicalize().unwrap_or(P);
92					}
93					let mut Out:Vec<std::path::Component> = Vec::new();
94					for C in P.components() {
95						match C {
96							std::path::Component::ParentDir => {
97								Out.pop();
98							},
99							_ => Out.push(C),
100						}
101					}
102					Out.iter().collect()
103				};
104
105				// New canonical path: ../Resources/Static/Application/extensions.
106				// Extensions land here when tauri.conf.json bundle.resources maps
107				// them to "Static/Application/extensions" (single source layout).
108				let StaticAppExtPath = Parent.join("../Resources/Static/Application/extensions");
109
110				if StaticAppExtPath.exists() {
111					let StaticAppExtPath = Normalize(StaticAppExtPath);
112					dev_log!(
113						"extensions",
114						"[Extensions] [ScanPaths] + {} (Static/Application canonical)",
115						StaticAppExtPath.display()
116					);
117
118					ScanPathsGuard.push(StaticAppExtPath);
119				}
120
121				// Legacy flat path: ../Resources/extensions (kept for backward
122				// compat while the tauri.conf.json resources remap takes effect).
123				let ResourcesPath = Normalize(Parent.join("../Resources/extensions"));
124
125				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesPath.display());
126
127				ScanPathsGuard.push(ResourcesPath);
128
129				// VS Code-style bundle layout: `.app/Contents/Resources/app/extensions`.
130				let ResourcesAppPath = Normalize(Parent.join("../Resources/app/extensions"));
131
132				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesAppPath.display());
133
134				ScanPathsGuard.push(ResourcesAppPath);
135
136				// Debug/dev path: Target/debug/extensions
137				let LocalPath = Parent.join("extensions");
138
139				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", LocalPath.display());
140
141				ScanPathsGuard.push(LocalPath);
142
143				// Monorepo-layout fallback paths: resolved relative to
144				// `Element/Mountain/Target/{debug,release}/`, so they only
145				// materialise when the binary runs from inside the repo.
146				// Shipped `.app`s launched from `/Applications/` hit the
147				// `.exists()` guard and silently skip - no need for a
148				// `cfg(debug_assertions)` gate. Keeping these live in release
149				// lets a raw `Target/release/<name>` launch find the same 98
150				// built-in extensions a debug build does.
151				//
152				// Sky Target path: where CopyVSCodeAssets copies built-in
153				// extensions during the Sky build.
154				let SkyTargetPath = Parent.join("../../../Sky/Target/Static/Application/extensions");
155
156				if SkyTargetPath.exists() {
157					let SkyTargetPath = Normalize(SkyTargetPath);
158					dev_log!(
159						"extensions",
160						"[Extensions] [ScanPaths] + {} (Sky Target, repo-layout)",
161						SkyTargetPath.display()
162					);
163
164					ScanPathsGuard.push(SkyTargetPath);
165				}
166
167				// VS Code dependency path: built-in extensions from the VS
168				// Code source checkout - avoids requiring a copy step.
169				let DependencyPath = Parent.join("../../../../Dependency/Microsoft/Dependency/Editor/extensions");
170
171				if DependencyPath.exists() {
172					dev_log!(
173						"extensions",
174						"[Extensions] [ScanPaths] + {} (VS Code Dependency, repo-layout)",
175						DependencyPath.display()
176					);
177
178					ScanPathsGuard.push(DependencyPath);
179				}
180			}
181		}
182	} // end !SkipBuiltins
183
184	// User-scope paths: always scanned, independent of whether the binary
185	// was launched from the repo, a `.app`, or a symlink on the Desktop.
186	// Mirrors VS Code's `~/.vscode-oss/extensions` convention.
187	//
188	// Atom U1: `Lodge` overrides the default
189	// `~/.fiddee/extensions`. Useful for per-workspace sandboxes, shared
190	// caches on CI, or running against a test extensions set without
191	// polluting the user's real profile. The default root is resolved
192	// through the `Utilities::FiddeeRoot` atom so the dotfile name lives
193	// in one place.
194	if let Ok(UserOverride) = std::env::var("Lodge") {
195		let OverridePath = ExpandUserPath(&UserOverride);
196
197		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Lodge)", OverridePath.display());
198
199		ScanPathsGuard.push(OverridePath);
200	} else {
201		let UserExtensionPath = crate::IPC::WindServiceHandlers::Utilities::FiddeeRoot::Fn().join("extensions");
202
203		dev_log!(
204			"extensions",
205			"[Extensions] [ScanPaths] + {} (User)",
206			UserExtensionPath.display()
207		);
208
209		ScanPathsGuard.push(UserExtensionPath);
210	}
211
212	// Atom U1: additional paths via `Extend`. Mirrors
213	// VS Code's `--extensions-dir=<a>:<b>:<c>` CLI. Platform-separator:
214	// semicolon on Windows (matches PATHEXT), colon elsewhere.
215	if let Ok(Extras) = std::env::var("Extend") {
216		let Separator = if cfg!(target_os = "windows") { ';' } else { ':' };
217
218		for Candidate in Extras.split(Separator) {
219			let Trimmed = Candidate.trim();
220
221			if Trimmed.is_empty() {
222				continue;
223			}
224
225			let ExtraPath = ExpandUserPath(Trimmed);
226
227			dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Extend)", ExtraPath.display());
228
229			ScanPathsGuard.push(ExtraPath);
230		}
231	}
232
233	// Atom U1: development extensions path - the VS Code equivalent of
234	// `--extensionDevelopmentPath=<dir>`. Extensions here always load
235	// regardless of enablement state; kept separate from user-scope so a
236	// broken dev extension doesn't persist into the user's profile.
237	if let Ok(DevExtensions) = std::env::var("Probe") {
238		let DevPath = ExpandUserPath(&DevExtensions);
239
240		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Probe)", DevPath.display());
241
242		ScanPathsGuard.push(DevPath);
243	}
244
245	let ScanPaths = ScanPathsGuard.clone();
246
247	dev_log!("extensions", "[Extensions] [ScanPaths] Configured: {:?}", ScanPaths);
248
249	Ok(ScanPaths)
250}
251
252/// Expand a leading `~/` to `$HOME/` for user-provided paths. Env-var
253/// overrides frequently come from operators typing `~/.vscode/extensions`
254/// without shell expansion (e.g. in `.env` files, GUI launchers, sidecar
255/// manifests). Leaves absolute and relative paths untouched.
256fn ExpandUserPath(Raw:&str) -> PathBuf {
257	if let Some(Stripped) = Raw.strip_prefix("~/") {
258		if let Some(Home) = dirs::home_dir() {
259			return Home.join(Stripped);
260		}
261	}
262
263	PathBuf::from(Raw)
264}