virtualenv_rs/
interpreter.rs1use crate::{crate_cache_dir, Error};
2use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
3use fs_err as fs;
4use fs_err::File;
5use serde::{Deserialize, Serialize};
6use std::io;
7use std::io::{BufReader, Write};
8use std::process::{Command, Stdio};
9use std::time::SystemTime;
10use tracing::{debug, error, info, warn};
11
12const QUERY_PYTHON: &str = include_str!("query_python.py");
13
14#[derive(Clone, Debug, Deserialize, Serialize)]
15pub struct InterpreterInfo {
16 pub base_exec_prefix: String,
17 pub base_prefix: String,
18 pub major: u8,
19 pub minor: u8,
20 pub python_version: String,
21}
22
23pub fn get_interpreter_info(interpreter: &Utf8Path) -> Result<InterpreterInfo, Error> {
25 let cache_dir = crate_cache_dir()?.join("interpreter_info");
26
27 let index = seahash::hash(interpreter.as_str().as_bytes());
28 let cache_file = cache_dir.join(index.to_string()).with_extension("json");
29
30 let modified = fs::metadata(interpreter)?
31 .modified()?
32 .duration_since(SystemTime::UNIX_EPOCH)
33 .unwrap_or_default()
34 .as_millis();
35
36 if cache_file.exists() {
37 let cache_entry: Result<CacheEntry, String> = File::open(&cache_file)
38 .map_err(|err| err.to_string())
39 .and_then(|cache_reader| {
40 serde_json::from_reader(BufReader::new(cache_reader)).map_err(|err| err.to_string())
41 });
42 match cache_entry {
43 Ok(cache_entry) => {
44 debug!("Using cache entry {cache_file}");
45 if modified == cache_entry.modified && interpreter == cache_entry.interpreter {
46 return Ok(cache_entry.interpreter_info);
47 } else {
48 debug!(
49 "Removing mismatching cache entry {cache_file} ({} {} {} {})",
50 modified, cache_entry.modified, interpreter, cache_entry.interpreter
51 );
52 if let Err(remove_err) = fs::remove_file(&cache_file) {
53 warn!("Failed to mismatching cache file at {cache_file}: {remove_err}")
54 }
55 }
56 }
57 Err(cache_err) => {
58 debug!("Removing broken cache entry {cache_file} ({cache_err})");
59 if let Err(remove_err) = fs::remove_file(&cache_file) {
60 warn!("Failed to remove broken cache file at {cache_file}: {remove_err} (original error: {cache_err})")
61 }
62 }
63 }
64 }
65
66 let interpreter_info = query_interpreter(interpreter)?;
67 fs::create_dir_all(&cache_dir)?;
68 let cache_entry = CacheEntry {
69 interpreter: interpreter.to_path_buf(),
70 modified,
71 interpreter_info: interpreter_info.clone(),
72 };
73 let mut cache_writer = File::create(&cache_file)?;
74 serde_json::to_writer(&mut cache_writer, &cache_entry).map_err(io::Error::from)?;
75
76 Ok(interpreter_info)
77}
78
79#[derive(Clone, Debug, Deserialize, Serialize)]
80struct CacheEntry {
81 interpreter: Utf8PathBuf,
82 modified: u128,
83 interpreter_info: InterpreterInfo,
84}
85
86fn query_interpreter(interpreter: &Utf8Path) -> Result<InterpreterInfo, Error> {
88 let mut child = Command::new(interpreter)
89 .stdin(Stdio::piped())
90 .stdout(Stdio::piped())
91 .stderr(Stdio::piped())
92 .spawn()?;
93
94 if let Some(mut stdin) = child.stdin.take() {
95 stdin
96 .write_all(QUERY_PYTHON.as_bytes())
97 .map_err(|err| Error::PythonSubcommand {
98 interpreter: interpreter.to_path_buf(),
99 err,
100 })?;
101 }
102 let output = child.wait_with_output()?;
103 let stdout = String::from_utf8(output.stdout).unwrap_or_else(|err| {
104 error!(
107 "The stdout of the failed call of the call to {} contains non-utf8 characters",
108 interpreter
109 );
110 String::from_utf8_lossy(err.as_bytes()).to_string()
111 });
112 let stderr = String::from_utf8(output.stderr).unwrap_or_else(|err| {
113 error!(
114 "The stderr of the failed call of the call to {} contains non-utf8 characters",
115 interpreter
116 );
117 String::from_utf8_lossy(err.as_bytes()).to_string()
118 });
119 if !output.status.success() || !stderr.trim().is_empty() {
122 return Err(Error::PythonSubcommand {
123 interpreter: interpreter.to_path_buf(),
124 err: io::Error::new(
125 io::ErrorKind::Other,
126 format!(
127 "Querying python at {} failed with status {}:\n--- stdout:\n{}\n--- stderr:\n{}",
128 interpreter,
129 output.status,
130 stdout.trim(),
131 stderr.trim()
132 ),
133 )
134 });
135 }
136 let data = serde_json::from_str::<InterpreterInfo>(&stdout).map_err(|err|
137 Error::PythonSubcommand {
138 interpreter: interpreter.to_path_buf(),
139 err: io::Error::new(
140 io::ErrorKind::Other,
141 format!(
142 "Querying python at {} did not return the expected data ({}):\n--- stdout:\n{}\n--- stderr:\n{}",
143 interpreter,
144 err,
145 stdout.trim(),
146 stderr.trim()
147 )
148 )
149 }
150 )?;
151 Ok(data)
152}
153
154pub fn parse_python_cli(cli_python: Option<Utf8PathBuf>) -> Result<Utf8PathBuf, crate::Error> {
157 let python = if let Some(python) = cli_python {
158 if let Some((major, minor)) = python
159 .as_str()
160 .split_once('.')
161 .and_then(|(major, minor)| Some((major.parse::<u8>().ok()?, minor.parse::<u8>().ok()?)))
162 {
163 if major != 3 {
164 return Err(crate::Error::InvalidPythonInterpreter(
165 "Only python 3 is supported".into(),
166 ));
167 }
168 info!("Looking for python {major}.{minor}");
169 Utf8PathBuf::from(format!("python{major}.{minor}"))
170 } else {
171 python
172 }
173 } else {
174 Utf8PathBuf::from("python3".to_string())
175 };
176
177 let python = if python.components().count() > 1 {
179 info!("Assuming {python} is a path");
182 python
183 } else {
184 let python_in_path = which::which(python.as_std_path())
185 .map_err(|err| {
186 crate::Error::InvalidPythonInterpreter(
187 format!("Can't find {python} ({err})").into(),
188 )
189 })?
190 .try_into()
191 .map_err(|err: FromPathBufError| err.into_io_error())?;
192 info!("Resolved {python} to {python_in_path}");
193 python_in_path
194 };
195 Ok(python)
196}