asimov_env/envs/
python.rs1use crate::{env::Env, paths::python_env};
4use std::{
5 borrow::Cow,
6 fs::ReadDir,
7 io::Result,
8 path::PathBuf,
9 process::{Command, ExitStatus},
10};
11
12pub struct PythonEnv {
13 venv: Option<PathBuf>,
14}
15
16impl Default for PythonEnv {
17 fn default() -> Self {
18 Self::at(python_env())
19 }
20}
21
22impl Env for PythonEnv {
23 fn path(&self) -> Option<&std::path::PathBuf> {
33 self.venv.as_ref()
34 }
35
36 fn is_initialized(&self) -> bool {
37 match self.venv {
38 None => python().is_some(),
39 Some(ref path) => path.is_dir(),
40 }
41 }
42
43 fn initialize(&self) -> Result<()> {
44 if self.is_initialized() {
45 return Ok(());
46 }
47
48 let Some(ref path) = self.venv else {
49 return Ok(());
50 };
51
52 std::fs::create_dir_all(path)?;
54
55 let mut python = if self
56 .venv
57 .as_ref()
58 .is_some_and(|path| path.join("bin").join("python3").exists())
59 {
60 self.python()
61 } else {
62 Command::new(python().unwrap().as_ref())
63 };
64
65 python
67 .args(["-m", "venv", path.to_str().unwrap()])
68 .status()?;
69
70 Ok(())
71 }
72
73 fn available_modules(&self) -> std::io::Result<Vec<String>> {
74 Ok(vec![]) }
76
77 fn installed_modules(&self) -> std::io::Result<Vec<String>> {
78 let mut result = vec![];
79 for entry in self.packages_dir()? {
80 let Ok(entry) = entry else {
81 continue; };
83 if !entry.path().is_dir() {
84 continue; }
86 let name = entry.file_name();
87 let Some(name) = name.to_str() else {
88 continue; };
90 let Some(name) = name.strip_prefix("asimov_") else {
91 continue;
92 };
93 let Some(pos) = name.find("_module-") else {
94 continue;
95 };
96 result.push(name[..pos].to_string());
97 }
98 Ok(result)
99 }
100
101 fn install_module(
102 &self,
103 module_name: impl ToString,
104 verbosity: Option<u8>,
105 ) -> Result<ExitStatus> {
106 if !self.is_initialized() {
107 self.initialize()?;
108 }
109
110 let package_name = format!("asimov-{}-module", module_name.to_string());
111
112 self.pip_command("install", verbosity.unwrap_or(0))
113 .args(["--pre", &package_name])
114 .status()
115 }
116
117 fn uninstall_module(
118 &self,
119 module_name: impl ToString,
120 verbosity: Option<u8>,
121 ) -> Result<ExitStatus> {
122 if !self.is_initialized() {
123 return Ok(ExitStatus::default());
124 }
125
126 let package_name = format!("asimov-{}-module", module_name.to_string());
127
128 self.pip_command("uninstall", verbosity.unwrap_or(0))
129 .args(["--yes", &package_name])
130 .status()
131 }
132}
133
134impl PythonEnv {
135 pub fn system() -> Self {
136 Self { venv: None }
137 }
138
139 pub fn at(path: PathBuf) -> Self {
140 Self { venv: Some(path) }
141 }
142
143 pub fn python(&self) -> Command {
144 match self.venv {
145 None => Command::new(python().unwrap().as_ref()),
146 Some(ref path) => {
147 let mut command = Command::new(path.join("bin/python3"));
148 command.env("VIRTUAL_ENV", path.as_os_str());
149 command
150 },
151 }
152 }
153
154 pub fn pip(&self) -> Command {
155 let mut command = self.python();
156 command.args(["-m", "pip"]);
157 command
158 }
159
160 pub fn pip_command(&self, subcommand: &str, verbosity: u8) -> Command {
161 let mut command = self.pip();
162 command.arg(subcommand).args(Self::pip_verbosity(verbosity));
163 command.args(["--no-input", "--disable-pip-version-check"]);
164 command
165 }
166
167 pub fn pip_verbosity(verbosity: u8) -> Vec<&'static str> {
169 match verbosity {
170 0 => vec!["-q"],
171 1 => vec![],
172 2 => vec!["-v"],
173 3 => vec!["-vv"],
174 _ => vec!["-vvv"],
175 }
176 }
177
178 pub fn packages_dir(&self) -> std::io::Result<ReadDir> {
179 let Some(ref path) = self.packages_path() else {
180 return Err(std::io::Error::new(
181 std::io::ErrorKind::NotFound,
182 "`site-packages` directory not found",
183 ));
184 };
185 std::fs::read_dir(path)
186 }
187
188 pub fn packages_path(&self) -> Option<PathBuf> {
189 self.lib_path().map(|p| p.join("site-packages"))
190 }
191
192 pub fn lib_path(&self) -> Option<PathBuf> {
193 let Some(ref path) = self.venv else {
194 return None;
195 };
196 let path = path.join("lib/python3.13");
199 if path.is_dir() {
200 return Some(path);
201 }
202 let path = path.join("lib/python3.12");
203 if path.is_dir() {
204 return Some(path);
205 }
206 let path = path.join("lib/python3.11");
207 if path.is_dir() {
208 return Some(path);
209 }
210 let path = path.join("lib/python3.10");
211 if path.is_dir() {
212 return Some(path);
213 }
214 let path = path.join("lib/python3.9");
215 if path.is_dir() {
216 return Some(path);
217 }
218 None }
220}
221
222pub fn python() -> Option<Cow<'static, str>> {
223 getenv::python()
224 .map(Cow::from)
225 .or_else(|| Some(Cow::from("python3")))
226 .or_else(|| Some(Cow::from("python")))
227}