py_executer_lib/
uv.rs

1use crate::warning_println;
2use anyhow::{anyhow, Result};
3use colored::*;
4use std::env;
5use std::path::PathBuf;
6use std::process::{Command, Stdio};
7
8pub fn get_uv_path() -> Result<String> {
9    // For Unix-like systems (Linux, macOS)
10    #[cfg(not(target_os = "windows"))]
11    let find_executable = "which";
12
13    // For Windows
14    #[cfg(target_os = "windows")]
15    let find_executable = "where";
16
17    let output = Command::new(find_executable).arg("uv").output()?;
18    if output.status.success() {
19        // found uv
20        let path = String::from_utf8(output.stdout)?.trim().to_string();
21        Ok(path)
22    } else {
23        // not found uv, hint to install it
24
25        // for unix, run wget -qO- https://astral.sh/uv/install.sh | sh
26        eprintln!("Please run the following command to install uv:");
27        #[cfg(not(target_os = "windows"))]
28        eprintln!("wget -qO- https://astral.sh/uv/install.sh | sh");
29
30        // for windows, run powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
31        #[cfg(target_os = "windows")]
32        eprintln!(
33            "powershell -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\""
34        );
35        Err(anyhow!("uv not installed"))
36    }
37}
38
39fn prepare_uv_project(project_path: &PathBuf, quiet: bool) -> Result<()> {
40    let uv_path = get_uv_path()?;
41    let output = Command::new(&uv_path)
42        .args(["init", "--bare", project_path.to_str().unwrap()])
43        .stdout(if quiet {
44            Stdio::null()
45        } else {
46            Stdio::inherit()
47        })
48        .stderr(Stdio::piped())
49        .output()?;
50
51    if output.status.success() {
52        Ok(())
53    } else {
54        if let Ok(stderr) = String::from_utf8(output.stderr) {
55            if stderr.contains("Project is already initialized") {
56                return Ok(());
57            }
58            eprintln!("{}", stderr);
59        }
60        Err(anyhow!("Failed to prepare uv project"))
61    }
62}
63
64pub fn prepare_venv(venv_path: &PathBuf, quiet: bool) -> Result<()> {
65    let uv_path = get_uv_path()?;
66    let output = Command::new(&uv_path)
67        .args(["venv", venv_path.to_str().unwrap()])
68        .stdout(if quiet {
69            Stdio::null()
70        } else {
71            Stdio::inherit()
72        })
73        .stderr(if quiet {
74            Stdio::null()
75        } else {
76            Stdio::inherit()
77        })
78        .output()?;
79
80    if output.status.success() {
81        Ok(())
82    } else {
83        Err(anyhow!("Failed to prepare venv"))
84    }
85}
86
87fn install_requirements(
88    venv_path: &PathBuf,
89    requirements_path: &PathBuf,
90    quiet: bool,
91) -> Result<()> {
92    if !requirements_path.exists() {
93        if !quiet {
94            warning_println!(
95                "{} not exists, skipping",
96                requirements_path.display().to_string().bold()
97            );
98        }
99        return Ok(());
100    }
101
102    let uv_path = get_uv_path()?;
103    let output = Command::new(&uv_path)
104        .args([
105            "add",
106            "--quiet",
107            "--directory",
108            venv_path.to_str().unwrap(),
109            "-r",
110            venv_path.join(requirements_path).to_str().unwrap(),
111        ])
112        .stdout(if quiet {
113            Stdio::null()
114        } else {
115            Stdio::inherit()
116        })
117        .stderr(if quiet {
118            Stdio::null()
119        } else {
120            Stdio::inherit()
121        })
122        .output()?;
123
124    if output.status.success() {
125        Ok(())
126    } else {
127        Err(anyhow!("Failed to install requirements"))
128    }
129}
130
131fn use_uv_venv(
132    quiet: bool,
133    clean: bool,
134    files_to_clean: &mut Vec<PathBuf>,
135) -> Result<(PathBuf, Vec<PathBuf>)> {
136    let current_dir = env::current_dir()?;
137
138    if clean {
139        let original_venv = current_dir.join(".venv");
140        if !original_venv.exists() {
141            // means originally .venv does not exist
142            files_to_clean.push(original_venv);
143        }
144
145        let pyproject_toml_path = current_dir.join("pyproject.toml");
146        if !pyproject_toml_path.exists() {
147            // means originally pyproject.toml does not exist
148            files_to_clean.push(pyproject_toml_path);
149        }
150
151        let uv_lock_path = current_dir.join("uv.lock");
152        if !uv_lock_path.exists() {
153            // means originally uv.lock does not exist
154            files_to_clean.push(uv_lock_path);
155        }
156    }
157
158    prepare_uv_project(&current_dir, quiet)?;
159    let venv_path = current_dir.join(".venv");
160    if !venv_path.exists() {
161        prepare_venv(&venv_path, quiet)?;
162    }
163    install_requirements(&venv_path, &current_dir.join("requirements.txt"), quiet)?;
164
165    Ok((venv_path, files_to_clean.to_vec()))
166}
167
168pub fn venv(
169    venv_path_from_arg: &PathBuf,
170    quiet: bool,
171    clean: bool,
172) -> Result<(PathBuf, Vec<PathBuf>)> {
173    let current_dir = env::current_dir()?;
174    let mut files_to_clean: Vec<PathBuf> = Vec::new();
175
176    if !venv_path_from_arg.exists() {
177        if venv_path_from_arg.to_str().unwrap_or("") == ".venv" {
178            // means using default .venv
179            // then try alternative venv
180            if !quiet {
181                warning_println!("No .venv found in current directory, trying venv");
182            }
183            let venv_path_alternate = current_dir.join("venv");
184            if venv_path_alternate.exists() {
185                if clean && !quiet {
186                    warning_println!("Clean mode is not activated when using existing venv");
187                }
188                if current_dir.join("requirements.txt").exists() && !quiet {
189                    warning_println!(
190                        "You are about to use an existing venv, it is not possible for now to install requirements.txt on it"
191                    );
192                }
193                return Ok((venv_path_alternate, files_to_clean));
194            }
195        }
196        if !quiet {
197            warning_println!("No venv found in current directory, trying uv");
198        }
199        let (venv_path, files_to_clean) = use_uv_venv(quiet, clean, &mut files_to_clean)?;
200        return Ok((venv_path, files_to_clean));
201    }
202
203    // provided venv exists (either default .venv or custom venv)
204    let venv_path_from_arg_absolute = match venv_path_from_arg.canonicalize() {
205        Ok(path) => path,
206        Err(err) => {
207            return Err(anyhow!(
208                "Can not get absolute path of {} , {}",
209                venv_path_from_arg.display().to_string().bold(),
210                err
211            ));
212        }
213    };
214
215    if clean && !quiet {
216        warning_println!("Clean mode is not activated when using existing venv");
217    }
218    if current_dir.join("requirements.txt").exists() {
219        if !quiet {
220            warning_println!(
221                "You are about to use an existing venv, it is not possible for now to install requirements.txt on it"
222            );
223        }
224    }
225    Ok((venv_path_from_arg_absolute, files_to_clean))
226}