virtualenv_rs/
interpreter.rs

1use 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
23/// Gets the interpreter.rs info, either cached or by running it.
24pub 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
86/// Runs a python script that returns the relevant info about the interpreter.rs as json
87fn 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        // At this point, there was most likely an error caused by a non-utf8 character, so we're in
105        // an ugly case but still very much want to give the user a chance
106        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    // stderr isn't technically a criterion for success, but i don't know of any cases where there
120    // should be stderr output and if there is, we want to know
121    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
154/// Parse the value of the `-p`/`--python` option, which can be e.g. `3.11`, `python3.11`,
155/// `tools/bin/python3.11` or `/usr/bin/python3.11`.
156pub 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    // Call `which` to find it in path, if not given a path
178    let python = if python.components().count() > 1 {
179        // Does this path contain a slash (unix) or backslash (windows)? In that case, assume it's
180        // relative or absolute path that we don't need to resolve
181        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}