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}