feat: complete structured CLI output

- unify human, plain, and JSON CLI output modes

- standardize diagnostics, lifecycle events, colors, quiet, and verbose handling

- preserve argument forwarding and improve CLI help and summaries

- document and release version 0.9.5
This commit is contained in:
2026-10-21 18:28:31 +08:00
parent bfd2b9d251
commit 0f456b4201
63 changed files with 2619 additions and 1317 deletions

View File

@@ -15,17 +15,15 @@
// 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::deps::register;
use crate::prelude::*;
use crate::core::{build_config::Config, deps::register};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use toml::Value;
use toml::map::Map;
use toml::{Value, map::Map};
/// Parsed arguments for the `dcr add` command.
///
/// Exactly one of `path`, `git`, or `version_from_registry` is expected to be set,
/// Exactly one of `path`, `git`, `pkg_config`, or `version_from_registry` is expected to be set,
/// depending on how the dependency source was specified.
pub struct AddArgs {
/// Dependency name as it will appear under `[dependencies]` in `dcr.toml`.
@@ -42,6 +40,8 @@ pub struct AddArgs {
pub rev: Option<String>,
/// Version string resolved from the package registry when no explicit source is given.
pub version_from_registry: Option<String>,
/// Package names resolved through pkg-config.
pub pkg_config: Option<Vec<String>>,
}
/// Adds a dependency to the current project's `dcr.toml`.
@@ -57,26 +57,22 @@ pub struct AddArgs {
/// Process exit code: `0` on success, non-zero on usage or I/O errors.
pub fn add(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Adds a dependency to the project.");
println!();
printc("SOURCES:", BOLD_GREEN);
println!(" github:user/repo GitHub repository");
println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository");
println!(" path:./path/to/lib Local path dependency");
println!(" <version> Version from registry");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --branch <b> Use a specific branch");
println!(" --tag <t> Use a specific tag");
println!(" --rev <r> Use a specific commit hash");
let mut help = crate::utils::help::HelpBuilder::new("dcr add");
help.section("USAGE")
.example("dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]");
help.description("Adds a dependency to the project.");
help.section("SOURCES")
.item("github:user/repo", "GitHub repository")
.item("gitlab:user/repo", "GitLab repository")
.item("git:host.com/user/repo", "Generic git repository")
.item("path:./path/to/lib", "Local path dependency")
.item("pkg-config:fmt", "System pkg-config dependency")
.item("<version>", "Version from registry");
help.section("OPTIONS")
.item("--branch <b>", "Use a specific branch")
.item("--tag <t>", "Use a specific tag")
.item("--rev <r>", "Use a specific commit hash");
help.render();
return 0;
}
@@ -88,7 +84,7 @@ pub fn add(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
@@ -96,11 +92,11 @@ pub fn add(args: &[String]) -> i32 {
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -108,7 +104,7 @@ pub fn add(args: &[String]) -> i32 {
let mut config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(cfg) => cfg,
Err(err) => {
error(&err.to_string());
error!("{err}");
return 1;
}
};
@@ -133,6 +129,17 @@ pub fn add(args: &[String]) -> i32 {
}
Value::Table(table)
}
} else if let Some(packages) = add_args.pkg_config {
let mut table = Map::new();
if packages.len() == 1 {
table.insert("pkg-config".to_string(), Value::String(packages[0].clone()));
} else {
table.insert(
"pkg-config".to_string(),
Value::Array(packages.into_iter().map(Value::String).collect()),
);
}
Value::Table(table)
} else if let Some(path) = add_args.path {
let mut table = Map::new();
table.insert("path".to_string(), Value::String(path));
@@ -140,21 +147,17 @@ pub fn add(args: &[String]) -> i32 {
} else if let Some(version) = add_args.version_from_registry {
Value::String(version)
} else {
error("Dependency source (path or git) must be provided");
error!("Dependency source (path, git, or pkg-config) must be provided");
return 1;
};
let key = format!("dependencies.{}", add_args.name);
if let Err(err) = config.edit(&key, dep_value) {
error(&format!("Failed to update dcr.toml: {}", err));
error!("Failed to update dcr.toml: {err}");
return 1;
}
println!(
" {} dependency `{}` to dcr.toml",
colored("Added", BOLD_GREEN),
add_args.name
);
crate::utils::output::success(format!("Added dependency `{}` to dcr.toml", add_args.name));
0
}
@@ -165,12 +168,13 @@ pub fn add(args: &[String]) -> i32 {
/// `path:`, `github:`, `gitlab:`, or `git:` prefixes, or a full git URL.
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:");
error(" github:user/repo -> github.com/user/repo");
error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo");
error(" path:./path/to/lib -> local path");
error!("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error!("Sources:");
error!(" github:user/repo -> github.com/user/repo");
error!(" gitlab:user/repo -> gitlab.com/user/repo");
error!(" git:host.com/user/repo -> host.com/user/repo");
error!(" path:./path/to/lib -> local path");
error!(" pkg-config:fmt -> system pkg-config package");
return Err(1);
}
@@ -183,7 +187,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
let mut iter = args.iter();
if let Some(n) = iter.next() {
if n.starts_with("--") {
error("First argument must be the dependency name");
error!("First argument must be the dependency name");
return Err(1);
}
name = n.clone();
@@ -191,7 +195,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if let Some(s) = iter.next() {
if s.starts_with("--") {
error("Second argument must be the dependency source");
error!("Second argument must be the dependency source");
return Err(1);
}
source_spec = s.clone();
@@ -210,7 +214,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
)
})
.map_err(|(msg, code)| {
error(&msg);
error!("{msg}");
code
})?;
return Ok(AddArgs {
@@ -221,10 +225,11 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag: None,
rev: None,
version_from_registry: Some(version.to_string()),
pkg_config: None,
});
}
Err(e) => {
error(&format!("Cannot add `{}` without source: {}", name, e));
error!("Cannot add `{}` without source: {}", name, e);
return Err(1);
}
}
@@ -235,26 +240,26 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
"--branch" => {
branch = iter.next().cloned();
if branch.is_none() {
error("--branch requires a value");
error!("--branch requires a value");
return Err(1);
}
}
"--tag" => {
tag = iter.next().cloned();
if tag.is_none() {
error("--tag requires a value");
error!("--tag requires a value");
return Err(1);
}
}
"--rev" => {
rev = iter.next().cloned();
if rev.is_none() {
error("--rev requires a value");
error!("--rev requires a value");
return Err(1);
}
}
_ => {
error(&format!("Unknown argument: {}", arg));
error!("Unknown argument: {arg}");
return Err(1);
}
}
@@ -262,9 +267,25 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
let mut path = None;
let mut git = None;
let mut pkg_config = None;
// Map source prefixes / bare URLs to path or git fields
if let Some(p) = source_spec.strip_prefix("path:") {
if let Some(p) = source_spec
.strip_prefix("pkg-config:")
.or_else(|| source_spec.strip_prefix("pkg:"))
{
let packages = p
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string)
.collect::<Vec<_>>();
if packages.is_empty() {
error!("pkg-config source must name at least one package");
return Err(1);
}
pkg_config = Some(packages);
} else if let Some(p) = source_spec.strip_prefix("path:") {
path = Some(p.to_string());
} else if let Some(g) = source_spec.strip_prefix("github:") {
git = Some(format!("https://github.com/{}", g));
@@ -294,8 +315,8 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
{
path = Some(source_spec);
} else {
error(
"Source must be a path, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
error!(
"Source must be a path, pkg-config:, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
);
return Err(1);
}
@@ -308,5 +329,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag,
rev,
version_from_registry: None,
pkg_config,
})
}

View File

@@ -15,6 +15,8 @@
// 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::prelude::*;
use crate::cli::clean::clean;
use crate::cli::flags::parse_build_run_flags;
use crate::cli::r#gen::{ProjectInfo, write_dcr_metadata};
@@ -27,34 +29,16 @@ use crate::utils::build::{
normalize_kind, resolve_artifact_target_dir, resolve_compiler,
};
use crate::utils::fs::{canonicalize_path, find_project_root};
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use crate::utils::output;
use std::io::IsTerminal;
use std::path::Path;
use std::sync::{Arc, atomic::AtomicBool};
pub use crate::core::build::get_build_string_with_profile;
/// Formats a left-aligned status verb with the given style for CLI output.
fn status(verb: &str, style: &str) -> String {
colored(&format!("{verb:<9}"), style)
}
/// CLI build reporter that prints human-readable progress to stderr/stdout.
struct CliReporter;
impl CliReporter {
/// Prints a single status line with a styled verb and free-form rest text.
///
/// # Parameters
/// - `verb`: Short status label (e.g. `"target"`, `"dep"`).
/// - `style`: Color/style constant for the verb.
/// - `rest`: Remaining message text after the verb.
fn line(&self, verb: &str, style: &str, rest: &str) {
eprintln!(" {} {}", status(verb, style), rest);
}
}
impl BuildReporter for CliReporter {
/// Handles build events by printing progress, status lines, and compiler output.
///
@@ -68,7 +52,13 @@ impl BuildReporter for CliReporter {
target,
} => {
common::finish_progress_line();
self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}"));
output::emit_event(
"target_started",
serde_json::json!({
"index": index, "total": total, "target": target
}),
&format!("target {index}/{total} {target}"),
);
}
BuildEvent::ProjectStart {
name,
@@ -76,15 +66,23 @@ impl BuildReporter for CliReporter {
target,
} => {
common::finish_progress_line();
self.line(
"project",
BOLD_CYAN,
&format!("{name} ({profile}, {target})"),
output::emit_event(
"project_started",
serde_json::json!({
"name": name, "profile": profile, "target": target
}),
&format!("project {name} ({profile}, {target})"),
);
}
BuildEvent::DepBuilding { name, version } => {
common::finish_progress_line();
self.line("dep", BOLD_YELLOW, &format!("{name} v{version}"));
output::emit_event(
"dependency_building",
serde_json::json!({
"name": name, "version": version
}),
&format!("dep {name} v{version}"),
);
}
BuildEvent::DepReady {
name,
@@ -94,34 +92,55 @@ impl BuildReporter for CliReporter {
common::finish_progress_line();
// Only announce deps that were actually rebuilt this run.
if rebuilt {
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
output::emit_event(
"dependency_ready",
serde_json::json!({
"name": name, "version": version, "rebuilt": rebuilt
}),
&format!("ready {name} v{version}"),
);
}
}
BuildEvent::Compiling { name, version } => {
common::finish_progress_line();
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
let label = format!(
" {}",
output::progress_label("compile", &format!("{name} v{version}"))
);
// Terminals use an in-place progress line; non-TTY gets a plain log line.
common::set_progress_label(Some(label.clone()));
if !std::io::stderr().is_terminal() {
eprintln!("{label}");
if output::mode() == output::Mode::Human {
common::set_progress_label(Some(label.clone()));
}
if output::mode() != output::Mode::Human || !std::io::stderr().is_terminal() {
output::emit_event(
"compiling",
serde_json::json!({
"name": name, "version": version
}),
&format!("compile {name} v{version}"),
);
}
}
BuildEvent::Packing { path } => {
common::finish_progress_line();
self.line("pack", BOLD_CYAN, path);
output::emit_event(
"packing",
serde_json::json!({ "path": path }),
&format!("pack {path}"),
);
}
BuildEvent::Finished { secs } => {
common::finish_progress_line();
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
output::emit_event(
"finished",
serde_json::json!({ "seconds": secs }),
&format!("done in {secs}s"),
);
}
BuildEvent::CompilerOutput { stream, text } => {
// Pause the progress spinner so compiler text is not overwritten.
common::interrupt_progress_for_output();
if stream == "stderr" {
eprint!("{text}");
} else {
print!("{text}");
}
output::compiler_output(stream, text);
}
}
}
@@ -140,26 +159,22 @@ impl BuildReporter for CliReporter {
/// Process exit code: `0` on success or help, non-zero on failure.
pub fn build(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [--print-artifact-path]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Compiles the project. Default profile is --debug.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Build with debug profile (default)");
println!(" --release Build with release profile");
println!(" --target <triple> Cross-compile for the given target");
println!(" --force Force a full rebuild");
println!(" --clean Clean before building");
println!(" --verbose Print detailed build output");
println!(" --workspace <name> Build a specific workspace member");
println!(
" --print-artifact-path Print absolute main artifact path on stdout after success"
);
let mut help = crate::utils::help::HelpBuilder::new("dcr build");
help.description("Compiles the project. Default profile is --debug.");
help.section("USAGE").example("dcr build [options]");
help.section("OPTIONS")
.item("--debug", "Build with debug profile (default)")
.item("--release", "Build with release profile")
.item("--target <triple>", "Cross-compile for the given target")
.item("--force", "Force a full rebuild")
.item("--clean", "Clean before building")
.item("--verbose", "Print detailed build output")
.item("--workspace <name>", "Build a specific workspace member")
.item(
"--print-artifact-path",
"Print the absolute main artifact path",
);
help.render();
return 0;
}
@@ -173,18 +188,18 @@ pub fn build(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => canonicalize_path(&dir),
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -230,19 +245,27 @@ pub fn build(args: &[String]) -> i32 {
if flags.print_artifact_path
&& let Some(ref path) = info.artifact_path
{
println!("{path}");
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
}
} else if flags.print_artifact_path
&& let Some(path) =
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
{
println!("{path}");
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
}
0
}
Err(err) => {
common::finish_progress_line();
error(&err.message);
error!("{}", err.message);
1
}
}

View File

@@ -15,15 +15,14 @@
// 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::prelude::*;
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;
use std::{fs, path::Path};
/// Entry point for the `dcr clean` command.
///
@@ -36,45 +35,41 @@ use std::path::Path;
/// 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");
let mut help = crate::utils::help::HelpBuilder::new("dcr clean");
help.section("USAGE")
.example("dcr clean [--debug | --release] [--target <triple>] [--all]");
help.description("Removes build artifacts from the target directory.");
help.section("OPTIONS")
.item("--debug", "Clean debug artifacts (default)")
.item("--release", "Clean release artifacts")
.item("--target <triple>", "Clean artifacts for a specific target")
.item("--all", "Clean all workspace members");
help.render();
return 0;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
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");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
let flags = match parse_clean_flags(args) {
Ok(v) => v,
Err(msg) => {
error(&msg);
error!("{msg}");
return 1;
}
};
@@ -83,7 +78,7 @@ pub fn clean(args: &[String]) -> i32 {
match with_dir(&root, || clean_from_root(&root, &flags)) {
Ok(()) => 0,
Err(msg) => {
error(&msg);
error!("{msg}");
1
}
}
@@ -210,10 +205,7 @@ fn clean_project_at(
if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string());
}
println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
crate::utils::output::status("Cleaning", format!("project `{project_name}`"));
if let Some(profile) = profile {
let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}")
@@ -229,18 +221,14 @@ fn clean_project_at(
target_items.contains(&profile.to_string())
};
if !dir_exists {
warn(&format!("Directory target/{} not found", target_dir));
warn!("Directory target/{target_dir} not found");
} else {
println!(" Profile: {}", colored(profile, BOLD_GREEN));
crate::utils::output::status("Profile", profile);
if let Some(t) = target {
println!(" Target: {}", colored(t, BOLD_GREEN));
crate::utils::output::status("Target", t);
}
let _ = fs::remove_dir_all(&target_dir);
println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
crate::utils::output::removed(format!("directory {target_dir}"));
}
clean_custom_paths(&config, profile)?;
return Ok(());
@@ -248,12 +236,9 @@ fn clean_project_at(
if items.contains(&"target".to_string()) {
let _ = fs::remove_dir_all("target");
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
crate::utils::output::removed("directory target");
} else {
warn("Directory target not found");
warn!("Directory target not found");
}
clean_custom_paths(&config, "debug")?;
clean_custom_paths(&config, "release")?;

View File

@@ -15,8 +15,8 @@
// 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::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use crate::prelude::*;
use serde::Deserialize;
use std::fs;
use std::io::Read;
@@ -55,32 +55,32 @@ struct ReleaseAsset {
/// `0` on success / already up to date / pacman-managed refuse; `1` on error.
pub fn flag_update(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr --update", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Updates DCR to the latest version from GitHub Releases.");
let mut help = crate::utils::help::HelpBuilder::new("dcr --update");
help.section("USAGE").example("dcr --update");
help.description("Updates DCR to the latest version from GitHub Releases.");
help.render();
return 0;
}
if !args.is_empty() {
warn("Command does not support additional arguments");
warn!("Command does not support additional arguments");
return 1;
}
let current_exe = match std::env::current_exe() {
Ok(path) => path,
Err(_) => {
error("Failed to resolve current binary path");
error!("Failed to resolve current binary path");
return 1;
}
};
// Refuse self-update when the binary is owned by pacman/AUR.
if let Some(package_name) = pacman_owned_package(&current_exe) {
warn("This dcr binary is managed by pacman/AUR");
println!(
"Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
warn!("This dcr binary is managed by pacman/AUR");
crate::utils::output::status(
"Update via package manager:",
format!("yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"),
);
return 0;
}
@@ -91,7 +91,7 @@ pub fn flag_update(args: &[String]) -> i32 {
let release = match fetch_latest_release() {
Ok(release) => release,
Err(err) => {
error(&format!("Failed to check for updates: {err}"));
error!("Failed to check for updates: {err}");
return 1;
}
};
@@ -99,7 +99,7 @@ pub fn flag_update(args: &[String]) -> i32 {
// Normalize the tag by stripping a leading `v` before comparison.
let latest_version = release.tag_name.trim_start_matches('v');
if latest_version == current_version {
println!("Latest version is already installed: {current_version}");
crate::utils::output::status("Up to date", current_version);
return 0;
}
@@ -109,14 +109,14 @@ pub fn flag_update(args: &[String]) -> i32 {
.iter()
.find(|asset| candidate_names.iter().any(|name| name == &asset.name))
else {
error(&format!("Binary for target {target} not found"));
error!("Binary for target {target} not found");
return 1;
};
let bytes = match download_asset(&asset.browser_download_url) {
Ok(bytes) => bytes,
Err(err) => {
error(&format!("Failed to download update: {err}"));
error!("Failed to download update: {err}");
return 1;
}
};
@@ -124,7 +124,7 @@ pub fn flag_update(args: &[String]) -> i32 {
let temp_path = temp_binary_path(&current_exe);
if fs::write(&temp_path, &bytes).is_err() {
error("Failed to write temporary binary");
error!("Failed to write temporary binary");
return 1;
}
set_executable_permissions(&temp_path);
@@ -132,12 +132,14 @@ pub fn flag_update(args: &[String]) -> i32 {
// Replace the running executable in place, then clean up the temp file.
if self_replace::self_replace(&temp_path).is_err() {
let _ = fs::remove_file(&temp_path);
error("Failed to replace current binary");
error!("Failed to replace current binary");
return 1;
}
let _ = fs::remove_file(&temp_path);
println!("Update completed: {current_version} -> {latest_version}");
crate::utils::output::success(format!(
"Updated DCR: {current_version} -> {latest_version}"
));
0
}

View File

@@ -15,8 +15,9 @@
// 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::prelude::*;
use crate::config::{PROFILE, flags};
use crate::utils::log::warn;
/// Flags shared by the `build` and `run` CLI commands.
pub struct BuildRunFlags {
@@ -82,7 +83,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
while let Some(arg) = iter.next() {
// DCR build/run options are long flags only (`--name`).
if !arg.starts_with("--") {
warn("Unknown argument");
warn!("Unknown argument");
return Err(1);
}
let candidate = arg.trim_start_matches("--");
@@ -106,7 +107,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
if let Some(w) = iter.next() {
workspace = Some(w.clone());
} else {
warn("--workspace requires a value");
warn!("--workspace requires a value");
return Err(1);
}
continue;
@@ -115,7 +116,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
if let Some(t) = iter.next() {
target = Some(t.clone());
} else {
warn("--target requires a value");
warn!("--target requires a value");
return Err(1);
}
continue;
@@ -123,13 +124,13 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
// Profile names come from config::flags; only one profile may be set.
if flags(candidate).is_some() {
if profile != PROFILE {
warn("Duplicate profile flag");
warn!("Duplicate profile flag");
return Err(1);
}
profile = candidate.to_string();
continue;
}
warn("Unknown build flag");
warn!("Unknown build flag");
return Err(1);
}

View File

@@ -15,8 +15,8 @@
// 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::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use crate::prelude::*;
use glob::glob;
use std::process::Command;
@@ -28,12 +28,11 @@ use std::process::Command;
/// If the first argument is `--help`, prints usage information and returns 0.
pub fn fmt(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr fmt", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Formats all C/C++ source files using clang-format.");
println!(" Scans src/ and tests/ directories recursively.");
let mut help = crate::utils::help::HelpBuilder::new("dcr fmt");
help.section("USAGE").example("dcr fmt");
help.description("Formats all C/C++ source files using clang-format.")
.note("Scans src/ and tests/ directories recursively.");
help.render();
return 0;
}
@@ -69,30 +68,26 @@ pub fn fmt(args: &[String]) -> i32 {
}
if files.is_empty() {
println!(" {} No files to format", colored("Format", BOLD_GREEN));
crate::utils::output::status("Format", "no files to format");
return 0;
}
// Format the collected files
println!(
" {} {} files",
colored("Formatting", BOLD_GREEN),
files.len()
);
crate::utils::output::status("Formatting", format!("{} files", files.len()));
let status = Command::new("clang-format").arg("-i").args(&files).status();
match status {
Ok(s) if s.success() => {
println!(" {} successful", colored("Format", BOLD_GREEN));
crate::utils::output::success("Format successful");
0
}
Ok(s) => {
error(&format!("clang-format failed with status: {}", s));
error!("clang-format failed with status: {s}");
1
}
Err(e) => {
error(&format!("Failed to execute clang-format: {}", e));
error!("Failed to execute clang-format: {e}");
1
}
}

View File

@@ -15,6 +15,8 @@
// 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::prelude::*;
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact::{absolute_artifact_path, resolve_artifact_path};
use crate::core::build::builder::collect_sources;
@@ -29,8 +31,6 @@ use crate::utils::build::{
use crate::utils::fs::{
absolute_join, atomic_write, canonicalize_path, ensure_dcr_dir, find_project_root,
};
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -42,11 +42,13 @@ struct GenDeps {
include_dirs: Vec<String>,
lib_dirs: Vec<String>,
libs: Vec<String>,
cflags: Vec<String>,
ldflags: Vec<String>,
}
/// Collects path-dependency include/lib/libs from `dcr.toml` for code generation.
/// Collects dependency metadata from `dcr.toml` for code generation.
///
/// Skips `system` deps and non-path entries. Falls back to common include/lib
/// Resolves path layouts and pkg-config flags while skipping system deps. Falls back to common include/lib
/// directory names only when those directories exist.
fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps {
let deps_val = match config.get("dependencies") {
@@ -56,6 +58,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs: vec![],
lib_dirs: vec![],
libs: vec![],
cflags: vec![],
ldflags: vec![],
};
}
};
@@ -66,6 +70,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs: vec![],
lib_dirs: vec![],
libs: vec![],
cflags: vec![],
ldflags: vec![],
};
}
};
@@ -73,12 +79,37 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
let mut include_dirs = Vec::new();
let mut lib_dirs = Vec::new();
let mut libs = Vec::new();
let mut cflags = Vec::new();
let mut ldflags = Vec::new();
for (name, value) in deps_table {
let tbl = match value.as_table() {
Some(t) => t,
None => continue,
};
let pkg_names = tbl
.get("pkg-config")
.or_else(|| tbl.get("pkg_config"))
.map(|raw| {
raw.as_str()
.map(|s| vec![s.to_string()])
.or_else(|| {
raw.as_array().map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
})
.unwrap_or_default()
})
.unwrap_or_default();
if !pkg_names.is_empty() {
let (flags, link_flags) =
crate::utils::build::resolve_pkg_config_flags_lossy(&pkg_names, &[], &[]);
cflags.extend(flags);
ldflags.extend(link_flags);
continue;
}
// skip system deps
if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) {
continue;
@@ -169,6 +200,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs,
lib_dirs,
libs,
cflags,
ldflags,
}
}
@@ -218,33 +251,30 @@ pub struct ProjectInfo {
pub fn r#gen(args: &[String]) -> i32 {
let subcommand = match args.first() {
Some(s) if s == "--help" => {
printc("USAGE:", BOLD_GREEN);
printc(" dcr gen <subcommand>", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Generates IDE and tooling integration files.");
println!();
printc("SUBCOMMANDS:", BOLD_GREEN);
println!(" project-info Print project metadata as JSON");
println!(" compile-commands Generate .dcr/compile_commands.json");
println!(" vscode Generate .vscode/ integration files");
println!(" clion Generate .idea/ integration files");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug | --release Profile (default: debug)");
println!(" --quiet | -q Suppress success messages on stdout");
let mut help = crate::utils::help::HelpBuilder::new("dcr gen");
help.section("USAGE").example("dcr gen <subcommand>");
help.description("Generates IDE and tooling integration files.");
help.section("SUBCOMMANDS")
.item("project-info", "Print project metadata as JSON")
.item("compile-commands", "Generate .dcr/compile_commands.json")
.item("vscode", "Generate .vscode/ integration files")
.item("clion", "Generate .idea/ integration files");
help.section("OPTIONS")
.item("--debug | --release", "Profile (default: debug)")
.item("--quiet | -q", "Suppress success messages on stdout");
help.render();
return 0;
}
Some(s) => s.as_str(),
None => {
printc("USAGE:", BOLD_GREEN);
printc(" dcr gen <subcommand>", BOLD_CYAN);
println!();
printc("SUBCOMMANDS:", BOLD_GREEN);
println!(" project-info Print project metadata as JSON");
println!(" compile-commands Generate .dcr/compile_commands.json");
println!(" vscode Generate .vscode/ integration files");
println!(" clion Generate .idea/ integration files");
let mut help = crate::utils::help::HelpBuilder::new("dcr gen");
help.section("USAGE").example("dcr gen <subcommand>");
help.section("SUBCOMMANDS")
.item("project-info", "Print project metadata as JSON")
.item("compile-commands", "Generate .dcr/compile_commands.json")
.item("vscode", "Generate .vscode/ integration files")
.item("clion", "Generate .idea/ integration files");
help.render();
return 1;
}
};
@@ -257,7 +287,7 @@ pub fn r#gen(args: &[String]) -> i32 {
"vscode" => gen_vscode(rest),
"clion" => gen_clion(rest),
_ => {
error(&format!("Unknown gen subcommand: {subcommand}"));
error!("Unknown gen subcommand: {subcommand}");
1
}
}
@@ -350,8 +380,10 @@ fn collect_project_info_inner(
});
let resolved = resolve_deps_for_gen(&config, profile, &root);
let (resolved_cflags, resolved_ldflags) =
let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags);
resolved_cflags.extend(resolved.cflags.clone());
resolved_ldflags.extend(resolved.ldflags.clone());
// Build exclude/include pattern lists (same logic as cli::build)
let mut combined_excludes: Vec<PathBuf> = Vec::new();
@@ -741,8 +773,8 @@ fn ensure_clangd(root: &Path, quiet: bool) {
}
Ok(_) => {
if !quiet {
eprintln!(
"Warning: .clangd already exists and is not managed by dcr; \
crate::utils::output::warning!(
".clangd already exists and is not managed by dcr; \
set CompilationDatabase to .dcr manually if needed"
);
}
@@ -781,8 +813,8 @@ fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
let member_path = canonicalize_path(&member.path);
match collect_project_info(&member_path, profile, ws_root) {
Ok(info) => all.push(info),
Err(e) => eprintln!(
"Warning: skipping workspace member {}: {e}",
Err(e) => crate::utils::output::warning!(
"skipping workspace member {}: {e}",
member_path.display()
),
}
@@ -793,7 +825,7 @@ fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
match collect_project_info(&root, profile, ws_root) {
Ok(info) => all.push(info),
Err(e) if is_workspace => {
eprintln!("Warning: skipping workspace root package: {e}");
crate::utils::output::warning!("skipping workspace root package: {e}");
}
Err(e) => return Err(e),
}
@@ -818,7 +850,7 @@ fn gen_project_info(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -827,20 +859,12 @@ fn gen_project_info(args: &[String]) -> i32 {
&& let Err(e) = write_dcr_metadata(&root, info)
&& !quiet
{
eprintln!("Warning: {e}");
crate::utils::output::warning!("{e}");
}
ensure_clangd(&root, quiet);
print!("[");
for (i, info) in all.iter().enumerate() {
if i > 0 {
print!(",");
}
println!();
print!("{}", project_info_to_json(info));
}
println!();
println!("]");
let entries = all.iter().map(project_info_to_json).collect::<Vec<_>>();
crate::utils::output::write_stdout_raw(&format!("[\n{}\n]\n", entries.join(",\n")));
0
}
@@ -940,7 +964,7 @@ fn gen_compile_commands(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -953,7 +977,7 @@ fn gen_compile_commands(args: &[String]) -> i32 {
/// Used by `compile-commands`, `vscode`, and `clion`.
fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], quiet: bool) -> i32 {
if let Err(e) = ensure_dcr_dir(root) {
error(&format!("Failed to create .dcr/: {e}"));
error!("Failed to create .dcr/: {e}");
return 1;
}
@@ -965,16 +989,16 @@ fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], q
&& let Err(e) = write_dcr_metadata(root, info)
&& !quiet
{
eprintln!("Warning: {e}");
crate::utils::output::warning!("{e}");
}
ensure_clangd(root, quiet);
if !quiet {
println!("Generated {}", out_path.display());
crate::utils::output::created(format!("{}", out_path.display()));
}
0
}
Err(e) => {
error(&format!("Failed to write compile_commands.json: {e}"));
error!("Failed to write compile_commands.json: {e}");
1
}
}
@@ -1128,7 +1152,7 @@ fn gen_vscode(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -1141,39 +1165,39 @@ fn gen_vscode(args: &[String]) -> i32 {
let vscode_dir = root.join(".vscode");
if let Err(e) = std::fs::create_dir_all(&vscode_dir) {
error(&format!("Failed to create .vscode/: {e}"));
error!("Failed to create .vscode/: {e}");
return 1;
}
// tasks.json
if let Err(e) = std::fs::write(vscode_dir.join("tasks.json"), gen_tasks_json()) {
error(&format!("Failed to write tasks.json: {e}"));
error!("Failed to write tasks.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("tasks.json").display());
crate::utils::output::created(format!("{}", vscode_dir.join("tasks.json").display()));
// launch.json — one entry per binary target
let launch = gen_launch_json(&all, &root);
if let Err(e) = std::fs::write(vscode_dir.join("launch.json"), launch) {
error(&format!("Failed to write launch.json: {e}"));
error!("Failed to write launch.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("launch.json").display());
crate::utils::output::created(format!("{}", vscode_dir.join("launch.json").display()));
// settings.json (clangd compile-commands-dir)
let settings = gen_settings_json(&root);
if let Err(e) = std::fs::write(vscode_dir.join("settings.json"), settings) {
error(&format!("Failed to write settings.json: {e}"));
error!("Failed to write settings.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("settings.json").display());
crate::utils::output::created(format!("{}", vscode_dir.join("settings.json").display()));
// extensions.json — disable cpptools, recommend clangd
if let Err(e) = std::fs::write(vscode_dir.join("extensions.json"), gen_extensions_json()) {
error(&format!("Failed to write extensions.json: {e}"));
error!("Failed to write extensions.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("extensions.json").display());
crate::utils::output::created(format!("{}", vscode_dir.join("extensions.json").display()));
0
}
@@ -1418,7 +1442,7 @@ fn gen_clion(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -1431,46 +1455,46 @@ fn gen_clion(args: &[String]) -> i32 {
let idea_dir = root.join(".idea");
if let Err(e) = std::fs::create_dir_all(&idea_dir) {
error(&format!("Failed to create .idea/: {e}"));
error!("Failed to create .idea/: {e}");
return 1;
}
let run_configs_dir = idea_dir.join("runConfigurations");
if let Err(e) = std::fs::create_dir_all(&run_configs_dir) {
error(&format!("Failed to create .idea/runConfigurations/: {e}"));
error!("Failed to create .idea/runConfigurations/: {e}");
return 1;
}
// externalTools.xml
let ext_tools = gen_clion_external_tools();
if let Err(e) = std::fs::write(idea_dir.join("externalTools.xml"), ext_tools) {
error(&format!("Failed to write externalTools.xml: {e}"));
error!("Failed to write externalTools.xml: {e}");
return 1;
}
println!("Generated {}", idea_dir.join("externalTools.xml").display());
crate::utils::output::created(format!("{}", idea_dir.join("externalTools.xml").display()));
// customTargets.xml
let targets = gen_clion_custom_targets();
if let Err(e) = std::fs::write(idea_dir.join("customTargets.xml"), targets) {
error(&format!("Failed to write customTargets.xml: {e}"));
error!("Failed to write customTargets.xml: {e}");
return 1;
}
println!("Generated {}", idea_dir.join("customTargets.xml").display());
crate::utils::output::created(format!("{}", idea_dir.join("customTargets.xml").display()));
// misc.xml — point CLion at compile_commands.json
let misc = gen_clion_misc_xml(&root);
if let Err(e) = std::fs::write(idea_dir.join("misc.xml"), misc) {
error(&format!("Failed to write misc.xml: {e}"));
error!("Failed to write misc.xml: {e}");
return 1;
}
println!("Generated {}", idea_dir.join("misc.xml").display());
crate::utils::output::created(format!("{}", idea_dir.join("misc.xml").display()));
// .idea/.gitignore
let gitignore = "# CLion generated files\nworkspace.xml\n*.iml\n";
if let Err(e) = std::fs::write(idea_dir.join(".gitignore"), gitignore) {
error(&format!("Failed to write .idea/.gitignore: {e}"));
error!("Failed to write .idea/.gitignore: {e}");
return 1;
}
println!("Generated {}", idea_dir.join(".gitignore").display());
crate::utils::output::created(format!("{}", idea_dir.join(".gitignore").display()));
// runConfigurations/<name>.xml — one per binary
for info in &all {
@@ -1481,10 +1505,10 @@ fn gen_clion(args: &[String]) -> i32 {
let fname = format!("{}.xml", sanitize_filename(&info.name));
let path = run_configs_dir.join(&fname);
if let Err(e) = std::fs::write(&path, xml) {
error(&format!("Failed to write runConfigurations/{fname}: {e}"));
error!("Failed to write runConfigurations/{fname}: {e}");
return 1;
}
println!("Generated {}", path.display());
crate::utils::output::created(format!("{}", path.display()));
}
0
@@ -1685,18 +1709,18 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String, bool), i32> {
}
let start = std::env::current_dir().map_err(|_| {
error("Failed to determine current directory");
error!("Failed to determine current directory");
1i32
})?;
let root = match find_project_root(&start) {
Ok(Some(r)) => canonicalize_path(&r),
Ok(None) => {
error("dcr.toml not found");
error!("dcr.toml not found");
return Err(1);
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return Err(1);
}
};

View File

@@ -15,8 +15,6 @@
// 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::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
/// Displays the help information for the DCR CLI tool.
///
/// Prints usage, commands, flags, options, examples, and a tip.
@@ -24,43 +22,40 @@ use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
/// # Returns
/// Always `0`.
pub fn help() -> i32 {
println!("DCR (Dexoron Cargo Realization)");
println!("C project manager inspired by Cargo.");
println!();
printc("USAGE:", BOLD_GREEN);
printc(" dcr <command> [options]", BOLD_CYAN);
println!();
printc("COMMANDS:", BOLD_GREEN);
println!(" new <name> Create a new project");
println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run the project (default: --debug)");
println!(" add <name> ... Add a dependency to dcr.toml");
println!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)");
println!(" fmt Format all C/C++ source files using clang-format");
println!(" lint Run clang-tidy on C/C++ sources");
println!(" setup Show configured package registries");
println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN);
println!(" --help Show command help");
println!(" --update Update dcr to the latest version");
println!(" --version Show dcr version");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Use debug profile");
println!(" --release Use release profile");
println!(" --force Force rebuild (build/run)");
println!(" --clean Clean before build (build/run)");
println!(" --all Clean all workspace members (clean)");
println!();
printc("EXAMPLES:", BOLD_GREEN);
printc(" dcr new hello", BOLD_CYAN);
printc(" dcr build --release", BOLD_CYAN);
printc(" dcr run --debug", BOLD_CYAN);
println!();
printc("TIP:", BOLD_GREEN);
println!(" Run 'dcr <command> --help' for command-specific help.");
use crate::utils::help::HelpBuilder;
let mut help = HelpBuilder::new("DCR (Dexoron Cargo Realization)");
help.description("C/C++ project manager inspired by Cargo.");
help.section("USAGE").example("dcr <command> [options]");
help.section("COMMANDS")
.item("new <name>", "Create a new project")
.item("init", "Initialize the current directory as a project")
.item("build", "Build the project (default: --debug)")
.item("run", "Build and run the project")
.item("add <name> ...", "Add a dependency to dcr.toml")
.item("tree", "Display the dependency tree")
.item("test", "Run project tests (alias: tests)")
.item("fmt", "Format C/C++ sources using clang-format")
.item("lint", "Run clang-tidy on C/C++ sources")
.item("setup", "Show configured package registries")
.item("clean", "Remove build artifacts")
.item("gen <subcommand>", "Generate IDE integration files");
help.section("FLAGS")
.item("--help", "Show command help")
.item("--update", "Update DCR")
.item("--version", "Show DCR version");
help.section("OPTIONS")
.item("--output human|plain|json", "Select output format")
.item("--color auto|always|never", "Select color policy")
.item("--quiet", "Suppress ordinary diagnostics")
.item("--verbose", "Enable detailed diagnostics")
.item("--debug / --release", "Select build profile")
.item("--force", "Force rebuild (build/run)")
.item("--clean", "Clean before build (build/run)")
.item("--all", "Clean all workspace members");
help.example("dcr new hello")
.example("dcr build --release")
.example("dcr run --debug");
help.tip("Run 'dcr <command> --help' for command-specific help.");
help.render();
0
}

View File

@@ -15,15 +15,13 @@
// 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::prelude::*;
use crate::config::FILE_MAIN_C;
use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs;
use std::io::Write;
use toml::Value;
use std::{fs, io::Write};
/// Initializes the current working directory as a new DCR project.
///
@@ -31,12 +29,11 @@ use toml::Value;
/// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable.
pub fn init(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Initializes the current directory as a DCR project.");
println!(" The directory must be empty.");
let mut help = crate::utils::help::HelpBuilder::new("dcr init");
help.section("USAGE").example("dcr init [--vcs <git|none>]");
help.description("Initializes the current directory as a DCR project.")
.note("The directory must be empty.");
help.render();
return 0;
}
@@ -49,7 +46,7 @@ pub fn init(args: &[String]) -> i32 {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
error!("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
@@ -60,7 +57,7 @@ pub fn init(args: &[String]) -> i32 {
}
if !clean_args.is_empty() {
warn("Command does not support additional arguments");
warn!("Command does not support additional arguments");
return 1;
}
@@ -72,66 +69,44 @@ pub fn init(args: &[String]) -> i32 {
.unwrap_or_else(|| "project".to_string());
if !items.is_empty() {
error("Directory not empty");
error!("Directory not empty");
return 1;
}
if let Err(e) = validate_package_name(&project_name) {
error(&format!(
"Invalid project name `{}`: {}",
colored(&project_name, BOLD_CYAN),
e
));
error!("Invalid project name `{}`: {e}", project_name,);
return 1;
}
let cwd = std::env::current_dir()
.map(|v| v.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string());
println!("Initializing the project in {cwd}");
crate::utils::output::status("Initializing", format!("project in {cwd}"));
// Create and initialize default manifest (dcr.toml).
let mut config = match Config::new("./dcr.toml") {
Ok(cfg) => cfg,
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.clone()))
.is_err()
{
error("Failed to write dcr.toml");
if Config::create("./dcr.toml", Some(&project_name)).is_err() {
error!("Failed to create dcr.toml");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
crate::utils::output::created("dcr.toml");
// Create src directory and write template main.c file.
if fs::create_dir("src").is_err() {
error("Failed to create src/");
error!("Failed to create src/");
return 1;
}
let mut main_c = match fs::File::create("./src/main.c") {
Ok(file) => file,
Err(_) => {
error("Failed to create src/main.c");
error!("Failed to create src/main.c");
return 1;
}
};
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
error("Failed to write src/main.c");
error!("Failed to write src/main.c");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
crate::utils::output::created("src/main.c");
// Resolve target VCS provider or detect existing repositories.
let mut vcs_kind = VcsKind::Git;
@@ -139,7 +114,7 @@ pub fn init(args: &[String]) -> i32 {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
}
@@ -153,29 +128,22 @@ pub fn init(args: &[String]) -> i32 {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(".");
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
warn!("Failed to initialize git repository: {e}");
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
crate::utils::output::success("Initialized git repository");
}
} else {
warn(
warn!(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(std::path::Path::new(".")) {
warn(&format!("Failed to update .gitignore: {e}"));
warn!("Failed to update .gitignore: {e}");
}
println!(
"Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(" dcr run", BOLD_CYAN);
crate::utils::output::success(format!("Initialized project `{project_name}`"));
crate::utils::output::info("Next step:\n dcr run");
0
}

View File

@@ -15,12 +15,12 @@
// 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::prelude::*;
use crate::core::build::common;
use crate::core::build_config::Config;
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -29,15 +29,13 @@ use std::process::Command;
/// Scans source roots (configurable via build.roots) and tests/ directory.
pub fn lint(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr lint [--fix]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Runs clang-tidy on all C/C++ source files.");
println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --fix Apply clang-tidy suggestions automatically");
let mut help = crate::utils::help::HelpBuilder::new("dcr lint");
help.section("USAGE").example("dcr lint [--fix]");
help.description("Runs clang-tidy on all C/C++ source files.")
.note("Scans source roots (configurable via build.roots) and tests/ directory.");
help.section("OPTIONS")
.item("--fix", "Apply clang-tidy suggestions automatically");
help.render();
return 0;
}
@@ -46,7 +44,7 @@ pub fn lint(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
@@ -54,11 +52,11 @@ pub fn lint(args: &[String]) -> i32 {
let root = match find_project_root(&start_dir) {
Ok(Some(r)) => r,
Ok(None) => {
error("dcr.toml not found");
error!("dcr.toml not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -66,7 +64,7 @@ pub fn lint(args: &[String]) -> i32 {
let config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(c) => c,
Err(e) => {
error(&format!("Failed to load dcr.toml: {e}"));
error!("Failed to load dcr.toml: {e}");
return 1;
}
};
@@ -98,7 +96,7 @@ pub fn lint(args: &[String]) -> i32 {
match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) {
Ok(f) => f,
Err(e) => {
error(&format!("Failed to collect source files: {e}"));
error!("Failed to collect source files: {e}");
return 1;
}
};
@@ -116,15 +114,17 @@ pub fn lint(args: &[String]) -> i32 {
files.sort();
if files.is_empty() {
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
crate::utils::output::status("Lint", "no files to lint");
return 0;
}
println!(
" {} {} files {}",
colored("Linting", BOLD_GREEN),
files.len(),
if do_fix { "(with --fix)" } else { "" }
crate::utils::output::status(
"Linting",
format!(
"{} files {}",
files.len(),
if do_fix { "(with --fix)" } else { "" }
),
);
let mut total_issues = 0u32;
@@ -142,7 +142,7 @@ pub fn lint(args: &[String]) -> i32 {
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
error(&format!("Failed to execute clang-tidy: {}", e));
error!("Failed to execute clang-tidy: {e}");
return 1;
}
};
@@ -160,14 +160,12 @@ pub fn lint(args: &[String]) -> i32 {
if issue_count > 0 {
total_issues += issue_count as u32;
failed += 1;
print!(" {} ", colored("×", BOLD_YELLOW));
println!("{} ({} issues)", file, issue_count);
crate::utils::output::warning(format!("{file} ({issue_count} issues)"));
for issue in &issues {
println!(" {}", issue);
crate::utils::output::compiler_output("stderr", issue);
}
} else {
print!(" {} ", colored("✓", BOLD_GREEN));
println!("{}", file);
crate::utils::output::status("Checked", file);
}
}
@@ -175,19 +173,11 @@ pub fn lint(args: &[String]) -> i32 {
let passed = total - failed as usize;
if failed == 0 {
println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
total
);
crate::utils::output::success(format!("Finished checking {total} files; no issues"));
} else {
println!(
" {} {}/{} files passed, {} issues found",
colored("Lint", BOLD_YELLOW),
passed,
total,
total_issues
);
crate::utils::output::warning(format!(
"Finished checking {total} files; {passed} passed, {total_issues} issues found"
));
}
0

View File

@@ -15,15 +15,13 @@
// 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::prelude::*;
use crate::config::FILE_MAIN_C;
use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs;
use std::io::Write;
use toml::Value;
use std::{fs, io::Write};
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
///
@@ -31,12 +29,12 @@ use toml::Value;
/// template source file generation (`src/main.c`), and initial VCS setup.
pub fn new(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Creates a new C/C++ project with the given name.");
println!(" The name may only contain ASCII letters, digits, '_' and '-'.");
let mut help = crate::utils::help::HelpBuilder::new("dcr new");
help.section("USAGE")
.example("dcr new <name> [--vcs <git|none>]");
help.description("Creates a new C/C++ project with the given name.")
.note("The name may only contain ASCII letters, digits, '_' and '-'.");
help.render();
return 0;
}
@@ -51,7 +49,7 @@ pub fn new(args: &[String]) -> i32 {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
error!("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
@@ -62,96 +60,63 @@ pub fn new(args: &[String]) -> i32 {
}
if clean_args.is_empty() {
error("Project name not specified");
error!("Project name not specified");
return 1;
}
if clean_args.len() > 1 {
warn("Command does not support additional arguments");
warn!("Command does not support additional arguments");
return 1;
}
let project_name = &clean_args[0];
println!(
"Creating a Project `{}`...",
colored(project_name, BOLD_CYAN)
);
if let Err(e) = validate_package_name(project_name) {
error(&format!(
"Invalid project name `{}`: {}",
colored(project_name, BOLD_CYAN),
e
));
error!("Invalid project name `{}`: {e}", project_name,);
return 1;
}
if items.contains(project_name) {
error(&format!(
"Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN)
));
printc("Hint:", BOLD_CYAN);
println!(
error!("Directory `{}` already exists", project_name);
crate::utils::output::info(format!(
" Use `{}` to initialize an existing project\n or specify a different project name",
colored("dcr init", BOLD_CYAN)
);
"dcr init"
));
return 1;
}
crate::utils::output::status("Creating", format!("project `{project_name}`"));
if fs::create_dir(project_name).is_err() {
error("Failed to create directory");
error!("Failed to create directory");
return 1;
}
println!(
" {} Directory created {}",
colored("✔", BOLD_GREEN),
project_name
);
crate::utils::output::created(format!("directory `{project_name}`"));
// Initialize and write project configuration manifest (dcr.toml).
let toml_path = format!("./{project_name}/dcr.toml");
let mut config = match Config::new(&toml_path) {
Ok(cfg) => cfg,
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.to_string()))
.is_err()
{
error("Failed to write dcr.toml");
if Config::create(&toml_path, Some(project_name)).is_err() {
error!("Failed to create dcr.toml");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
crate::utils::output::created("dcr.toml");
// Generate src directory and populate initial main.c file.
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
error("Failed to create src/");
error!("Failed to create src/");
return 1;
}
let main_c_path = format!("./{project_name}/src/main.c");
let mut main_c = match fs::File::create(&main_c_path) {
Ok(file) => file,
Err(_) => {
error("Failed to create src/main.c");
error!("Failed to create src/main.c");
return 1;
}
};
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
error("Failed to write src/main.c");
error!("Failed to write src/main.c");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
crate::utils::output::created("src/main.c");
// Resolve target VCS mode based on CLI flags or parent directory state.
let mut vcs_kind = VcsKind::Git;
@@ -159,7 +124,8 @@ pub fn new(args: &[String]) -> i32 {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
}
@@ -173,15 +139,12 @@ pub fn new(args: &[String]) -> i32 {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
warn!("Failed to initialize git repository: {e}");
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
crate::utils::output::success("Initialized git repository");
}
} else {
warn(
warn!(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
@@ -189,14 +152,10 @@ pub fn new(args: &[String]) -> i32 {
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(project_path) {
warn(&format!("Failed to update .gitignore: {e}"));
warn!("Failed to update .gitignore: {e}");
}
println!(
"Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(&format!(" cd {}\n dcr run", project_name), BOLD_CYAN);
crate::utils::output::success(format!("Created project `{project_name}`"));
crate::utils::output::info(format!("Next steps:\n cd {project_name}\n dcr run"));
0
}

View File

@@ -15,17 +15,15 @@
// 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::prelude::*;
use crate::cli::build::build;
use crate::cli::flags::parse_build_run_flags;
use crate::core::build_config::Config;
use crate::core::runner::run_binary;
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::path::Path;
use std::process::Command;
use crate::utils::fs::{find_project_root, with_dir};
use std::{path::Path, process::Command};
/// Retrieves the run command from the config, preferring target-specific,
/// then profile-specific, then the base `run.cmd`.
@@ -67,42 +65,37 @@ fn get_run_cmd(
/// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure.
pub fn run(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Builds and runs the project. Only available for kind = \"bin\".");
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Run with debug profile (default)");
println!(" --release Run with release profile");
println!(" --target <triple> Cross-compile for the given target");
println!(" --force Force a full rebuild");
println!(" --clean Clean before building");
println!(" --verbose Print detailed build output");
println!(" -- <args>... Arguments forwarded to the binary");
let mut help = crate::utils::help::HelpBuilder::new("dcr run");
help.section("USAGE").example("dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]");
help.description("Builds and runs the project. Only available for kind = \"bin\".")
.note("Arguments after `--` are passed to the built binary (cargo-style).");
help.section("OPTIONS")
.item("--debug", "Run with debug profile (default)")
.item("--release", "Run with release profile")
.item("--target <triple>", "Cross-compile for the given target")
.item("--force", "Force a full rebuild")
.item("--clean", "Clean before building")
.item("--verbose", "Print detailed build output")
.item("-- <args>...", "Arguments forwarded to the binary");
help.render();
return 0;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
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");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -111,7 +104,8 @@ pub fn run(args: &[String]) -> i32 {
}) {
Ok(cfg) => cfg,
Err(err) => {
error(&err);
error!("{err}");
return 1;
}
};
@@ -125,11 +119,7 @@ pub fn run(args: &[String]) -> i32 {
let build_status = build(&args_for_build(&flags));
if build_status == 0 {
let display = display_run_cmd(&cmd, &flags.bin_args);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
crate::utils::output::status("run", display);
return run_shell_with_args(&cmd, &flags.bin_args);
}
return build_status;
@@ -143,11 +133,12 @@ pub fn run(args: &[String]) -> i32 {
) {
Ok(Some(ws)) => ws,
Ok(None) => {
error("Workspace root has no members defined");
error!("Workspace root has no members defined");
return 1;
}
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -159,9 +150,9 @@ pub fn run(args: &[String]) -> i32 {
Some(m) => m,
None => {
if let Some(name) = &flags.workspace {
error(&format!("Workspace member '{name}' not found"));
error!("Workspace member '{name}' not found");
} else {
error("No workspace member to run (set `main = true` on one member)");
error!("No workspace member to run (set `main = true` on one member)");
}
return 1;
}
@@ -172,7 +163,8 @@ pub fn run(args: &[String]) -> i32 {
}) {
Ok(code) => code,
Err(e) => {
error(&e);
error!("{e}");
1
}
};
@@ -181,7 +173,7 @@ pub fn run(args: &[String]) -> i32 {
match run_project(&root, &flags, None) {
Ok(code) => code,
Err(e) => {
error(&e);
error!("{e}");
1
}
}
@@ -263,19 +255,11 @@ fn run_project(
if build_status == 0 {
if let Some(cmd) = run_cmd {
let display = display_run_cmd(&cmd, &flags.bin_args);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
crate::utils::output::status("run", display);
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
}
let display = display_bin_run(&bin_path, &flags.bin_args);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
crate::utils::output::status("run", display);
return Ok(run_binary(
project_name,
&flags.profile,

View File

@@ -15,32 +15,31 @@
// 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::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
/// Sets up DCR registries by loading the registry manager and printing the list of registries.
pub fn setup(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr setup", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Sets up DCR registries. Downloads and indexes");
println!(" package registries for dependency resolution.");
let mut help = crate::utils::help::HelpBuilder::new("dcr setup");
help.section("USAGE").example("dcr setup");
help.description("Show configured package registries and their priorities.");
help.render();
return 0;
}
println!("Setting up DCR registries...");
// Load and process the registries, returning 0 on success or 1 on error
match crate::core::registry::RegistryManager::load() {
Ok(manager) => {
println!("Loaded {} registries.", manager.config.registry.len());
let mut lines = vec![format!(
"Loaded {} registries (configured)",
manager.config.registry.len()
)];
for (name, reg) in manager.config.registry {
println!("- {}: {} (priority {})", name, reg.url, reg.priority);
lines.push(format!(" {name}: {} (priority {})", reg.url, reg.priority));
}
crate::say!("{}", lines.join("\n"));
0
}
Err(e) => {
println!("Error: {}", e);
crate::utils::output::error!("{e}");
1
}
}

View File

@@ -15,6 +15,8 @@
// 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::prelude::*;
use crate::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::build_config::Config;
@@ -23,33 +25,29 @@ use crate::utils::build::{
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
};
use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_RED, RESET, colored, printc};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
/// Runs project tests and prints a unified testsuite report.
pub fn test(args: &[String]) -> i32 {
let mut init_header = false;
let mut profile = "debug";
for arg in args {
if arg == "--help" {
printc("USAGE:", BOLD_GREEN);
printc(" dcr test [--init] [--debug | --release]", BOLD_CYAN);
println!();
printc("ALIASES:", BOLD_GREEN);
println!(" dcr tests");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Runs project tests and prints a unified testsuite report.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --init Create tests/dcr_test.h in current project");
println!(" --debug Build and run tests with debug profile (default)");
println!(" --release Build and run tests with release profile");
let mut help = crate::utils::help::HelpBuilder::new("dcr test");
help.section("USAGE")
.example("dcr test [--init] [--debug | --release]");
help.section("ALIASES").example("dcr tests");
help.description("Runs project tests and prints a unified testsuite report.");
help.section("OPTIONS")
.item("--init", "Create tests/dcr_test.h in current project")
.item(
"--debug",
"Build and run tests with debug profile (default)",
)
.item("--release", "Build and run tests with release profile");
help.render();
return 0;
}
if arg == "--init" {
@@ -60,25 +58,25 @@ pub fn test(args: &[String]) -> i32 {
profile = arg.trim_start_matches("--");
continue;
}
error("Unknown argument");
error!("Unknown argument");
return 1;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
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");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -87,7 +85,8 @@ pub fn test(args: &[String]) -> i32 {
match with_dir(&root, ensure_test_header) {
Ok(()) => {}
Err(msg) => {
error(&msg);
error!("{msg}");
return 1;
}
}
@@ -96,7 +95,7 @@ pub fn test(args: &[String]) -> i32 {
match with_dir(&root, || run_testsuite(profile)) {
Ok(code) => code,
Err(msg) => {
error(&msg);
error!("{msg}");
1
}
}
@@ -134,8 +133,12 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
cflags.extend(get_list_with_profile(&config, "cflags", profile));
let ldflags = get_list_with_profile(&config, "ldflags", profile);
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile);
let (cflags, ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
let includes = test_include_dirs(&config, profile);
let (mut cflags, mut ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
let deps = crate::core::deps::resolve_deps(&config, profile, None, Path::new("."))?;
cflags.extend(deps.cflags);
ldflags.extend(deps.ldflags);
let mut includes = test_include_dirs(&config, profile);
includes.extend(deps.include_dirs);
let test_sources = collect_test_sources()?;
if test_sources.is_empty() {
return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
@@ -174,7 +177,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
stdout.push_str(&String::from_utf8_lossy(&out.stdout));
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.trim().is_empty() {
eprint!("{}", stderr);
crate::utils::output::compiler_output("stderr", &stderr);
}
if !out.status.success() {
suite_success = false;
@@ -189,19 +192,19 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
for line in stdout.lines() {
let line = line.trim();
if let Some(name) = line.strip_prefix("[PASS] ") {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
crate::utils::output::success(format!("Passed {name}"));
pass += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[SKIP] ") {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
crate::utils::output::status("Skipped", name);
skip += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[FAIL] ") {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
crate::utils::output::error(format!("Failed {name}"));
fail += 1;
parsed_any = true;
continue;
@@ -209,17 +212,17 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if let Some((status, name)) = line.split_once('\t') {
match status {
"PASS" => {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
crate::utils::output::success(format!("Passed {name}"));
pass += 1;
parsed_any = true;
}
"SKIP" => {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
crate::utils::output::status("Skipped", name);
skip += 1;
parsed_any = true;
}
"FAIL" => {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
crate::utils::output::error(format!("Failed {name}"));
fail += 1;
parsed_any = true;
}
@@ -231,11 +234,11 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if !parsed_any {
if suite_success {
for name in &declared {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
crate::utils::output::success(format!("Passed {name}"));
}
pass = declared.len() as i32;
} else {
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
crate::utils::output::error("testsuite failed");
fail = 1;
}
}
@@ -246,15 +249,10 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
declared.len() as i32
};
println!();
println!("{}", colored("=====================", BOLD_GREEN));
println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
println!("{}", colored("=====================", BOLD_GREEN));
println!("TOTAL: {}", total);
print_field("PASS", pass, BOLD_GREEN);
print_field("SKIP", skip, BOLD_BLUE);
print_field("FAIL", fail, BOLD_RED);
println!("{}", colored("=====================", BOLD_GREEN));
crate::utils::output::status(
"test result:",
format!("{pass} passed; {fail} failed; {skip} skipped; {total} total"),
);
if fail > 0 || !suite_success {
return Ok(1);
@@ -352,10 +350,10 @@ fn link_test_binary(
/// Prints the stdout and stderr from a command execution.
fn print_command_output(output: &std::process::Output) {
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
crate::utils::output::write_stdout(&format!("{}", String::from_utf8_lossy(&output.stdout)));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
crate::utils::output::compiler_output("stderr", &String::from_utf8_lossy(&output.stderr));
}
}
@@ -364,10 +362,10 @@ fn ensure_test_header() -> Result<(), String> {
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
.map_err(|_| "Failed to write tests/dcr_test.h".to_string())?;
println!("Created tests/dcr_test.h");
crate::utils::output::created("tests/dcr_test.h");
fs::write("./tests/test.c", FILE_TEST_C)
.map_err(|_| "Failed to write tests/test.c".to_string())?;
println!("Created tests/test.c");
crate::utils::output::created("tests/test.c");
Ok(())
}
@@ -387,12 +385,3 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
}
names
}
/// Prints a test result field with color coding.
fn print_field(label: &str, value: i32, color: &str) {
if value == 0 {
println!("{}: {}", label, value);
} else {
println!("{}{}: {}{}", color, label, value, RESET);
}
}

View File

@@ -15,9 +15,9 @@
// 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::prelude::*;
use crate::core::build_config::Config;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use toml::Value;
@@ -28,11 +28,10 @@ use toml::Value;
/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree.
pub fn tree(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr tree", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Displays the dependency tree of the current project.");
let mut help = crate::utils::help::HelpBuilder::new("dcr tree");
help.section("USAGE").example("dcr tree");
help.description("Displays the dependency tree of the current project.");
help.render();
return 0;
}
@@ -40,7 +39,7 @@ pub fn tree(args: &[String]) -> i32 {
let config = match Config::open("dcr.toml") {
Ok(c) => c,
Err(_) => {
error("dcr.toml not found in current directory");
error!("dcr.toml not found in current directory");
return 1;
}
};
@@ -54,7 +53,7 @@ pub fn tree(args: &[String]) -> i32 {
.and_then(|v| v.as_str())
.unwrap_or("0.0.0");
printc(&format!("{} v{}", name, version), BOLD_CYAN);
crate::utils::output::write_stdout_raw(&format!("{} v{}\n", name, version));
let mut seen = HashSet::new();
seen.insert(name.to_string());
@@ -115,6 +114,19 @@ fn print_deps(
format!(" v{}", v)
} else if let Some(git) = t.get("git").and_then(|v| v.as_str()) {
format!(" ({})", git)
} else if let Some(pkg) = t.get("pkg-config").or_else(|| t.get("pkg_config")) {
if let Some(s) = pkg.as_str() {
format!(" (pkg-config: {})", s)
} else if let Some(arr) = pkg.as_array() {
let names = arr
.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(", ");
format!(" (pkg-config: {})", names)
} else {
"".to_string()
}
} else {
"".to_string()
}
@@ -123,7 +135,10 @@ fn print_deps(
};
}
println!("{}{}{}{}", prefix, connector, name, version);
crate::utils::output::write_stdout_raw(&format!(
"{}{}{}{}\n",
prefix, connector, name, version
));
// Recurse only if sub-dependencies exist and this dep hasn't been seen
if let Some(s_deps) = sub_deps

View File

@@ -15,8 +15,7 @@
// 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::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use crate::platform;
use crate::utils::build::is_bare_metal_target;
use std::path::{Path, PathBuf};

View File

@@ -15,9 +15,7 @@
// 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::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::utils::build::{is_bare_metal_target, is_compile_only, is_flat_bin};
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -20,15 +20,10 @@
/// This module provides the MSVC-specific build implementation for C and C++
/// projects, including object compilation and linking for executables, static
/// libraries, shared libraries, and flat binaries.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::platform;
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
use std::{fs, path::Path, process::Command, time::Instant};
/// Builds the project target using the MSVC compiler.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {

View File

@@ -15,8 +15,7 @@
// 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::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -769,8 +769,10 @@ fn build_project_at_inner(
)?;
}
let (resolved_cflags, resolved_ldflags) =
let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
resolved_cflags.extend(resolved.cflags.clone());
resolved_ldflags.extend(resolved.ldflags.clone());
let mut combined_excludes = Vec::new();
for dir in exclude_dirs {
combined_excludes.push(dir.clone());
@@ -1011,6 +1013,8 @@ fn extend_transitive_link_deps(
consumer_deps.include_dirs.extend(deps.include_dirs);
consumer_deps.lib_dirs.extend(deps.lib_dirs);
consumer_deps.libs.extend(deps.libs);
consumer_deps.cflags.extend(deps.cflags);
consumer_deps.ldflags.extend(deps.ldflags);
for child_root in deps.package_roots {
extend_transitive_link_deps(
Path::new(&child_root),

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::path::Path;
use std::time::Instant;
use std::{path::Path, time::Instant};
/// Keeps only assembler-relevant flags from a cflags list.
///

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds an assembly file using the FASM assembler.
///

View File

@@ -15,12 +15,9 @@
// 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::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds GAS (`.s`) sources via the GNU Assembler.
///

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds MASM (`.asm`) sources via `ml`.
///

View File

@@ -16,12 +16,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// Support for NASM assembly language in the DCR build system.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds assembly sources using the NASM assembler.
///

View File

@@ -15,8 +15,7 @@
// 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::builder::BuildContext;
use crate::core::build::common;
use crate::core::{build::builder::BuildContext, build::common};
use crate::utils::build::run_pkg_config;
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -1,10 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::Language;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common, language::Language};
use std::{fs, path::Path, process::Command};
/// Represents the LLVM IR language implementation for the build system.
pub struct LlvmIr;

View File

@@ -15,8 +15,7 @@
// 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::engine::ToolchainExecs;
use crate::core::build_config::Config;
use crate::core::{build::engine::ToolchainExecs, build_config::Config};
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
use glob::glob;
use std::fs;

View File

@@ -15,12 +15,13 @@
// 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::prelude::*;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
use toml::map::Map;
use toml::{Value, map::Map};
use toml_edit::{DocumentMut, Item, Table};
const DEFAULT_VERSION: &str = "0.1.0";
@@ -244,13 +245,19 @@ pub struct WorkspaceMemberConfig {
}
impl Config {
#[allow(dead_code)]
/// Creates a new Config instance from a TOML file path. If the file does not exist, it creates a default one.
pub fn new(path: &str) -> Result<Self, ConfigError> {
Self::create(path, None)
}
/// Creates a new Config instance with an explicit package name for the default template when creating a new file.
pub fn create(path: &str, package_name: Option<&str>) -> Result<Self, ConfigError> {
let path = PathBuf::from(path);
let (data, doc) = if path.exists() {
load_parts(&path)?
} else {
let content = default_toml_text();
let content = default_toml_text(package_name)?;
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
fs::write(&path, doc.to_string())?;
let data: Value = toml::from_str(&content)?;
@@ -328,35 +335,35 @@ impl Config {
.map(str::trim)
.filter(|s| !s.is_empty());
let Some(required) = required else {
crate::utils::log::warn(&format!(
warn!(
"package.dcr-version is missing in {}. Add e.g. dcr-version = \"{tool}\" \
so collaborators know which dcr this project targets \
(written automatically by `dcr new` / `dcr init`).",
self.path.display()
));
so collaborators know which dcr this project targets \
(written automatically by `dcr new` / `dcr init`).",
self.path.display(),
);
return;
};
match crate::utils::build::compare_semver(tool, required) {
Some(std::cmp::Ordering::Less) => {
crate::utils::log::warn(&format!(
warn!(
"this project requires dcr {required} (package.dcr-version), \
but you are running dcr {tool}. Some features may be missing or misbehave. \
Upgrade dcr (e.g. `dcr --update`) or lower package.dcr-version if intentional."
));
);
}
Some(std::cmp::Ordering::Greater) => {
crate::utils::log::warn(&format!(
warn!(
"this project pins dcr {required} (package.dcr-version), \
but you are running newer dcr {tool}. Config/defaults may be outdated. \
Review the changelog and bump package.dcr-version when the project is verified."
));
but you are running newer dcr {tool}. Config/defaults may be outdated. \
Review the changelog and bump package.dcr-version when the project is verified.",
);
}
Some(std::cmp::Ordering::Equal) => {}
None => {
crate::utils::log::warn(&format!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring"
));
warn!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring",
);
}
}
}
@@ -830,14 +837,20 @@ fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
Ok((data, doc))
}
fn default_toml_text() -> String {
let name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
fn default_toml_text(package_name: Option<&str>) -> Result<String, ConfigError> {
let name = package_name
.map(|s| s.to_string())
.or_else(|| {
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());
validate_non_empty_string(&name, "package.name")?;
validate_package_name(&name)?;
let dcr_version = env!("CARGO_PKG_VERSION");
format!(
Ok(format!(
"[package]\n\
name = \"{name}\"\n\
version = \"{DEFAULT_VERSION}\"\n\
@@ -854,7 +867,7 @@ fn default_toml_text() -> String {
kind = \"{DEFAULT_KIND}\"\n\
\n\
[dependencies]\n"
)
))
}
fn dcr_version_warn_once(path_key: &str) -> bool {
@@ -1285,6 +1298,22 @@ mod tests {
);
}
#[test]
fn create_rejects_invalid_name_without_writing_manifest() {
let dir = temp_dir("invalid_create_name");
let path = dir.join("dcr.toml");
for name in ["", "bad name", "../bad", "bad\"name", " bad", "bad."] {
let result = Config::create(&path.to_string_lossy(), Some(name));
assert!(matches!(result, Err(ConfigError::Invalid(_))), "{name:?}");
assert!(!path.exists(), "invalid name must not create a manifest");
}
let config = Config::create(&path.to_string_lossy(), Some("valid-name")).unwrap();
assert_eq!(
config.get("package.name").and_then(Value::as_str),
Some("valid-name")
);
}
#[test]
fn new_creates_default_config() {
let dir = temp_dir("new_default");

View File

@@ -4,5 +4,7 @@ pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
pub cflags: Vec<String>,
pub ldflags: Vec<String>,
pub package_roots: Vec<String>,
}

View File

@@ -15,8 +15,7 @@
// 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 std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Represents a locked dependency entry containing its name, version, source, and checksum.
#[derive(Debug, Clone)]

View File

@@ -23,6 +23,7 @@ pub mod register;
use crate::core::build_config::Config;
use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock};
use crate::utils::build::{resolve_pkg_config_flags, run_pkg_config};
use std::path::Path;
/// Returns the version string from a dependency's dcr.toml file, or an empty string if missing.
@@ -73,7 +74,22 @@ pub fn resolve_deps(
if let Some(deps) = deps_table {
// Process each dependency declaration in the TOML table
for (name, value) in deps {
if register::is_registry_dep(value) {
if let Some(packages) = pkg_config_dep(value, name)? {
let (cflags, ldflags) = resolve_pkg_config_flags(&packages, &[], &[])?;
resolved.cflags.extend(cflags);
resolved.ldflags.extend(ldflags);
let version = packages
.first()
.and_then(|pkg| run_pkg_config(pkg, "--modversion").ok())
.map(|v| v.trim().to_string())
.unwrap_or_default();
lock_packages.push(DepLock {
name: name.clone(),
version,
checksum: String::new(),
source: format!("pkg-config+{}", packages.join(",")),
});
} else if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
@@ -274,6 +290,48 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
None
}
/// Extracts one or more pkg-config package names from a dependency table.
/// Both `pkg-config` and the legacy-style `pkg_config` spelling are accepted.
fn pkg_config_dep(
value: &toml::Value,
dependency_name: &str,
) -> Result<Option<Vec<String>>, String> {
let Some(table) = value.as_table() else {
return Ok(None);
};
let raw = table.get("pkg-config").or_else(|| table.get("pkg_config"));
let Some(raw) = raw else {
return Ok(None);
};
let packages = if let Some(pkg) = raw.as_str() {
vec![pkg.to_string()]
} else {
raw.as_array()
.ok_or_else(|| {
format!(
"Dependency `{dependency_name}` field `pkg-config` must be a string or array of strings"
)
})?
.iter()
.map(|value| {
value.as_str().map(str::to_string).ok_or_else(|| {
format!(
"Dependency `{dependency_name}` field `pkg-config` must contain only strings"
)
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.collect()
};
if packages.is_empty() || packages.iter().any(|pkg| pkg.trim().is_empty()) {
return Err(format!(
"Dependency `{dependency_name}` field `pkg-config` must not be empty"
));
}
Ok(Some(packages))
}
/// Extracts the path for a path-based dependency, supporting both table format and legacy string format.
fn path_dep_path(value: &toml::Value) -> Option<&str> {
if let Some(table) = value.as_table() {
@@ -436,6 +494,20 @@ mod tests {
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
}
#[test]
fn pkg_config_dep_accepts_string_and_array() {
let single: Value = toml::from_str("pkg-config = \"fmt\"").unwrap();
assert_eq!(
pkg_config_dep(&single, "fmt"),
Ok(Some(vec!["fmt".to_string()]))
);
let many: Value = toml::from_str("pkg_config = [\"fmt\", \"zlib\"]").unwrap();
assert_eq!(
pkg_config_dep(&many, "base"),
Ok(Some(vec!["fmt".to_string(), "zlib".to_string()]))
);
}
/// Verifies that push_default_lib_dirs includes the target/lib directory when it exists.
#[test]
fn default_lib_dirs_include_packaged_library_output() {

View File

@@ -17,9 +17,7 @@
/// Registry configuration management for DCR.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::{collections::HashMap, fs, path::PathBuf};
/// Represents a registry configuration entry with its URL and priority.
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -16,8 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::platform;
use std::path::Path;
use std::process::Command;
use std::{path::Path, process::Command};
/// Runs the built binary for a project, forwarding `bin_args`.
///

View File

@@ -15,8 +15,7 @@
// 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 std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Initializes a Git repository in the target directory and configures its `.gitignore` file.
///

View File

@@ -14,20 +14,29 @@
//
// 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::utils::log::error;
mod cli;
mod config;
mod core;
mod platform;
mod prelude;
mod utils;
use prelude::*;
/// Main entry point for DCR, a Cargo-like C/C++ project manager.
///
/// Collects command-line arguments via `std::env::args()` and dispatches to the
/// appropriate subcommand handler. With fewer than two arguments, shows help and
/// exits with code 0; otherwise exits with the handler's exit code.
fn main() {
let args: Vec<String> = std::env::args().collect();
let args = match utils::output::configure(std::env::args().collect()) {
Ok(args) => args,
Err(message) => {
utils::output::emit("error", &message);
std::process::exit(2);
}
};
utils::log::init();
if args.len() < 2 {
std::process::exit(cli::help::help());
@@ -61,7 +70,7 @@ fn main() {
"--help" => cli::help::help(),
"--update" => cli::flag_update::flag_update(rest),
_ => {
error("Unknown command or argument");
error!("Unknown command or argument");
1
}
};

View File

@@ -58,11 +58,11 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::bin_path(profile, name, target_dir);
macos::bin_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::bin_path(profile, name, target_dir);
windows::bin_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -71,7 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::bin_path(profile, name, target_dir);
bsd::bin_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -83,7 +83,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::bin_path(profile, name, target_dir);
posix::bin_path(profile, name, target_dir)
}
}
@@ -97,11 +97,11 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::elf_path(profile, name, target_dir);
macos::elf_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::elf_path(profile, name, target_dir);
windows::elf_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -110,7 +110,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::elf_path(profile, name, target_dir);
bsd::elf_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -122,7 +122,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::elf_path(profile, name, target_dir);
posix::elf_path(profile, name, target_dir)
}
}
@@ -136,11 +136,11 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::efi_path(profile, name, target_dir);
macos::efi_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::efi_path(profile, name, target_dir);
windows::efi_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -149,7 +149,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::efi_path(profile, name, target_dir);
bsd::efi_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -161,7 +161,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::efi_path(profile, name, target_dir);
posix::efi_path(profile, name, target_dir)
}
}
@@ -175,11 +175,11 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
return macos::lib_path(profile, name, target_dir);
macos::lib_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::lib_path(profile, name, target_dir);
windows::lib_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -188,7 +188,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
))]
{
return bsd::lib_path(profile, name, target_dir);
bsd::lib_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -200,7 +200,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
return posix::lib_path(profile, name, target_dir);
posix::lib_path(profile, name, target_dir)
}
}
@@ -214,11 +214,11 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
}
#[cfg(target_os = "macos")]
{
return macos::shared_lib_path(profile, name, target_dir);
macos::shared_lib_path(profile, name, target_dir)
}
#[cfg(target_os = "windows")]
{
return windows::shared_lib_path(profile, name, target_dir);
windows::shared_lib_path(profile, name, target_dir)
}
#[cfg(any(
target_os = "freebsd",
@@ -227,7 +227,7 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly"
))]
{
return bsd::shared_lib_path(profile, name, target_dir);
bsd::shared_lib_path(profile, name, target_dir)
}
#[cfg(not(any(
target_os = "linux",
@@ -239,6 +239,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly"
)))]
{
return posix::shared_lib_path(profile, name, target_dir);
posix::shared_lib_path(profile, name, target_dir)
}
}

1
src/prelude.rs Normal file
View File

@@ -0,0 +1 @@
pub use log::{error, warn};

View File

@@ -15,8 +15,9 @@
// 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::prelude::*;
use crate::core::build_config::Config;
use crate::utils::log::warn;
use std::path::{Path, PathBuf};
/// Version information parsed from a version string.
@@ -251,10 +252,9 @@ pub fn normalize_target_os(target: &str) -> &str {
"windows" => "x86_64-pc-windows-msvc",
_ if target.contains('-') => target,
_ => {
warn(&format!(
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
target
));
warn!(
"Unknown target '{target}', using as-is. Supported short names: linux, macos, windows"
);
target
}
}

View File

@@ -15,39 +15,33 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[allow(dead_code)]
pub const RESET: &str = "\x1b[0m";
pub use owo_colors::{OwoColorize as Colorize, Style};
// Сli theme styles
// Main
#[allow(dead_code)]
pub const BOLD: &str = "\x1b[1m";
pub const BOLD_RED: Style = Style::new().bright_red().bold();
#[allow(dead_code)]
pub const BOLD_GREEN: Style = Style::new().bright_green().bold();
#[allow(dead_code)]
pub const BOLD_YELLOW: Style = Style::new().bright_yellow().bold();
#[allow(dead_code)]
pub const BOLD_CYAN: Style = Style::new().bright_cyan().bold();
#[allow(dead_code)]
pub const BOLD_BLUE: Style = Style::new().bright_blue().bold();
// Help
#[allow(dead_code)]
pub const BRIGHT_RED: &str = "\x1b[91m";
pub const HELP_SECTION_TITLE_ST: Style = BOLD_GREEN;
#[allow(dead_code)]
pub const BRIGHT_GREEN: &str = "\x1b[92m";
#[allow(dead_code)]
pub const BRIGHT_YELLOW: &str = "\x1b[93m";
#[allow(dead_code)]
pub const BRIGHT_CYAN: &str = "\x1b[96m";
#[allow(dead_code)]
pub const BOLD_RED: &str = "\x1b[1m\x1b[91m";
#[allow(dead_code)]
pub const BOLD_GREEN: &str = "\x1b[1m\x1b[92m";
#[allow(dead_code)]
pub const BOLD_YELLOW: &str = "\x1b[1m\x1b[93m";
#[allow(dead_code)]
pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m";
pub const HELP_EXAMPLES_ST: Style = BOLD_CYAN;
/// Applies ANSI escape codes to format the message with the given style.
///
/// This is a utility for colored output in terminal applications.
// Other
#[allow(dead_code)]
pub fn colored(msg: &str, style: &str) -> String {
format!("{style}{msg}{RESET}")
}
/// Prints `msg` to stdout with the given ANSI `style`.
pub const ERROR_ST: Style = BOLD_RED;
#[allow(dead_code)]
pub fn printc(msg: &str, style: &str) {
println!("{style}{msg}{RESET}");
}
pub const SUCCESS_ST: Style = BOLD_GREEN;
#[allow(dead_code)]
pub const ALERT_ST: Style = BOLD_YELLOW;
#[allow(dead_code)]
pub const SKIP_ST: Style = BOLD_BLUE;

View File

@@ -1,5 +1,4 @@
use std::path::Path;
use std::process::Command;
use std::{path::Path, process::Command};
/// Checks if the `git` executable is available in the system's PATH.
///

124
src/utils/help.rs Normal file
View File

@@ -0,0 +1,124 @@
use super::cli_styles::{BOLD_CYAN, BOLD_GREEN, Colorize};
use super::output;
pub struct HelpBuilder {
title: String,
description: Option<String>,
sections: Vec<Section>,
tip: Option<String>,
}
pub struct HelpSection<'a> {
help: &'a mut HelpBuilder,
index: usize,
}
struct Section {
title: String,
entries: Vec<(String, String)>,
}
impl HelpBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
description: None,
sections: Vec::new(),
tip: None,
}
}
pub fn description(&mut self, text: impl Into<String>) -> &mut Self {
self.description = Some(text.into());
self
}
/// Add an example to a dedicated `EXAMPLES` section.
pub fn example(&mut self, text: impl Into<String>) -> &mut Self {
let text = text.into();
if let Some(section) = self.sections.iter_mut().find(|s| s.title == "EXAMPLES") {
section.entries.push((text, String::new()));
} else {
self.sections.push(Section {
title: "EXAMPLES".to_owned(),
entries: vec![(text, String::new())],
});
}
self
}
pub fn note(&mut self, text: impl Into<String>) -> &mut Self {
let text = text.into();
match &mut self.description {
Some(description) if !description.is_empty() => {
description.push('\n');
description.push_str(&text);
}
Some(description) => description.push_str(&text),
None => self.description = Some(text),
}
self
}
pub fn section(&mut self, title: impl Into<String>) -> HelpSection<'_> {
self.sections.push(Section {
title: title.into(),
entries: Vec::new(),
});
HelpSection {
index: self.sections.len() - 1,
help: self,
}
}
pub fn tip(&mut self, text: impl Into<String>) -> &mut Self {
self.tip = Some(text.into());
self
}
pub fn render(&self) {
if output::mode() == output::Mode::Json {
let sections: Vec<_> = self.sections.iter().map(|s| serde_json::json!({"title": s.title, "items": s.entries.iter().map(|(n,d)| serde_json::json!({"name":n,"description":d})).collect::<Vec<_>>() })).collect();
let event = serde_json::json!({
"schema_version": 1,
"event": "help",
"title": self.title,
"description": self.description,
"sections": sections,
"tip": self.tip
});
output::write_stdout_raw(&format!("{event}\n"));
return;
}
let mut rendered = String::new();
rendered.push_str(&format!("{}\n", self.title.style(BOLD_GREEN)));
if let Some(d) = &self.description {
rendered.push_str(&format!("{}\n\n", d));
}
for s in &self.sections {
rendered.push_str(&format!("{}:\n", s.title.style(BOLD_GREEN)));
let width = s.entries.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
for (n, d) in &s.entries {
if d.is_empty() {
rendered.push_str(&format!(" {}\n", n.style(BOLD_CYAN)));
} else {
rendered.push_str(&format!(
" {} {d}\n",
format!("{n:<width$}").style(BOLD_CYAN)
));
}
}
rendered.push('\n');
}
if let Some(t) = &self.tip {
rendered.push_str(&format!("{}:\n {t}\n", "TIP".style(BOLD_GREEN)));
}
output::write_stdout(&rendered);
}
}
impl HelpSection<'_> {
pub fn item(self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.help.sections[self.index]
.entries
.push((name.into(), description.into()));
self
}
pub fn example(self, command: impl Into<String>) -> Self {
self.item(command, "")
}
}

View File

@@ -15,17 +15,37 @@
// 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::utils::text::{BOLD_RED, BOLD_YELLOW, colored};
use log::{LevelFilter, Log, Metadata, Record};
/// Logs an error message to the standard error stream.
///
/// The message is prefixed with a red "error" label for visibility.
#[allow(dead_code)]
pub fn error(msg: &str) {
eprintln!("{}: {msg}", colored("error", BOLD_RED));
struct CliLogger;
impl Log for CliLogger {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
metadata.level().to_level_filter() <= log::max_level()
}
fn log(&self, record: &Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
let kind = match record.level() {
log::Level::Warn => "warning",
log::Level::Error => "error",
log::Level::Info => "info",
log::Level::Debug => "debug",
log::Level::Trace => "trace",
};
crate::utils::output::emit(kind, &record.args().to_string());
}
fn flush(&self) {}
}
#[allow(dead_code)]
pub fn warn(msg: &str) {
eprintln!("{}: {msg}", colored("warn", BOLD_YELLOW));
// A function for initializing the logging lib with basic settings.
pub fn init() {
static LOGGER: CliLogger = CliLogger;
log::set_logger(&LOGGER).expect("logger already initialized");
log::set_max_level(if crate::utils::output::verbose() {
LevelFilter::Debug
} else {
LevelFilter::Info
});
}

View File

@@ -29,4 +29,6 @@ pub mod git;
pub mod log;
/// Text processing and manipulation helpers.
pub mod text;
pub mod cli_styles;
pub mod help;
pub mod output;

535
src/utils/output.rs Normal file
View File

@@ -0,0 +1,535 @@
//! Central rendering policy for user-facing CLI events.
use std::io::{self, IsTerminal, Write};
use std::sync::OnceLock;
#[derive(Clone, Copy, Default, PartialEq)]
pub enum Mode {
#[default]
Human,
Plain,
Json,
}
#[derive(Default)]
pub struct Options {
pub mode: Mode,
pub quiet: bool,
pub verbose: bool,
pub color: Option<bool>,
}
static OPTIONS: OnceLock<Options> = OnceLock::new();
/// Consume global options without touching arguments after `--`.
pub fn configure(args: Vec<String>) -> Result<Vec<String>, String> {
let mut options = Options::default();
let mut remaining = Vec::new();
let mut args = args.into_iter();
if let Some(program) = args.next() {
remaining.push(program);
}
while let Some(arg) = args.next() {
if arg == "--" {
remaining.push(arg);
remaining.extend(args);
break;
}
match arg.as_str() {
"--quiet" => options.quiet = true,
"--verbose" => options.verbose = true,
"--output" | "--color" => {
let value = args
.next()
.ok_or_else(|| format!("{arg} requires a value"))?;
set_option(&mut options, &arg, &value)?;
}
_ if arg.starts_with("--output=") || arg.starts_with("--color=") => {
let (key, value) = arg.split_once('=').unwrap();
set_option(&mut options, key, value)?;
}
_ => {
remaining.push(arg);
remaining.extend(args);
break;
}
}
}
OPTIONS
.set(options)
.map_err(|_| "output already configured".to_owned())?;
// Help is styled before it reaches the output writer. Keep owo-colors'
// global decision in sync with DCR's explicit color policy; otherwise
// `--color always` can color the DCR banner but leave help sections plain.
let configured = OPTIONS.get().expect("output options were just configured");
let force_color = configured.color.or_else(|| {
if std::env::var_os("NO_COLOR").is_some() {
Some(false)
} else {
None
}
});
if let Some(enabled) = force_color {
owo_colors::set_override(enabled);
}
Ok(remaining)
}
fn set_option(options: &mut Options, key: &str, value: &str) -> Result<(), String> {
match (key, value) {
("--output", "human") => options.mode = Mode::Human,
("--output", "plain") => options.mode = Mode::Plain,
("--output", "json") => options.mode = Mode::Json,
("--color", "auto") => options.color = None,
("--color", "always") => options.color = Some(true),
("--color", "never") => options.color = Some(false),
_ => return Err(format!("invalid value `{value}` for {key}")),
}
Ok(())
}
pub fn emit(kind: &str, message: &str) {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
if options.quiet && !is_quiet_visible(kind) {
return;
}
if matches!(kind, "debug" | "trace") && !options.verbose {
return;
}
let mut stderr = io::stderr().lock();
let _ = render(
&mut stderr,
options,
kind,
message,
io::stderr().is_terminal(),
);
}
/// Emit a structured event while retaining the human-readable diagnostic form.
pub fn emit_event(event: &str, fields: serde_json::Value, human: &str) {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
if options.quiet && !is_quiet_visible(event) {
return;
}
if options.mode == Mode::Json {
let mut object = match fields {
serde_json::Value::Object(object) => object,
_ => serde_json::Map::new(),
};
object.insert("schema_version".into(), serde_json::json!(1));
object.insert("event".into(), serde_json::json!(event));
let mut stderr = io::stderr().lock();
let _ = writeln!(stderr, "{}", serde_json::Value::Object(object));
} else if event == "compiler_output" {
emit("compiler_output", human);
} else {
emit("build", human);
}
}
fn is_quiet_visible(kind: &str) -> bool {
matches!(kind, "error" | "warning" | "compiler_output")
}
pub fn mode() -> Mode {
OPTIONS.get().map_or(Mode::Human, |options| options.mode)
}
pub fn verbose() -> bool {
OPTIONS.get().is_some_and(|options| options.verbose)
}
pub fn colors_enabled(terminal: bool) -> bool {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
colors_enabled_for(options, terminal)
}
fn colors_enabled_for(options: &Options, terminal: bool) -> bool {
options.mode == Mode::Human
&& options
.color
.unwrap_or(terminal && std::env::var_os("NO_COLOR").is_none())
}
pub fn progress_label(action: &str, subject: &str) -> String {
let text = format!("{action:<12} {subject}");
if !colors_enabled(true) {
return text;
}
let code = match action.to_ascii_lowercase().as_str() {
"project" | "target" | "dep" | "pack" | "ready" | "profile" => 36,
_ => 32,
};
format!("\x1b[1;{code}m{action:>12}\x1b[0m {subject}")
}
/// A compact fluent writer for user-facing status output.
///
/// Commands can build a complete message without repeating the output macro:
/// `output::ui().status("Compiling", name).detail("release").finish();`
#[allow(dead_code)]
pub struct Ui {
lines: Vec<String>,
}
#[allow(dead_code)]
pub fn ui() -> Ui {
Ui { lines: Vec::new() }
}
pub fn say(message: impl AsRef<str>) {
write_stdout(&format!("{}\n", message.as_ref()));
}
pub fn status(action: impl AsRef<str>, subject: impl AsRef<str>) {
emit(
"status",
&format!("{} {}", action.as_ref(), subject.as_ref()),
);
}
pub fn success(message: impl AsRef<str>) {
emit("success", message.as_ref());
}
pub fn warning(message: impl AsRef<str>) {
emit("warning", message.as_ref());
}
pub fn info(message: impl AsRef<str>) {
emit("info", message.as_ref());
}
pub fn error(message: impl AsRef<str>) {
emit("error", message.as_ref());
}
pub fn created(subject: impl AsRef<str>) {
emit("success", &format!("Created {}", subject.as_ref()));
}
pub fn removed(subject: impl AsRef<str>) {
emit("success", &format!("Removed {}", subject.as_ref()));
}
pub fn compiler_output(stream: &str, text: &str) {
if mode() == Mode::Json {
emit_event(
"compiler_output",
serde_json::json!({"stream": stream, "text": text}),
"",
);
} else if stream == "stderr" {
emit("compiler_output", text);
} else {
write_stdout(text);
}
}
#[macro_export]
macro_rules! say { () => { $crate::utils::output::say("") }; ($($arg:tt)*) => { $crate::utils::output::say(format!($($arg)*)) }; }
#[macro_export]
macro_rules! status { ($action:expr, $($arg:tt)*) => { $crate::utils::output::status($action, format!($($arg)*)) }; }
#[macro_export]
macro_rules! success { ($($arg:tt)*) => { $crate::utils::output::success(format!($($arg)*)) }; }
impl Ui {
#[allow(dead_code)]
pub fn status(mut self, action: impl Into<String>, subject: impl Into<String>) -> Self {
self.lines
.push(format!("{} {}", action.into(), subject.into()));
self
}
#[allow(dead_code)]
pub fn detail(mut self, detail: impl Into<String>) -> Self {
if let Some(line) = self.lines.last_mut() {
line.push_str(&format!(" ({})", detail.into()));
}
self
}
#[allow(dead_code)]
pub fn item(mut self, item: impl Into<String>) -> Self {
self.lines.push(format!(" {}", item.into()));
self
}
#[allow(dead_code)]
pub fn finish(self) {
emit("status", &self.lines.join("\n"));
}
}
#[macro_export]
macro_rules! output_warning {
($($arg:tt)*) => { $crate::utils::output::emit("warning", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! output_error {
($($arg:tt)*) => { $crate::utils::output::emit("error", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! output_info {
($($arg:tt)*) => { $crate::utils::output::emit("info", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! output_success {
($($arg:tt)*) => { $crate::utils::output::emit("success", &format!($($arg)*)) };
}
// Namespaced aliases avoid collisions with the technical `log` macros.
#[allow(unused_imports)]
pub use crate::{
output_error as error, output_info as info, output_success as success,
output_warning as warning,
};
/// User-facing stdout macros. Data/help output belongs on stdout; diagnostics
/// and build status use `emit` on stderr.
#[macro_export]
macro_rules! output_println {
() => { $crate::utils::output::write_stdout("\n") };
($($arg:tt)*) => { $crate::utils::output::write_stdout(&format!("{}\n", format_args!($($arg)*))) };
}
#[macro_export]
macro_rules! output_print {
($($arg:tt)*) => { $crate::utils::output::write_stdout(&format!($($arg)*)) };
}
pub fn write_stdout(text: &str) {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
let colored = colors_enabled(io::stdout().is_terminal());
let has_ansi = text.contains('\x1b');
let rendered = if colored || !has_ansi {
text.to_owned()
} else {
strip_ansi(text)
};
let mut stdout = io::stdout().lock();
let result = if options.mode == Mode::Json {
writeln!(
stdout,
"{}",
serde_json::json!({"schema_version": 1, "event": "output", "text": rendered})
)
} else {
stdout.write_all(rendered.as_bytes())
};
if let Err(error) = result
&& error.kind() == io::ErrorKind::BrokenPipe
{
std::process::exit(0);
}
}
pub fn write_stdout_raw(text: &str) {
let mut stdout = io::stdout().lock();
if let Err(error) = stdout.write_all(text.as_bytes())
&& error.kind() == io::ErrorKind::BrokenPipe
{
std::process::exit(0);
}
}
fn strip_ansi(text: &str) -> String {
let mut chars = text.chars().peekable();
let mut clean = String::new();
while let Some(ch) = chars.next() {
if ch == '\x1b' && chars.peek() == Some(&'[') {
chars.next();
for ch in chars.by_ref() {
if ('@'..='~').contains(&ch) {
break;
}
}
} else {
clean.push(ch);
}
}
clean
}
#[macro_export]
macro_rules! output_sprintln {
($style:expr, $($arg:tt)*) => {{
use $crate::utils::cli_styles::Colorize as _;
$crate::output_println!("{}", format!($($arg)*).style($style));
}};
}
/// Render a help heading through the same output policy as the rest of the CLI.
/// Keeping this separate from ordinary styled output makes help formatting easy
/// to change globally without touching every command.
#[macro_export]
macro_rules! output_help_heading {
($style:expr, $($arg:tt)*) => {{
use $crate::utils::cli_styles::Colorize as _;
$crate::utils::output::write_stdout(&format!("{}\n", format!($($arg)*).style($style)));
}};
}
#[macro_export]
macro_rules! output_sprint {
($style:expr, $($arg:tt)*) => {{
use $crate::utils::cli_styles::Colorize as _;
$crate::output_print!("{}", format!($($arg)*).style($style));
}};
}
fn render(
writer: &mut impl Write,
options: &Options,
kind: &str,
message: &str,
terminal: bool,
) -> io::Result<()> {
if options.mode == Mode::Json {
return writeln!(
writer,
"{}",
serde_json::json!({
"schema_version": 1, "event": kind, "message": message
})
);
}
let color = colors_enabled_for(options, terminal);
if kind == "compiler_output" {
let message = strip_ansi(message);
for line in message.lines() {
let code = if line.contains("error:") || line.contains("fatal error") {
31
} else if line.contains("warning:") {
33
} else if line.contains("note:") {
36
} else {
37
};
if color {
writeln!(writer, "\x1b[{code}m{line}\x1b[0m")?;
} else {
writeln!(writer, "{line}")?;
}
}
return Ok(());
}
if matches!(kind, "status" | "success" | "build") {
let message = strip_ansi(message);
let (action, subject) = message.split_once(' ').unwrap_or((&message, ""));
if color {
let code = match action.to_ascii_lowercase().as_str() {
"project" | "target" | "dep" | "pack" | "ready" | "profile" => 36,
"compile" | "compiling" | "created" | "creating" | "finished" | "done"
| "removed" | "initialized" | "added" | "checked" | "skipped" | "format"
| "formatting" | "lint" | "linting" | "run" | "up" | "loaded" => 32,
_ if kind == "success" => 32,
_ => 36,
};
return writeln!(writer, "\x1b[1;{code}m{action:>12}\x1b[0m {subject}");
}
return writeln!(writer, "{action:>12} {subject}");
}
if color {
let code = match kind {
"error" => 31,
"warning" => 33,
_ => 36,
};
writeln!(writer, "\x1b[1;{code}m{kind}:\x1b[0m {message}")
} else {
writeln!(writer, "{kind}: {}", strip_ansi(message))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_colors_status_action_and_diagnostics() {
let options = Options {
color: Some(true),
..Options::default()
};
for (kind, message, expected) in [
(
"status",
"Compiling hello",
"\x1b[1;32m Compiling\x1b[0m hello\n",
),
(
"success",
"Created project",
"\x1b[1;32m Created\x1b[0m project\n",
),
("warning", "notice", "\x1b[1;33mwarning:\x1b[0m notice\n"),
("error", "failed", "\x1b[1;31merror:\x1b[0m failed\n"),
] {
let mut bytes = Vec::new();
render(&mut bytes, &options, kind, message, false).unwrap();
assert_eq!(String::from_utf8(bytes).unwrap(), expected);
}
}
#[test]
fn disabled_color_strips_existing_styles() {
let options = Options {
color: Some(false),
..Options::default()
};
let mut bytes = Vec::new();
render(
&mut bytes,
&options,
"warning",
"\x1b[32mnotice\x1b[0m",
true,
)
.unwrap();
assert_eq!(String::from_utf8(bytes).unwrap(), "warning: notice\n");
}
#[test]
fn human_colors_compiler_diagnostics_by_severity() {
let options = Options {
color: Some(true),
..Options::default()
};
let mut bytes = Vec::new();
render(
&mut bytes,
&options,
"compiler_output",
"file.c:1:1: warning: unused value\nfile.c:2:1: error: invalid token",
true,
)
.unwrap();
let output = String::from_utf8(bytes).unwrap();
assert!(output.contains("\x1b[33mfile.c:1:1: warning: unused value\x1b[0m"));
assert!(output.contains("\x1b[31mfile.c:2:1: error: invalid token\x1b[0m"));
}
#[test]
fn plain_never_contains_ansi() {
let mut bytes = Vec::new();
let options = Options {
mode: Mode::Plain,
color: Some(true),
..Options::default()
};
render(&mut bytes, &options, "error", "compiler missing", true).unwrap();
assert_eq!(
String::from_utf8(bytes).unwrap(),
"error: compiler missing\n"
);
}
#[test]
fn json_is_versioned_and_escapes_multiline_messages() {
let mut bytes = Vec::new();
let options = Options {
mode: Mode::Json,
..Options::default()
};
render(&mut bytes, &options, "warning", "first\nsecond", false).unwrap();
let event: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(event["schema_version"], 1);
assert_eq!(event["message"], "first\nsecond");
}
}