1pub use crate::_generated::python::MIN_PYTHON_VERSION;
3use crate::_generated::python::PYTHONS;
4use crate::built_info;
5use origen::core::status::DependencySrc;
6use origen::{Result, STATUS};
7use origen_metal::new_cmd;
8use origen_metal::utils::file::search_backwards_for_first;
9use semver::Version;
10use std::env;
11use std::path::PathBuf;
12use std::process::{Command, ExitStatus, Stdio};
13
14#[macro_export]
15macro_rules! strs_to_cli_arr {
16 ($name:expr, $strs:expr) => {{
17 format!(
18 "{}=[{}]",
19 $name,
20 $strs
21 .map(|t| format!("r'{}'", t))
22 .collect::<Vec<String>>()
23 .join(", ")
24 )
25 }};
26}
27
28lazy_static! {
29 pub static ref PYTHON_CONFIG: Config = Config::default();
30}
31
32macro_rules! pyproject_str {
33 () => {
34 "pyproject.toml"
35 };
36}
37macro_rules! user_env_str {
38 () => {
39 "ORIGEN_PYPROJECT"
40 };
41}
42
43const PYPROJECT: &'static str = pyproject_str!();
44const USER_ENV: &'static str = user_env_str!();
45
46lazy_static! {
47 pub static ref NO_ORIGEN_BOOT_MODULE_ERROR: &'static str =
48 "ModuleNotFoundError: No module named 'origen.boot'";
49}
50
51pub struct Config {
52 pub available: bool,
53 pub command: String,
54}
55
56pub fn resolve_pyproject() -> Result<DependencySrc> {
57 if let Some(app) = STATUS.app.as_ref() {
60 let path = app.root.join(PYPROJECT);
61 log_trace!("Found app pyproject: {}", path.display());
62 return Ok(DependencySrc::App(path));
63 }
64
65 if let Some(p) = search_backwards_for_first(env::current_dir()?, |p| {
66 let f = p.join(PYPROJECT);
67 log_trace!("Searching for workspace project from {}", p.display());
68 if f.exists() {
69 log_trace!("Found workspace pyproject: {}", f.display());
70 Ok(Some(f))
71 } else {
72 Ok(None)
73 }
74 })? {
75 return Ok(DependencySrc::Workspace(p.to_path_buf()));
76 }
77
78 if let Some(p) = env::var_os(USER_ENV) {
79 log_trace!(
80 "Attempting to find user-given pyproject: {}",
81 p.to_string_lossy()
82 );
83 let mut f = PathBuf::from(p);
84 if f.exists() {
85 f = f.join(PYPROJECT);
86 if f.exists() {
87 log_trace!("Found user-given pyproject: {}", f.display());
88 return Ok(DependencySrc::UserGlobal(f));
89 } else {
90 bail!(
91 concat!(
92 "Could not locate ",
93 pyproject_str!(),
94 " from ",
95 user_env_str!(),
96 " {}"
97 ),
98 f.display()
99 );
100 }
101 } else {
102 bail!(
103 concat!(user_env_str!(), " '{}' does not exists!"),
104 f.display()
105 );
106 }
107 }
108
109 let path = std::env::current_exe()?;
111 log_trace!(
112 "Searching CLI installation directories for pyproject: {}",
113 path.display()
114 );
115 if let Some(p) = search_backwards_for_first(path, |p| {
116 let f = p.join(PYPROJECT);
117 log_trace!("Searching for workspace project from {}", p.display());
118 if f.exists() {
119 log_trace!("Found workspace pyproject: {}", f.display());
120 Ok(Some(f))
121 } else {
122 Ok(None)
123 }
124 })? {
125 return Ok(DependencySrc::Global(p.to_path_buf()));
126 }
127
128 log_trace!("No pyproject found. Skipping UV invocations...");
129 Ok(DependencySrc::NoneFound)
130}
131
132impl Config {
133 pub fn base_cmd(&self) -> Result<Command> {
134 let dep_src = STATUS.dependency_src();
135 if let Some(path) = dep_src.as_ref().and_then(DependencySrc::src_file) {
136 crate::commands::env::guard_uv_manifest(path)?;
137 }
138 let mut c = if matches!(dep_src.as_ref(), Some(DependencySrc::NoneFound) | None) {
139 new_cmd!(&self.command)
140 } else {
141 new_cmd!("uv")
142 };
143
144 if let Some(dep_src) = dep_src.as_ref() {
145 match dep_src {
146 DependencySrc::App(path)
147 | DependencySrc::Workspace(path)
148 | DependencySrc::UserGlobal(path)
149 | DependencySrc::Global(path) => {
150 c.arg("--project");
151 c.arg(path.parent().unwrap_or(path));
152 }
153 DependencySrc::NoneFound => {}
154 }
155 } else {
156 log_error!(
157 "Dependency source has not been set - defaulting to global Python installation"
158 );
159 }
160 Ok(c)
161 }
162
163 pub fn run_cmd(&self, code: &str) -> Result<Command> {
164 let mut c = self.base_cmd()?;
165 if let Some(d) = STATUS.dependency_src().as_ref() {
166 if d.src_available() {
167 c.arg("run");
168 c.arg("--no-sync");
169 c.arg("--no-editable");
170 c.arg(&self.command);
171 }
172 }
173 c.arg("-c");
174 c.arg(code);
175 if let Some(d) = STATUS.dependency_src().as_ref() {
176 c.arg(format!("invocation={}", d));
177 if let Some(path) = d.src_file() {
178 c.arg(format!("pyproject_src={}", path.display()));
179 }
180 }
181 Ok(c)
182 }
183
184 pub fn uv_command(&self) -> Result<Command> {
185 let mut c = Command::new("uv");
186 if let Some(d) = STATUS.dependency_src().as_ref() {
187 if let Some(path) = d.src_file() {
188 crate::commands::env::guard_uv_manifest(path)?;
189 c.arg("--project");
190 c.arg(path.parent().unwrap_or(path));
191 }
192 }
193 Ok(c)
194 }
195}
196
197impl Default for Config {
198 fn default() -> Config {
199 match resolve_pyproject() {
200 Ok(deps) => STATUS.set_dependency_src(Some(deps)),
201 Err(e) => log_error!("Errors encountered resolving pyproject: {}", e),
202 }
203 for cmd in PYTHONS.iter() {
204 log_trace!("Searching for installed python at '{}'", cmd);
205 match get_version(cmd) {
206 Some(version) => {
207 if version >= Version::parse(MIN_PYTHON_VERSION).unwrap() {
208 log_trace!("Found python version '{}'", cmd);
209 return Config {
210 available: true,
211 command: cmd.to_string(),
212 };
213 }
214 }
215 None => {}
216 }
217 }
218 Config {
219 available: false,
220 command: String::from("python"),
221 }
222 }
223}
224
225fn get_version(command: &str) -> Option<Version> {
227 match new_cmd!(command).arg("--version").output() {
228 Ok(output) => return extract_version(std::str::from_utf8(&output.stdout).unwrap()),
229 Err(_e) => return None,
230 }
231}
232
233pub fn python_version() -> Option<Version> {
235 get_version(&PYTHON_CONFIG.command)
236}
237
238pub fn uv_version() -> Option<Version> {
240 match Command::new("uv").arg("--version").output() {
241 Ok(output) => {
242 let text = std::str::from_utf8(&output.stdout).unwrap();
243 log_trace!("{}", text);
244 extract_version(text)
245 }
246 Err(e) => {
247 log_debug!("{}", e);
248 None
249 }
250 }
251}
252
253fn extract_version(text: &str) -> Option<Version> {
254 let re = regex::Regex::new(r".*(\d+\.\d+\.\d+)([^\s\)]+)?").unwrap();
255
256 match re.captures(text) {
257 Some(x) => {
258 let c = {
259 let v = x.get(1).unwrap().as_str();
260 if let Some(p) = x.get(2) {
261 let mut p = p.as_str();
262 if p.starts_with("-") {
263 format!("{}{}", v, p)
264 } else {
265 if p.starts_with(".") || p.starts_with("+") {
266 p = &p[1..];
267 }
268 format!("{}-{}", v, p)
269 }
270 } else {
271 v.to_string()
272 }
273 };
274 match Version::parse(&c) {
275 Ok(v) => {
276 return Some(v);
277 }
278 Err(e) => {
279 panic!("Unable to parse version {}. Received Error:\n {}", c, e);
280 }
281 }
282 }
283 None => {
284 return None;
285 }
286 };
287}
288
289pub fn run(code: &str) -> Result<ExitStatus> {
291 let mut cmd = PYTHON_CONFIG.run_cmd(code)?;
292 if let Ok(p) = std::env::current_exe() {
295 cmd.arg(&format!("origen_cli={}", p.display()));
296 };
297 cmd.arg(&format!("origen_cli_version={}", built_info::PKG_VERSION));
298
299 add_origen_env(&mut cmd);
300
301 log_trace!("Running Python command: '{:?}'", cmd);
302
303 Ok(cmd.status()?)
304}
305
306#[macro_export]
308macro_rules! python_cmd {
309 ($code:expr) => {{
310 let mut cmd = PYTHON_CONFIG.run_cmd($code)?;
311 if let Ok(p) = std::env::current_exe() {
312 cmd.arg(&format!("origen_cli={}", p.display()));
313 };
314 cmd.arg(&format!("origen_cli_version={}", built_info::PKG_VERSION));
315 add_origen_env(&mut cmd);
316 log_trace!("Running Python command: '{:?}'", cmd);
317 cmd
318 }};
319}
320
321pub fn run_and_capture_stdout(code: &str) -> Result<String> {
323 let mut cmd = python_cmd!(code);
324 cmd.stdout(Stdio::piped());
325
326 let output = cmd.output()?;
327 if output.status.success() {
328 Ok(String::from_utf8_lossy(&output.stdout).to_string())
329 } else {
330 bail!(
331 "Something went wrong running the operation '{}', the log may have more details",
332 code
333 )
334 }
335}
336
337pub fn run_with_callbacks(
339 code: &str,
340 stdout_callback: Option<&mut dyn FnMut(&str)>,
341 stderr_callback: Option<&mut dyn FnMut(&str)>,
342) -> Result<()> {
343 use origen::utility::command_helpers::log_stdout_and_stderr;
344
345 let mut cmd = PYTHON_CONFIG.run_cmd(code)?;
346 if let Ok(p) = std::env::current_exe() {
349 cmd.arg(&format!("origen_cli={}", p.display()));
350 };
351 cmd.arg(&format!("origen_cli_version={}", built_info::PKG_VERSION));
352 cmd.stdout(Stdio::piped());
353 cmd.stderr(Stdio::piped());
354
355 add_origen_env(&mut cmd);
356
357 log_trace!("Running Python command: '{:?}'", cmd);
358
359 let mut process = cmd.spawn()?;
360
361 log_stdout_and_stderr(&mut process, stdout_callback, stderr_callback);
362
363 if process.wait()?.success() {
364 Ok(())
365 } else {
366 bail!(
367 "Something went wrong running the operation '{}', the log may have more details",
368 code
369 )
370 }
371}
372
373pub fn add_origen_env(cmd: &mut Command) {
375 if origen::STATUS.is_origen_present || origen::STATUS.is_app_in_origen_dev_mode {
376 let root = &origen::STATUS.origen_wksp_root;
383 let mut paths = vec![
384 root.join("rust").join("pyapi").join("target"),
385 root.join("python").join("origen_metal"),
386 ];
387 if let Some(inherited) = std::env::var_os("PYTHONPATH") {
388 paths.extend(std::env::split_paths(&inherited));
389 }
390 let mut deduped: Vec<std::path::PathBuf> = Vec::with_capacity(paths.len());
391 for path in paths {
392 if !deduped.contains(&path) {
393 deduped.push(path);
394 }
395 }
396 match std::env::join_paths(deduped) {
397 Ok(joined) => {
398 cmd.env("PYTHONPATH", joined);
399 }
400 Err(e) => {
401 log_debug!(
402 "Could not compose PYTHONPATH for the Origen environment: {}",
403 e
404 );
405 }
406 }
407 }
408}
409
410pub fn is_backend_origen_mod_missing_err(err: &origen::Error) -> bool {
411 err.to_string()
412 .contains("ModuleNotFoundError: No module named '_origen'")
413}
414
415pub fn get_current_user_and_email() -> Result<(String, String, String)> {
417 let out = run_and_capture_stdout("import origen; print('--user start--'); print(origen.current_user.first_name); print(origen.current_user.last_name); print(origen.current_user.email)")?;
418 let split = out.split_once("--user start--");
419 if let Some(s) = split {
420 let mut info = s.1.trim().split("\n");
421 let (fnm, lnm, email): (String, String, String);
422 if let Some(i) = info.next() {
423 fnm = i.trim().to_string();
424 } else {
425 bail!("Expected a first name when retrieving user info");
426 }
427 if let Some(i) = info.next() {
428 lnm = i.trim().to_string();
429 } else {
430 bail!("Expected a last name when retrieving user info");
431 }
432 if let Some(i) = info.next() {
433 email = i.trim().to_string();
434 } else {
435 bail!("Expected an email name when retrieving user info");
436 }
437 Ok((fnm, lnm, email))
438 } else {
439 bail!("Unable to find '--user start--' when retrieving user information");
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn extract_version_works() {
449 assert_eq!(
450 Version::parse("2.7.15-a").unwrap(),
451 extract_version("Python 2.7.15+a\n").unwrap()
452 );
453 assert_eq!(
454 Version::parse("3.6.8").unwrap(),
455 extract_version("Python 3.6.8 \n").unwrap()
456 );
457 assert_eq!(
458 Version::parse("1.1.0-rc1").unwrap(),
459 extract_version("uv 1.1.0rc1\n").unwrap()
460 );
461 }
462}