// DCR — Cargo-like C/C++ project manager. // // Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use crate::core::build_config::Config; use crate::core::workspace::parse_workspace; use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info}; use crate::utils::fs::{check_dir, find_project_root, with_dir}; use crate::utils::log::{error, warn}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use glob::glob; use std::fs; use std::path::Path; /// Entry point for the `dcr clean` command. /// /// Removes build artifacts from the project (and optionally workspace members). /// /// # Parameters /// - `args`: Tokens after `dcr clean` (`--debug`/`--release`, `--target`, `--all`). /// /// # Returns /// Process exit code: `0` on success, `1` on failure. pub fn clean(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); printc( " dcr clean [--debug | --release] [--target ] [--all]", BOLD_CYAN, ); println!(); printc("DESCRIPTION:", BOLD_GREEN); println!(" Removes build artifacts from the target directory."); println!(); printc("OPTIONS:", BOLD_GREEN); println!(" --debug Clean debug artifacts (default)"); println!(" --release Clean release artifacts"); println!(" --target Clean artifacts for a specific target"); println!(" --all Clean all workspace members"); return 0; } let start_dir = match std::env::current_dir() { Ok(dir) => dir, Err(_) => { error("Failed to determine current directory"); return 1; } }; let root = match find_project_root(&start_dir) { Ok(Some(dir)) => dir, Ok(None) => { error("dcr.toml file not found"); return 1; } Err(_) => { error("Failed to find project root"); return 1; } }; let flags = match parse_clean_flags(args) { Ok(v) => v, Err(msg) => { error(&msg); return 1; } }; // Run cleanup relative to the project root so paths resolve consistently. match with_dir(&root, || clean_from_root(&root, &flags)) { Ok(()) => 0, Err(msg) => { error(&msg); 1 } } } /// Parsed flags controlling which artifacts `clean` should remove. struct CleanFlags { /// Optional build profile name (e.g. `debug`, `release`). profile: Option, /// Optional target triple to clean under `target//...`. target: Option, /// When true, also clean each workspace member that has a `target` directory. all: bool, } /// Parses CLI arguments into [`CleanFlags`]. /// /// Recognizes `--all`, `--target `, and profile flags such as `--debug` /// / `--release` (validated via [`default_profile_flags`]). fn parse_clean_flags(args: &[String]) -> Result { let mut profile: Option = None; let mut target: Option = None; let mut all = false; let mut iter = args.iter(); while let Some(arg) = iter.next() { if arg == "--all" { all = true; continue; } if arg == "--target" { if let Some(t) = iter.next() { target = Some(t.clone()); } else { return Err("--target requires a value".to_string()); } continue; } if arg.starts_with("--") { // Accept only known profile names (e.g. debug/release), not arbitrary flags. let candidate = arg.trim_start_matches("--").to_string(); if !default_profile_flags(&candidate).is_empty() { if profile.is_some() { return Err("Duplicate profile flag".to_string()); } profile = Some(candidate); continue; } } return Err("Unknown argument".to_string()); } Ok(CleanFlags { profile, target, all, }) } /// Cleans the project at `root`, and optionally all workspace members. /// /// Resolves the effective target triple from flags, `dcr.toml`, or host defaults, /// then removes the appropriate `target` subdirectories and custom clean paths. fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> { let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; // Prefer CLI --target, then build.target from config, then host defaults on Linux/BSD. let target = flags.target.clone().or_else(|| { config .get("build.target") .and_then(|v| v.as_str()) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) }); let target = match target { Some(t) => Some(crate::utils::build::normalize_target_os(&t).to_string()), None if cfg!(target_os = "linux") || cfg!(any( target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly" )) => { Some(default_target_triple()) } None => None, }; if flags.all && let Some(workspace) = parse_workspace( &config, flags.profile.as_deref().unwrap_or("debug"), target.as_deref(), root, )? { for member in &workspace.members { let member_target = member.path.join("target"); if member_target.is_dir() { clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?; } } } clean_project_at(root, flags.profile.as_deref(), target.as_deref()) } /// Removes build artifacts for a single project directory. /// /// When `profile` is set, only that profile's directory under `target` (optionally /// scoped by `target` triple) is removed, plus custom paths for that profile. /// When `profile` is `None`, the entire `target` tree is removed and custom paths /// are cleaned for both `debug` and `release`. fn clean_project_at( project_root: &Path, profile: Option<&str>, target: Option<&str>, ) -> Result<(), String> { with_dir(project_root, || { let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let project_name = std::env::current_dir() .ok() .and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string())) .unwrap_or_else(|| "project".to_string()); let items = check_dir(None).map_err(|_| "Failed to read project directory".to_string())?; if !items.contains(&"dcr.toml".to_string()) { return Err("dcr.toml file not found".to_string()); } println!( " Cleaning project `{}`", colored(&project_name, BOLD_GREEN) ); if let Some(profile) = profile { let target_dir = if let Some(t) = target { format!("target/{t}/{profile}") } else { format!("target/{profile}") }; let target_items = check_dir(Some("target")).unwrap_or_default(); let parent_dir = target.unwrap_or(""); // Existence check differs: with a triple, look for target/; otherwise target/. let dir_exists = if target.is_some() { target_items.contains(&parent_dir.to_string()) } else { target_items.contains(&profile.to_string()) }; if !dir_exists { warn(&format!("Directory target/{} not found", target_dir)); } else { println!(" Profile: {}", colored(profile, BOLD_GREEN)); if let Some(t) = target { println!(" Target: {}", colored(t, BOLD_GREEN)); } let _ = fs::remove_dir_all(&target_dir); println!( "{} Removed directory {}", colored("\n ✔", BOLD_GREEN), target_dir ); } clean_custom_paths(&config, profile)?; return Ok(()); } if items.contains(&"target".to_string()) { let _ = fs::remove_dir_all("target"); println!( "{} Removed directory target", colored("\n ✔", BOLD_GREEN) ); } else { warn("Directory target not found"); } clean_custom_paths(&config, "debug")?; clean_custom_paths(&config, "release")?; Ok(()) }) } /// Deletes extra paths listed under `build.clean` in `dcr.toml` for a profile. /// /// Each pattern may contain `{profile}` and version placeholders, which are /// expanded before matching with `glob`. Matched files and directories are removed. fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> { let patterns = match config.get("build.clean") { Some(v) => v .as_array() .ok_or_else(|| "build.clean must be an array of strings".to_string())? .iter() .filter_map(|item| item.as_str()) .map(|s| { let value = s.replace("{profile}", profile); substitute_clean_vars(&value, config) }) .collect::>(), None => Vec::new(), }; if patterns.is_empty() { return Ok(()); } for pattern in patterns { for entry in glob(&pattern).map_err(|err| format!("glob error: {err}"))? { let path = entry.map_err(|err| format!("glob error: {err}"))?; if path.is_dir() { let _ = fs::remove_dir_all(&path); } else if path.is_file() { let _ = fs::remove_file(&path); } } } Ok(()) } /// Expands version-related placeholders in a clean path pattern. /// /// Reads `package.version` from `config`, parses it, and applies /// [`crate::utils::build::substitute_version_vars`]. fn substitute_clean_vars(value: &str, config: &Config) -> String { let version = config .get("package.version") .and_then(|v| v.as_str()) .unwrap_or(""); let info = parse_version_info(version); crate::utils::build::substitute_version_vars(value, &info) }