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				// Standard Tauri bundle path: ../Resources/extensions.
86				// When launched from a `.app`, Parent is `Contents/MacOS/` and
87				// this resolves to `Contents/Resources/extensions`.
88				let ResourcesPath = Parent.join("../Resources/extensions");
89
90				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesPath.display());
91
92				ScanPathsGuard.push(ResourcesPath);
93
94				// VS Code-style bundle layout: `.app/Contents/Resources/app/extensions`.
95				// Some tooling copies built-ins here; probe both conventions so a
96				// single bundle works regardless of which copy step placed them.
97				let ResourcesAppPath = Parent.join("../Resources/app/extensions");
98
99				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesAppPath.display());
100
101				ScanPathsGuard.push(ResourcesAppPath);
102
103				// Debug/dev path: Target/debug/extensions
104				let LocalPath = Parent.join("extensions");
105
106				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", LocalPath.display());
107
108				ScanPathsGuard.push(LocalPath);
109
110				// Monorepo-layout fallback paths: resolved relative to
111				// `Element/Mountain/Target/{debug,release}/`, so they only
112				// materialise when the binary runs from inside the repo.
113				// Shipped `.app`s launched from `/Applications/` hit the
114				// `.exists()` guard and silently skip - no need for a
115				// `cfg(debug_assertions)` gate. Keeping these live in release
116				// lets a raw `Target/release/<name>` launch find the same 98
117				// built-in extensions a debug build does.
118				//
119				// Sky Target path: where CopyVSCodeAssets copies built-in
120				// extensions during the Sky build.
121				let SkyTargetPath = Parent.join("../../../Sky/Target/Static/Application/extensions");
122
123				if SkyTargetPath.exists() {
124					dev_log!(
125						"extensions",
126						"[Extensions] [ScanPaths] + {} (Sky Target, repo-layout)",
127						SkyTargetPath.display()
128					);
129
130					ScanPathsGuard.push(SkyTargetPath);
131				}
132
133				// VS Code dependency path: built-in extensions from the VS
134				// Code source checkout - avoids requiring a copy step.
135				let DependencyPath = Parent.join("../../../../Dependency/Microsoft/Dependency/Editor/extensions");
136
137				if DependencyPath.exists() {
138					dev_log!(
139						"extensions",
140						"[Extensions] [ScanPaths] + {} (VS Code Dependency, repo-layout)",
141						DependencyPath.display()
142					);
143
144					ScanPathsGuard.push(DependencyPath);
145				}
146			}
147		}
148	} // end !SkipBuiltins
149
150	// User-scope paths: always scanned, independent of whether the binary
151	// was launched from the repo, a `.app`, or a symlink on the Desktop.
152	// Mirrors VS Code's `~/.vscode-oss/extensions` convention.
153	//
154	// Atom U1: `Lodge` overrides the default
155	// `~/.land/extensions`. Useful for per-workspace sandboxes, shared
156	// caches on CI, or running against a test extensions set without
157	// polluting the user's real profile.
158	if let Ok(UserOverride) = std::env::var("Lodge") {
159		let OverridePath = ExpandUserPath(&UserOverride);
160
161		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Lodge)", OverridePath.display());
162
163		ScanPathsGuard.push(OverridePath);
164	} else if let Some(HomeDirectory) = dirs::home_dir() {
165		let UserExtensionPath = HomeDirectory.join(".land/extensions");
166
167		dev_log!(
168			"extensions",
169			"[Extensions] [ScanPaths] + {} (User)",
170			UserExtensionPath.display()
171		);
172
173		ScanPathsGuard.push(UserExtensionPath);
174	}
175
176	// Atom U1: additional paths via `Extend`. Mirrors
177	// VS Code's `--extensions-dir=<a>:<b>:<c>` CLI. Platform-separator:
178	// semicolon on Windows (matches PATHEXT), colon elsewhere.
179	if let Ok(Extras) = std::env::var("Extend") {
180		let Separator = if cfg!(target_os = "windows") { ';' } else { ':' };
181
182		for Candidate in Extras.split(Separator) {
183			let Trimmed = Candidate.trim();
184
185			if Trimmed.is_empty() {
186				continue;
187			}
188
189			let ExtraPath = ExpandUserPath(Trimmed);
190
191			dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Extend)", ExtraPath.display());
192
193			ScanPathsGuard.push(ExtraPath);
194		}
195	}
196
197	// Atom U1: development extensions path - the VS Code equivalent of
198	// `--extensionDevelopmentPath=<dir>`. Extensions here always load
199	// regardless of enablement state; kept separate from user-scope so a
200	// broken dev extension doesn't persist into the user's profile.
201	if let Ok(DevExtensions) = std::env::var("Probe") {
202		let DevPath = ExpandUserPath(&DevExtensions);
203
204		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Probe)", DevPath.display());
205
206		ScanPathsGuard.push(DevPath);
207	}
208
209	let ScanPaths = ScanPathsGuard.clone();
210
211	dev_log!("extensions", "[Extensions] [ScanPaths] Configured: {:?}", ScanPaths);
212
213	Ok(ScanPaths)
214}
215
216/// Expand a leading `~/` to `$HOME/` for user-provided paths. Env-var
217/// overrides frequently come from operators typing `~/.vscode/extensions`
218/// without shell expansion (e.g. in `.env` files, GUI launchers, sidecar
219/// manifests). Leaves absolute and relative paths untouched.
220fn ExpandUserPath(Raw:&str) -> PathBuf {
221	if let Some(Stripped) = Raw.strip_prefix("~/") {
222		if let Some(Home) = dirs::home_dir() {
223			return Home.join(Stripped);
224		}
225	}
226
227	PathBuf::from(Raw)
228}