kotoba_build/
tasks.rs

1//! タスク実行モジュール
2
3use 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
12/// タスク実行エンジン
13pub 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    /// 新しいタスク実行エンジンを作成
21    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    /// タスクを実行
30    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    /// タスクを非同期で実行
47    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    /// タスクの実行を待機
85    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    /// タスクを実行(内部実装)
96    async fn execute_task(&self, task: &TaskConfig, task_name: &str) -> Result<()> {
97        // 依存タスクの実行(将来の拡張用)
98        if !task.depends_on.is_empty() {
99            println!("📋 Task {} has dependencies: {:?}", task_name, task.depends_on);
100            // 依存タスクの実行処理をここに追加
101        }
102
103        let mut cmd = Command::new(&task.command);
104        cmd.args(&task.args);
105
106        // 作業ディレクトリを設定
107        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        // 環境変数を設定
116        if let Some(env) = &task.env {
117            for (key, value) in env {
118                cmd.env(key, value);
119            }
120        }
121
122        // 出力設定
123        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        // コマンドを実行
130        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    /// 複数のタスクを順次実行
151    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    /// 複数のタスクを並列実行
159    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        // すべてのタスクが完了するのを待つ
177        for handle in handles {
178            handle.await.map_err(|e| BuildError::Task(format!("Task execution failed: {}", e)))??;
179        }
180
181        Ok(())
182    }
183
184    /// 全タスクを実行
185    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    /// タスクの依存関係を解決して実行順序を決定
193    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    /// 単一タスクの依存関係を解決
207    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        // 循環依存のチェック
216        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            // 依存タスクを再帰的に解決
228            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    /// タスクの実行時間を測定して実行
241    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    /// 実行中のタスク一覧を取得
250    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    /// 実行中のタスクをキャンセル
256    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    /// 全実行中のタスクをキャンセル
269    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    /// タスクの実行結果をキャッシュ
285    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    /// キャッシュキーを生成
301    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        // ファイルの変更時間を考慮
315        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    /// キャッシュが有効かどうかをチェック
325    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            // キャッシュファイルが存在し、最近更新されている場合は有効
334            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)); // 1時間以内
338                }
339            }
340        }
341
342        Ok(false)
343    }
344
345    /// キャッシュを更新
346    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/// タスクの実行オプション
362#[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
383/// タスク実行のユーティリティ関数
384pub 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
402/// シェルコマンドを実行
403pub 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}