cyrce_forge_langs/
python.rs1use std::path::Path;
8use std::process::Stdio;
9
10use colored::Colorize;
11
12use cyrce_forge_core::config::ForgeConfig;
13use cyrce_forge_core::error::{ForgeError, ForgeResult};
14
15pub struct PythonModule;
17
18impl PythonModule {
19 pub async fn setup(config: &ForgeConfig, project_dir: &Path) -> ForgeResult<()> {
21 let venv_dir = project_dir.join(".forge").join("venv");
22
23 if !venv_dir.exists() {
25 println!(" {}", "🐍 Creando entorno virtual Python...".cyan());
26
27 let python_cmd = Self::find_python().await?;
28
29 let output = tokio::process::Command::new(&python_cmd)
30 .args(["-m", "venv"])
31 .arg(&venv_dir)
32 .current_dir(project_dir)
33 .stdout(Stdio::piped())
34 .stderr(Stdio::piped())
35 .output()
36 .await
37 .map_err(|e| ForgeError::CommandNotFound {
38 command: format!("{}: {}", python_cmd, e),
39 })?;
40
41 if !output.status.success() {
42 let stderr = String::from_utf8_lossy(&output.stderr);
43 return Err(ForgeError::TaskFailed {
44 task_name: format!("python venv: {}", stderr),
45 exit_code: output.status.code().unwrap_or(-1),
46 }
47 .into());
48 }
49
50 println!(" {}", "✅ Entorno virtual creado".green());
51 }
52
53 if !config.dependencies.is_empty() {
55 Self::install_deps(config, project_dir).await?;
56 }
57
58 Ok(())
59 }
60
61 async fn install_deps(config: &ForgeConfig, project_dir: &Path) -> ForgeResult<()> {
63 let pip = Self::pip_path(project_dir);
64
65 println!(
66 " {}",
67 format!(
68 "📦 Instalando {} dependencias Python...",
69 config.dependencies.len()
70 )
71 .cyan()
72 );
73
74 let deps: Vec<String> = config
76 .dependencies
77 .iter()
78 .map(|(name, version)| {
79 if version == "*" || version.is_empty() {
80 name.clone()
81 } else {
82 format!("{}=={}", name, version)
83 }
84 })
85 .collect();
86
87 let mut cmd = tokio::process::Command::new(&pip);
88 cmd.arg("install").args(&deps);
89 cmd.current_dir(project_dir)
90 .stdout(Stdio::piped())
91 .stderr(Stdio::piped());
92
93 let output = cmd.output().await.map_err(|e| ForgeError::CommandNotFound {
94 command: format!("pip: {}", e),
95 })?;
96
97 if !output.status.success() {
98 let stderr = String::from_utf8_lossy(&output.stderr);
99 return Err(ForgeError::TaskFailed {
100 task_name: format!("pip install: {}", stderr),
101 exit_code: output.status.code().unwrap_or(-1),
102 }
103 .into());
104 }
105
106 println!(
107 " {}",
108 format!("✅ {} dependencias instaladas", deps.len()).green()
109 );
110
111 Ok(())
112 }
113
114 pub async fn compile(config: &ForgeConfig, project_dir: &Path) -> ForgeResult<()> {
116 let python_config = config.python.as_ref();
117 let source_dir = project_dir.join(
118 python_config
119 .map(|p| p.source.as_str())
120 .unwrap_or("src"),
121 );
122
123 if !source_dir.exists() {
124 return Err(ForgeError::IoError {
125 path: source_dir,
126 message: "Directorio fuente no existe. ¿Olvidaste crear tus archivos .py?"
127 .to_string(),
128 }
129 .into());
130 }
131
132 println!(" {}", "🐍 Verificando sintaxis Python...".cyan());
133
134 let python = Self::python_path(project_dir);
135
136 let output = tokio::process::Command::new(&python)
137 .args(["-m", "py_compile"])
138 .arg(
139 config
140 .main_entry()
141 .map(|s| source_dir.join(s).to_string_lossy().to_string())
142 .unwrap_or_else(|| source_dir.to_string_lossy().to_string()),
143 )
144 .current_dir(project_dir)
145 .stdout(Stdio::piped())
146 .stderr(Stdio::piped())
147 .output()
148 .await;
149
150 match output {
151 Ok(out) if out.status.success() => {
152 println!(" {}", "✅ Sintaxis Python válida".green());
153 }
154 Ok(out) => {
155 let stderr = String::from_utf8_lossy(&out.stderr);
156 println!(" {}", format!("⚠️ Advertencias: {}", stderr).yellow());
157 }
158 Err(_) => {
159 println!(
160 " {}",
161 "⚠️ No se pudo verificar la sintaxis (Python no encontrado en venv)".yellow()
162 );
163 }
164 }
165
166 Ok(())
167 }
168
169 pub async fn run(config: &ForgeConfig, project_dir: &Path) -> ForgeResult<()> {
171 let main_script = config
172 .main_entry()
173 .ok_or_else(|| ForgeError::ConfigMissingField {
174 field: "python.main-script".to_string(),
175 })?;
176
177 let python_config = config.python.as_ref();
178 let source_dir = project_dir.join(
179 python_config
180 .map(|p| p.source.as_str())
181 .unwrap_or("src"),
182 );
183
184 let script_path = source_dir.join(&main_script);
185
186 if !script_path.exists() {
187 return Err(ForgeError::IoError {
188 path: script_path,
189 message: format!(
190 "Script principal '{}' no encontrado",
191 main_script
192 ),
193 }
194 .into());
195 }
196
197 Self::setup(config, project_dir).await?;
199
200 let python = Self::python_path(project_dir);
201
202 println!(
203 " {}",
204 format!("🚀 Ejecutando {}...", main_script).cyan()
205 );
206 println!();
207
208 let mut cmd = tokio::process::Command::new(&python);
209 cmd.arg(&script_path)
210 .current_dir(project_dir)
211 .stdout(Stdio::inherit())
212 .stderr(Stdio::inherit());
213
214 let status = cmd.status().await.map_err(|e| ForgeError::CommandNotFound {
215 command: format!("python: {}", e),
216 })?;
217
218 if !status.success() {
219 return Err(ForgeError::TaskFailed {
220 task_name: "python".to_string(),
221 exit_code: status.code().unwrap_or(-1),
222 }
223 .into());
224 }
225
226 Ok(())
227 }
228
229 pub async fn test(config: &ForgeConfig, project_dir: &Path) -> ForgeResult<()> {
231 Self::setup(config, project_dir).await?;
232
233 let python = Self::python_path(project_dir);
234
235 println!(" {}", "🧪 Ejecutando tests Python...".cyan());
236
237 let mut cmd = tokio::process::Command::new(&python);
238 cmd.args(["-m", "pytest", "tests/"]) .current_dir(project_dir)
240 .stdout(Stdio::inherit())
241 .stderr(Stdio::inherit());
242
243 let status = cmd.status().await;
244
245 match status {
246 Ok(s) if s.success() => {
247 println!(" {}", "✅ Todos los tests pasaron exitosamente!".green());
248 }
249 Ok(s) => {
250 println!(
252 " {}",
253 "⚠️ pytest no disponible, intentando con unittest...".yellow()
254 );
255 let mut cmd2 = tokio::process::Command::new(&python);
256 cmd2.args(["-m", "unittest", "discover", "-v"])
257 .current_dir(project_dir)
258 .stdout(Stdio::inherit())
259 .stderr(Stdio::inherit());
260
261 let status2 = cmd2.status().await.map_err(|e| ForgeError::CommandNotFound {
262 command: format!("python unittest: {}", e),
263 })?;
264
265 if !status2.success() {
266 return Err(ForgeError::TaskFailed {
267 task_name: "python test".to_string(),
268 exit_code: s.code().unwrap_or(-1),
269 }
270 .into());
271 }
272 }
273 Err(e) => {
274 return Err(ForgeError::CommandNotFound {
275 command: format!("python: {}", e),
276 }
277 .into());
278 }
279 }
280
281 Ok(())
282 }
283
284 async fn find_python() -> ForgeResult<String> {
286 for cmd in &["python3", "python", "py"] {
288 let result = tokio::process::Command::new(cmd)
289 .arg("--version")
290 .stdout(Stdio::piped())
291 .stderr(Stdio::piped())
292 .output()
293 .await;
294
295 if let Ok(output) = result {
296 if output.status.success() {
297 return Ok(cmd.to_string());
298 }
299 }
300 }
301
302 Err(ForgeError::CommandNotFound {
303 command: "python/python3".to_string(),
304 }
305 .into())
306 }
307
308 fn python_path(project_dir: &Path) -> String {
310 let venv = project_dir.join(".forge").join("venv");
311 if cfg!(target_os = "windows") {
312 venv.join("Scripts").join("python.exe")
313 } else {
314 venv.join("bin").join("python")
315 }
316 .to_string_lossy()
317 .to_string()
318 }
319
320 fn pip_path(project_dir: &Path) -> String {
322 let venv = project_dir.join(".forge").join("venv");
323 if cfg!(target_os = "windows") {
324 venv.join("Scripts").join("pip.exe")
325 } else {
326 venv.join("bin").join("pip")
327 }
328 .to_string_lossy()
329 .to_string()
330 }
331}