10 Commits

Author SHA1 Message Date
2258ae53fe fix: avoid implicit clang target on native builds 2026-10-21 21:50:42 +08:00
0f2182fe96 Merge origin/master into master 2026-10-21 20:50:46 +08:00
0f456b4201 feat: complete structured CLI output
- unify human, plain, and JSON CLI output modes

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

- preserve argument forwarding and improve CLI help and summaries

- document and release version 0.9.5
2026-10-21 18:55:23 +08:00
7d2340fb52 Merge pull request #39 from dexoron/dev
v0.9.1
2026-09-13 15:02:16 +08:00
6c79340736 feat: add package name validation in default manifest generation; clarify pkg-config dependency requirements 2026-09-13 14:50:40 +08:00
be14cfb8e2 Update release date for version 0.9.1 2026-09-13 13:58:26 +08:00
3305915c07 Update release date for version 0.9.1 2026-09-13 13:58:04 +08:00
5cb51878a5 feat: add pkg-config system dependencies and fix dcr new in non-ascii paths 2026-09-08 20:40:49 +08:00
d587eba5a4 Update release.yml to use VERSION variable 2026-09-05 21:39:33 +08:00
bfd2b9d251 Merge pull request #36 from dexoron/dev
Release v0.9.0
2026-08-31 14:16:57 +08:00
63 changed files with 2632 additions and 1319 deletions

View File

@@ -1191,6 +1191,7 @@ jobs:
- name: Update Formula/dcr.rb
run: |
VERSION="${{ needs.setup.outputs.version }}"
cat > Formula/dcr.rb <<FORMULA
class Dcr < Formula
desc "Cargo-like utility to manage C/C++ projects"
@@ -1199,37 +1200,36 @@ jobs:
on_macos do
on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-apple-darwin-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-apple-darwin-${VERSION}"
sha256 "${{ steps.sha.outputs.macos_x86 }}"
end
on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-apple-darwin-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-apple-darwin-${VERSION}"
sha256 "${{ steps.sha.outputs.macos_arm }}"
end
end
on_linux do
on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-unknown-linux-gnu-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-unknown-linux-gnu-${VERSION}"
sha256 "${{ steps.sha.outputs.linux_x86 }}"
end
on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-unknown-linux-gnu-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-unknown-linux-gnu-${VERSION}"
sha256 "${{ steps.sha.outputs.linux_arm }}"
end
end
def install
bin.install Dir["dcr*"].first => "dcr"
end
resource "man-pages" do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-man-pages-#{version}.tar.gz"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-man-pages-${VERSION}.tar.gz"
sha256 "${{ steps.sha.outputs.man_pages }}"
end
def install
bin.install Dir["dcr*"].first => "dcr"
resource("man-pages").stage do
man1.install Dir["usr/share/man/man1/*.1"]
end
@@ -1326,4 +1326,4 @@ jobs:
aws s3 sync dist/ "s3://${{ secrets.R2_BUCKET_NAME }}/${TAG}/" \
--endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \
--region auto
--region auto

View File

@@ -1,5 +1,67 @@
# Changelog
## [v0.9.5] - 2026-10-21 "Структурированный вывод CLI / Structured CLI Output"
### RU
**Добавлено:**
- Человекочитаемый, plain- и JSON-режимы вывода в стиле Cargo с общими хелперами вывода.
- Явная обработка `--color auto|always|never`, `NO_COLOR`, `--quiet` и `--verbose`.
- Структурированные события жизненного цикла сборки и JSON-диагностика компилятора.
- Сценарии CLI output sandbox для help, жизненного цикла, диагностики, ошибок и broken pipe.
**Исправлено:**
- Единообразная маршрутизация stdout/stderr и обработка цветов во всех основных командах.
- Глобальный парсер опций теперь останавливается на subcommand и сохраняет аргументы после `--`.
- Перенаправленный вывод подпроцессов теперь захватывается конкурентно в output lab.
### EN
**Added:**
- Cargo-like human, plain, and JSON output modes with shared output helpers.
- Explicit `--color auto|always|never`, `NO_COLOR`, `--quiet`, and `--verbose` handling.
- Structured build lifecycle events and JSON compiler diagnostics.
- CLI output sandbox scenarios for help, lifecycle, diagnostics, errors, and broken pipes.
**Fixed:**
- Consistent stdout/stderr routing and color handling across the main commands.
- Global option parsing now stops at the subcommand and preserves arguments after `--`.
- Redirected subprocess output is captured concurrently in the output lab.
## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies"
### RU
**Добавлено:**
- **Зависимости через `pkg-config`** — `fmt = { pkg-config = "fmt" }` или массив имён пакетов (`pkg-config = ["openssl", "libcrypto"]`) подключает системные библиотеки через `.pc`-метаданные.
- **Поддержка префиксов в `dcr add`** — команды `dcr add fmt pkg-config:fmt` и `dcr add openssl pkg:openssl,libcrypto` добавляют зависимости от системных пакетов в `dcr.toml`.
- **Полный набор флагов** — DCR собирает и пробрасывает compiler/linker flags (`-I`, `-L`, `-l`, `-pthread` и другие), включая транзитивные зависимости и интеграцию с `dcr gen` / `dcr test`.
- **Фиксация в `dcr.lock`** — записывается источник `pkg-config+...` и обнаруженная версия пакета.
- **Отображение в `dcr tree`** — `pkg-config` зависимости наглядно помечаются в дереве зависимостей.
**Исправлено:**
- **Ошибка создания проекта в папках со спецсимволами** — `dcr new` больше не завершается ошибкой `Failed to create dcr.toml`, если родительская директория содержит пробелы или не-ASCII символы.
### EN
**Added:**
- **`pkg-config` dependencies** — `fmt = { pkg-config = "fmt" }` or an array of package names (`pkg-config = ["openssl", "libcrypto"]`) connects system libraries through their `.pc` metadata.
- **Prefix support in `dcr add`** — `dcr add fmt pkg-config:fmt` and `dcr add openssl pkg:openssl,libcrypto` add system package dependencies to `dcr.toml`.
- **Complete flag propagation** — compiler/linker flags (`-I`, `-L`, `-l`, `-pthread`, and others) are preserved and passed to builds, `dcr test`, and IDE integrations via `dcr gen`.
- **Lockfile entries** — `dcr.lock` records the `pkg-config+...` source and detected package version.
- **`dcr tree` representation** — `pkg-config` dependencies are clearly labeled in the dependency tree.
**Fixed:**
- **Project creation failure in special character directories** — `dcr new` no longer fails with `Failed to create dcr.toml` when invoked inside parent folders with spaces or non-ASCII characters.
## [v0.9.0] - 2026-08-31 "Полные Path/Git-зависимости и готовые библиотеки / Complete Path/Git Dependencies & Prebuilt Libraries"
### RU

333
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -11,6 +20,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.102"
@@ -106,6 +165,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -154,11 +219,14 @@ dependencies = [
[[package]]
name = "dcr"
version = "0.9.0"
version = "0.9.5"
dependencies = [
"ctrlc",
"env_logger",
"fatfs",
"glob",
"log",
"owo-colors",
"self-replace",
"serde",
"serde_json",
@@ -168,6 +236,37 @@ dependencies = [
"ureq",
]
[[package]]
name = "defmt"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
dependencies = [
"defmt-parser",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -198,7 +297,30 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
name = "env_filter"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
@@ -330,6 +452,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -475,12 +603,71 @@ dependencies = [
"serde_core",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
dependencies = [
"defmt",
"jiff-core",
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
dependencies = [
"defmt",
]
[[package]]
name = "jiff-static"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
dependencies = [
"jiff-core",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "js-sys"
version = "0.3.103"
@@ -518,9 +705,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "log"
version = "0.4.30"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
@@ -570,12 +757,28 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "owo-colors"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
dependencies = [
"supports-color 2.1.0",
"supports-color 3.0.2",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -588,6 +791,21 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -604,7 +822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -631,6 +849,35 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustix"
version = "1.1.4"
@@ -757,7 +1004,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -817,6 +1064,25 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "supports-color"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
dependencies = [
"is-terminal",
"is_ci",
]
[[package]]
name = "supports-color"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
dependencies = [
"is_ci",
]
[[package]]
name = "syn"
version = "2.0.117"
@@ -828,6 +1094,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -836,7 +1113,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -852,6 +1129,26 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -976,6 +1273,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -1032,7 +1335,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wasm-bindgen-shared",
]
@@ -1100,7 +1403,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1111,7 +1414,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1271,7 +1574,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
"syn",
"syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -1287,7 +1590,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -1354,7 +1657,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"synstructure",
]
@@ -1375,7 +1678,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"synstructure",
]
@@ -1415,7 +1718,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "dcr"
version = "0.9.0"
version = "0.9.5"
edition = "2024"
build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -75,6 +75,11 @@ serde_json = "1"
ctrlc = "3"
fatfs = { version = "0.3", optional = true }
# Logging
log = "0.4.34"
env_logger = "0.11.11"
owo-colors = { version = "4", features = ["supports-colors"] }
[profile.release]
opt-level = "z"
lto = "thin"
@@ -97,4 +102,4 @@ debug = "line-tables-only"
[profile.dev.package."*"]
opt-level = 1
debug = false
debug = false

View File

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

View File

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

996
dcrup.sh Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,20 @@ sidebar_label: Changelog
# Changelog
## 0.9.5 (2026-10-21)
- **Structured CLI output** — shared helpers now provide Cargo-like human, plain, and JSON output modes.
- **Color and diagnostics policy** — `--color`, `NO_COLOR`, `--quiet`, and `--verbose` are applied consistently across commands.
- **Build reporting** — lifecycle events and compiler diagnostics are available as structured JSON events.
- **Output lab** — sandbox scenarios cover help, lifecycle commands, compiler warnings/errors, stream capture, and broken pipes.
## 0.9.1 (2026-09-13)
- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`.
- **`dcr add` support for `pkg-config`** — easily add system package dependencies with `dcr add fmt pkg-config:fmt` or `dcr add openssl pkg:openssl,libcrypto`.
- **`dcr tree` & IDE integration** — `pkg-config` packages are marked in `dcr tree` and resolved into compiler flags in `dcr gen` (`compile_commands.json`).
- **Fixed `dcr new` in non-ASCII / spaced paths** — initializing projects from folders with non-ASCII characters or spaces no longer fails validation.
## 0.9.0 (2026-08-31)
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.

View File

@@ -15,6 +15,8 @@ dcr add mylib path:./lib # explicit path prefix
dcr add mylib git:https://github.com/user/repo # explicit git prefix
dcr add mylib github:user/repo # GitHub shorthand
dcr add mylib gitlab:user/repo # GitLab shorthand
dcr add fmt pkg-config:fmt # system package via pkg-config
dcr add openssl pkg:openssl,libcrypto # multiple pkg-config packages
```
Source prefixes:
@@ -22,6 +24,7 @@ Source prefixes:
- `git:` — generic git URL
- `github:` — expands to `https://github.com/<user>/<repo>`
- `gitlab:` — expands to `https://gitlab.com/<user>/<repo>`
- `pkg-config:` / `pkg:` — one or more packages resolved by `pkg-config`
- `http://` / `https://` / `git@` — full URL
Flags:
@@ -31,6 +34,11 @@ Flags:
If source is omitted — DCR searches connected registries. A bare relative or absolute path is accepted as a path dependency.
For `pkg-config` dependencies, the referenced packages must already be installed
and provide `.pc` files. DCR invokes `pkg-config` during the build (using
`PATH` and `PKG_CONFIG_PATH`) and forwards the returned compiler and linker
flags; it does not download or build these packages.
## `dcr tree`
Displays the project dependency tree.

View File

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

View File

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

View File

@@ -111,7 +111,11 @@ Never fails the command — warnings only. Pre-release suffixes (`1.0.0-dev`) ar
| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) |
Settings from raw config (not in typed struct):
- `pkg_config` — list of pkg-config packages
- `pkg_config` — list of packages queried for build-wide compiler/linker flags
Dependencies can also declare a system package directly, for example
`fmt = { pkg-config = "fmt" }`. This records the package in `dcr.lock` and applies
the flags returned by `pkg-config --cflags` and `pkg-config --libs`.
- `ldscript` — linker script path
- `build.steps` / `build.post_steps` — codegen steps

View File

@@ -6,7 +6,7 @@ sidebar_label: Dependencies
## Formats
The `[dependencies]` section supports three formats:
The `[dependencies]` section supports registry, path, Git, and `pkg-config` dependencies:
### String (registry)
@@ -44,6 +44,23 @@ names (`libs`):
dcr-math = { path = "../deps/dcr-math", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
```
### Table (`pkg-config`)
Use an installed `.pc` package when the library is provided by the operating system
or another system package manager. DCR invokes `pkg-config --cflags` and
`pkg-config --libs`, preserving include, library, linker, and other flags:
```toml
[dependencies]
fmt = { pkg-config = "fmt" }
openssl = { pkg-config = ["openssl", "libcrypto"] }
```
The `pkg-config` executable must be available in `PATH`. The named `.pc` files must
be available through pkg-config's standard search paths or via `PKG_CONFIG_PATH`.
These dependencies are not downloaded or built by DCR; they are recorded in
`dcr.lock` with a `pkg-config+...` source.
## Registry
DCR uses a package registry for dependency lookup by name.

View File

@@ -25,6 +25,9 @@ Generic git repository.
.B path:./path/to/lib
Local path dependency.
.TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:\fR or comma-separated packages).
.TP
.B <version>
Version from registry.
.SH OPTIONS

View File

@@ -1,4 +1,4 @@
.TH DCR 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.TH DCR 1 "2026-10-21" "dcr 0.9.5" "DCR Manual"
.SH NAME
dcr \- Cargo\-like build tool and package manager for C/C++ projects
.SH SYNOPSIS
@@ -120,6 +120,9 @@ Generic git repository.
.B path:./path/to/lib
Local path dependency.
.TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:name1,name2\fR).
.TP
.B \fI<version>\fR
Version from registry (e.g. \fB1.2.3\fR).
.SH GEN SUBCOMMANDS

View File

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

View File

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

View File

@@ -15,15 +15,14 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::core::build_config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob;
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Entry point for the `dcr clean` command.
///
@@ -36,45 +35,41 @@ use std::path::Path;
/// Process exit code: `0` on success, `1` on failure.
pub fn clean(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr clean [--debug | --release] [--target <triple>] [--all]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Removes build artifacts from the target directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Clean debug artifacts (default)");
println!(" --release Clean release artifacts");
println!(" --target <triple> Clean artifacts for a specific target");
println!(" --all Clean all workspace members");
let mut help = crate::utils::help::HelpBuilder::new("dcr clean");
help.section("USAGE")
.example("dcr clean [--debug | --release] [--target <triple>] [--all]");
help.description("Removes build artifacts from the target directory.");
help.section("OPTIONS")
.item("--debug", "Clean debug artifacts (default)")
.item("--release", "Clean release artifacts")
.item("--target <triple>", "Clean artifacts for a specific target")
.item("--all", "Clean all workspace members");
help.render();
return 0;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
let flags = match parse_clean_flags(args) {
Ok(v) => v,
Err(msg) => {
error(&msg);
error!("{msg}");
return 1;
}
};
@@ -83,7 +78,7 @@ pub fn clean(args: &[String]) -> i32 {
match with_dir(&root, || clean_from_root(&root, &flags)) {
Ok(()) => 0,
Err(msg) => {
error(&msg);
error!("{msg}");
1
}
}
@@ -210,10 +205,7 @@ fn clean_project_at(
if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string());
}
println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
crate::utils::output::status("Cleaning", format!("project `{project_name}`"));
if let Some(profile) = profile {
let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}")
@@ -229,18 +221,14 @@ fn clean_project_at(
target_items.contains(&profile.to_string())
};
if !dir_exists {
warn(&format!("Directory target/{} not found", target_dir));
warn!("Directory target/{target_dir} not found");
} else {
println!(" Profile: {}", colored(profile, BOLD_GREEN));
crate::utils::output::status("Profile", profile);
if let Some(t) = target {
println!(" Target: {}", colored(t, BOLD_GREEN));
crate::utils::output::status("Target", t);
}
let _ = fs::remove_dir_all(&target_dir);
println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
crate::utils::output::removed(format!("directory {target_dir}"));
}
clean_custom_paths(&config, profile)?;
return Ok(());
@@ -248,12 +236,9 @@ fn clean_project_at(
if items.contains(&"target".to_string()) {
let _ = fs::remove_dir_all("target");
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
crate::utils::output::removed("directory target");
} else {
warn("Directory target not found");
warn!("Directory target not found");
}
clean_custom_paths(&config, "debug")?;
clean_custom_paths(&config, "release")?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use crate::platform;
use crate::utils::build::is_bare_metal_target;
use std::path::{Path, PathBuf};

View File

@@ -15,9 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::utils::build::{is_bare_metal_target, is_compile_only, is_flat_bin};
use std::fs;
use std::path::{Path, PathBuf};

View File

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

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -609,10 +609,14 @@ fn build_project_at_inner(
tc_as.as_deref(),
);
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler);
// The host triple is still used for native target-specific configuration and
// artifact paths, but native Clang must select the platform SDK implicitly.
// Pass --target only when the user/config explicitly requested a target.
let compiler_target = if explicit_target { build_target } else { None };
prepend_clang_target_flag(&mut build_cflags, compiler_target, &resolved_compiler);
prepend_clang_target_flag(
&mut build_ldflags,
build_target,
compiler_target,
resolved_linker.as_deref().unwrap_or(&resolved_compiler),
);
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
@@ -769,8 +773,10 @@ fn build_project_at_inner(
)?;
}
let (resolved_cflags, resolved_ldflags) =
let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
resolved_cflags.extend(resolved.cflags.clone());
resolved_ldflags.extend(resolved.ldflags.clone());
let mut combined_excludes = Vec::new();
for dir in exclude_dirs {
combined_excludes.push(dir.clone());
@@ -1011,6 +1017,8 @@ fn extend_transitive_link_deps(
consumer_deps.include_dirs.extend(deps.include_dirs);
consumer_deps.lib_dirs.extend(deps.lib_dirs);
consumer_deps.libs.extend(deps.libs);
consumer_deps.cflags.extend(deps.cflags);
consumer_deps.ldflags.extend(deps.ldflags);
for child_root in deps.package_roots {
extend_transitive_link_deps(
Path::new(&child_root),

View File

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

View File

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

View File

@@ -15,12 +15,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds GAS (`.s`) sources via the GNU Assembler.
///

View File

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

View File

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

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::{build::builder::BuildContext, build::common};
use crate::utils::build::run_pkg_config;
use std::fs;
use std::path::{Path, PathBuf};

View File

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

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::engine::ToolchainExecs;
use crate::core::build_config::Config;
use crate::core::{build::engine::ToolchainExecs, build_config::Config};
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
use glob::glob;
use std::fs;

View File

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

View File

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

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Represents a locked dependency entry containing its name, version, source, and checksum.
#[derive(Debug, Clone)]

View File

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

View File

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

View File

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

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Initializes a Git repository in the target directory and configures its `.gitignore` file.
///

View File

@@ -14,20 +14,29 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::error;
mod cli;
mod config;
mod core;
mod platform;
mod prelude;
mod utils;
use prelude::*;
/// Main entry point for DCR, a Cargo-like C/C++ project manager.
///
/// Collects command-line arguments via `std::env::args()` and dispatches to the
/// appropriate subcommand handler. With fewer than two arguments, shows help and
/// exits with code 0; otherwise exits with the handler's exit code.
fn main() {
let args: Vec<String> = std::env::args().collect();
let args = match utils::output::configure(std::env::args().collect()) {
Ok(args) => args,
Err(message) => {
utils::output::emit("error", &message);
std::process::exit(2);
}
};
utils::log::init();
if args.len() < 2 {
std::process::exit(cli::help::help());
@@ -61,7 +70,7 @@ fn main() {
"--help" => cli::help::help(),
"--update" => cli::flag_update::flag_update(rest),
_ => {
error("Unknown command or argument");
error!("Unknown command or argument");
1
}
};

View File

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

1
src/prelude.rs Normal file
View File

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

View File

@@ -15,8 +15,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::core::build_config::Config;
use crate::utils::log::warn;
use std::path::{Path, PathBuf};
/// Version information parsed from a version string.
@@ -251,10 +252,9 @@ pub fn normalize_target_os(target: &str) -> &str {
"windows" => "x86_64-pc-windows-msvc",
_ if target.contains('-') => target,
_ => {
warn(&format!(
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
target
));
warn!(
"Unknown target '{target}', using as-is. Supported short names: linux, macos, windows"
);
target
}
}
@@ -782,6 +782,13 @@ mod tests {
assert_eq!(flags, vec!["-O2".to_string()]);
}
#[test]
fn native_clang_target_is_not_added_without_explicit_target() {
let mut flags = vec!["-O3".to_string()];
prepend_clang_target_flag(&mut flags, None, "clang");
assert_eq!(flags, vec!["-O3".to_string()]);
}
/// Test for parse_version_parts function.
#[test]
fn parse_version_parts() {

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

@@ -52,6 +52,24 @@ fn dcr_add_dependencies() {
"git short dep not found in toml"
);
// Test pkg-config prefix.
let out = run_dcr(&["add", "fmt", "pkg-config:fmt"], &dir);
assert!(out.status.success(), "dcr add pkg-config should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("fmt = { pkg-config = \"fmt\" }"),
"pkg-config dep not found in toml"
);
// Test multiple pkg-config packages via pkg: prefix.
let out = run_dcr(&["add", "openssl", "pkg:openssl,libcrypto"], &dir);
assert!(out.status.success(), "dcr add multiple pkg should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("openssl = { pkg-config = [\"openssl\", \"libcrypto\"] }"),
"multi pkg-config dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
@@ -146,6 +164,56 @@ fn path_prebuilt_dependency_without_manifest_links() {
);
}
#[test]
fn pkg_config_dependency_resolves_system_library() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping pkg-config dependency test");
return;
};
if !std::process::Command::new("pkg-config")
.args(["--exists", "zlib"])
.status()
.map(|status| status.success())
.unwrap_or(false)
{
eprintln!("pkg-config zlib unavailable; skipping pkg-config dependency test");
return;
}
let root = unique_sandbox_dir("pkg_config_dep");
let app = root.join("app");
std::fs::create_dir_all(app.join("src")).unwrap();
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nzlib = { pkg-config = \"zlib\" }\n",
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include <zlib.h>\nint main(void) { return zlibVersion() == 0 ? 1 : 0; }\n",
)
.unwrap();
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
assert!(
out.status.success(),
"pkg-config dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
assert!(lock.contains("source = \"pkg-config+zlib\""));
// Check dcr tree displays pkg-config info
let tree_out = run_dcr(&["tree"], &app);
assert!(tree_out.status.success(), "dcr tree should succeed");
let tree_str = String::from_utf8_lossy(&tree_out.stdout);
assert!(
tree_str.contains("zlib (pkg-config: zlib)"),
"dcr tree should format pkg-config dependency, got: {}",
tree_str
);
}
#[test]
fn path_dcr_dependency_is_built_before_consumer() {
let Some(compiler) = available_compiler() else {

View File

@@ -79,3 +79,24 @@ fn init_vcs_options_work() {
assert!(gitignore.contains(".dcr/"));
}
}
#[test]
fn new_in_non_ascii_parent_directory() {
let parent_dir = unique_sandbox_dir("Проекты с пробелами");
let out = run_dcr(&["new", "my_app"], &parent_dir);
assert!(
out.status.success(),
"dcr new should succeed inside non-ascii parent dir: {}",
String::from_utf8_lossy(&out.stderr)
);
let project_dir = parent_dir.join("my_app");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
let toml_content = std::fs::read_to_string(project_dir.join("dcr.toml")).unwrap();
assert!(
toml_content.contains("name = \"my_app\""),
"package name should be my_app in dcr.toml"
);
}

View File

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