uv_python_interpreter/
virtualenv.rs1use std::borrow::Cow;
2use std::str::FromStr;
3use std::{
4 io,
5 path::{Path, PathBuf},
6};
7
8use fs_err as fs;
9use thiserror::Error;
10
11use uv_pypi_types::Scheme;
12
13use uv_python_types::PythonVersion;
14
15#[derive(Debug)]
17pub struct VirtualEnvironment {
18 pub root: PathBuf,
20
21 pub executable: PathBuf,
24
25 pub base_executable: PathBuf,
27
28 pub scheme: Scheme,
30}
31
32#[derive(Debug, Clone)]
34pub struct PyVenvConfiguration {
35 pub(super) home: Option<PathBuf>,
37 pub(super) virtualenv: bool,
39 pub(super) uv: bool,
41 pub(super) relocatable: bool,
43 pub(super) seed: bool,
45 pub(super) include_system_site_packages: bool,
47 pub(super) version: Option<PythonVersion>,
49}
50
51#[derive(Debug, Error)]
52pub enum Error {
53 #[error(transparent)]
54 Io(#[from] io::Error),
55 #[error("Broken virtual environment `{0}`: missing `pyvenv.cfg`")]
56 MissingPyVenvCfg(PathBuf),
57 #[error("Broken virtual environment `{0}`: could not parse `pyvenv.cfg`")]
58 ParsePyVenvCfg(PathBuf, #[source] io::Error),
59}
60
61pub fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
63 let venv = venv.as_ref();
64 if cfg!(windows) {
65 let default_executable = venv.join("Scripts").join("python.exe");
67 if default_executable.exists() {
68 return default_executable;
69 }
70
71 let executable = venv.join("bin").join("python.exe");
74 if executable.exists() {
75 return executable;
76 }
77
78 let executable = venv.join("python.exe");
80 if executable.exists() {
81 return executable;
82 }
83
84 default_executable
86 } else {
87 let default_executable = venv.join("bin").join("python");
89 if default_executable.exists() {
90 return default_executable;
91 }
92
93 let executable = venv.join("bin").join("python3");
95 if executable.exists() {
96 return executable;
97 }
98
99 default_executable
101 }
102}
103
104impl PyVenvConfiguration {
105 pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
107 let mut home = None;
108 let mut virtualenv = false;
109 let mut uv = false;
110 let mut relocatable = false;
111 let mut seed = false;
112 let mut include_system_site_packages = true;
113 let mut version = None;
114
115 let content = fs::read_to_string(&cfg)
119 .map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
120 for line in content.lines() {
121 let Some((key, value)) = line.split_once('=') else {
122 continue;
123 };
124 match key.trim() {
125 "home" => {
126 home = Some(PathBuf::from(value.trim()));
127 }
128 "virtualenv" => {
129 virtualenv = true;
130 }
131 "uv" => {
132 uv = true;
133 }
134 "relocatable" => {
135 relocatable = value.trim().to_lowercase() == "true";
136 }
137 "seed" => {
138 seed = value.trim().to_lowercase() == "true";
139 }
140 "include-system-site-packages" => {
141 include_system_site_packages = value.trim().to_lowercase() == "true";
142 }
143 "version" | "version_info" => {
144 version = Some(
145 PythonVersion::from_str(value.trim())
146 .map_err(|e| io::Error::new(std::io::ErrorKind::InvalidData, e))?,
147 );
148 }
149 _ => {}
150 }
151 }
152
153 Ok(Self {
154 home,
155 virtualenv,
156 uv,
157 relocatable,
158 seed,
159 include_system_site_packages,
160 version,
161 })
162 }
163
164 pub fn is_virtualenv(&self) -> bool {
166 self.virtualenv
167 }
168
169 pub fn is_uv(&self) -> bool {
171 self.uv
172 }
173
174 pub(crate) fn is_relocatable(&self) -> bool {
176 self.relocatable
177 }
178
179 pub fn is_seed(&self) -> bool {
181 self.seed
182 }
183
184 pub fn include_system_site_packages(&self) -> bool {
186 self.include_system_site_packages
187 }
188
189 pub fn set(content: &str, key: &str, value: &str) -> String {
191 let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
192 let mut found = false;
193 for line in &mut lines {
194 if let Some((lhs, _)) = line.split_once('=')
195 && lhs.trim() == key
196 {
197 *line = Cow::Owned(format!("{key} = {value}"));
198 found = true;
199 break;
200 }
201 }
202 if !found {
203 lines.push(Cow::Owned(format!("{key} = {value}")));
204 }
205 if lines.is_empty() {
206 String::new()
207 } else {
208 format!("{}\n", lines.join("\n"))
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215
216 use indoc::indoc;
217
218 use super::*;
219
220 #[test]
221 fn test_set_existing_key() {
222 let content = indoc! {"
223 home = /path/to/python
224 version = 3.8.0
225 include-system-site-packages = false
226 "};
227 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
228 assert_eq!(
229 result,
230 indoc! {"
231 home = /path/to/python
232 version = 3.9.0
233 include-system-site-packages = false
234 "}
235 );
236 }
237
238 #[test]
239 fn test_set_new_key() {
240 let content = indoc! {"
241 home = /path/to/python
242 version = 3.8.0
243 "};
244 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
245 assert_eq!(
246 result,
247 indoc! {"
248 home = /path/to/python
249 version = 3.8.0
250 include-system-site-packages = false
251 "}
252 );
253 }
254
255 #[test]
256 fn test_set_key_no_spaces() {
257 let content = indoc! {"
258 home=/path/to/python
259 version=3.8.0
260 "};
261 let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
262 assert_eq!(
263 result,
264 indoc! {"
265 home=/path/to/python
266 version=3.8.0
267 include-system-site-packages = false
268 "}
269 );
270 }
271
272 #[test]
273 fn test_set_key_prefix() {
274 let content = indoc! {"
275 home = /path/to/python
276 home_dir = /other/path
277 "};
278 let result = PyVenvConfiguration::set(content, "home", "new/path");
279 assert_eq!(
280 result,
281 indoc! {"
282 home = new/path
283 home_dir = /other/path
284 "}
285 );
286 }
287
288 #[test]
289 fn test_set_empty_content() {
290 let content = "";
291 let result = PyVenvConfiguration::set(content, "version", "3.9.0");
292 assert_eq!(
293 result,
294 indoc! {"
295 version = 3.9.0
296 "}
297 );
298 }
299}