vx_core/
symlink_venv.rs

1//! Symlink-based virtual environment system
2//!
3//! This module provides functionality for creating virtual environments
4//! that use symlinks to reference globally installed tools, similar to pnpm.
5
6use crate::{GlobalToolManager, Result, VxEnvironment, VxError};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use tokio::fs;
11
12/// Virtual environment information
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct SymlinkVenv {
15    /// Virtual environment name
16    pub name: String,
17    /// Virtual environment path
18    pub path: PathBuf,
19    /// Tools linked in this venv
20    pub linked_tools: HashMap<String, String>, // tool_name -> version
21    /// Creation timestamp
22    pub created_at: chrono::DateTime<chrono::Utc>,
23    /// Last modified timestamp
24    pub modified_at: chrono::DateTime<chrono::Utc>,
25}
26
27/// Symlink virtual environment manager
28#[derive(Debug)]
29pub struct SymlinkVenvManager {
30    /// VX environment for path management
31    env: VxEnvironment,
32    /// Global tool manager for dependency tracking
33    global_manager: GlobalToolManager,
34    /// Path to venv registry file
35    registry_path: PathBuf,
36}
37
38impl SymlinkVenvManager {
39    /// Create a new symlink venv manager
40    pub fn new() -> Result<Self> {
41        let env = VxEnvironment::new()?;
42        let global_manager = GlobalToolManager::new()?;
43
44        let data_dir = env
45            .get_base_install_dir()
46            .parent()
47            .ok_or_else(|| VxError::Other {
48                message: "Failed to get VX data directory".to_string(),
49            })?
50            .to_path_buf();
51
52        let registry_path = data_dir.join("symlink_venvs.json");
53
54        Ok(Self {
55            env,
56            global_manager,
57            registry_path,
58        })
59    }
60
61    /// Create a new virtual environment
62    pub async fn create_venv(&self, name: &str, path: &Path) -> Result<()> {
63        // Check if venv already exists
64        if self.venv_exists(name).await? {
65            return Err(VxError::Other {
66                message: format!("Virtual environment '{}' already exists", name),
67            });
68        }
69
70        // Create venv directory structure
71        fs::create_dir_all(path).await.map_err(|e| VxError::Other {
72            message: format!("Failed to create venv directory: {}", e),
73        })?;
74
75        // Create bin directory for symlinks
76        let bin_dir = path.join("bin");
77        fs::create_dir_all(&bin_dir)
78            .await
79            .map_err(|e| VxError::Other {
80                message: format!("Failed to create venv bin directory: {}", e),
81            })?;
82
83        // Create venv info
84        let venv = SymlinkVenv {
85            name: name.to_string(),
86            path: path.to_path_buf(),
87            linked_tools: HashMap::new(),
88            created_at: chrono::Utc::now(),
89            modified_at: chrono::Utc::now(),
90        };
91
92        // Register venv
93        let mut venvs = self.load_venvs().await?;
94        venvs.insert(name.to_string(), venv);
95        self.save_venvs(&venvs).await?;
96
97        Ok(())
98    }
99
100    /// Link a global tool to a virtual environment
101    pub async fn link_tool(&self, venv_name: &str, tool_name: &str, version: &str) -> Result<()> {
102        // Check if tool is globally installed
103        if !self.global_manager.is_tool_installed(tool_name).await? {
104            return Err(VxError::Other {
105                message: format!("Tool '{}' is not globally installed", tool_name),
106            });
107        }
108
109        // Get venv info
110        let mut venvs = self.load_venvs().await?;
111        let venv = venvs.get_mut(venv_name).ok_or_else(|| VxError::Other {
112            message: format!("Virtual environment '{}' not found", venv_name),
113        })?;
114
115        // Get global tool installation path
116        let global_install_dir = self.env.get_version_install_dir(tool_name, version);
117        let global_exe = self
118            .env
119            .find_executable_in_dir(&global_install_dir, tool_name)?;
120
121        // Create symlink in venv bin directory
122        let venv_bin_dir = venv.path.join("bin");
123        let venv_exe = venv_bin_dir.join(tool_name);
124
125        // Remove existing symlink if it exists
126        if venv_exe.exists() {
127            fs::remove_file(&venv_exe)
128                .await
129                .map_err(|e| VxError::Other {
130                    message: format!("Failed to remove existing symlink: {}", e),
131                })?;
132        }
133
134        // Create symlink
135        self.create_symlink(&global_exe, &venv_exe).await?;
136
137        // Update venv registry
138        venv.linked_tools
139            .insert(tool_name.to_string(), version.to_string());
140        venv.modified_at = chrono::Utc::now();
141
142        // Update global tool dependencies
143        self.global_manager
144            .add_venv_dependency(venv_name, tool_name)
145            .await?;
146
147        // Save changes
148        self.save_venvs(&venvs).await?;
149
150        Ok(())
151    }
152
153    /// Unlink a tool from a virtual environment
154    pub async fn unlink_tool(&self, venv_name: &str, tool_name: &str) -> Result<()> {
155        // Get venv info
156        let mut venvs = self.load_venvs().await?;
157        let venv = venvs.get_mut(venv_name).ok_or_else(|| VxError::Other {
158            message: format!("Virtual environment '{}' not found", venv_name),
159        })?;
160
161        // Remove symlink
162        let venv_exe = venv.path.join("bin").join(tool_name);
163        if venv_exe.exists() {
164            fs::remove_file(&venv_exe)
165                .await
166                .map_err(|e| VxError::Other {
167                    message: format!("Failed to remove symlink: {}", e),
168                })?;
169        }
170
171        // Update venv registry
172        venv.linked_tools.remove(tool_name);
173        venv.modified_at = chrono::Utc::now();
174
175        // Update global tool dependencies
176        self.global_manager
177            .remove_venv_dependency(venv_name, tool_name)
178            .await?;
179
180        // Save changes
181        self.save_venvs(&venvs).await?;
182
183        Ok(())
184    }
185
186    /// Remove a virtual environment
187    pub async fn remove_venv(&self, name: &str) -> Result<()> {
188        let mut venvs = self.load_venvs().await?;
189
190        if let Some(venv) = venvs.get(name) {
191            // Remove all tool dependencies
192            for tool_name in venv.linked_tools.keys() {
193                self.global_manager
194                    .remove_venv_dependency(name, tool_name)
195                    .await?;
196            }
197
198            // Remove venv directory
199            if venv.path.exists() {
200                fs::remove_dir_all(&venv.path)
201                    .await
202                    .map_err(|e| VxError::Other {
203                        message: format!("Failed to remove venv directory: {}", e),
204                    })?;
205            }
206
207            // Remove from registry
208            venvs.remove(name);
209            self.save_venvs(&venvs).await?;
210        }
211
212        Ok(())
213    }
214
215    /// List all virtual environments
216    pub async fn list_venvs(&self) -> Result<Vec<SymlinkVenv>> {
217        let venvs = self.load_venvs().await?;
218        Ok(venvs.into_values().collect())
219    }
220
221    /// Get virtual environment info
222    pub async fn get_venv(&self, name: &str) -> Result<Option<SymlinkVenv>> {
223        let venvs = self.load_venvs().await?;
224        Ok(venvs.get(name).cloned())
225    }
226
227    /// Check if virtual environment exists
228    pub async fn venv_exists(&self, name: &str) -> Result<bool> {
229        let venvs = self.load_venvs().await?;
230        Ok(venvs.contains_key(name))
231    }
232
233    /// Create a symlink (cross-platform)
234    async fn create_symlink(&self, target: &Path, link: &Path) -> Result<()> {
235        #[cfg(unix)]
236        {
237            tokio::fs::symlink(target, link)
238                .await
239                .map_err(|e| VxError::Other {
240                    message: format!("Failed to create symlink: {}", e),
241                })
242        }
243
244        #[cfg(windows)]
245        {
246            // On Windows, try to create a symlink, but fall back to copying if it fails
247            match tokio::fs::symlink_file(target, link).await {
248                Ok(()) => Ok(()),
249                Err(_) => {
250                    // Fall back to copying the file
251                    tokio::fs::copy(target, link)
252                        .await
253                        .map_err(|e| VxError::Other {
254                            message: format!("Failed to copy file (symlink fallback): {}", e),
255                        })?;
256                    Ok(())
257                }
258            }
259        }
260    }
261
262    /// Load virtual environments registry from disk
263    async fn load_venvs(&self) -> Result<HashMap<String, SymlinkVenv>> {
264        if !self.registry_path.exists() {
265            return Ok(HashMap::new());
266        }
267
268        let content =
269            fs::read_to_string(&self.registry_path)
270                .await
271                .map_err(|e| VxError::Other {
272                    message: format!("Failed to read venv registry: {}", e),
273                })?;
274
275        serde_json::from_str(&content).map_err(|e| VxError::Other {
276            message: format!("Failed to parse venv registry: {}", e),
277        })
278    }
279
280    /// Save virtual environments registry to disk
281    async fn save_venvs(&self, venvs: &HashMap<String, SymlinkVenv>) -> Result<()> {
282        // Ensure parent directory exists
283        if let Some(parent) = self.registry_path.parent() {
284            fs::create_dir_all(parent)
285                .await
286                .map_err(|e| VxError::Other {
287                    message: format!("Failed to create registry directory: {}", e),
288                })?;
289        }
290
291        let content = serde_json::to_string_pretty(venvs).map_err(|e| VxError::Other {
292            message: format!("Failed to serialize venv registry: {}", e),
293        })?;
294
295        fs::write(&self.registry_path, content)
296            .await
297            .map_err(|e| VxError::Other {
298                message: format!("Failed to write venv registry: {}", e),
299            })
300    }
301}
302
303impl Default for SymlinkVenvManager {
304    fn default() -> Self {
305        Self::new().expect("Failed to create SymlinkVenvManager")
306    }
307}