kotoba_build/
utils.rs

1//! ユーティリティ関数モジュール
2
3use super::{Result, BuildError};
4use colored::Colorize;
5use std::path::{Path, PathBuf};
6use std::fs;
7
8/// プロジェクトのルートディレクトリを検出
9pub fn find_project_root() -> Result<PathBuf> {
10    let current_dir = std::env::current_dir()
11        .map_err(|e| BuildError::Build(format!("Failed to get current directory: {}", e)))?;
12
13    let mut dir = current_dir.as_path();
14
15    loop {
16        // 設定ファイルが存在するかチェック
17        let config_files = [
18            "kotoba-build.toml",
19            "kotoba-build.json",
20            "kotoba-build.yaml",
21            "package.json",
22            "Cargo.toml",
23        ];
24
25        for config_file in &config_files {
26            if dir.join(config_file).exists() {
27                return Ok(dir.to_path_buf());
28            }
29        }
30
31        // 親ディレクトルに移動
32        if let Some(parent) = dir.parent() {
33            dir = parent;
34        } else {
35            break;
36        }
37    }
38
39    // ルートが見つからない場合はカレントディレクトリを使用
40    Ok(current_dir)
41}
42
43/// プロジェクトタイプを検出
44pub fn detect_project_type(project_root: &Path) -> ProjectType {
45    // Rustプロジェクト
46    if project_root.join("Cargo.toml").exists() {
47        return ProjectType::Rust;
48    }
49
50    // Node.jsプロジェクト
51    if project_root.join("package.json").exists() {
52        return ProjectType::NodeJs;
53    }
54
55    // Pythonプロジェクト
56    if project_root.join("requirements.txt").exists() ||
57       project_root.join("pyproject.toml").exists() ||
58       project_root.join("setup.py").exists() {
59        return ProjectType::Python;
60    }
61
62    // Goプロジェクト
63    if project_root.join("go.mod").exists() {
64        return ProjectType::Go;
65    }
66
67    // 汎用プロジェクト
68    ProjectType::Generic
69}
70
71/// プロジェクトタイプ
72#[derive(Debug, Clone, PartialEq)]
73pub enum ProjectType {
74    Rust,
75    NodeJs,
76    Python,
77    Go,
78    Generic,
79}
80
81impl std::fmt::Display for ProjectType {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            ProjectType::Rust => write!(f, "Rust"),
85            ProjectType::NodeJs => write!(f, "Node.js"),
86            ProjectType::Python => write!(f, "Python"),
87            ProjectType::Go => write!(f, "Go"),
88            ProjectType::Generic => write!(f, "Generic"),
89        }
90    }
91}
92
93/// ディレクトリを作成(存在しない場合のみ)
94pub fn ensure_dir_exists(dir_path: &Path) -> Result<()> {
95    if !dir_path.exists() {
96        fs::create_dir_all(dir_path)
97            .map_err(|e| BuildError::Build(format!("Failed to create directory {}: {}", dir_path.display(), e)))?;
98        println!("📁 Created directory: {}", dir_path.display());
99    }
100    Ok(())
101}
102
103/// ファイルをコピー
104pub fn copy_file(src: &Path, dst: &Path) -> Result<()> {
105    if let Some(parent) = dst.parent() {
106        ensure_dir_exists(parent)?;
107    }
108
109    fs::copy(src, dst)
110        .map_err(|e| BuildError::Build(format!("Failed to copy {} to {}: {}", src.display(), dst.display(), e)))?;
111
112    println!("📄 Copied: {} -> {}", src.display(), dst.display());
113    Ok(())
114}
115
116/// ディレクトリをコピー(再帰的)
117pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
118    if !src.exists() {
119        return Ok(());
120    }
121
122    ensure_dir_exists(dst)?;
123
124    for entry in fs::read_dir(src)
125        .map_err(|e| BuildError::Build(format!("Failed to read directory {}: {}", src.display(), e)))?
126    {
127        let entry = entry
128            .map_err(|e| BuildError::Build(format!("Failed to read entry: {}", e)))?;
129        let entry_path = entry.path();
130        let dst_path = dst.join(entry.file_name());
131
132        if entry_path.is_dir() {
133            copy_dir_recursive(&entry_path, &dst_path)?;
134        } else {
135            copy_file(&entry_path, &dst_path)?;
136        }
137    }
138
139    Ok(())
140}
141
142/// ファイルを削除
143pub fn remove_file(file_path: &Path) -> Result<()> {
144    if file_path.exists() {
145        fs::remove_file(file_path)
146            .map_err(|e| BuildError::Build(format!("Failed to remove file {}: {}", file_path.display(), e)))?;
147        println!("🗑️  Removed: {}", file_path.display());
148    }
149    Ok(())
150}
151
152/// ディレクトリを削除(再帰的)
153pub fn remove_dir_recursive(dir_path: &Path) -> Result<()> {
154    if dir_path.exists() {
155        fs::remove_dir_all(dir_path)
156            .map_err(|e| BuildError::Build(format!("Failed to remove directory {}: {}", dir_path.display(), e)))?;
157        println!("🗑️  Removed directory: {}", dir_path.display());
158    }
159    Ok(())
160}
161
162/// ファイルサイズを人間が読みやすい形式にフォーマット
163pub fn format_file_size(bytes: u64) -> String {
164    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
165
166    if bytes == 0 {
167        return "0 B".to_string();
168    }
169
170    let base = 1024_f64;
171    let log = (bytes as f64).log(base);
172    let unit_index = log.floor() as usize;
173
174    if unit_index >= UNITS.len() {
175        return format!("{} {}", bytes, UNITS[0]);
176    }
177
178    let size = bytes as f64 / base.powi(unit_index as i32);
179    format!("{:.1} {}", size, UNITS[unit_index])
180}
181
182/// 実行時間をフォーマット
183pub fn format_duration(duration: std::time::Duration) -> String {
184    let total_seconds = duration.as_secs();
185    let hours = total_seconds / 3600;
186    let minutes = (total_seconds % 3600) / 60;
187    let seconds = total_seconds % 60;
188    let millis = duration.subsec_millis();
189
190    if hours > 0 {
191        format!("{}h {}m {}s", hours, minutes, seconds)
192    } else if minutes > 0 {
193        format!("{}m {}s", minutes, seconds)
194    } else if seconds > 0 {
195        format!("{}.{:03}s", seconds, millis)
196    } else {
197        format!("{}ms", millis)
198    }
199}
200
201/// プログレスバーの作成
202pub fn create_progress_bar(total: u64, message: &str) -> indicatif::ProgressBar {
203    use indicatif::{ProgressBar, ProgressStyle};
204
205    let pb = ProgressBar::new(total);
206    pb.set_style(
207        ProgressStyle::default_bar()
208            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {msg}")
209            .unwrap()
210            .progress_chars("#>-"),
211    );
212    pb.set_message(message.to_string());
213    pb
214}
215
216/// スピナーの作成
217pub fn create_spinner(message: &str) -> indicatif::ProgressBar {
218    use indicatif::{ProgressBar, ProgressStyle};
219
220    let pb = ProgressBar::new_spinner();
221    pb.set_style(
222        ProgressStyle::default_spinner()
223            .template("{spinner:.green} {msg}")
224            .unwrap(),
225    );
226    pb.set_message(message.to_string());
227    pb.enable_steady_tick(std::time::Duration::from_millis(100));
228    pb
229}
230
231/// 成功メッセージを表示
232pub fn print_success(message: &str) {
233    println!("✅ {}", message.green());
234}
235
236/// エラーメッセージを表示
237pub fn print_error(message: &str) {
238    println!("❌ {}", message.red());
239}
240
241/// 警告メッセージを表示
242pub fn print_warning(message: &str) {
243    println!("⚠️  {}", message.yellow());
244}
245
246/// 情報メッセージを表示
247pub fn print_info(message: &str) {
248    println!("ℹ️  {}", message.blue());
249}
250
251/// コマンドライン引数を解析してオプションを取得
252pub fn parse_cli_args() -> clap::Command {
253    use clap::{Arg, Command};
254
255    Command::new("kotoba-build")
256        .version(env!("CARGO_PKG_VERSION"))
257        .author("Kotoba Team")
258        .about("Kotoba Build Tool - Project build and task management")
259        .arg(
260            Arg::new("task")
261                .help("Task to run")
262                .value_name("TASK")
263                .index(1),
264        )
265        .arg(
266            Arg::new("config")
267                .long("config")
268                .short('c')
269                .help("Path to config file")
270                .value_name("FILE"),
271        )
272        .arg(
273            Arg::new("watch")
274                .long("watch")
275                .short('w')
276                .help("Watch for file changes")
277                .action(clap::ArgAction::SetTrue),
278        )
279        .arg(
280            Arg::new("verbose")
281                .long("verbose")
282                .short('v')
283                .help("Verbose output")
284                .action(clap::ArgAction::SetTrue),
285        )
286        .arg(
287            Arg::new("list")
288                .long("list")
289                .short('l')
290                .help("List available tasks")
291                .action(clap::ArgAction::SetTrue),
292        )
293        .arg(
294            Arg::new("clean")
295                .long("clean")
296                .help("Clean build artifacts")
297                .action(clap::ArgAction::SetTrue),
298        )
299}
300
301/// 環境変数を取得(デフォルト値付き)
302pub fn get_env_var(key: &str, default: &str) -> String {
303    std::env::var(key).unwrap_or_else(|_| default.to_string())
304}
305
306/// プラットフォームを検出
307pub fn detect_platform() -> String {
308    format!("{}-{}",
309        std::env::consts::OS,
310        std::env::consts::ARCH
311    )
312}
313
314/// キャッシュディレクトリを取得
315pub fn get_cache_dir() -> Result<PathBuf> {
316    let cache_dir = dirs::cache_dir()
317        .unwrap_or_else(|| std::env::temp_dir())
318        .join("kotoba-build");
319
320    ensure_dir_exists(&cache_dir)?;
321    Ok(cache_dir)
322}
323
324/// 一時ディレクトリを取得
325pub fn get_temp_dir() -> Result<PathBuf> {
326    let temp_dir = std::env::temp_dir().join("kotoba-build");
327    ensure_dir_exists(&temp_dir)?;
328    Ok(temp_dir)
329}
330
331/// 設定ファイルのテンプレートを生成
332pub fn generate_config_template() -> String {
333    r#"# Kotoba Build Configuration
334name = "my-project"
335version = "0.1.0"
336description = "My awesome project"
337
338[tasks.dev]
339command = "cargo"
340args = ["run"]
341description = "Start development server"
342
343[tasks.build]
344command = "cargo"
345args = ["build", "--release"]
346description = "Build project in release mode"
347
348[tasks.test]
349command = "cargo"
350args = ["test"]
351description = "Run tests"
352
353[tasks.clean]
354command = "cargo"
355args = ["clean"]
356description = "Clean build artifacts"
357
358[tasks.lint]
359command = "cargo"
360args = ["clippy"]
361description = "Run linter"
362
363[dependencies]
364tokio = "1.0"
365serde = "1.0"
366
367[build]
368target = "x86_64-unknown-linux-gnu"
369release = false
370opt_level = "0"
371debug = true
372
373[dev]
374port = 3000
375host = "localhost"
376hot_reload = true
377open = false
378"#.to_string()
379}
380
381/// シェルコマンドを安全に実行
382pub async fn run_command_safely(command: &str, args: &[&str], cwd: Option<&Path>) -> Result<String> {
383    use tokio::process::Command;
384
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        .map_err(|e| BuildError::Build(format!("Failed to execute command: {}", e)))?;
394
395    if output.status.success() {
396        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
397        Ok(stdout)
398    } else {
399        let stderr = String::from_utf8_lossy(&output.stderr);
400        Err(BuildError::Build(format!("Command failed: {}", stderr)))
401    }
402}
403
404/// プロセスが実行中かどうかをチェック
405pub fn is_process_running(pid: u32) -> bool {
406    #[cfg(unix)]
407    {
408        use std::os::unix::process::ExitStatusExt;
409        use nix::sys::signal;
410        use nix::unistd::Pid;
411
412        let pid = Pid::from_raw(pid as i32);
413        signal::kill(pid, None).is_ok()
414    }
415
416    #[cfg(windows)]
417    {
418        use std::process::Command;
419        let output = Command::new("tasklist")
420            .args(&["/FI", &format!("PID eq {}", pid), "/NH"])
421            .output();
422
423        matches!(output, Ok(o) if o.status.success())
424    }
425
426    #[cfg(not(any(unix, windows)))]
427    {
428        false
429    }
430}
431
432/// 利用可能なCPUコア数を取得
433pub fn get_cpu_count() -> usize {
434    std::thread::available_parallelism()
435        .map(|n| n.get())
436        .unwrap_or(1)
437}
438
439/// メモリ使用量を取得(MB単位)
440pub fn get_memory_usage() -> Result<f64> {
441    #[cfg(unix)]
442    {
443        use std::fs;
444        let statm = fs::read_to_string("/proc/self/statm")
445            .map_err(|e| BuildError::Build(format!("Failed to read memory stats: {}", e)))?;
446
447        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as f64;
448        let rss_pages: f64 = statm.split_whitespace()
449            .nth(1)
450            .and_then(|s| s.parse().ok())
451            .unwrap_or(0.0);
452
453        Ok((rss_pages * page_size) / (1024.0 * 1024.0))
454    }
455
456    #[cfg(windows)]
457    {
458        // Windowsでは簡易的な実装
459        Ok(0.0)
460    }
461
462    #[cfg(not(any(unix, windows)))]
463    {
464        Ok(0.0)
465    }
466}