mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-23 14:29:17 +00:00
Compare commits
8 Commits
v0.9.5
...
552f6c8e4f
| Author | SHA1 | Date | |
|---|---|---|---|
| 552f6c8e4f | |||
|
|
da84916e10 | ||
| cb6ae3dcc7 | |||
|
|
10123ddedb | ||
|
|
0a140c75d8 | ||
|
|
aa1d4ba807 | ||
|
|
991d1c20af | ||
|
|
df674defe1 |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,37 +1,5 @@
|
|||||||
# Changelog
|
# 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"
|
## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies"
|
||||||
|
|
||||||
### RU
|
### RU
|
||||||
|
|||||||
48
Cargo.lock
generated
48
Cargo.lock
generated
@@ -219,7 +219,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.9.5"
|
version = "0.9.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@@ -452,12 +452,6 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hermit-abi"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.65"
|
version = "0.1.65"
|
||||||
@@ -603,23 +597,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -774,10 +751,6 @@ name = "owo-colors"
|
|||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
|
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
|
||||||
dependencies = [
|
|
||||||
"supports-color 2.1.0",
|
|
||||||
"supports-color 3.0.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
@@ -1064,25 +1037,6 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.9.5"
|
version = "0.9.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
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
|
# Logging
|
||||||
log = "0.4.34"
|
log = "0.4.34"
|
||||||
env_logger = "0.11.11"
|
env_logger = "0.11.11"
|
||||||
owo-colors = { version = "4", features = ["supports-colors"] }
|
owo-colors = "4"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|||||||
13
dcrup.cmd
Executable file
13
dcrup.cmd
Executable 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
|
||||||
44
dcrup.ps1
44
dcrup.ps1
@@ -1,4 +1,4 @@
|
|||||||
#Requires -Version 5.1
|
#Requires -Version 5.1
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
$DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' }
|
$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'
|
$DcrupTc = Join-Path $DcrupHome 'toolchains'
|
||||||
$DcrupMeta = Join-Path $DcrupHome 'meta'
|
$DcrupMeta = Join-Path $DcrupHome 'meta'
|
||||||
$RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' }
|
$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' }
|
$DcrupMirror = if ($env:DCRUP_MIRROR) { $env:DCRUP_MIRROR } else { 'auto' }
|
||||||
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
|
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
|
||||||
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
|
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
|
||||||
@@ -134,18 +133,6 @@ function Link-Default([string]$id) {
|
|||||||
Ok "default → $id ($dst)"
|
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) {
|
function Set-Mirror([string]$value) {
|
||||||
if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) {
|
if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) {
|
||||||
Die "invalid --mirror: $value (use 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 {
|
function Cmd-SelfInstall {
|
||||||
Ensure-Dirs
|
Ensure-Dirs
|
||||||
if ([string]::IsNullOrWhiteSpace($PSCommandPath) -or -not (Test-Path $PSCommandPath)) {
|
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
|
||||||
# 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
|
|
||||||
}
|
|
||||||
Install-CmdShim
|
Install-CmdShim
|
||||||
Ensure-UserPath
|
Ensure-UserPath
|
||||||
Ok "dcrup installed to $DcrupBin (use: dcrup …)"
|
Ok "dcrup installed to $DcrupBin (use: dcrup …)"
|
||||||
@@ -442,7 +417,7 @@ switch ($cmd) {
|
|||||||
if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') }
|
if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') }
|
||||||
elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') }
|
elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') }
|
||||||
elseif ($a -like 'dev-*') { Cmd-Install @('dev', '--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' {
|
'uninstall' {
|
||||||
if ($rest -contains '--all') {
|
if ($rest -contains '--all') {
|
||||||
@@ -451,18 +426,7 @@ switch ($cmd) {
|
|||||||
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
|
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
|
||||||
Ok 'removed all'
|
Ok 'removed all'
|
||||||
} else {
|
} else {
|
||||||
if ($rest.Count -ne 1) { Die 'usage: dcrup uninstall <spec>|--all' }
|
Die 'use: dcrup uninstall --all (or install a replacement first)'
|
||||||
$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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'self-install' { Cmd-SelfInstall }
|
'self-install' { Cmd-SelfInstall }
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ sidebar_label: Changelog
|
|||||||
|
|
||||||
# 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)
|
## 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`.
|
- **`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`.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Install and switch DCR versions (stable / dev / night, pin `0.8.2`, optional bui
|
|||||||
### Linux / macOS / BSD / Windows (bash)
|
### Linux / macOS / BSD / Windows (bash)
|
||||||
|
|
||||||
```sh
|
```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"
|
export PATH="$HOME/.dcr/bin:$PATH"
|
||||||
dcrup install stable
|
dcrup install stable
|
||||||
```
|
```
|
||||||
@@ -19,19 +19,28 @@ dcrup install stable
|
|||||||
Non-interactive install of DCR in one shot:
|
Non-interactive install of DCR in one shot:
|
||||||
|
|
||||||
```sh
|
```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"
|
export PATH="$HOME/.dcr/bin:$PATH"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows (PowerShell)
|
### Windows (PowerShell)
|
||||||
|
|
||||||
```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"
|
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
|
||||||
dcrup install stable
|
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
|
### Common dcrup commands
|
||||||
|
|
||||||
@@ -49,8 +58,6 @@ dcrup update
|
|||||||
dcrup list
|
dcrup list
|
||||||
dcrup show
|
dcrup show
|
||||||
dcrup which
|
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.
|
Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
.SH NAME
|
||||||
dcr \- Cargo\-like build tool and package manager for C/C++ projects
|
dcr \- Cargo\-like build tool and package manager for C/C++ projects
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::prelude::*;
|
|||||||
|
|
||||||
use crate::core::{build_config::Config, deps::register};
|
use crate::core::{build_config::Config, deps::register};
|
||||||
use crate::utils::fs::find_project_root;
|
use crate::utils::fs::find_project_root;
|
||||||
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
use toml::{Value, map::Map};
|
use toml::{Value, map::Map};
|
||||||
|
|
||||||
/// Parsed arguments for the `dcr add` command.
|
/// 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.
|
/// Process exit code: `0` on success, non-zero on usage or I/O errors.
|
||||||
pub fn add(args: &[String]) -> i32 {
|
pub fn add(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr add");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE")
|
printc(
|
||||||
.example("dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]");
|
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
|
||||||
help.description("Adds a dependency to the project.");
|
BOLD_CYAN,
|
||||||
help.section("SOURCES")
|
);
|
||||||
.item("github:user/repo", "GitHub repository")
|
println!();
|
||||||
.item("gitlab:user/repo", "GitLab repository")
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
.item("git:host.com/user/repo", "Generic git repository")
|
println!(" Adds a dependency to the project.");
|
||||||
.item("path:./path/to/lib", "Local path dependency")
|
println!();
|
||||||
.item("pkg-config:fmt", "System pkg-config dependency")
|
printc("SOURCES:", BOLD_GREEN);
|
||||||
.item("<version>", "Version from registry");
|
println!(" github:user/repo GitHub repository");
|
||||||
help.section("OPTIONS")
|
println!(" gitlab:user/repo GitLab repository");
|
||||||
.item("--branch <b>", "Use a specific branch")
|
println!(" git:host.com/user/repo Generic git repository");
|
||||||
.item("--tag <t>", "Use a specific tag")
|
println!(" path:./path/to/lib Local path dependency");
|
||||||
.item("--rev <r>", "Use a specific commit hash");
|
println!(" pkg-config:fmt System pkg-config dependency");
|
||||||
help.render();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +163,11 @@ pub fn add(args: &[String]) -> i32 {
|
|||||||
return 1;
|
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
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
138
src/cli/build.rs
138
src/cli/build.rs
@@ -29,16 +29,33 @@ use crate::utils::build::{
|
|||||||
normalize_kind, resolve_artifact_target_dir, resolve_compiler,
|
normalize_kind, resolve_artifact_target_dir, resolve_compiler,
|
||||||
};
|
};
|
||||||
use crate::utils::fs::{canonicalize_path, find_project_root};
|
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::io::IsTerminal;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, atomic::AtomicBool};
|
use std::sync::{Arc, atomic::AtomicBool};
|
||||||
|
|
||||||
pub use crate::core::build::get_build_string_with_profile;
|
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.
|
/// CLI build reporter that prints human-readable progress to stderr/stdout.
|
||||||
struct CliReporter;
|
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 {
|
impl BuildReporter for CliReporter {
|
||||||
/// Handles build events by printing progress, status lines, and compiler output.
|
/// Handles build events by printing progress, status lines, and compiler output.
|
||||||
///
|
///
|
||||||
@@ -52,13 +69,7 @@ impl BuildReporter for CliReporter {
|
|||||||
target,
|
target,
|
||||||
} => {
|
} => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
output::emit_event(
|
self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}"));
|
||||||
"target_started",
|
|
||||||
serde_json::json!({
|
|
||||||
"index": index, "total": total, "target": target
|
|
||||||
}),
|
|
||||||
&format!("target {index}/{total} {target}"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
BuildEvent::ProjectStart {
|
BuildEvent::ProjectStart {
|
||||||
name,
|
name,
|
||||||
@@ -66,23 +77,15 @@ impl BuildReporter for CliReporter {
|
|||||||
target,
|
target,
|
||||||
} => {
|
} => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
output::emit_event(
|
self.line(
|
||||||
"project_started",
|
"project",
|
||||||
serde_json::json!({
|
BOLD_CYAN,
|
||||||
"name": name, "profile": profile, "target": target
|
&format!("{name} ({profile}, {target})"),
|
||||||
}),
|
|
||||||
&format!("project {name} ({profile}, {target})"),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
BuildEvent::DepBuilding { name, version } => {
|
BuildEvent::DepBuilding { name, version } => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
output::emit_event(
|
self.line("dep", BOLD_YELLOW, &format!("{name} v{version}"));
|
||||||
"dependency_building",
|
|
||||||
serde_json::json!({
|
|
||||||
"name": name, "version": version
|
|
||||||
}),
|
|
||||||
&format!("dep {name} v{version}"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
BuildEvent::DepReady {
|
BuildEvent::DepReady {
|
||||||
name,
|
name,
|
||||||
@@ -92,55 +95,34 @@ impl BuildReporter for CliReporter {
|
|||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
// Only announce deps that were actually rebuilt this run.
|
// Only announce deps that were actually rebuilt this run.
|
||||||
if rebuilt {
|
if rebuilt {
|
||||||
output::emit_event(
|
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
|
||||||
"dependency_ready",
|
|
||||||
serde_json::json!({
|
|
||||||
"name": name, "version": version, "rebuilt": rebuilt
|
|
||||||
}),
|
|
||||||
&format!("ready {name} v{version}"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BuildEvent::Compiling { name, version } => {
|
BuildEvent::Compiling { name, version } => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
let label = format!(
|
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
|
||||||
" {}",
|
|
||||||
output::progress_label("compile", &format!("{name} v{version}"))
|
|
||||||
);
|
|
||||||
// Terminals use an in-place progress line; non-TTY gets a plain log line.
|
// 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()));
|
||||||
common::set_progress_label(Some(label.clone()));
|
if !std::io::stderr().is_terminal() {
|
||||||
}
|
eprintln!("{label}");
|
||||||
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 } => {
|
BuildEvent::Packing { path } => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
output::emit_event(
|
self.line("pack", BOLD_CYAN, path);
|
||||||
"packing",
|
|
||||||
serde_json::json!({ "path": path }),
|
|
||||||
&format!("pack {path}"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
BuildEvent::Finished { secs } => {
|
BuildEvent::Finished { secs } => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
output::emit_event(
|
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
|
||||||
"finished",
|
|
||||||
serde_json::json!({ "seconds": secs }),
|
|
||||||
&format!("done in {secs}s"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
BuildEvent::CompilerOutput { stream, text } => {
|
BuildEvent::CompilerOutput { stream, text } => {
|
||||||
// Pause the progress spinner so compiler text is not overwritten.
|
// Pause the progress spinner so compiler text is not overwritten.
|
||||||
common::interrupt_progress_for_output();
|
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.
|
/// Process exit code: `0` on success or help, non-zero on failure.
|
||||||
pub fn build(args: &[String]) -> i32 {
|
pub fn build(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr build");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.description("Compiles the project. Default profile is --debug.");
|
printc(
|
||||||
help.section("USAGE").example("dcr build [options]");
|
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [--print-artifact-path]",
|
||||||
help.section("OPTIONS")
|
BOLD_CYAN,
|
||||||
.item("--debug", "Build with debug profile (default)")
|
);
|
||||||
.item("--release", "Build with release profile")
|
println!();
|
||||||
.item("--target <triple>", "Cross-compile for the given target")
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
.item("--force", "Force a full rebuild")
|
println!(" Compiles the project. Default profile is --debug.");
|
||||||
.item("--clean", "Clean before building")
|
println!();
|
||||||
.item("--verbose", "Print detailed build output")
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
.item("--workspace <name>", "Build a specific workspace member")
|
println!(" --debug Build with debug profile (default)");
|
||||||
.item(
|
println!(" --release Build with release profile");
|
||||||
"--print-artifact-path",
|
println!(" --target <triple> Cross-compile for the given target");
|
||||||
"Print the absolute main artifact path",
|
println!(" --force Force a full rebuild");
|
||||||
);
|
println!(" --clean Clean before building");
|
||||||
help.render();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,21 +231,13 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
if flags.print_artifact_path
|
if flags.print_artifact_path
|
||||||
&& let Some(ref path) = info.artifact_path
|
&& let Some(ref path) = info.artifact_path
|
||||||
{
|
{
|
||||||
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
|
println!("{path}");
|
||||||
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
|
|
||||||
} else {
|
|
||||||
crate::say!("{path}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if flags.print_artifact_path
|
} else if flags.print_artifact_path
|
||||||
&& let Some(path) =
|
&& let Some(path) =
|
||||||
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
|
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
|
||||||
{
|
{
|
||||||
if crate::utils::output::mode() == crate::utils::output::Mode::Json {
|
println!("{path}");
|
||||||
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
|
|
||||||
} else {
|
|
||||||
crate::say!("{path}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use crate::core::build_config::Config;
|
|||||||
use crate::core::workspace::parse_workspace;
|
use crate::core::workspace::parse_workspace;
|
||||||
use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
|
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::fs::{check_dir, find_project_root, with_dir};
|
||||||
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
use std::{fs, path::Path};
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
@@ -35,16 +36,20 @@ use std::{fs, path::Path};
|
|||||||
/// Process exit code: `0` on success, `1` on failure.
|
/// Process exit code: `0` on success, `1` on failure.
|
||||||
pub fn clean(args: &[String]) -> i32 {
|
pub fn clean(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr clean");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE")
|
printc(
|
||||||
.example("dcr clean [--debug | --release] [--target <triple>] [--all]");
|
" dcr clean [--debug | --release] [--target <triple>] [--all]",
|
||||||
help.description("Removes build artifacts from the target directory.");
|
BOLD_CYAN,
|
||||||
help.section("OPTIONS")
|
);
|
||||||
.item("--debug", "Clean debug artifacts (default)")
|
println!();
|
||||||
.item("--release", "Clean release artifacts")
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
.item("--target <triple>", "Clean artifacts for a specific target")
|
println!(" Removes build artifacts from the target directory.");
|
||||||
.item("--all", "Clean all workspace members");
|
println!();
|
||||||
help.render();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +210,10 @@ fn clean_project_at(
|
|||||||
if !items.contains(&"dcr.toml".to_string()) {
|
if !items.contains(&"dcr.toml".to_string()) {
|
||||||
return Err("dcr.toml file not found".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 {
|
if let Some(profile) = profile {
|
||||||
let target_dir = if let Some(t) = target {
|
let target_dir = if let Some(t) = target {
|
||||||
format!("target/{t}/{profile}")
|
format!("target/{t}/{profile}")
|
||||||
@@ -223,12 +231,16 @@ fn clean_project_at(
|
|||||||
if !dir_exists {
|
if !dir_exists {
|
||||||
warn!("Directory target/{target_dir} not found");
|
warn!("Directory target/{target_dir} not found");
|
||||||
} else {
|
} else {
|
||||||
crate::utils::output::status("Profile", profile);
|
println!(" Profile: {}", colored(profile, BOLD_GREEN));
|
||||||
if let Some(t) = target {
|
if let Some(t) = target {
|
||||||
crate::utils::output::status("Target", t);
|
println!(" Target: {}", colored(t, BOLD_GREEN));
|
||||||
}
|
}
|
||||||
let _ = fs::remove_dir_all(&target_dir);
|
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)?;
|
clean_custom_paths(&config, profile)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -236,7 +248,10 @@ fn clean_project_at(
|
|||||||
|
|
||||||
if items.contains(&"target".to_string()) {
|
if items.contains(&"target".to_string()) {
|
||||||
let _ = fs::remove_dir_all("target");
|
let _ = fs::remove_dir_all("target");
|
||||||
crate::utils::output::removed("directory target");
|
println!(
|
||||||
|
"{} Removed directory target",
|
||||||
|
colored("\n ✔", BOLD_GREEN)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
warn!("Directory target not found");
|
warn!("Directory target not found");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
@@ -55,10 +56,11 @@ struct ReleaseAsset {
|
|||||||
/// `0` on success / already up to date / pacman-managed refuse; `1` on error.
|
/// `0` on success / already up to date / pacman-managed refuse; `1` on error.
|
||||||
pub fn flag_update(args: &[String]) -> i32 {
|
pub fn flag_update(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr --update");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr --update");
|
printc(" dcr --update", BOLD_CYAN);
|
||||||
help.description("Updates DCR to the latest version from GitHub Releases.");
|
println!();
|
||||||
help.render();
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
|
println!(" Updates DCR to the latest version from GitHub Releases.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,9 +80,8 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
// Refuse self-update when the binary is owned by pacman/AUR.
|
// Refuse self-update when the binary is owned by pacman/AUR.
|
||||||
if let Some(package_name) = pacman_owned_package(¤t_exe) {
|
if let Some(package_name) = pacman_owned_package(¤t_exe) {
|
||||||
warn!("This dcr binary is managed by pacman/AUR");
|
warn!("This dcr binary is managed by pacman/AUR");
|
||||||
crate::utils::output::status(
|
println!(
|
||||||
"Update via package manager:",
|
"Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
|
||||||
format!("yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"),
|
|
||||||
);
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
// Normalize the tag by stripping a leading `v` before comparison.
|
// Normalize the tag by stripping a leading `v` before comparison.
|
||||||
let latest_version = release.tag_name.trim_start_matches('v');
|
let latest_version = release.tag_name.trim_start_matches('v');
|
||||||
if latest_version == current_version {
|
if latest_version == current_version {
|
||||||
crate::utils::output::status("Up to date", current_version);
|
println!("Latest version is already installed: {current_version}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +138,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _ = fs::remove_file(&temp_path);
|
let _ = fs::remove_file(&temp_path);
|
||||||
crate::utils::output::success(format!(
|
println!("Update completed: {current_version} -> {latest_version}");
|
||||||
"Updated DCR: {current_version} -> {latest_version}"
|
|
||||||
));
|
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@@ -28,11 +29,12 @@ use std::process::Command;
|
|||||||
/// If the first argument is `--help`, prints usage information and returns 0.
|
/// If the first argument is `--help`, prints usage information and returns 0.
|
||||||
pub fn fmt(args: &[String]) -> i32 {
|
pub fn fmt(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr fmt");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr fmt");
|
printc(" dcr fmt", BOLD_CYAN);
|
||||||
help.description("Formats all C/C++ source files using clang-format.")
|
println!();
|
||||||
.note("Scans src/ and tests/ directories recursively.");
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
help.render();
|
println!(" Formats all C/C++ source files using clang-format.");
|
||||||
|
println!(" Scans src/ and tests/ directories recursively.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,18 +70,22 @@ pub fn fmt(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
crate::utils::output::status("Format", "no files to format");
|
println!(" {} No files to format", colored("Format", BOLD_GREEN));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the collected files
|
// 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();
|
let status = Command::new("clang-format").arg("-i").args(&files).status();
|
||||||
|
|
||||||
match status {
|
match status {
|
||||||
Ok(s) if s.success() => {
|
Ok(s) if s.success() => {
|
||||||
crate::utils::output::success("Format successful");
|
println!(" {} successful", colored("Format", BOLD_GREEN));
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use crate::utils::build::{
|
|||||||
use crate::utils::fs::{
|
use crate::utils::fs::{
|
||||||
absolute_join, atomic_write, canonicalize_path, ensure_dcr_dir, find_project_root,
|
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::path::{Path, PathBuf};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -251,30 +252,33 @@ pub struct ProjectInfo {
|
|||||||
pub fn r#gen(args: &[String]) -> i32 {
|
pub fn r#gen(args: &[String]) -> i32 {
|
||||||
let subcommand = match args.first() {
|
let subcommand = match args.first() {
|
||||||
Some(s) if s == "--help" => {
|
Some(s) if s == "--help" => {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr gen");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr gen <subcommand>");
|
printc(" dcr gen <subcommand>", BOLD_CYAN);
|
||||||
help.description("Generates IDE and tooling integration files.");
|
println!();
|
||||||
help.section("SUBCOMMANDS")
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
.item("project-info", "Print project metadata as JSON")
|
println!(" Generates IDE and tooling integration files.");
|
||||||
.item("compile-commands", "Generate .dcr/compile_commands.json")
|
println!();
|
||||||
.item("vscode", "Generate .vscode/ integration files")
|
printc("SUBCOMMANDS:", BOLD_GREEN);
|
||||||
.item("clion", "Generate .idea/ integration files");
|
println!(" project-info Print project metadata as JSON");
|
||||||
help.section("OPTIONS")
|
println!(" compile-commands Generate .dcr/compile_commands.json");
|
||||||
.item("--debug | --release", "Profile (default: debug)")
|
println!(" vscode Generate .vscode/ integration files");
|
||||||
.item("--quiet | -q", "Suppress success messages on stdout");
|
println!(" clion Generate .idea/ integration files");
|
||||||
help.render();
|
println!();
|
||||||
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
|
println!(" --debug | --release Profile (default: debug)");
|
||||||
|
println!(" --quiet | -q Suppress success messages on stdout");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Some(s) => s.as_str(),
|
Some(s) => s.as_str(),
|
||||||
None => {
|
None => {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr gen");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr gen <subcommand>");
|
printc(" dcr gen <subcommand>", BOLD_CYAN);
|
||||||
help.section("SUBCOMMANDS")
|
println!();
|
||||||
.item("project-info", "Print project metadata as JSON")
|
printc("SUBCOMMANDS:", BOLD_GREEN);
|
||||||
.item("compile-commands", "Generate .dcr/compile_commands.json")
|
println!(" project-info Print project metadata as JSON");
|
||||||
.item("vscode", "Generate .vscode/ integration files")
|
println!(" compile-commands Generate .dcr/compile_commands.json");
|
||||||
.item("clion", "Generate .idea/ integration files");
|
println!(" vscode Generate .vscode/ integration files");
|
||||||
help.render();
|
println!(" clion Generate .idea/ integration files");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -773,8 +777,8 @@ fn ensure_clangd(root: &Path, quiet: bool) {
|
|||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
crate::utils::output::warning!(
|
eprintln!(
|
||||||
".clangd already exists and is not managed by dcr; \
|
"Warning: .clangd already exists and is not managed by dcr; \
|
||||||
set CompilationDatabase to .dcr manually if needed"
|
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);
|
let member_path = canonicalize_path(&member.path);
|
||||||
match collect_project_info(&member_path, profile, ws_root) {
|
match collect_project_info(&member_path, profile, ws_root) {
|
||||||
Ok(info) => all.push(info),
|
Ok(info) => all.push(info),
|
||||||
Err(e) => crate::utils::output::warning!(
|
Err(e) => eprintln!(
|
||||||
"skipping workspace member {}: {e}",
|
"Warning: skipping workspace member {}: {e}",
|
||||||
member_path.display()
|
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) {
|
match collect_project_info(&root, profile, ws_root) {
|
||||||
Ok(info) => all.push(info),
|
Ok(info) => all.push(info),
|
||||||
Err(e) if is_workspace => {
|
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),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
@@ -859,12 +863,20 @@ fn gen_project_info(args: &[String]) -> i32 {
|
|||||||
&& let Err(e) = write_dcr_metadata(&root, info)
|
&& let Err(e) = write_dcr_metadata(&root, info)
|
||||||
&& !quiet
|
&& !quiet
|
||||||
{
|
{
|
||||||
crate::utils::output::warning!("{e}");
|
eprintln!("Warning: {e}");
|
||||||
}
|
}
|
||||||
ensure_clangd(&root, quiet);
|
ensure_clangd(&root, quiet);
|
||||||
|
|
||||||
let entries = all.iter().map(project_info_to_json).collect::<Vec<_>>();
|
print!("[");
|
||||||
crate::utils::output::write_stdout_raw(&format!("[\n{}\n]\n", entries.join(",\n")));
|
for (i, info) in all.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
print!(",");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
print!("{}", project_info_to_json(info));
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!("]");
|
||||||
0
|
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)
|
&& let Err(e) = write_dcr_metadata(root, info)
|
||||||
&& !quiet
|
&& !quiet
|
||||||
{
|
{
|
||||||
crate::utils::output::warning!("{e}");
|
eprintln!("Warning: {e}");
|
||||||
}
|
}
|
||||||
ensure_clangd(root, quiet);
|
ensure_clangd(root, quiet);
|
||||||
if !quiet {
|
if !quiet {
|
||||||
crate::utils::output::created(format!("{}", out_path.display()));
|
println!("Generated {}", out_path.display());
|
||||||
}
|
}
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -1174,7 +1186,7 @@ fn gen_vscode(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write tasks.json: {e}");
|
error!("Failed to write tasks.json: {e}");
|
||||||
return 1;
|
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
|
// launch.json — one entry per binary target
|
||||||
let launch = gen_launch_json(&all, &root);
|
let launch = gen_launch_json(&all, &root);
|
||||||
@@ -1182,7 +1194,7 @@ fn gen_vscode(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write launch.json: {e}");
|
error!("Failed to write launch.json: {e}");
|
||||||
return 1;
|
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)
|
// settings.json (clangd compile-commands-dir)
|
||||||
let settings = gen_settings_json(&root);
|
let settings = gen_settings_json(&root);
|
||||||
@@ -1190,14 +1202,14 @@ fn gen_vscode(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write settings.json: {e}");
|
error!("Failed to write settings.json: {e}");
|
||||||
return 1;
|
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
|
// extensions.json — disable cpptools, recommend clangd
|
||||||
if let Err(e) = std::fs::write(vscode_dir.join("extensions.json"), gen_extensions_json()) {
|
if let Err(e) = std::fs::write(vscode_dir.join("extensions.json"), gen_extensions_json()) {
|
||||||
error!("Failed to write extensions.json: {e}");
|
error!("Failed to write extensions.json: {e}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
crate::utils::output::created(format!("{}", vscode_dir.join("extensions.json").display()));
|
println!("Generated {}", vscode_dir.join("extensions.json").display());
|
||||||
|
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -1470,7 +1482,7 @@ fn gen_clion(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write externalTools.xml: {e}");
|
error!("Failed to write externalTools.xml: {e}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
crate::utils::output::created(format!("{}", idea_dir.join("externalTools.xml").display()));
|
println!("Generated {}", idea_dir.join("externalTools.xml").display());
|
||||||
|
|
||||||
// customTargets.xml
|
// customTargets.xml
|
||||||
let targets = gen_clion_custom_targets();
|
let targets = gen_clion_custom_targets();
|
||||||
@@ -1478,7 +1490,7 @@ fn gen_clion(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write customTargets.xml: {e}");
|
error!("Failed to write customTargets.xml: {e}");
|
||||||
return 1;
|
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
|
// misc.xml — point CLion at compile_commands.json
|
||||||
let misc = gen_clion_misc_xml(&root);
|
let misc = gen_clion_misc_xml(&root);
|
||||||
@@ -1486,7 +1498,7 @@ fn gen_clion(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write misc.xml: {e}");
|
error!("Failed to write misc.xml: {e}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
crate::utils::output::created(format!("{}", idea_dir.join("misc.xml").display()));
|
println!("Generated {}", idea_dir.join("misc.xml").display());
|
||||||
|
|
||||||
// .idea/.gitignore
|
// .idea/.gitignore
|
||||||
let gitignore = "# CLion generated files\nworkspace.xml\n*.iml\n";
|
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}");
|
error!("Failed to write .idea/.gitignore: {e}");
|
||||||
return 1;
|
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
|
// runConfigurations/<name>.xml — one per binary
|
||||||
for info in &all {
|
for info in &all {
|
||||||
@@ -1508,7 +1520,7 @@ fn gen_clion(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write runConfigurations/{fname}: {e}");
|
error!("Failed to write runConfigurations/{fname}: {e}");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
crate::utils::output::created(format!("{}", path.display()));
|
println!("Generated {}", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// 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.
|
/// Displays the help information for the DCR CLI tool.
|
||||||
///
|
///
|
||||||
/// Prints usage, commands, flags, options, examples, and a tip.
|
/// Prints usage, commands, flags, options, examples, and a tip.
|
||||||
@@ -22,40 +24,43 @@
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// Always `0`.
|
/// Always `0`.
|
||||||
pub fn help() -> i32 {
|
pub fn help() -> i32 {
|
||||||
use crate::utils::help::HelpBuilder;
|
println!("DCR (Dexoron Cargo Realization)");
|
||||||
let mut help = HelpBuilder::new("DCR (Dexoron Cargo Realization)");
|
println!("C project manager inspired by Cargo.");
|
||||||
help.description("C/C++ project manager inspired by Cargo.");
|
println!();
|
||||||
help.section("USAGE").example("dcr <command> [options]");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("COMMANDS")
|
printc(" dcr <command> [options]", BOLD_CYAN);
|
||||||
.item("new <name>", "Create a new project")
|
println!();
|
||||||
.item("init", "Initialize the current directory as a project")
|
printc("COMMANDS:", BOLD_GREEN);
|
||||||
.item("build", "Build the project (default: --debug)")
|
println!(" new <name> Create a new project");
|
||||||
.item("run", "Build and run the project")
|
println!(" init Initialize the current directory as a project");
|
||||||
.item("add <name> ...", "Add a dependency to dcr.toml")
|
println!(" build [--profile] Build the project (default: --debug)");
|
||||||
.item("tree", "Display the dependency tree")
|
println!(" run [--profile] Build and run the project (default: --debug)");
|
||||||
.item("test", "Run project tests (alias: tests)")
|
println!(" add <name> ... Add a dependency to dcr.toml");
|
||||||
.item("fmt", "Format C/C++ sources using clang-format")
|
println!(" tree Display the dependency tree");
|
||||||
.item("lint", "Run clang-tidy on C/C++ sources")
|
println!(" test Run project tests (alias: tests)");
|
||||||
.item("setup", "Show configured package registries")
|
println!(" fmt Format all C/C++ source files using clang-format");
|
||||||
.item("clean", "Remove build artifacts")
|
println!(" lint Run clang-tidy on C/C++ sources");
|
||||||
.item("gen <subcommand>", "Generate IDE integration files");
|
println!(" setup Show configured package registries");
|
||||||
help.section("FLAGS")
|
println!(" clean Remove the target directory");
|
||||||
.item("--help", "Show command help")
|
println!(" gen <subcommand> Generate IDE integration files");
|
||||||
.item("--update", "Update DCR")
|
printc("FLAGS:", BOLD_GREEN);
|
||||||
.item("--version", "Show DCR version");
|
println!(" --help Show command help");
|
||||||
help.section("OPTIONS")
|
println!(" --update Update dcr to the latest version");
|
||||||
.item("--output human|plain|json", "Select output format")
|
println!(" --version Show dcr version");
|
||||||
.item("--color auto|always|never", "Select color policy")
|
println!();
|
||||||
.item("--quiet", "Suppress ordinary diagnostics")
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
.item("--verbose", "Enable detailed diagnostics")
|
println!(" --debug Use debug profile");
|
||||||
.item("--debug / --release", "Select build profile")
|
println!(" --release Use release profile");
|
||||||
.item("--force", "Force rebuild (build/run)")
|
println!(" --force Force rebuild (build/run)");
|
||||||
.item("--clean", "Clean before build (build/run)")
|
println!(" --clean Clean before build (build/run)");
|
||||||
.item("--all", "Clean all workspace members");
|
println!(" --all Clean all workspace members (clean)");
|
||||||
help.example("dcr new hello")
|
println!();
|
||||||
.example("dcr build --release")
|
printc("EXAMPLES:", BOLD_GREEN);
|
||||||
.example("dcr run --debug");
|
printc(" dcr new hello", BOLD_CYAN);
|
||||||
help.tip("Run 'dcr <command> --help' for command-specific help.");
|
printc(" dcr build --release", BOLD_CYAN);
|
||||||
help.render();
|
printc(" dcr run --debug", BOLD_CYAN);
|
||||||
|
println!();
|
||||||
|
printc("TIP:", BOLD_GREEN);
|
||||||
|
println!(" Run 'dcr <command> --help' for command-specific help.");
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use crate::config::FILE_MAIN_C;
|
|||||||
use crate::core::build_config::{Config, validate_package_name};
|
use crate::core::build_config::{Config, validate_package_name};
|
||||||
use crate::core::vcs::VcsKind;
|
use crate::core::vcs::VcsKind;
|
||||||
use crate::utils::fs::check_dir;
|
use crate::utils::fs::check_dir;
|
||||||
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
use std::{fs, io::Write};
|
use std::{fs, io::Write};
|
||||||
|
|
||||||
/// Initializes the current working directory as a new DCR project.
|
/// 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.
|
/// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable.
|
||||||
pub fn init(args: &[String]) -> i32 {
|
pub fn init(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr init");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr init [--vcs <git|none>]");
|
printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
|
||||||
help.description("Initializes the current directory as a DCR project.")
|
println!();
|
||||||
.note("The directory must be empty.");
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
help.render();
|
println!(" Initializes the current directory as a DCR project.");
|
||||||
|
println!(" The directory must be empty.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,21 +76,28 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = validate_package_name(&project_name) {
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cwd = std::env::current_dir()
|
let cwd = std::env::current_dir()
|
||||||
.map(|v| v.to_string_lossy().to_string())
|
.map(|v| v.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|_| ".".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).
|
// Create and initialize default manifest (dcr.toml).
|
||||||
if Config::create("./dcr.toml", Some(&project_name)).is_err() {
|
if Config::create("./dcr.toml", Some(&project_name)).is_err() {
|
||||||
error!("Failed to create dcr.toml");
|
error!("Failed to create dcr.toml");
|
||||||
return 1;
|
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.
|
// Create src directory and write template main.c file.
|
||||||
if fs::create_dir("src").is_err() {
|
if fs::create_dir("src").is_err() {
|
||||||
@@ -106,7 +115,11 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
error!("Failed to write src/main.c");
|
error!("Failed to write src/main.c");
|
||||||
return 1;
|
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.
|
// Resolve target VCS provider or detect existing repositories.
|
||||||
let mut vcs_kind = VcsKind::Git;
|
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) {
|
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
|
||||||
warn!("Failed to initialize git repository: {e}");
|
warn!("Failed to initialize git repository: {e}");
|
||||||
} else {
|
} else {
|
||||||
crate::utils::output::success("Initialized git repository");
|
println!(
|
||||||
|
" {} Initialized git repository",
|
||||||
|
colored("✔", BOLD_GREEN)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -143,7 +159,11 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
warn!("Failed to update .gitignore: {e}");
|
warn!("Failed to update .gitignore: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::utils::output::success(format!("Initialized project `{project_name}`"));
|
println!(
|
||||||
crate::utils::output::info("Next step:\n dcr run");
|
"Project `{}` successfully created\n",
|
||||||
|
colored(project_name.as_str(), BOLD_GREEN)
|
||||||
|
);
|
||||||
|
printc("Next step:", BOLD_GREEN);
|
||||||
|
printc(" dcr run", BOLD_CYAN);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use crate::core::build::common;
|
|||||||
use crate::core::build_config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
|
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
|
||||||
use crate::utils::fs::find_project_root;
|
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::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@@ -29,13 +30,15 @@ use std::process::Command;
|
|||||||
/// Scans source roots (configurable via build.roots) and tests/ directory.
|
/// Scans source roots (configurable via build.roots) and tests/ directory.
|
||||||
pub fn lint(args: &[String]) -> i32 {
|
pub fn lint(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr lint");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr lint [--fix]");
|
printc(" dcr lint [--fix]", BOLD_CYAN);
|
||||||
help.description("Runs clang-tidy on all C/C++ source files.")
|
println!();
|
||||||
.note("Scans source roots (configurable via build.roots) and tests/ directory.");
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
help.section("OPTIONS")
|
println!(" Runs clang-tidy on all C/C++ source files.");
|
||||||
.item("--fix", "Apply clang-tidy suggestions automatically");
|
println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
|
||||||
help.render();
|
println!();
|
||||||
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
|
println!(" --fix Apply clang-tidy suggestions automatically");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,17 +117,15 @@ pub fn lint(args: &[String]) -> i32 {
|
|||||||
files.sort();
|
files.sort();
|
||||||
|
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
crate::utils::output::status("Lint", "no files to lint");
|
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::utils::output::status(
|
println!(
|
||||||
"Linting",
|
" {} {} files {}",
|
||||||
format!(
|
colored("Linting", BOLD_GREEN),
|
||||||
"{} files {}",
|
files.len(),
|
||||||
files.len(),
|
if do_fix { "(with --fix)" } else { "" }
|
||||||
if do_fix { "(with --fix)" } else { "" }
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut total_issues = 0u32;
|
let mut total_issues = 0u32;
|
||||||
@@ -160,12 +161,14 @@ pub fn lint(args: &[String]) -> i32 {
|
|||||||
if issue_count > 0 {
|
if issue_count > 0 {
|
||||||
total_issues += issue_count as u32;
|
total_issues += issue_count as u32;
|
||||||
failed += 1;
|
failed += 1;
|
||||||
crate::utils::output::warning(format!("{file} ({issue_count} issues)"));
|
print!(" {} ", colored("×", BOLD_YELLOW));
|
||||||
|
println!("{} ({} issues)", file, issue_count);
|
||||||
for issue in &issues {
|
for issue in &issues {
|
||||||
crate::utils::output::compiler_output("stderr", issue);
|
println!(" {}", issue);
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
let passed = total - failed as usize;
|
||||||
|
|
||||||
if failed == 0 {
|
if failed == 0 {
|
||||||
crate::utils::output::success(format!("Finished checking {total} files; no issues"));
|
println!(
|
||||||
|
" {} {} files, no issues",
|
||||||
|
colored("Lint", BOLD_GREEN),
|
||||||
|
total
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
crate::utils::output::warning(format!(
|
println!(
|
||||||
"Finished checking {total} files; {passed} passed, {total_issues} issues found"
|
" {} {}/{} files passed, {} issues found",
|
||||||
));
|
colored("Lint", BOLD_YELLOW),
|
||||||
|
passed,
|
||||||
|
total,
|
||||||
|
total_issues
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use crate::config::FILE_MAIN_C;
|
|||||||
use crate::core::build_config::{Config, validate_package_name};
|
use crate::core::build_config::{Config, validate_package_name};
|
||||||
use crate::core::vcs::VcsKind;
|
use crate::core::vcs::VcsKind;
|
||||||
use crate::utils::fs::check_dir;
|
use crate::utils::fs::check_dir;
|
||||||
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
use std::{fs, io::Write};
|
use std::{fs, io::Write};
|
||||||
|
|
||||||
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
|
/// 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.
|
/// template source file generation (`src/main.c`), and initial VCS setup.
|
||||||
pub fn new(args: &[String]) -> i32 {
|
pub fn new(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr new");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE")
|
printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
|
||||||
.example("dcr new <name> [--vcs <git|none>]");
|
println!();
|
||||||
help.description("Creates a new C/C++ project with the given name.")
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
.note("The name may only contain ASCII letters, digits, '_' and '-'.");
|
println!(" Creates a new C/C++ project with the given name.");
|
||||||
help.render();
|
println!(" The name may only contain ASCII letters, digits, '_' and '-'.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,27 +70,41 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let project_name = &clean_args[0];
|
let project_name = &clean_args[0];
|
||||||
|
println!(
|
||||||
|
"Creating a Project `{}`...",
|
||||||
|
colored(project_name, BOLD_CYAN)
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(e) = validate_package_name(project_name) {
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if items.contains(project_name) {
|
if items.contains(project_name) {
|
||||||
error!("Directory `{}` already exists", project_name);
|
error!(
|
||||||
crate::utils::output::info(format!(
|
"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",
|
" Use `{}` to initialize an existing project\n or specify a different project name",
|
||||||
"dcr init"
|
colored("dcr init", BOLD_CYAN)
|
||||||
));
|
);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::utils::output::status("Creating", format!("project `{project_name}`"));
|
|
||||||
|
|
||||||
if fs::create_dir(project_name).is_err() {
|
if fs::create_dir(project_name).is_err() {
|
||||||
error!("Failed to create directory");
|
error!("Failed to create directory");
|
||||||
return 1;
|
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).
|
// Initialize and write project configuration manifest (dcr.toml).
|
||||||
let toml_path = format!("./{project_name}/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");
|
error!("Failed to create dcr.toml");
|
||||||
return 1;
|
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.
|
// Generate src directory and populate initial main.c file.
|
||||||
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
|
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");
|
error!("Failed to write src/main.c");
|
||||||
return 1;
|
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.
|
// Resolve target VCS mode based on CLI flags or parent directory state.
|
||||||
let mut vcs_kind = VcsKind::Git;
|
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) {
|
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
|
||||||
warn!("Failed to initialize git repository: {e}");
|
warn!("Failed to initialize git repository: {e}");
|
||||||
} else {
|
} else {
|
||||||
crate::utils::output::success("Initialized git repository");
|
println!(
|
||||||
|
" {} Initialized git repository",
|
||||||
|
colored("✔", BOLD_GREEN)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -155,7 +181,11 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
warn!("Failed to update .gitignore: {e}");
|
warn!("Failed to update .gitignore: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::utils::output::success(format!("Created project `{project_name}`"));
|
println!(
|
||||||
crate::utils::output::info(format!("Next steps:\n cd {project_name}\n dcr run"));
|
"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
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use crate::core::build_config::Config;
|
|||||||
use crate::core::runner::run_binary;
|
use crate::core::runner::run_binary;
|
||||||
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
|
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
|
||||||
use crate::utils::fs::{find_project_root, with_dir};
|
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};
|
use std::{path::Path, process::Command};
|
||||||
|
|
||||||
/// Retrieves the run command from the config, preferring target-specific,
|
/// 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.
|
/// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure.
|
||||||
pub fn run(args: &[String]) -> i32 {
|
pub fn run(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr run");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]");
|
printc(
|
||||||
help.description("Builds and runs the project. Only available for kind = \"bin\".")
|
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
|
||||||
.note("Arguments after `--` are passed to the built binary (cargo-style).");
|
BOLD_CYAN,
|
||||||
help.section("OPTIONS")
|
);
|
||||||
.item("--debug", "Run with debug profile (default)")
|
println!();
|
||||||
.item("--release", "Run with release profile")
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
.item("--target <triple>", "Cross-compile for the given target")
|
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
||||||
.item("--force", "Force a full rebuild")
|
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
|
||||||
.item("--clean", "Clean before building")
|
println!();
|
||||||
.item("--verbose", "Print detailed build output")
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
.item("-- <args>...", "Arguments forwarded to the binary");
|
println!(" --debug Run with debug profile (default)");
|
||||||
help.render();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +125,11 @@ pub fn run(args: &[String]) -> i32 {
|
|||||||
let build_status = build(&args_for_build(&flags));
|
let build_status = build(&args_for_build(&flags));
|
||||||
if build_status == 0 {
|
if build_status == 0 {
|
||||||
let display = display_run_cmd(&cmd, &flags.bin_args);
|
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 run_shell_with_args(&cmd, &flags.bin_args);
|
||||||
}
|
}
|
||||||
return build_status;
|
return build_status;
|
||||||
@@ -255,11 +265,19 @@ fn run_project(
|
|||||||
if build_status == 0 {
|
if build_status == 0 {
|
||||||
if let Some(cmd) = run_cmd {
|
if let Some(cmd) = run_cmd {
|
||||||
let display = display_run_cmd(&cmd, &flags.bin_args);
|
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));
|
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
|
||||||
}
|
}
|
||||||
let display = display_bin_run(&bin_path, &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(
|
return Ok(run_binary(
|
||||||
project_name,
|
project_name,
|
||||||
&flags.profile,
|
&flags.profile,
|
||||||
|
|||||||
@@ -15,31 +15,32 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// 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.
|
/// Sets up DCR registries by loading the registry manager and printing the list of registries.
|
||||||
pub fn setup(args: &[String]) -> i32 {
|
pub fn setup(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr setup");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr setup");
|
printc(" dcr setup", BOLD_CYAN);
|
||||||
help.description("Show configured package registries and their priorities.");
|
println!();
|
||||||
help.render();
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
|
println!(" Sets up DCR registries. Downloads and indexes");
|
||||||
|
println!(" package registries for dependency resolution.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("Setting up DCR registries...");
|
||||||
// Load and process the registries, returning 0 on success or 1 on error
|
// Load and process the registries, returning 0 on success or 1 on error
|
||||||
match crate::core::registry::RegistryManager::load() {
|
match crate::core::registry::RegistryManager::load() {
|
||||||
Ok(manager) => {
|
Ok(manager) => {
|
||||||
let mut lines = vec![format!(
|
println!("Loaded {} registries.", manager.config.registry.len());
|
||||||
"Loaded {} registries (configured)",
|
|
||||||
manager.config.registry.len()
|
|
||||||
)];
|
|
||||||
for (name, reg) in manager.config.registry {
|
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
|
0
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
crate::utils::output::error!("{e}");
|
println!("Error: {}", e);
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,29 +25,32 @@ use crate::utils::build::{
|
|||||||
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
|
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
|
||||||
};
|
};
|
||||||
use crate::utils::fs::{find_project_root, with_dir};
|
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::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
|
||||||
|
|
||||||
/// Runs project tests and prints a unified testsuite report.
|
/// Runs project tests and prints a unified testsuite report.
|
||||||
pub fn test(args: &[String]) -> i32 {
|
pub fn test(args: &[String]) -> i32 {
|
||||||
let mut init_header = false;
|
let mut init_header = false;
|
||||||
let mut profile = "debug";
|
let mut profile = "debug";
|
||||||
for arg in args {
|
for arg in args {
|
||||||
if arg == "--help" {
|
if arg == "--help" {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr test");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE")
|
printc(" dcr test [--init] [--debug | --release]", BOLD_CYAN);
|
||||||
.example("dcr test [--init] [--debug | --release]");
|
println!();
|
||||||
help.section("ALIASES").example("dcr tests");
|
printc("ALIASES:", BOLD_GREEN);
|
||||||
help.description("Runs project tests and prints a unified testsuite report.");
|
println!(" dcr tests");
|
||||||
help.section("OPTIONS")
|
println!();
|
||||||
.item("--init", "Create tests/dcr_test.h in current project")
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
.item(
|
println!(" Runs project tests and prints a unified testsuite report.");
|
||||||
"--debug",
|
println!();
|
||||||
"Build and run tests with debug profile (default)",
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
)
|
println!(" --init Create tests/dcr_test.h in current project");
|
||||||
.item("--release", "Build and run tests with release profile");
|
println!(" --debug Build and run tests with debug profile (default)");
|
||||||
help.render();
|
println!(" --release Build and run tests with release profile");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if arg == "--init" {
|
if arg == "--init" {
|
||||||
@@ -177,7 +180,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
|||||||
stdout.push_str(&String::from_utf8_lossy(&out.stdout));
|
stdout.push_str(&String::from_utf8_lossy(&out.stdout));
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
if !stderr.trim().is_empty() {
|
if !stderr.trim().is_empty() {
|
||||||
crate::utils::output::compiler_output("stderr", &stderr);
|
eprint!("{}", stderr);
|
||||||
}
|
}
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
suite_success = false;
|
suite_success = false;
|
||||||
@@ -192,19 +195,19 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
|||||||
for line in stdout.lines() {
|
for line in stdout.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
if let Some(name) = line.strip_prefix("[PASS] ") {
|
if let Some(name) = line.strip_prefix("[PASS] ") {
|
||||||
crate::utils::output::success(format!("Passed {name}"));
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
pass += 1;
|
pass += 1;
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(name) = line.strip_prefix("[SKIP] ") {
|
if let Some(name) = line.strip_prefix("[SKIP] ") {
|
||||||
crate::utils::output::status("Skipped", name);
|
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||||
skip += 1;
|
skip += 1;
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(name) = line.strip_prefix("[FAIL] ") {
|
if let Some(name) = line.strip_prefix("[FAIL] ") {
|
||||||
crate::utils::output::error(format!("Failed {name}"));
|
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||||
fail += 1;
|
fail += 1;
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -212,17 +215,17 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
|||||||
if let Some((status, name)) = line.split_once('\t') {
|
if let Some((status, name)) = line.split_once('\t') {
|
||||||
match status {
|
match status {
|
||||||
"PASS" => {
|
"PASS" => {
|
||||||
crate::utils::output::success(format!("Passed {name}"));
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
pass += 1;
|
pass += 1;
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
}
|
}
|
||||||
"SKIP" => {
|
"SKIP" => {
|
||||||
crate::utils::output::status("Skipped", name);
|
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||||
skip += 1;
|
skip += 1;
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
}
|
}
|
||||||
"FAIL" => {
|
"FAIL" => {
|
||||||
crate::utils::output::error(format!("Failed {name}"));
|
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||||
fail += 1;
|
fail += 1;
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
}
|
}
|
||||||
@@ -234,11 +237,11 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
|||||||
if !parsed_any {
|
if !parsed_any {
|
||||||
if suite_success {
|
if suite_success {
|
||||||
for name in &declared {
|
for name in &declared {
|
||||||
crate::utils::output::success(format!("Passed {name}"));
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
}
|
}
|
||||||
pass = declared.len() as i32;
|
pass = declared.len() as i32;
|
||||||
} else {
|
} else {
|
||||||
crate::utils::output::error("testsuite failed");
|
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
|
||||||
fail = 1;
|
fail = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,10 +252,15 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
|
|||||||
declared.len() as i32
|
declared.len() as i32
|
||||||
};
|
};
|
||||||
|
|
||||||
crate::utils::output::status(
|
println!();
|
||||||
"test result:",
|
println!("{}", colored("=====================", BOLD_GREEN));
|
||||||
format!("{pass} passed; {fail} failed; {skip} skipped; {total} total"),
|
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 {
|
if fail > 0 || !suite_success {
|
||||||
return Ok(1);
|
return Ok(1);
|
||||||
@@ -350,10 +358,10 @@ fn link_test_binary(
|
|||||||
/// Prints the stdout and stderr from a command execution.
|
/// Prints the stdout and stderr from a command execution.
|
||||||
fn print_command_output(output: &std::process::Output) {
|
fn print_command_output(output: &std::process::Output) {
|
||||||
if !output.stdout.is_empty() {
|
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() {
|
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::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
|
||||||
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
||||||
.map_err(|_| "Failed to write tests/dcr_test.h".to_string())?;
|
.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)
|
fs::write("./tests/test.c", FILE_TEST_C)
|
||||||
.map_err(|_| "Failed to write tests/test.c".to_string())?;
|
.map_err(|_| "Failed to write tests/test.c".to_string())?;
|
||||||
crate::utils::output::created("tests/test.c");
|
println!("Created tests/test.c");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,3 +393,12 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
names
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
use crate::core::build_config::Config;
|
use crate::core::build_config::Config;
|
||||||
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
@@ -28,10 +29,11 @@ use toml::Value;
|
|||||||
/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree.
|
/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree.
|
||||||
pub fn tree(args: &[String]) -> i32 {
|
pub fn tree(args: &[String]) -> i32 {
|
||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
let mut help = crate::utils::help::HelpBuilder::new("dcr tree");
|
printc("USAGE:", BOLD_GREEN);
|
||||||
help.section("USAGE").example("dcr tree");
|
printc(" dcr tree", BOLD_CYAN);
|
||||||
help.description("Displays the dependency tree of the current project.");
|
println!();
|
||||||
help.render();
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
|
println!(" Displays the dependency tree of the current project.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ pub fn tree(args: &[String]) -> i32 {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("0.0.0");
|
.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();
|
let mut seen = HashSet::new();
|
||||||
seen.insert(name.to_string());
|
seen.insert(name.to_string());
|
||||||
@@ -135,10 +137,7 @@ fn print_deps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::utils::output::write_stdout_raw(&format!(
|
println!("{}{}{}{}", prefix, connector, name, version);
|
||||||
"{}{}{}{}\n",
|
|
||||||
prefix, connector, name, version
|
|
||||||
));
|
|
||||||
|
|
||||||
// Recurse only if sub-dependencies exist and this dep hasn't been seen
|
// Recurse only if sub-dependencies exist and this dep hasn't been seen
|
||||||
if let Some(s_deps) = sub_deps
|
if let Some(s_deps) = sub_deps
|
||||||
|
|||||||
@@ -609,14 +609,10 @@ fn build_project_at_inner(
|
|||||||
tc_as.as_deref(),
|
tc_as.as_deref(),
|
||||||
);
|
);
|
||||||
let resolved_linker = resolve_tool("DCR_LD", tc_ld.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
|
prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler);
|
||||||
// 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(
|
prepend_clang_target_flag(
|
||||||
&mut build_ldflags,
|
&mut build_ldflags,
|
||||||
compiler_target,
|
build_target,
|
||||||
resolved_linker.as_deref().unwrap_or(&resolved_compiler),
|
resolved_linker.as_deref().unwrap_or(&resolved_compiler),
|
||||||
);
|
);
|
||||||
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
|
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
|
||||||
|
|||||||
@@ -29,15 +29,10 @@ use prelude::*;
|
|||||||
/// appropriate subcommand handler. With fewer than two arguments, shows help and
|
/// appropriate subcommand handler. With fewer than two arguments, shows help and
|
||||||
/// exits with code 0; otherwise exits with the handler's exit code.
|
/// exits with code 0; otherwise exits with the handler's exit code.
|
||||||
fn main() {
|
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();
|
utils::log::init();
|
||||||
|
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
std::process::exit(cli::help::help());
|
std::process::exit(cli::help::help());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
macos::bin_path(profile, name, target_dir)
|
return macos::bin_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
windows::bin_path(profile, name, target_dir)
|
return windows::bin_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
@@ -71,7 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
bsd::bin_path(profile, name, target_dir)
|
return bsd::bin_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
@@ -83,7 +83,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
macos::elf_path(profile, name, target_dir)
|
return macos::elf_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
windows::elf_path(profile, name, target_dir)
|
return windows::elf_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
@@ -110,7 +110,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
bsd::elf_path(profile, name, target_dir)
|
return bsd::elf_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
@@ -122,7 +122,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
macos::efi_path(profile, name, target_dir)
|
return macos::efi_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
windows::efi_path(profile, name, target_dir)
|
return windows::efi_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
@@ -149,7 +149,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
bsd::efi_path(profile, name, target_dir)
|
return bsd::efi_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
@@ -161,7 +161,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
macos::lib_path(profile, name, target_dir)
|
return macos::lib_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
windows::lib_path(profile, name, target_dir)
|
return windows::lib_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
@@ -188,7 +188,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
bsd::lib_path(profile, name, target_dir)
|
return bsd::lib_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
@@ -200,7 +200,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
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")]
|
#[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")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
windows::shared_lib_path(profile, name, target_dir)
|
return windows::shared_lib_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
@@ -227,7 +227,7 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
bsd::shared_lib_path(profile, name, target_dir)
|
return bsd::shared_lib_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
@@ -239,6 +239,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)))]
|
)))]
|
||||||
{
|
{
|
||||||
posix::shared_lib_path(profile, name, target_dir)
|
return posix::shared_lib_path(profile, name, target_dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -782,13 +782,6 @@ mod tests {
|
|||||||
assert_eq!(flags, vec!["-O2".to_string()]);
|
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 for parse_version_parts function.
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_version_parts() {
|
fn parse_version_parts() {
|
||||||
|
|||||||
@@ -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, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,37 +15,30 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use log::{LevelFilter, Log, Metadata, Record};
|
use env_logger::{Builder, WriteStyle};
|
||||||
|
use log::{Level, LevelFilter};
|
||||||
struct CliLogger;
|
use owo_colors::OwoColorize;
|
||||||
|
use std::io::Write;
|
||||||
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) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A function for initializing the logging lib with basic settings.
|
// A function for initializing the logging lib with basic settings.
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
static LOGGER: CliLogger = CliLogger;
|
Builder::new()
|
||||||
log::set_logger(&LOGGER).expect("logger already initialized");
|
.filter_level(LevelFilter::Warn)
|
||||||
log::set_max_level(if crate::utils::output::verbose() {
|
.write_style(WriteStyle::Auto)
|
||||||
LevelFilter::Debug
|
.format(|buf, record| {
|
||||||
} else {
|
let colored_lv = match record.level() {
|
||||||
LevelFilter::Info
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,4 @@ pub mod git;
|
|||||||
pub mod log;
|
pub mod log;
|
||||||
|
|
||||||
/// Text processing and manipulation helpers.
|
/// Text processing and manipulation helpers.
|
||||||
pub mod cli_styles;
|
pub mod text;
|
||||||
pub mod help;
|
|
||||||
pub mod output;
|
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,33 +15,39 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// 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)]
|
#[allow(dead_code)]
|
||||||
pub const BOLD_RED: Style = Style::new().bright_red().bold();
|
pub const BOLD: &str = "\x1b[1m";
|
||||||
#[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)]
|
#[allow(dead_code)]
|
||||||
pub const HELP_SECTION_TITLE_ST: Style = BOLD_GREEN;
|
pub const BRIGHT_RED: &str = "\x1b[91m";
|
||||||
#[allow(dead_code)]
|
#[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)]
|
#[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)]
|
#[allow(dead_code)]
|
||||||
pub const SUCCESS_ST: Style = BOLD_GREEN;
|
pub fn printc(msg: &str, style: &str) {
|
||||||
#[allow(dead_code)]
|
println!("{style}{msg}{RESET}");
|
||||||
pub const ALERT_ST: Style = BOLD_YELLOW;
|
}
|
||||||
#[allow(dead_code)]
|
|
||||||
pub const SKIP_ST: Style = BOLD_BLUE;
|
|
||||||
@@ -20,15 +20,10 @@ fn dcr_test_runs_without_sandbox_dependency() {
|
|||||||
assert!(out.status.success(), "dcr test should succeed");
|
assert!(out.status.success(), "dcr test should succeed");
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
|
||||||
assert!(
|
assert!(
|
||||||
stderr.contains("test result: 1 passed; 0 failed; 0 skipped; 1 total"),
|
stdout.contains("PASS: 1"),
|
||||||
"test result summary line missing\nstdout:\n{}\nstderr:\n{}",
|
"PASS 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,
|
stdout,
|
||||||
stderr
|
stderr
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user