mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 18:59:41 +00:00
310 lines
11 KiB
Rust
310 lines
11 KiB
Rust
// DCR — Cargo-like C/C++ project manager.
|
|
//
|
|
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
|
//
|
|
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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 <triple>] [--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 <triple> 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<String>,
|
|
/// Optional target triple to clean under `target/<triple>/...`.
|
|
target: Option<String>,
|
|
/// When true, also clean each workspace member that has a `target` directory.
|
|
all: bool,
|
|
}
|
|
|
|
/// Parses CLI arguments into [`CleanFlags`].
|
|
///
|
|
/// Recognizes `--all`, `--target <triple>`, and profile flags such as `--debug`
|
|
/// / `--release` (validated via [`default_profile_flags`]).
|
|
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
|
let mut profile: Option<String> = None;
|
|
let mut target: Option<String> = 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/<triple>; otherwise target/<profile>.
|
|
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::<Vec<String>>(),
|
|
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)
|
|
}
|