1use super::{BuildConfig, TaskConfig, Result, BuildError};
4use colored::Colorize;
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::process::Stdio;
8use tokio::process::Command;
9use tokio::sync::RwLock;
10use std::sync::Arc;
11
12pub struct TaskRunner {
14 config: Arc<RwLock<BuildConfig>>,
15 project_root: PathBuf,
16 running_tasks: Arc<RwLock<HashMap<String, tokio::task::JoinHandle<Result<()>>>>>,
17}
18
19impl TaskRunner {
20 pub fn new(config: Arc<RwLock<BuildConfig>>, project_root: PathBuf) -> Self {
22 Self {
23 config,
24 project_root,
25 running_tasks: Arc::new(RwLock::new(HashMap::new())),
26 }
27 }
28
29 pub async fn run_task(&self, task_name: &str) -> Result<()> {
31 let config = self.config.read().await;
32
33 match config.tasks.get(task_name) {
34 Some(task_config) => {
35 println!("🚀 Running task: {}", task_name.green());
36 self.execute_task(task_config, task_name).await?;
37 println!("✅ Task {} completed successfully!", task_name.green());
38 Ok(())
39 }
40 None => {
41 Err(BuildError::Task(format!("Task '{}' not found", task_name)))
42 }
43 }
44 }
45
46 pub async fn run_task_async(&self, task_name: &str) -> Result<String> {
48 let task_name_clone = task_name.to_string();
49 let config = self.config.read().await;
50 let task_config = if let Some(tc) = config.tasks.get(&task_name_clone) {
51 Some(tc.clone())
52 } else {
53 None
54 };
55 let project_root = self.project_root.clone();
56
57 let handle = tokio::spawn(async move {
58 if let Some(task_config) = task_config {
59 let task_config_clone = task_config.clone();
60 let config_clone = BuildConfig {
61 name: "temp".to_string(),
62 version: "0.1.0".to_string(),
63 description: None,
64 tasks: std::collections::HashMap::from([(task_name_clone.clone(), task_config)]),
65 dependencies: std::collections::HashMap::new(),
66 };
67 let runner = TaskRunner::new(
68 Arc::new(RwLock::new(config_clone)),
69 project_root
70 );
71 runner.execute_task(&task_config_clone, &task_name_clone).await?;
72 Ok(())
73 } else {
74 Err(BuildError::Task(format!("Task '{}' not found", task_name_clone)))
75 }
76 });
77
78 let task_id = format!("task_{}_{}", task_name, chrono::Utc::now().timestamp());
79 self.running_tasks.write().await.insert(task_id.clone(), handle);
80
81 Ok(task_id)
82 }
83
84 pub async fn wait_for_task(&self, task_id: &str) -> Result<()> {
86 let mut running_tasks = self.running_tasks.write().await;
87
88 if let Some(handle) = running_tasks.remove(task_id) {
89 handle.await.map_err(|e| BuildError::Task(format!("Task execution failed: {}", e)))??;
90 }
91
92 Ok(())
93 }
94
95 async fn execute_task(&self, task: &TaskConfig, task_name: &str) -> Result<()> {
97 if !task.depends_on.is_empty() {
99 println!("📋 Task {} has dependencies: {:?}", task_name, task.depends_on);
100 }
102
103 let mut cmd = Command::new(&task.command);
104 cmd.args(&task.args);
105
106 let cwd = if let Some(cwd) = &task.cwd {
108 self.project_root.join(cwd)
109 } else {
110 self.project_root.clone()
111 };
112
113 cmd.current_dir(&cwd);
114
115 if let Some(env) = &task.env {
117 for (key, value) in env {
118 cmd.env(key, value);
119 }
120 }
121
122 cmd.stdout(Stdio::inherit());
124 cmd.stderr(Stdio::inherit());
125
126 println!("📝 Executing: {} {}", task.command, task.args.join(" "));
127 println!("📁 Working directory: {}", cwd.display());
128
129 let output = cmd.output().await?;
131
132 if output.status.success() {
133 Ok(())
134 } else {
135 let stderr = String::from_utf8_lossy(&output.stderr);
136 let stdout = String::from_utf8_lossy(&output.stdout);
137
138 println!("❌ Task {} failed!", task_name.red());
139 if !stdout.is_empty() {
140 println!("📄 stdout: {}", stdout);
141 }
142 if !stderr.is_empty() {
143 println!("⚠️ stderr: {}", stderr);
144 }
145
146 Err(BuildError::Task(format!("Command exited with status: {}", output.status)))
147 }
148 }
149
150 pub async fn run_tasks(&self, task_names: &[String]) -> Result<()> {
152 for task_name in task_names {
153 self.run_task(task_name).await?;
154 }
155 Ok(())
156 }
157
158 pub async fn run_tasks_parallel(&self, task_names: &[String]) -> Result<()> {
160 let mut handles = vec![];
161
162 for task_name in task_names {
163 let task_name = task_name.clone();
164 let self_clone = TaskRunner::new(
165 Arc::clone(&self.config),
166 self.project_root.clone(),
167 );
168
169 let handle = tokio::spawn(async move {
170 self_clone.run_task(&task_name).await
171 });
172
173 handles.push(handle);
174 }
175
176 for handle in handles {
178 handle.await.map_err(|e| BuildError::Task(format!("Task execution failed: {}", e)))??;
179 }
180
181 Ok(())
182 }
183
184 pub async fn run_all_tasks(&self) -> Result<()> {
186 let config = self.config.read().await;
187 let task_names: Vec<String> = config.tasks.keys().cloned().collect();
188
189 self.run_tasks(&task_names).await
190 }
191
192 pub async fn resolve_dependencies(&self, task_names: &[String]) -> Result<Vec<String>> {
194 let config = self.config.read().await;
195 let mut resolved = vec![];
196 let mut visited = std::collections::HashSet::new();
197 let mut visiting = std::collections::HashSet::new();
198
199 for task_name in task_names {
200 self.resolve_task_dependencies(&config, task_name, &mut resolved, &mut visited, &mut visiting)?;
201 }
202
203 Ok(resolved)
204 }
205
206 fn resolve_task_dependencies(
208 &self,
209 config: &BuildConfig,
210 task_name: &str,
211 resolved: &mut Vec<String>,
212 visited: &mut std::collections::HashSet<String>,
213 visiting: &mut std::collections::HashSet<String>,
214 ) -> Result<()> {
215 if visiting.contains(task_name) {
217 return Err(BuildError::Task(format!("Circular dependency detected involving task '{}'", task_name)));
218 }
219
220 if visited.contains(task_name) {
221 return Ok(());
222 }
223
224 visiting.insert(task_name.to_string());
225
226 if let Some(task) = config.tasks.get(task_name) {
227 for dep in &task.depends_on {
229 self.resolve_task_dependencies(config, dep, resolved, visited, visiting)?;
230 }
231 }
232
233 visiting.remove(task_name);
234 visited.insert(task_name.to_string());
235 resolved.push(task_name.to_string());
236
237 Ok(())
238 }
239
240 pub async fn run_task_with_timing(&self, task_name: &str) -> Result<std::time::Duration> {
242 let start = std::time::Instant::now();
243
244 self.run_task(task_name).await?;
245
246 Ok(start.elapsed())
247 }
248
249 pub async fn list_running_tasks(&self) -> Vec<String> {
251 let running_tasks = self.running_tasks.read().await;
252 running_tasks.keys().cloned().collect()
253 }
254
255 pub async fn cancel_task(&self, task_id: &str) -> Result<()> {
257 let mut running_tasks = self.running_tasks.write().await;
258
259 if let Some(handle) = running_tasks.remove(task_id) {
260 handle.abort();
261 println!("🛑 Task {} cancelled", task_id);
262 Ok(())
263 } else {
264 Err(BuildError::Task(format!("Task '{}' not found", task_id)))
265 }
266 }
267
268 pub async fn cancel_all_tasks(&self) -> Result<()> {
270 let mut running_tasks = self.running_tasks.write().await;
271
272 let task_ids: Vec<String> = running_tasks.keys().cloned().collect();
273
274 for task_id in task_ids {
275 if let Some(handle) = running_tasks.remove(&task_id) {
276 handle.abort();
277 println!("🛑 Task {} cancelled", task_id);
278 }
279 }
280
281 Ok(())
282 }
283
284 pub async fn run_task_with_cache(&self, task_name: &str) -> Result<()> {
286 let cache_key = self.generate_cache_key(task_name).await?;
287
288 if self.is_cache_valid(&cache_key).await? {
289 println!("📋 Using cached result for task: {}", task_name);
290 return Ok(());
291 }
292
293 self.run_task(task_name).await?;
294
295 self.update_cache(&cache_key).await?;
296
297 Ok(())
298 }
299
300 async fn generate_cache_key(&self, task_name: &str) -> Result<String> {
302 use std::collections::hash_map::DefaultHasher;
303 use std::hash::{Hash, Hasher};
304
305 let config = self.config.read().await;
306 let task = config.tasks.get(task_name)
307 .ok_or_else(|| BuildError::Task(format!("Task '{}' not found", task_name)))?;
308
309 let mut hasher = DefaultHasher::new();
310 task_name.hash(&mut hasher);
311 task.command.hash(&mut hasher);
312 task.args.hash(&mut hasher);
313
314 if let Ok(metadata) = tokio::fs::metadata(&self.project_root).await {
316 if let Ok(modified) = metadata.modified() {
317 modified.hash(&mut hasher);
318 }
319 }
320
321 Ok(format!("{:x}", hasher.finish()))
322 }
323
324 async fn is_cache_valid(&self, cache_key: &str) -> Result<bool> {
326 let cache_dir = dirs::cache_dir()
327 .unwrap_or_else(|| std::env::temp_dir())
328 .join("kotoba-build");
329
330 let cache_file = cache_dir.join(format!("{}.cache", cache_key));
331
332 if cache_file.exists() {
333 if let Ok(metadata) = tokio::fs::metadata(&cache_file).await {
335 if let Ok(modified) = metadata.modified() {
336 let age = modified.elapsed().unwrap_or(std::time::Duration::from_secs(0));
337 return Ok(age < std::time::Duration::from_secs(3600)); }
339 }
340 }
341
342 Ok(false)
343 }
344
345 async fn update_cache(&self, cache_key: &str) -> Result<()> {
347 let cache_dir = dirs::cache_dir()
348 .unwrap_or_else(|| std::env::temp_dir())
349 .join("kotoba-build");
350
351 tokio::fs::create_dir_all(&cache_dir).await?;
352
353 let cache_file = cache_dir.join(format!("{}.cache", cache_key));
354
355 tokio::fs::write(&cache_file, "").await?;
356
357 Ok(())
358 }
359}
360
361#[derive(Debug, Clone)]
363pub struct TaskOptions {
364 pub parallel: bool,
365 pub continue_on_error: bool,
366 pub verbose: bool,
367 pub timing: bool,
368 pub cache: bool,
369}
370
371impl Default for TaskOptions {
372 fn default() -> Self {
373 Self {
374 parallel: false,
375 continue_on_error: false,
376 verbose: false,
377 timing: false,
378 cache: false,
379 }
380 }
381}
382
383pub async fn run_script_command(command: &str, args: &[String], cwd: Option<&std::path::Path>) -> Result<()> {
385 let mut cmd = Command::new(command);
386 cmd.args(args);
387
388 if let Some(cwd) = cwd {
389 cmd.current_dir(cwd);
390 }
391
392 let output = cmd.output().await?;
393
394 if output.status.success() {
395 Ok(())
396 } else {
397 let stderr = String::from_utf8_lossy(&output.stderr);
398 Err(BuildError::Task(format!("Script execution failed: {}", stderr)))
399 }
400}
401
402pub async fn run_shell_command(command: &str, cwd: Option<&std::path::Path>) -> Result<String> {
404 let mut cmd = if cfg!(target_os = "windows") {
405 let mut c = Command::new("cmd");
406 c.args(&["/C", command]);
407 c
408 } else {
409 let mut c = Command::new("sh");
410 c.args(&["-c", command]);
411 c
412 };
413
414 if let Some(cwd) = cwd {
415 cmd.current_dir(cwd);
416 }
417
418 let output = cmd.output().await?;
419
420 if output.status.success() {
421 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
422 Ok(stdout)
423 } else {
424 let stderr = String::from_utf8_lossy(&output.stderr);
425 Err(BuildError::Task(format!("Shell command failed: {}", stderr)))
426 }
427}