8 Commits

Author SHA1 Message Date
552f6c8e4f Merge pull request #41 from alex-xr555/refactor
refactor: Simplify use blocks
2026-09-20 18:28:22 +08:00
alex-xr555
da84916e10 Simplified use blocks 2026-09-19 18:11:28 +03:00
cb6ae3dcc7 Merge pull request #40 from alex-xr555/log 2026-09-19 09:28:18 +08:00
alex-xr555
10123ddedb Translation of comments into English, minor changes for completeness. 2026-09-15 18:34:54 +03:00
alex-xr555
0a140c75d8 Clarified the use block 2026-09-15 18:34:54 +03:00
alex-xr555
aa1d4ba807 The logging system has been brought to a fully functional level. 2026-09-15 18:34:54 +03:00
alex-xr555
991d1c20af Absolutely all logs have been replaced with the new system on log. 2026-09-15 18:34:54 +03:00
alex-xr555
df674defe1 Start of the logging system replacement 2026-09-15 18:34:54 +03:00
34 changed files with 1040 additions and 1769 deletions

View File

@@ -1,37 +1,5 @@
# Changelog
## [v0.9.5] - 2026-10-21 "Структурированный вывод CLI / Structured CLI Output"
### RU
**Добавлено:**
- Человекочитаемый, plain- и JSON-режимы вывода в стиле Cargo с общими хелперами вывода.
- Явная обработка `--color auto|always|never`, `NO_COLOR`, `--quiet` и `--verbose`.
- Структурированные события жизненного цикла сборки и JSON-диагностика компилятора.
- Сценарии CLI output sandbox для help, жизненного цикла, диагностики, ошибок и broken pipe.
**Исправлено:**
- Единообразная маршрутизация stdout/stderr и обработка цветов во всех основных командах.
- Глобальный парсер опций теперь останавливается на subcommand и сохраняет аргументы после `--`.
- Перенаправленный вывод подпроцессов теперь захватывается конкурентно в output lab.
### EN
**Added:**
- Cargo-like human, plain, and JSON output modes with shared output helpers.
- Explicit `--color auto|always|never`, `NO_COLOR`, `--quiet`, and `--verbose` handling.
- Structured build lifecycle events and JSON compiler diagnostics.
- CLI output sandbox scenarios for help, lifecycle, diagnostics, errors, and broken pipes.
**Fixed:**
- Consistent stdout/stderr routing and color handling across the main commands.
- Global option parsing now stops at the subcommand and preserves arguments after `--`.
- Redirected subprocess output is captured concurrently in the output lab.
## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies"
### RU

48
Cargo.lock generated
View File

@@ -219,7 +219,7 @@ dependencies = [
[[package]]
name = "dcr"
version = "0.9.5"
version = "0.9.1"
dependencies = [
"ctrlc",
"env_logger",
@@ -452,12 +452,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -603,23 +597,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -774,10 +751,6 @@ name = "owo-colors"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
dependencies = [
"supports-color 2.1.0",
"supports-color 3.0.2",
]
[[package]]
name = "percent-encoding"
@@ -1064,25 +1037,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "supports-color"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
dependencies = [
"is-terminal",
"is_ci",
]
[[package]]
name = "supports-color"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
dependencies = [
"is_ci",
]
[[package]]
name = "syn"
version = "2.0.117"

View File

@@ -1,6 +1,6 @@
[package]
name = "dcr"
version = "0.9.5"
version = "0.9.1"
edition = "2024"
build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -78,7 +78,7 @@ fatfs = { version = "0.3", optional = true }
# Logging
log = "0.4.34"
env_logger = "0.11.11"
owo-colors = { version = "4", features = ["supports-colors"] }
owo-colors = "4"
[profile.release]
opt-level = "z"

13
dcrup.cmd Executable file
View File

@@ -0,0 +1,13 @@
@echo off
setlocal
set "SCRIPT_DIR=%~dp0"
if exist "%SCRIPT_DIR%dcrup.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%dcrup.ps1" %*
exit /b %ERRORLEVEL%
)
if exist "%USERPROFILE%\.dcr\bin\dcrup.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.dcr\bin\dcrup.ps1" %*
exit /b %ERRORLEVEL%
)
echo [error] dcrup.ps1 not found near dcrup.cmd >&2
exit /b 1

View File

@@ -1,4 +1,4 @@
#Requires -Version 5.1
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
$DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' }
@@ -6,7 +6,6 @@ $DcrupBin = if ($env:DCRUP_BIN) { $env:DCRUP_BIN } else { Join-Path $DcrupHom
$DcrupTc = Join-Path $DcrupHome 'toolchains'
$DcrupMeta = Join-Path $DcrupHome 'meta'
$RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' }
$InstallUrl = if ($env:DCRUP_INSTALL_URL) { $env:DCRUP_INSTALL_URL } else { 'https://ps1.dcr-tool.ru' }
$DcrupMirror = if ($env:DCRUP_MIRROR) { $env:DCRUP_MIRROR } else { 'auto' }
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
@@ -134,18 +133,6 @@ function Link-Default([string]$id) {
Ok "default → $id ($dst)"
}
function Resolve-InstalledId([string]$spec) {
Parse-Spec $spec
if ($script:SpecChannel -eq 'night' -or $script:SpecFloating) {
$item = Get-ChildItem $DcrupTc -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "$($script:SpecChannel)-*" } |
Sort-Object Name | Select-Object -Last 1
if ($item) { return $item.Name }
return $null
}
return (Toolchain-Id)
}
function Set-Mirror([string]$value) {
if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) {
Die "invalid --mirror: $value (use auto|cf|gh|sf|ya)"
@@ -392,19 +379,7 @@ function Cmd-Show {
function Cmd-SelfInstall {
Ensure-Dirs
if ([string]::IsNullOrWhiteSpace($PSCommandPath) -or -not (Test-Path $PSCommandPath)) {
# With `irm URL | iex`, PowerShell evaluates text and there is no
# source path. Download the canonical installer for self-install.
$tmp = Join-Path $env:TEMP ("dcrup-" + [guid]::NewGuid().ToString('N') + '.ps1')
try {
Invoke-WebRequest -Uri $InstallUrl -OutFile $tmp -UseBasicParsing -UserAgent 'dcrup'
Copy-Item $tmp (Join-Path $DcrupBin 'dcrup.ps1') -Force
} finally {
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
}
} else {
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
}
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
Install-CmdShim
Ensure-UserPath
Ok "dcrup installed to $DcrupBin (use: dcrup …)"
@@ -442,7 +417,7 @@ switch ($cmd) {
if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') }
elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') }
elseif ($a -like 'dev-*') { Cmd-Install @('dev', '--force') }
else { Ok "pinned $a - install a newer version explicitly" }
else { Ok "pinned $a — install a newer version explicitly" }
}
'uninstall' {
if ($rest -contains '--all') {
@@ -451,18 +426,7 @@ switch ($cmd) {
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
Ok 'removed all'
} else {
if ($rest.Count -ne 1) { Die 'usage: dcrup uninstall <spec>|--all' }
$id = Resolve-InstalledId $rest[0]
if (-not $id) { Die "not installed: $($rest[0])" }
$dir = Join-Path $DcrupTc $id
if (-not (Test-Path $dir)) { Die "not installed: $($rest[0])" }
Remove-Item -Recurse -Force $dir
if ((Read-Active) -eq $id) {
Remove-Item (Join-Path $DcrupBin 'dcr.exe') -ErrorAction SilentlyContinue
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
Warn 'removed active toolchain; install or select another default'
}
Ok "removed $id"
Die 'use: dcrup uninstall --all (or install a replacement first)'
}
}
'self-install' { Cmd-SelfInstall }

996
dcrup.sh Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,6 @@ sidebar_label: Changelog
# Changelog
## 0.9.5 (2026-10-21)
- **Structured CLI output** — shared helpers now provide Cargo-like human, plain, and JSON output modes.
- **Color and diagnostics policy** — `--color`, `NO_COLOR`, `--quiet`, and `--verbose` are applied consistently across commands.
- **Build reporting** — lifecycle events and compiler diagnostics are available as structured JSON events.
- **Output lab** — sandbox scenarios cover help, lifecycle commands, compiler warnings/errors, stream capture, and broken pipes.
## 0.9.1 (2026-09-13)
- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`.

View File

@@ -11,7 +11,7 @@ Install and switch DCR versions (stable / dev / night, pin `0.8.2`, optional bui
### Linux / macOS / BSD / Windows (bash)
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- self-install
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable
```
@@ -19,19 +19,28 @@ dcrup install stable
Non-interactive install of DCR in one shot:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- install stable
curl -fsSL https://sh.dcr-tool.ru | sh -s -- install stable
export PATH="$HOME/.dcr/bin:$PATH"
```
### Windows (PowerShell)
```powershell
& ([scriptblock]::Create((irm https://ps1.dcr-tool.ru))) self-install
irm https://ps1.dcr-tool.ru | iex
# or download then run:
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
# powershell -File .\dcrup.ps1 self-install
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
dcrup install stable
```
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`) and is installed in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows).
Optional cmd bootstrap (if you prefer `curl` of the cmd shim):
```bat
curl -fsSL -o dcrup.cmd https://cmd.dcr-tool.ru
```
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows `dcrup.cmd`).
### Common dcrup commands
@@ -49,8 +58,6 @@ dcrup update
dcrup list
dcrup show
dcrup which
dcrup uninstall 0.8.2 # remove one installed version
dcrup uninstall --all # remove all installed versions
```
Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version.

View File

@@ -1,34 +0,0 @@
---
sidebar_label: CLI output
---
# CLI output (experimental)
The structured output implementation is being developed incrementally.
Currently these global options apply to diagnostics emitted through the logger:
```sh
dcr --output plain build
dcr --output json build
dcr --color never build
dcr --quiet build
dcr --verbose build
```
`--output` accepts `human` (default), `plain`, or `json`.
Diagnostics are written to stderr. JSON diagnostics are newline-delimited objects
with `schema_version: 1`, `event`, and `message` fields. Command statuses use the
shared output helpers, build reporting emits structured lifecycle events, and
compiler output is represented as JSON events with its original stream and text.
The protocol is still experimental: some raw command data and subprocess stdout
remain unstructured.
`--color` accepts `auto`, `always`, or `never`. Automatic coloring requires a
terminal and respects `NO_COLOR`. Plain and JSON diagnostics never add colors.
Quiet mode preserves warnings, errors, and compiler diagnostics; verbose mode
enables debug and trace logging. Arguments following `--` are passed through
unchanged.
Remaining work includes migrating the remaining raw command data and subprocess
stdout, improving progress rendering and contextual errors, and expanding
integration coverage for stdout/stderr and closed pipes.

View File

@@ -1,4 +1,4 @@
.TH DCR 1 "2026-10-21" "dcr 0.9.5" "DCR Manual"
.TH DCR 1 "2026-08-31" "dcr 0.9.1" "DCR Manual"
.SH NAME
dcr \- Cargo\-like build tool and package manager for C/C++ projects
.SH SYNOPSIS

View File

@@ -19,6 +19,7 @@ use crate::prelude::*;
use crate::core::{build_config::Config, deps::register};
use crate::utils::fs::find_project_root;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use toml::{Value, map::Map};
/// Parsed arguments for the `dcr add` command.
@@ -57,22 +58,27 @@ 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") {
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();
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!(" pkg-config:fmt System pkg-config 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");
return 0;
}
@@ -157,7 +163,11 @@ pub fn add(args: &[String]) -> i32 {
return 1;
}
crate::utils::output::success(format!("Added dependency `{}` to dcr.toml", add_args.name));
println!(
" {} dependency `{}` to dcr.toml",
colored("Added", BOLD_GREEN),
add_args.name
);
0
}

View File

@@ -29,16 +29,33 @@ use crate::utils::build::{
normalize_kind, resolve_artifact_target_dir, resolve_compiler,
};
use crate::utils::fs::{canonicalize_path, find_project_root};
use crate::utils::output;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
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.
///
@@ -52,13 +69,7 @@ impl BuildReporter for CliReporter {
target,
} => {
common::finish_progress_line();
output::emit_event(
"target_started",
serde_json::json!({
"index": index, "total": total, "target": target
}),
&format!("target {index}/{total} {target}"),
);
self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}"));
}
BuildEvent::ProjectStart {
name,
@@ -66,23 +77,15 @@ impl BuildReporter for CliReporter {
target,
} => {
common::finish_progress_line();
output::emit_event(
"project_started",
serde_json::json!({
"name": name, "profile": profile, "target": target
}),
&format!("project {name} ({profile}, {target})"),
self.line(
"project",
BOLD_CYAN,
&format!("{name} ({profile}, {target})"),
);
}
BuildEvent::DepBuilding { name, version } => {
common::finish_progress_line();
output::emit_event(
"dependency_building",
serde_json::json!({
"name": name, "version": version
}),
&format!("dep {name} v{version}"),
);
self.line("dep", BOLD_YELLOW, &format!("{name} v{version}"));
}
BuildEvent::DepReady {
name,
@@ -92,55 +95,34 @@ impl BuildReporter for CliReporter {
common::finish_progress_line();
// Only announce deps that were actually rebuilt this run.
if rebuilt {
output::emit_event(
"dependency_ready",
serde_json::json!({
"name": name, "version": version, "rebuilt": rebuilt
}),
&format!("ready {name} v{version}"),
);
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
}
}
BuildEvent::Compiling { name, version } => {
common::finish_progress_line();
let label = format!(
" {}",
output::progress_label("compile", &format!("{name} v{version}"))
);
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
// Terminals use an in-place progress line; non-TTY gets a plain log line.
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}"),
);
common::set_progress_label(Some(label.clone()));
if !std::io::stderr().is_terminal() {
eprintln!("{label}");
}
}
BuildEvent::Packing { path } => {
common::finish_progress_line();
output::emit_event(
"packing",
serde_json::json!({ "path": path }),
&format!("pack {path}"),
);
self.line("pack", BOLD_CYAN, path);
}
BuildEvent::Finished { secs } => {
common::finish_progress_line();
output::emit_event(
"finished",
serde_json::json!({ "seconds": secs }),
&format!("done in {secs}s"),
);
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
}
BuildEvent::CompilerOutput { stream, text } => {
// Pause the progress spinner so compiler text is not overwritten.
common::interrupt_progress_for_output();
output::compiler_output(stream, text);
if stream == "stderr" {
eprint!("{text}");
} else {
print!("{text}");
}
}
}
}
@@ -159,22 +141,26 @@ 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") {
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();
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"
);
return 0;
}
@@ -245,21 +231,13 @@ pub fn build(args: &[String]) -> i32 {
if flags.print_artifact_path
&& let Some(ref path) = info.artifact_path
{
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
println!("{path}");
}
} else if flags.print_artifact_path
&& let Some(path) =
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
{
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
println!("{path}");
}
0
}

View File

@@ -21,6 +21,7 @@ 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::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob;
use std::{fs, path::Path};
@@ -35,16 +36,20 @@ use std::{fs, 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") {
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();
printc("USAGE:", BOLD_GREEN);
printc(
" dcr clean [--debug | --release] [--target <triple>] [--all]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Removes build artifacts from the target directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Clean debug artifacts (default)");
println!(" --release Clean release artifacts");
println!(" --target <triple> Clean artifacts for a specific target");
println!(" --all Clean all workspace members");
return 0;
}
@@ -205,7 +210,10 @@ fn clean_project_at(
if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string());
}
crate::utils::output::status("Cleaning", format!("project `{project_name}`"));
println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
if let Some(profile) = profile {
let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}")
@@ -223,12 +231,16 @@ fn clean_project_at(
if !dir_exists {
warn!("Directory target/{target_dir} not found");
} else {
crate::utils::output::status("Profile", profile);
println!(" Profile: {}", colored(profile, BOLD_GREEN));
if let Some(t) = target {
crate::utils::output::status("Target", t);
println!(" Target: {}", colored(t, BOLD_GREEN));
}
let _ = fs::remove_dir_all(&target_dir);
crate::utils::output::removed(format!("directory {target_dir}"));
println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
}
clean_custom_paths(&config, profile)?;
return Ok(());
@@ -236,7 +248,10 @@ fn clean_project_at(
if items.contains(&"target".to_string()) {
let _ = fs::remove_dir_all("target");
crate::utils::output::removed("directory target");
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
} else {
warn!("Directory target not found");
}

View File

@@ -17,6 +17,7 @@
use crate::prelude::*;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use serde::Deserialize;
use std::fs;
use std::io::Read;
@@ -55,10 +56,11 @@ 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") {
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();
printc("USAGE:", BOLD_GREEN);
printc(" dcr --update", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Updates DCR to the latest version from GitHub Releases.");
return 0;
}
@@ -78,9 +80,8 @@ pub fn flag_update(args: &[String]) -> i32 {
// 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");
crate::utils::output::status(
"Update via package manager:",
format!("yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"),
println!(
"Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
);
return 0;
}
@@ -99,7 +100,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 {
crate::utils::output::status("Up to date", current_version);
println!("Latest version is already installed: {current_version}");
return 0;
}
@@ -137,9 +138,7 @@ pub fn flag_update(args: &[String]) -> i32 {
}
let _ = fs::remove_file(&temp_path);
crate::utils::output::success(format!(
"Updated DCR: {current_version} -> {latest_version}"
));
println!("Update completed: {current_version} -> {latest_version}");
0
}

View File

@@ -17,6 +17,7 @@
use crate::prelude::*;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob;
use std::process::Command;
@@ -28,11 +29,12 @@ 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") {
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();
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.");
return 0;
}
@@ -68,18 +70,22 @@ pub fn fmt(args: &[String]) -> i32 {
}
if files.is_empty() {
crate::utils::output::status("Format", "no files to format");
println!(" {} No files to format", colored("Format", BOLD_GREEN));
return 0;
}
// Format the collected files
crate::utils::output::status("Formatting", format!("{} files", files.len()));
println!(
" {} {} files",
colored("Formatting", BOLD_GREEN),
files.len()
);
let status = Command::new("clang-format").arg("-i").args(&files).status();
match status {
Ok(s) if s.success() => {
crate::utils::output::success("Format successful");
println!(" {} successful", colored("Format", BOLD_GREEN));
0
}
Ok(s) => {

View File

@@ -31,6 +31,7 @@ use crate::utils::build::{
use crate::utils::fs::{
absolute_join, atomic_write, canonicalize_path, ensure_dcr_dir, find_project_root,
};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -251,30 +252,33 @@ pub struct ProjectInfo {
pub fn r#gen(args: &[String]) -> i32 {
let subcommand = match args.first() {
Some(s) if s == "--help" => {
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();
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");
return 0;
}
Some(s) => s.as_str(),
None => {
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();
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");
return 1;
}
};
@@ -773,8 +777,8 @@ fn ensure_clangd(root: &Path, quiet: bool) {
}
Ok(_) => {
if !quiet {
crate::utils::output::warning!(
".clangd already exists and is not managed by dcr; \
eprintln!(
"Warning: .clangd already exists and is not managed by dcr; \
set CompilationDatabase to .dcr manually if needed"
);
}
@@ -813,8 +817,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) => crate::utils::output::warning!(
"skipping workspace member {}: {e}",
Err(e) => eprintln!(
"Warning: skipping workspace member {}: {e}",
member_path.display()
),
}
@@ -825,7 +829,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 => {
crate::utils::output::warning!("skipping workspace root package: {e}");
eprintln!("Warning: skipping workspace root package: {e}");
}
Err(e) => return Err(e),
}
@@ -859,12 +863,20 @@ fn gen_project_info(args: &[String]) -> i32 {
&& let Err(e) = write_dcr_metadata(&root, info)
&& !quiet
{
crate::utils::output::warning!("{e}");
eprintln!("Warning: {e}");
}
ensure_clangd(&root, quiet);
let entries = all.iter().map(project_info_to_json).collect::<Vec<_>>();
crate::utils::output::write_stdout_raw(&format!("[\n{}\n]\n", entries.join(",\n")));
print!("[");
for (i, info) in all.iter().enumerate() {
if i > 0 {
print!(",");
}
println!();
print!("{}", project_info_to_json(info));
}
println!();
println!("]");
0
}
@@ -989,11 +1001,11 @@ fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], q
&& let Err(e) = write_dcr_metadata(root, info)
&& !quiet
{
crate::utils::output::warning!("{e}");
eprintln!("Warning: {e}");
}
ensure_clangd(root, quiet);
if !quiet {
crate::utils::output::created(format!("{}", out_path.display()));
println!("Generated {}", out_path.display());
}
0
}
@@ -1174,7 +1186,7 @@ fn gen_vscode(args: &[String]) -> i32 {
error!("Failed to write tasks.json: {e}");
return 1;
}
crate::utils::output::created(format!("{}", vscode_dir.join("tasks.json").display()));
println!("Generated {}", vscode_dir.join("tasks.json").display());
// launch.json — one entry per binary target
let launch = gen_launch_json(&all, &root);
@@ -1182,7 +1194,7 @@ fn gen_vscode(args: &[String]) -> i32 {
error!("Failed to write launch.json: {e}");
return 1;
}
crate::utils::output::created(format!("{}", vscode_dir.join("launch.json").display()));
println!("Generated {}", vscode_dir.join("launch.json").display());
// settings.json (clangd compile-commands-dir)
let settings = gen_settings_json(&root);
@@ -1190,14 +1202,14 @@ fn gen_vscode(args: &[String]) -> i32 {
error!("Failed to write settings.json: {e}");
return 1;
}
crate::utils::output::created(format!("{}", vscode_dir.join("settings.json").display()));
println!("Generated {}", 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!("Failed to write extensions.json: {e}");
return 1;
}
crate::utils::output::created(format!("{}", vscode_dir.join("extensions.json").display()));
println!("Generated {}", vscode_dir.join("extensions.json").display());
0
}
@@ -1470,7 +1482,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write externalTools.xml: {e}");
return 1;
}
crate::utils::output::created(format!("{}", idea_dir.join("externalTools.xml").display()));
println!("Generated {}", idea_dir.join("externalTools.xml").display());
// customTargets.xml
let targets = gen_clion_custom_targets();
@@ -1478,7 +1490,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write customTargets.xml: {e}");
return 1;
}
crate::utils::output::created(format!("{}", idea_dir.join("customTargets.xml").display()));
println!("Generated {}", idea_dir.join("customTargets.xml").display());
// misc.xml — point CLion at compile_commands.json
let misc = gen_clion_misc_xml(&root);
@@ -1486,7 +1498,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write misc.xml: {e}");
return 1;
}
crate::utils::output::created(format!("{}", idea_dir.join("misc.xml").display()));
println!("Generated {}", idea_dir.join("misc.xml").display());
// .idea/.gitignore
let gitignore = "# CLion generated files\nworkspace.xml\n*.iml\n";
@@ -1494,7 +1506,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write .idea/.gitignore: {e}");
return 1;
}
crate::utils::output::created(format!("{}", idea_dir.join(".gitignore").display()));
println!("Generated {}", idea_dir.join(".gitignore").display());
// runConfigurations/<name>.xml — one per binary
for info in &all {
@@ -1508,7 +1520,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write runConfigurations/{fname}: {e}");
return 1;
}
crate::utils::output::created(format!("{}", path.display()));
println!("Generated {}", path.display());
}
0

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::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.
@@ -22,40 +24,43 @@
/// # Returns
/// Always `0`.
pub fn help() -> i32 {
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();
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.");
0
}

View File

@@ -21,6 +21,7 @@ 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::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::{fs, io::Write};
/// Initializes the current working directory as a new DCR project.
@@ -29,11 +30,12 @@ use std::{fs, io::Write};
/// `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") {
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();
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.");
return 0;
}
@@ -74,21 +76,28 @@ pub fn init(args: &[String]) -> i32 {
}
if let Err(e) = validate_package_name(&project_name) {
error!("Invalid project name `{}`: {e}", project_name,);
error!(
"Invalid project name `{}`: {e}",
colored(&project_name, BOLD_CYAN),
);
return 1;
}
let cwd = std::env::current_dir()
.map(|v| v.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string());
crate::utils::output::status("Initializing", format!("project in {cwd}"));
println!("Initializing the project in {cwd}");
// Create and initialize default manifest (dcr.toml).
if Config::create("./dcr.toml", Some(&project_name)).is_err() {
error!("Failed to create dcr.toml");
return 1;
}
crate::utils::output::created("dcr.toml");
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
// Create src directory and write template main.c file.
if fs::create_dir("src").is_err() {
@@ -106,7 +115,11 @@ pub fn init(args: &[String]) -> i32 {
error!("Failed to write src/main.c");
return 1;
}
crate::utils::output::created("src/main.c");
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
// Resolve target VCS provider or detect existing repositories.
let mut vcs_kind = VcsKind::Git;
@@ -130,7 +143,10 @@ pub fn init(args: &[String]) -> i32 {
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn!("Failed to initialize git repository: {e}");
} else {
crate::utils::output::success("Initialized git repository");
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn!(
@@ -143,7 +159,11 @@ pub fn init(args: &[String]) -> i32 {
warn!("Failed to update .gitignore: {e}");
}
crate::utils::output::success(format!("Initialized project `{project_name}`"));
crate::utils::output::info("Next step:\n dcr run");
println!(
"Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(" dcr run", BOLD_CYAN);
0
}

View File

@@ -21,6 +21,7 @@ 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::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -29,13 +30,15 @@ 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") {
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();
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");
return 0;
}
@@ -114,17 +117,15 @@ pub fn lint(args: &[String]) -> i32 {
files.sort();
if files.is_empty() {
crate::utils::output::status("Lint", "no files to lint");
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
return 0;
}
crate::utils::output::status(
"Linting",
format!(
"{} files {}",
files.len(),
if do_fix { "(with --fix)" } else { "" }
),
println!(
" {} {} files {}",
colored("Linting", BOLD_GREEN),
files.len(),
if do_fix { "(with --fix)" } else { "" }
);
let mut total_issues = 0u32;
@@ -160,12 +161,14 @@ pub fn lint(args: &[String]) -> i32 {
if issue_count > 0 {
total_issues += issue_count as u32;
failed += 1;
crate::utils::output::warning(format!("{file} ({issue_count} issues)"));
print!(" {} ", colored("×", BOLD_YELLOW));
println!("{} ({} issues)", file, issue_count);
for issue in &issues {
crate::utils::output::compiler_output("stderr", issue);
println!(" {}", issue);
}
} else {
crate::utils::output::status("Checked", file);
print!(" {} ", colored("✓", BOLD_GREEN));
println!("{}", file);
}
}
@@ -173,11 +176,19 @@ pub fn lint(args: &[String]) -> i32 {
let passed = total - failed as usize;
if failed == 0 {
crate::utils::output::success(format!("Finished checking {total} files; no issues"));
println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
total
);
} else {
crate::utils::output::warning(format!(
"Finished checking {total} files; {passed} passed, {total_issues} issues found"
));
println!(
" {} {}/{} files passed, {} issues found",
colored("Lint", BOLD_YELLOW),
passed,
total,
total_issues
);
}
0

View File

@@ -21,6 +21,7 @@ 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::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::{fs, io::Write};
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
@@ -29,12 +30,12 @@ use std::{fs, io::Write};
/// 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") {
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();
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 '-'.");
return 0;
}
@@ -69,27 +70,41 @@ pub fn new(args: &[String]) -> i32 {
}
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!("Invalid project name `{}`: {e}", project_name,);
error!(
"Invalid project name `{}`: {e}",
colored(project_name, BOLD_CYAN),
);
return 1;
}
if items.contains(project_name) {
error!("Directory `{}` already exists", project_name);
crate::utils::output::info(format!(
error!(
"Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN)
);
printc("Hint:", BOLD_CYAN);
println!(
" Use `{}` to initialize an existing project\n or specify a different project name",
"dcr init"
));
colored("dcr init", BOLD_CYAN)
);
return 1;
}
crate::utils::output::status("Creating", format!("project `{project_name}`"));
if fs::create_dir(project_name).is_err() {
error!("Failed to create directory");
return 1;
}
crate::utils::output::created(format!("directory `{project_name}`"));
println!(
" {} Directory created {}",
colored("✔", BOLD_GREEN),
project_name
);
// Initialize and write project configuration manifest (dcr.toml).
let toml_path = format!("./{project_name}/dcr.toml");
@@ -97,7 +112,11 @@ pub fn new(args: &[String]) -> i32 {
error!("Failed to create dcr.toml");
return 1;
}
crate::utils::output::created("dcr.toml");
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
// Generate src directory and populate initial main.c file.
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
@@ -116,7 +135,11 @@ pub fn new(args: &[String]) -> i32 {
error!("Failed to write src/main.c");
return 1;
}
crate::utils::output::created("src/main.c");
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
// Resolve target VCS mode based on CLI flags or parent directory state.
let mut vcs_kind = VcsKind::Git;
@@ -141,7 +164,10 @@ pub fn new(args: &[String]) -> i32 {
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn!("Failed to initialize git repository: {e}");
} else {
crate::utils::output::success("Initialized git repository");
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn!(
@@ -155,7 +181,11 @@ pub fn new(args: &[String]) -> i32 {
warn!("Failed to update .gitignore: {e}");
}
crate::utils::output::success(format!("Created project `{project_name}`"));
crate::utils::output::info(format!("Next steps:\n cd {project_name}\n dcr run"));
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);
0
}

View File

@@ -23,6 +23,7 @@ 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, with_dir};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::{path::Path, process::Command};
/// Retrieves the run command from the config, preferring target-specific,
@@ -65,19 +66,24 @@ 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") {
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();
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");
return 0;
}
@@ -119,7 +125,11 @@ 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);
crate::utils::output::status("run", display);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return run_shell_with_args(&cmd, &flags.bin_args);
}
return build_status;
@@ -255,11 +265,19 @@ fn run_project(
if build_status == 0 {
if let Some(cmd) = run_cmd {
let display = display_run_cmd(&cmd, &flags.bin_args);
crate::utils::output::status("run", display);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
}
let display = display_bin_run(&bin_path, &flags.bin_args);
crate::utils::output::status("run", display);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return Ok(run_binary(
project_name,
&flags.profile,

View File

@@ -15,31 +15,32 @@
// 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") {
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();
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.");
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) => {
let mut lines = vec![format!(
"Loaded {} registries (configured)",
manager.config.registry.len()
)];
println!("Loaded {} registries.", manager.config.registry.len());
for (name, reg) in manager.config.registry {
lines.push(format!(" {name}: {} (priority {})", reg.url, reg.priority));
println!("- {}: {} (priority {})", name, reg.url, reg.priority);
}
crate::say!("{}", lines.join("\n"));
0
}
Err(e) => {
crate::utils::output::error!("{e}");
println!("Error: {}", e);
1
}
}

View File

@@ -25,29 +25,32 @@ 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::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" {
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();
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");
return 0;
}
if arg == "--init" {
@@ -177,7 +180,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() {
crate::utils::output::compiler_output("stderr", &stderr);
eprint!("{}", stderr);
}
if !out.status.success() {
suite_success = false;
@@ -192,19 +195,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] ") {
crate::utils::output::success(format!("Passed {name}"));
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
pass += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[SKIP] ") {
crate::utils::output::status("Skipped", name);
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
skip += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[FAIL] ") {
crate::utils::output::error(format!("Failed {name}"));
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
fail += 1;
parsed_any = true;
continue;
@@ -212,17 +215,17 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if let Some((status, name)) = line.split_once('\t') {
match status {
"PASS" => {
crate::utils::output::success(format!("Passed {name}"));
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
pass += 1;
parsed_any = true;
}
"SKIP" => {
crate::utils::output::status("Skipped", name);
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
skip += 1;
parsed_any = true;
}
"FAIL" => {
crate::utils::output::error(format!("Failed {name}"));
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
fail += 1;
parsed_any = true;
}
@@ -234,11 +237,11 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if !parsed_any {
if suite_success {
for name in &declared {
crate::utils::output::success(format!("Passed {name}"));
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
}
pass = declared.len() as i32;
} else {
crate::utils::output::error("testsuite failed");
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
fail = 1;
}
}
@@ -249,10 +252,15 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
declared.len() as i32
};
crate::utils::output::status(
"test result:",
format!("{pass} passed; {fail} failed; {skip} skipped; {total} total"),
);
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));
if fail > 0 || !suite_success {
return Ok(1);
@@ -350,10 +358,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() {
crate::utils::output::write_stdout(&format!("{}", String::from_utf8_lossy(&output.stdout)));
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
crate::utils::output::compiler_output("stderr", &String::from_utf8_lossy(&output.stderr));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
}
@@ -362,10 +370,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())?;
crate::utils::output::created("tests/dcr_test.h");
println!("Created tests/dcr_test.h");
fs::write("./tests/test.c", FILE_TEST_C)
.map_err(|_| "Failed to write tests/test.c".to_string())?;
crate::utils::output::created("tests/test.c");
println!("Created tests/test.c");
Ok(())
}
@@ -385,3 +393,12 @@ 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

@@ -18,6 +18,7 @@
use crate::prelude::*;
use crate::core::build_config::Config;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use toml::Value;
@@ -28,10 +29,11 @@ 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") {
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();
printc("USAGE:", BOLD_GREEN);
printc(" dcr tree", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Displays the dependency tree of the current project.");
return 0;
}
@@ -53,7 +55,7 @@ pub fn tree(args: &[String]) -> i32 {
.and_then(|v| v.as_str())
.unwrap_or("0.0.0");
crate::utils::output::write_stdout_raw(&format!("{} v{}\n", name, version));
printc(&format!("{} v{}", name, version), BOLD_CYAN);
let mut seen = HashSet::new();
seen.insert(name.to_string());
@@ -135,10 +137,7 @@ fn print_deps(
};
}
crate::utils::output::write_stdout_raw(&format!(
"{}{}{}{}\n",
prefix, connector, name, version
));
println!("{}{}{}{}", 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

@@ -609,14 +609,10 @@ fn build_project_at_inner(
tc_as.as_deref(),
);
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
// The host triple is still used for native target-specific configuration and
// artifact paths, but native Clang must select the platform SDK implicitly.
// Pass --target only when the user/config explicitly requested a target.
let compiler_target = if explicit_target { build_target } else { None };
prepend_clang_target_flag(&mut build_cflags, compiler_target, &resolved_compiler);
prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler);
prepend_clang_target_flag(
&mut build_ldflags,
compiler_target,
build_target,
resolved_linker.as_deref().unwrap_or(&resolved_compiler),
);
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());

View File

@@ -29,15 +29,10 @@ use prelude::*;
/// 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 = 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();
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
std::process::exit(cli::help::help());
}

View File

@@ -58,11 +58,11 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
#[cfg(target_os = "macos")]
{
macos::bin_path(profile, name, target_dir)
return macos::bin_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
windows::bin_path(profile, name, target_dir)
return 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"
))]
{
bsd::bin_path(profile, name, target_dir)
return 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"
)))]
{
posix::bin_path(profile, name, target_dir)
return 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")]
{
macos::elf_path(profile, name, target_dir)
return macos::elf_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
windows::elf_path(profile, name, target_dir)
return 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"
))]
{
bsd::elf_path(profile, name, target_dir)
return 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"
)))]
{
posix::elf_path(profile, name, target_dir)
return 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")]
{
macos::efi_path(profile, name, target_dir)
return macos::efi_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
windows::efi_path(profile, name, target_dir)
return 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"
))]
{
bsd::efi_path(profile, name, target_dir)
return 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"
)))]
{
posix::efi_path(profile, name, target_dir)
return 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")]
{
macos::lib_path(profile, name, target_dir)
return macos::lib_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
windows::lib_path(profile, name, target_dir)
return 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"
))]
{
bsd::lib_path(profile, name, target_dir)
return 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"
)))]
{
posix::lib_path(profile, name, target_dir)
return 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")]
{
macos::shared_lib_path(profile, name, target_dir)
return macos::shared_lib_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
windows::shared_lib_path(profile, name, target_dir)
return 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"
))]
{
bsd::shared_lib_path(profile, name, target_dir)
return 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"
)))]
{
posix::shared_lib_path(profile, name, target_dir)
return posix::shared_lib_path(profile, name, target_dir);
}
}

View File

@@ -782,13 +782,6 @@ mod tests {
assert_eq!(flags, vec!["-O2".to_string()]);
}
#[test]
fn native_clang_target_is_not_added_without_explicit_target() {
let mut flags = vec!["-O3".to_string()];
prepend_clang_target_flag(&mut flags, None, "clang");
assert_eq!(flags, vec!["-O3".to_string()]);
}
/// Test for parse_version_parts function.
#[test]
fn parse_version_parts() {

View File

@@ -1,124 +0,0 @@
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,37 +15,30 @@
// 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 log::{LevelFilter, Log, Metadata, Record};
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) {}
}
use env_logger::{Builder, WriteStyle};
use log::{Level, LevelFilter};
use owo_colors::OwoColorize;
use std::io::Write;
// 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
});
Builder::new()
.filter_level(LevelFilter::Warn)
.write_style(WriteStyle::Auto)
.format(|buf, record| {
let colored_lv = match record.level() {
Level::Error => record.level().red().to_string(),
Level::Warn => record.level().yellow().to_string(),
Level::Info => record.level().default_color().to_string(),
_ => record.level().purple().italic().to_string(),
};
writeln!(
buf,
"{}: {}",
colored_lv.to_lowercase().bold(),
record.args(),
)
})
.init();
}

View File

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

View File

@@ -1,535 +0,0 @@
//! 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");
}
}

View File

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

View File

@@ -20,15 +20,10 @@ fn dcr_test_runs_without_sandbox_dependency() {
assert!(out.status.success(), "dcr test should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
assert!(
stderr.contains("test result: 1 passed; 0 failed; 0 skipped; 1 total"),
"test result summary line missing\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
stdout.contains("PASS") || stderr.contains("Passed"),
"test pass output missing\nstdout:\n{}\nstderr:\n{}",
stdout.contains("PASS: 1"),
"PASS summary line missing\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);