Skip to main content

cargo_mate/cmd/
ddr.rs

1use anyhow::{Result, Context, bail};
2use clap::{Args, Subcommand};
3use colored::*;
4use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet};
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::process::{Command, Stdio};
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12use tokio::sync::{Semaphore, RwLock};
13use tokio::task::JoinSet;
14use toml;
15#[derive(Debug, Args)]
16pub struct DdrArgs {
17    #[command(subcommand)]
18    pub action: Option<DdrAction>,
19}
20#[derive(Debug, Subcommand)]
21pub enum DdrAction {
22    Build {
23        #[arg(short, long)]
24        image: Option<String>,
25        #[arg(short = 't', long)]
26        target: Vec<String>,
27        #[arg(short = 'j', long, default_value = "16")]
28        jobs: usize,
29        #[arg(short = 'c', long, default_value = "ddr.toml")]
30        config: PathBuf,
31        #[arg(long)]
32        use_config: bool,
33    },
34    Generate {
35        #[arg(short, long, default_value = "ddr.toml")]
36        output: PathBuf,
37        #[arg(long)]
38        auto: bool,
39    },
40    Status { #[arg(short, long)] verbose: bool },
41    Clean { #[arg(long)] all: bool, #[arg(short, long)] project: Option<String> },
42    Validate { #[arg(short, long, default_value = "ddr.toml")] config: PathBuf },
43}
44#[derive(Debug, Serialize, Deserialize, Clone)]
45pub struct DdrConfig {
46    pub project: ProjectConfig,
47    pub docker: DockerConfig,
48    pub targets: HashMap<String, TargetConfig>,
49    pub parallel: ParallelConfig,
50    pub cache: Option<CacheConfig>,
51    pub artifacts: Option<ArtifactConfig>,
52}
53#[derive(Debug, Serialize, Deserialize, Clone)]
54pub struct ProjectConfig {
55    pub name: String,
56    pub version: String,
57    pub workspace: Option<PathBuf>,
58    pub cargo_toml: PathBuf,
59    pub src_dir: PathBuf,
60}
61#[derive(Debug, Serialize, Deserialize, Clone)]
62pub struct DockerConfig {
63    pub registry: Option<String>,
64    pub build_args: Option<HashMap<String, String>>,
65    pub network: Option<String>,
66    pub volumes: Option<Vec<String>>,
67    pub env: Option<HashMap<String, String>>,
68}
69#[derive(Debug, Serialize, Deserialize, Clone)]
70pub struct TargetConfig {
71    pub triple: String,
72    pub image: String,
73    pub dockerfile: Option<PathBuf>,
74    pub features: Option<Vec<String>>,
75    pub rustflags: Option<String>,
76    pub linker: Option<String>,
77    pub strip: Option<bool>,
78    pub upx: Option<bool>,
79    pub test: Option<bool>,
80    pub bench: Option<bool>,
81    pub priority: Option<u8>,
82}
83#[derive(Debug, Serialize, Deserialize, Clone)]
84pub struct ParallelConfig {
85    pub max_jobs: usize,
86    pub batch_size: Option<usize>,
87    pub timeout_minutes: Option<u64>,
88    pub retry_failed: Option<u8>,
89    pub fail_fast: Option<bool>,
90}
91#[derive(Debug, Serialize, Deserialize, Clone)]
92pub struct CacheConfig {
93    pub registry_cache: Option<bool>,
94    pub cargo_cache: Option<PathBuf>,
95    pub sccache: Option<bool>,
96    pub cache_from: Option<Vec<String>>,
97}
98#[derive(Debug, Serialize, Deserialize, Clone)]
99pub struct ArtifactConfig {
100    pub output_dir: PathBuf,
101    pub compress: Option<bool>,
102    pub checksum: Option<bool>,
103    pub manifest: Option<bool>,
104}
105#[derive(Debug, Clone)]
106pub struct BuildJob {
107    pub id: String,
108    pub target: String,
109    pub image: String,
110    pub status: BuildStatus,
111    pub start_time: Option<Instant>,
112    pub end_time: Option<Instant>,
113    pub container_id: Option<String>,
114    pub output: Option<String>,
115    pub artifact_path: Option<PathBuf>,
116}
117#[derive(Debug, Clone, PartialEq)]
118pub enum BuildStatus {
119    Queued,
120    Running,
121    Success,
122    Failed(String),
123    Skipped,
124    Retrying(u8),
125}
126pub struct BuildOrchestrator {
127    config: DdrConfig,
128    jobs: Arc<RwLock<HashMap<String, BuildJob>>>,
129    semaphore: Arc<Semaphore>,
130    progress: MultiProgress,
131}
132impl BuildOrchestrator {
133    pub fn new(config: DdrConfig) -> Self {
134        let max_jobs = config.parallel.max_jobs.min(32).max(1);
135        Self {
136            config,
137            jobs: Arc::new(RwLock::new(HashMap::new())),
138            semaphore: Arc::new(Semaphore::new(max_jobs)),
139            progress: MultiProgress::new(),
140        }
141    }
142    pub async fn run(&self) -> Result<BuildReport> {
143        let start_time = Instant::now();
144        let mut job_handles = JoinSet::new();
145        let main_pb = self.create_main_progress_bar(self.config.targets.len());
146        let mut sorted_targets: Vec<_> = self.config.targets.iter().collect();
147        sorted_targets.sort_by_key(|(_, tc)| tc.priority.unwrap_or(100));
148        for (target_name, target_config) in sorted_targets {
149            let job = BuildJob {
150                id: format!("{}_{}", self.config.project.name, target_name),
151                target: target_name.clone(),
152                image: target_config.image.clone(),
153                status: BuildStatus::Queued,
154                start_time: None,
155                end_time: None,
156                container_id: None,
157                output: None,
158                artifact_path: None,
159            };
160            self.jobs.write().await.insert(job.id.clone(), job.clone());
161            let orchestrator = self.clone_for_job();
162            let target_config = target_config.clone();
163            let pb = self.create_target_progress_bar(&target_name);
164            job_handles
165                .spawn(async move {
166                    orchestrator.run_build_job(job, target_config, pb).await
167                });
168        }
169        let mut results = Vec::new();
170        while let Some(result) = job_handles.join_next().await {
171            match result {
172                Ok(Ok(job)) => results.push(job),
173                Ok(Err(e)) => eprintln!("Build job failed: {}", e),
174                Err(e) => eprintln!("Task panicked: {}", e),
175            }
176            main_pb.inc(1);
177        }
178        main_pb.finish_with_message("All builds complete!");
179        Ok(self.generate_report(results, start_time.elapsed()).await)
180    }
181    async fn run_build_job(
182        &self,
183        mut job: BuildJob,
184        target_config: TargetConfig,
185        pb: ProgressBar,
186    ) -> Result<BuildJob> {
187        let _permit = self.semaphore.acquire().await?;
188        pb.set_message(format!("Building {}", job.target));
189        job.status = BuildStatus::Running;
190        job.start_time = Some(Instant::now());
191        self.update_job(&job).await;
192        let dockerfile = if let Some(ref df) = target_config.dockerfile {
193            fs::read_to_string(df)?
194        } else {
195            self.generate_dockerfile(&target_config)?
196        };
197        let image_tag = format!("{}:{}", job.id, chrono::Utc::now().timestamp());
198        self.build_docker_image(&image_tag, &dockerfile, &pb).await?;
199        let container_id = self.run_container(&image_tag, &target_config, &pb).await?;
200        job.container_id = Some(container_id.clone());
201        let artifact_path = self.extract_artifacts(&container_id, &job.target).await?;
202        job.artifact_path = Some(artifact_path);
203        self.cleanup_container(&container_id).await?;
204        job.status = BuildStatus::Success;
205        job.end_time = Some(Instant::now());
206        self.update_job(&job).await;
207        pb.finish_with_message(format!("✅ {} complete", job.target));
208        Ok(job)
209    }
210    fn generate_dockerfile(&self, target: &TargetConfig) -> Result<String> {
211        let mut dockerfile = String::new();
212        dockerfile.push_str(&format!("FROM {} AS builder\n\n", target.image));
213        dockerfile.push_str("RUN apt-get update && apt-get install -y \\\n");
214        dockerfile.push_str("    build-essential \\\n");
215        dockerfile.push_str("    pkg-config \\\n");
216        dockerfile.push_str("    libssl-dev \\\n");
217        dockerfile.push_str("    && rm -rf /var/lib/apt/lists/*\n\n");
218        if !target.image.contains("rust") {
219            dockerfile
220                .push_str(
221                    "RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\n",
222                );
223            dockerfile.push_str("ENV PATH=/root/.cargo/bin:$PATH\n\n");
224        }
225        dockerfile.push_str(&format!("RUN rustup target add {}\n\n", target.triple));
226        if let Some(cache) = &self.config.cache {
227            if cache.sccache.unwrap_or(false) {
228                dockerfile.push_str("RUN cargo install sccache\n");
229                dockerfile.push_str("ENV RUSTC_WRAPPER=sccache\n\n");
230            }
231        }
232        dockerfile.push_str("WORKDIR /build\n\n");
233        dockerfile.push_str("COPY Cargo.toml Cargo.lock* ./\n");
234        dockerfile.push_str("COPY src ./src\n\n");
235        let mut build_cmd = format!("cargo build --release --target {}", target.triple);
236        if let Some(features) = &target.features {
237            build_cmd.push_str(&format!(" --features {}", features.join(",")));
238        }
239        dockerfile.push_str(&format!("RUN {}\n\n", build_cmd));
240        if target.strip.unwrap_or(true) {
241            dockerfile
242                .push_str(&format!("RUN strip target/{}/release/*\n\n", target.triple));
243        }
244        if target.upx.unwrap_or(false) {
245            dockerfile.push_str("RUN apt-get update && apt-get install -y upx\n");
246            dockerfile
247                .push_str(
248                    &format!(
249                        "RUN upx --best --lzma target/{}/release/* || true\n\n", target
250                        .triple
251                    ),
252                );
253        }
254        dockerfile.push_str("FROM scratch AS final\n");
255        dockerfile
256            .push_str(
257                &format!(
258                    "COPY --from=builder /build/target/{}/release/* /artifacts/\n",
259                    target.triple
260                ),
261            );
262        Ok(dockerfile)
263    }
264    async fn build_docker_image(
265        &self,
266        tag: &str,
267        dockerfile: &str,
268        pb: &ProgressBar,
269    ) -> Result<()> {
270        pb.set_message("Building Docker image...");
271        let temp_dockerfile = format!(
272            ".ddr_dockerfile_{}", chrono::Utc::now().timestamp()
273        );
274        fs::write(&temp_dockerfile, dockerfile)?;
275        let output = Command::new("docker")
276            .args(&["build", "-t", tag, "-f", &temp_dockerfile, "."])
277            .output()?;
278        let _ = fs::remove_file(&temp_dockerfile);
279        if !output.status.success() {
280            bail!("Docker build failed: {}", String::from_utf8_lossy(& output.stderr));
281        }
282        Ok(())
283    }
284    async fn run_container(
285        &self,
286        image: &str,
287        target: &TargetConfig,
288        pb: &ProgressBar,
289    ) -> Result<String> {
290        pb.set_message("Running build container...");
291        let mut cmd = Command::new("docker");
292        cmd.args(&["run", "-d"]);
293        if let Some(volumes) = &self.config.docker.volumes {
294            for vol in volumes {
295                cmd.args(&["-v", vol]);
296            }
297        }
298        if let Some(env) = &self.config.docker.env {
299            for (key, val) in env {
300                cmd.args(&["-e", &format!("{}={}", key, val)]);
301            }
302        }
303        if let Some(rustflags) = &target.rustflags {
304            cmd.args(&["-e", &format!("RUSTFLAGS={}", rustflags)]);
305        }
306        cmd.arg(image);
307        cmd.arg("sleep");
308        cmd.arg("3600");
309        let output = cmd.output()?;
310        if !output.status.success() {
311            bail!(
312                "Failed to start container: {}", String::from_utf8_lossy(& output.stderr)
313            );
314        }
315        Ok(String::from_utf8(output.stdout)?.trim().to_string())
316    }
317    async fn extract_artifacts(
318        &self,
319        container_id: &str,
320        target: &str,
321    ) -> Result<PathBuf> {
322        let artifacts_dir = self
323            .config
324            .artifacts
325            .as_ref()
326            .map(|a| a.output_dir.clone())
327            .unwrap_or_else(|| PathBuf::from("target/ddr"));
328        fs::create_dir_all(&artifacts_dir)?;
329        let target_dir = artifacts_dir.join(target);
330        fs::create_dir_all(&target_dir)?;
331        let output = Command::new("docker")
332            .args(
333                &[
334                    "cp",
335                    &format!("{}:/artifacts/.", container_id),
336                    &target_dir.to_string_lossy(),
337                ],
338            )
339            .output()?;
340        if !output.status.success() {
341            bail!(
342                "Failed to extract artifacts: {}", String::from_utf8_lossy(& output
343                .stderr)
344            );
345        }
346        if let Some(artifacts) = &self.config.artifacts {
347            if artifacts.checksum.unwrap_or(false) {
348                self.generate_checksums(&target_dir)?;
349            }
350        }
351        Ok(target_dir)
352    }
353    async fn cleanup_container(&self, container_id: &str) -> Result<()> {
354        Command::new("docker").args(&["rm", "-f", container_id]).output()?;
355        Ok(())
356    }
357    async fn update_job(&self, job: &BuildJob) {
358        let mut jobs = self.jobs.write().await;
359        jobs.insert(job.id.clone(), job.clone());
360    }
361    fn create_main_progress_bar(&self, total: usize) -> ProgressBar {
362        let pb = self.progress.add(ProgressBar::new(total as u64));
363        pb.set_style(
364            ProgressStyle::default_bar()
365                .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
366                .unwrap()
367                .progress_chars("#>-"),
368        );
369        pb
370    }
371    fn create_target_progress_bar(&self, target: &str) -> ProgressBar {
372        let pb = self.progress.add(ProgressBar::new_spinner());
373        pb.set_style(
374            ProgressStyle::default_spinner().template("{spinner:.green} {msg}").unwrap(),
375        );
376        pb.set_message(format!("⏳ {}", target));
377        pb
378    }
379    fn clone_for_job(&self) -> Self {
380        Self {
381            config: self.config.clone(),
382            jobs: Arc::clone(&self.jobs),
383            semaphore: Arc::clone(&self.semaphore),
384            progress: MultiProgress::new(),
385        }
386    }
387    async fn generate_report(
388        &self,
389        jobs: Vec<BuildJob>,
390        total_time: Duration,
391    ) -> BuildReport {
392        let jobs_guard = self.jobs.read().await;
393        let successful = jobs
394            .iter()
395            .filter(|j| j.status == BuildStatus::Success)
396            .count();
397        let failed = jobs
398            .iter()
399            .filter(|j| matches!(j.status, BuildStatus::Failed(_)))
400            .count();
401        BuildReport {
402            total_jobs: jobs.len(),
403            successful,
404            failed,
405            skipped: 0,
406            total_time,
407            jobs: jobs.clone(),
408            artifacts: jobs.iter().filter_map(|j| j.artifact_path.clone()).collect(),
409        }
410    }
411    fn generate_checksums(&self, dir: &Path) -> Result<()> {
412        use sha2::{Sha256, Digest};
413        use std::io::Read;
414        let mut checksums = String::new();
415        for entry in fs::read_dir(dir)? {
416            let entry = entry?;
417            if entry.file_type()?.is_file() {
418                let path = entry.path();
419                let mut file = fs::File::open(&path)?;
420                let mut hasher = Sha256::new();
421                let mut buffer = Vec::new();
422                file.read_to_end(&mut buffer)?;
423                hasher.update(&buffer);
424                let result = hasher.finalize();
425                checksums
426                    .push_str(
427                        &format!(
428                            "{:x}  {}\n", result, path.file_name().unwrap()
429                            .to_string_lossy()
430                        ),
431                    );
432            }
433        }
434        fs::write(dir.join("SHA256SUMS"), checksums)?;
435        Ok(())
436    }
437}
438#[derive(Debug)]
439pub struct BuildReport {
440    pub total_jobs: usize,
441    pub successful: usize,
442    pub failed: usize,
443    pub skipped: usize,
444    pub total_time: Duration,
445    pub jobs: Vec<BuildJob>,
446    pub artifacts: Vec<PathBuf>,
447}
448impl BuildReport {
449    pub fn print(&self) {
450        println!(
451            "\n{}",
452            "═══════════════════════════════════════"
453            .bright_blue()
454        );
455        println!("{}", "       DDR BUILD REPORT".bright_white().bold());
456        println!(
457            "{}",
458            "═══════════════════════════════════════"
459            .bright_blue()
460        );
461        println!("\n📊 {} Summary", "Build".bright_cyan());
462        println!("   Total Jobs:  {}", self.total_jobs.to_string().bright_white());
463        println!("   ✅ Success:  {}", self.successful.to_string().bright_green());
464        println!("   ❌ Failed:   {}", self.failed.to_string().bright_red());
465        println!("   ⏩ Skipped:  {}", self.skipped.to_string().bright_yellow());
466        println!("   ⏱️  Duration:  {:.2}s", self.total_time.as_secs_f64());
467        if !self.jobs.is_empty() {
468            println!("\n📦 {} Details:", "Target".bright_cyan());
469            for job in &self.jobs {
470                let status_icon = match job.status {
471                    BuildStatus::Success => "✅",
472                    BuildStatus::Failed(_) => "❌",
473                    BuildStatus::Skipped => "⏩",
474                    _ => "❓",
475                };
476                let duration = if let (Some(start), Some(end)) = (
477                    job.start_time,
478                    job.end_time,
479                ) {
480                    format!("{:.2}s", (end - start).as_secs_f64())
481                } else {
482                    "N/A".to_string()
483                };
484                println!(
485                    "   {} {} ({})", status_icon, job.target.bright_white(), duration
486                    .bright_black()
487                );
488            }
489        }
490        if !self.artifacts.is_empty() {
491            println!("\n🎯 {} Generated:", "Artifacts".bright_cyan());
492            for artifact in &self.artifacts {
493                println!("   📁 {}", artifact.display());
494            }
495        }
496        println!(
497            "\n{}",
498            "═══════════════════════════════════════"
499            .bright_blue()
500        );
501    }
502}
503pub async fn handle_ddr(action: Option<DdrAction>) -> Result<()> {
504    match action {
505        Some(DdrAction::Build { image, target, jobs, config, use_config }) => {
506            handle_build(image, target, jobs, config, use_config).await
507        }
508        Some(DdrAction::Generate { output, auto }) => handle_generate(output, auto).await,
509        Some(DdrAction::Status { verbose }) => handle_status(verbose).await,
510        Some(DdrAction::Clean { all, project }) => handle_clean(all, project).await,
511        Some(DdrAction::Validate { config }) => handle_validate(config).await,
512        None => {
513            if Path::new("ddr.toml").exists() {
514                handle_build(None, vec![], 16, PathBuf::from("ddr.toml"), true).await
515            } else {
516                println!("No ddr.toml found. Run 'cm ddr generate' to create one.");
517                Ok(())
518            }
519        }
520    }
521}
522async fn handle_build(
523    image: Option<String>,
524    targets: Vec<String>,
525    jobs: usize,
526    config_path: PathBuf,
527    use_config: bool,
528) -> Result<()> {
529    println!("{}", "🚀 Starting DDR Build Orchestration".bright_cyan().bold());
530    let config = if config_path.exists() && use_config {
531        let content = fs::read_to_string(&config_path)?;
532        toml::from_str::<DdrConfig>(&content)?
533    } else if config_path.exists() {
534        println!("Found existing config: {}", config_path.display());
535        print!("Use existing config? [Y/n]: ");
536        let mut input = String::new();
537        std::io::stdin().read_line(&mut input)?;
538        if input.trim().to_lowercase() != "n" {
539            let content = fs::read_to_string(&config_path)?;
540            toml::from_str::<DdrConfig>(&content)?
541        } else {
542            generate_config(image, targets, jobs)?
543        }
544    } else {
545        println!("No config found. Generating default configuration...");
546        let config = generate_config(image, targets, jobs)?;
547        let toml_str = toml::to_string_pretty(&config)?;
548        fs::write(&config_path, toml_str)?;
549        println!("Config saved to: {}", config_path.display());
550        config
551    };
552    let orchestrator = BuildOrchestrator::new(config);
553    let report = orchestrator.run().await?;
554    report.print();
555    Ok(())
556}
557async fn handle_generate(output: PathBuf, auto: bool) -> Result<()> {
558    println!("{}", "📝 Generating DDR Configuration".bright_cyan().bold());
559    let config = if auto {
560        auto_detect_config()?
561    } else {
562        generate_config(None, vec![], 16)?
563    };
564    let toml_str = toml::to_string_pretty(&config)?;
565    fs::write(&output, toml_str)?;
566    println!("✅ Configuration saved to: {}", output.display());
567    println!("\nExample usage:");
568    println!("  cm ddr build --use-config");
569    println!("  cm ddr build -t x86_64-unknown-linux-musl -j 8");
570    Ok(())
571}
572async fn handle_status(verbose: bool) -> Result<()> {
573    println!("{}", "📊 DDR Build Status".bright_cyan().bold());
574    let output = Command::new("docker")
575        .args(
576            &[
577                "ps",
578                "--filter",
579                "label=ddr=true",
580                "--format",
581                "table {{.ID}}\t{{.Names}}\t{{.Status}}",
582            ],
583        )
584        .output()?;
585    if output.status.success() {
586        let containers = String::from_utf8_lossy(&output.stdout);
587        if !containers.trim().is_empty() {
588            println!("\nActive DDR Containers:");
589            println!("{}", containers);
590        } else {
591            println!("\nNo active DDR builds.");
592        }
593    }
594    if verbose {
595        let output = Command::new("docker")
596            .args(
597                &[
598                    "images",
599                    "--filter",
600                    "label=ddr=true",
601                    "--format",
602                    "table {{.Repository}}\t{{.Tag}}\t{{.Size}}",
603                ],
604            )
605            .output()?;
606        if output.status.success() {
607            let images = String::from_utf8_lossy(&output.stdout);
608            if !images.trim().is_empty() {
609                println!("\nDDR Images:");
610                println!("{}", images);
611            }
612        }
613    }
614    Ok(())
615}
616async fn handle_clean(all: bool, project: Option<String>) -> Result<()> {
617    println!("{}", "🧹 Cleaning DDR Artifacts".bright_cyan().bold());
618    if all {
619        println!("Removing all DDR containers...");
620        Command::new("docker")
621            .args(&["rm", "-f", "$(docker ps -aq --filter label=ddr=true)"])
622            .output()?;
623        println!("Removing all DDR images...");
624        Command::new("docker")
625            .args(&["rmi", "-f", "$(docker images -q --filter label=ddr=true)"])
626            .output()?;
627        println!("✅ All DDR artifacts cleaned.");
628    } else if let Some(proj) = project {
629        println!("Cleaning project: {}", proj);
630        Command::new("docker")
631            .args(
632                &[
633                    "rm",
634                    "-f",
635                    &format!("$(docker ps -aq --filter label=ddr.project={})", proj),
636                ],
637            )
638            .output()?;
639        println!("✅ Project {} cleaned.", proj);
640    } else {
641        println!("Specify --all or --project <name> to clean.");
642    }
643    Ok(())
644}
645async fn handle_validate(config_path: PathBuf) -> Result<()> {
646    println!("{}", "🔍 Validating DDR Configuration".bright_cyan().bold());
647    if !config_path.exists() {
648        bail!("Config file not found: {}", config_path.display());
649    }
650    let content = fs::read_to_string(&config_path)?;
651    match toml::from_str::<DdrConfig>(&content) {
652        Ok(config) => {
653            println!("✅ Configuration is valid!");
654            println!("\nProject: {} v{}", config.project.name, config.project.version);
655            println!("Targets: {}", config.targets.len());
656            println!("Max Jobs: {}", config.parallel.max_jobs);
657            let output = Command::new("docker")
658                .args(&["version", "--format", "{{.Server.Version}}"])
659                .output()?;
660            if output.status.success() {
661                let version = String::from_utf8_lossy(&output.stdout);
662                println!("Docker: ✅ v{}", version.trim());
663            } else {
664                println!("Docker: ❌ Not available");
665            }
666        }
667        Err(e) => {
668            bail!("Configuration invalid: {}", e);
669        }
670    }
671    Ok(())
672}
673fn generate_config(
674    image: Option<String>,
675    targets: Vec<String>,
676    jobs: usize,
677) -> Result<DdrConfig> {
678    let cargo_toml = fs::read_to_string("Cargo.toml")?;
679    let cargo: toml::Value = toml::from_str(&cargo_toml)?;
680    let project_name = cargo["package"]["name"]
681        .as_str()
682        .unwrap_or("myproject")
683        .to_string();
684    let project_version = cargo["package"]["version"]
685        .as_str()
686        .unwrap_or("0.1.0")
687        .to_string();
688    let default_targets = if targets.is_empty() {
689        vec![
690            "x86_64-unknown-linux-musl".to_string(), "x86_64-unknown-linux-gnu"
691            .to_string(), "x86_64-pc-windows-gnu".to_string(),
692            "aarch64-unknown-linux-musl".to_string(),
693        ]
694    } else {
695        targets
696    };
697    let mut target_configs = HashMap::new();
698    for target in default_targets {
699        let image = match target.as_str() {
700            t if t.contains("musl") => "messense/rust-musl-cross:x86_64-musl",
701            t if t.contains("windows") => "rust:latest",
702            _ => image.as_deref().unwrap_or("rust:latest"),
703        };
704        target_configs
705            .insert(
706                target.clone(),
707                TargetConfig {
708                    triple: target.clone(),
709                    image: image.to_string(),
710                    dockerfile: None,
711                    features: None,
712                    rustflags: None,
713                    linker: None,
714                    strip: Some(true),
715                    upx: Some(false),
716                    test: Some(false),
717                    bench: Some(false),
718                    priority: None,
719                },
720            );
721    }
722    Ok(DdrConfig {
723        project: ProjectConfig {
724            name: project_name,
725            version: project_version,
726            workspace: None,
727            cargo_toml: PathBuf::from("Cargo.toml"),
728            src_dir: PathBuf::from("src"),
729        },
730        docker: DockerConfig {
731            registry: None,
732            build_args: None,
733            network: None,
734            volumes: None,
735            env: None,
736        },
737        targets: target_configs,
738        parallel: ParallelConfig {
739            max_jobs: jobs,
740            batch_size: None,
741            timeout_minutes: Some(30),
742            retry_failed: Some(1),
743            fail_fast: Some(false),
744        },
745        cache: Some(CacheConfig {
746            registry_cache: Some(true),
747            cargo_cache: Some(PathBuf::from("~/.cargo")),
748            sccache: Some(false),
749            cache_from: None,
750        }),
751        artifacts: Some(ArtifactConfig {
752            output_dir: PathBuf::from("target/ddr"),
753            compress: Some(false),
754            checksum: Some(true),
755            manifest: Some(true),
756        }),
757    })
758}
759fn auto_detect_config() -> Result<DdrConfig> {
760    println!("🔍 Auto-detecting build configuration...");
761    let output = Command::new("rustup")
762        .args(&["target", "list", "--installed"])
763        .output()?;
764    let installed_targets = if output.status.success() {
765        String::from_utf8_lossy(&output.stdout)
766            .lines()
767            .map(|s| s.trim().to_string())
768            .collect::<Vec<_>>()
769    } else {
770        vec![]
771    };
772    println!("Found {} installed targets", installed_targets.len());
773    let cpu_count = num_cpus::get();
774    let recommended_jobs = (cpu_count / 2).max(1).min(16);
775    println!("System CPUs: {}, Recommended jobs: {}", cpu_count, recommended_jobs);
776    generate_config(None, installed_targets, recommended_jobs)
777}