From 0f456b4201c5195158f85a82ea36dc683d75f7c1 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 21 Oct 2026 18:28:31 +0800 Subject: [PATCH] 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 --- .github/workflows/release.yml | 20 +- CHANGELOG.md | 62 ++ Cargo.lock | 333 +++++++- Cargo.toml | 9 +- dcrup.cmd | 13 - dcrup.ps1 | 44 +- dcrup.sh | 996 ++++++++++++------------ docs/changelog.mdx | 14 + docs/commands/dependency-commands.mdx | 8 + docs/getting-started/installation.mdx | 19 +- docs/reference/cli-output.mdx | 34 + docs/reference/dcr-toml.mdx | 6 +- docs/reference/dependencies.mdx | 19 +- man/man1/dcr-add.1 | 3 + man/man1/dcr.1 | 5 +- src/cli/add.rs | 132 ++-- src/cli/build.rs | 149 ++-- src/cli/clean.rs | 65 +- src/cli/flag_update.rs | 40 +- src/cli/flags.rs | 13 +- src/cli/fmt.rs | 29 +- src/cli/gen.rs | 178 +++-- src/cli/help.rs | 75 +- src/cli/init.rs | 86 +- src/cli/lint.rs | 70 +- src/cli/new.rs | 113 +-- src/cli/run.rs | 82 +- src/cli/setup.rs | 23 +- src/cli/test.rs | 101 ++- src/cli/tree.rs | 35 +- src/core/build/builder/artifact.rs | 3 +- src/core/build/builder/cc_common.rs | 4 +- src/core/build/builder/msvc/mod.rs | 9 +- src/core/build/cache.rs | 3 +- src/core/build/engine.rs | 6 +- src/core/build/language/asm/common.rs | 7 +- src/core/build/language/asm/fasm/mod.rs | 7 +- src/core/build/language/asm/gas/mod.rs | 7 +- src/core/build/language/asm/masm/mod.rs | 7 +- src/core/build/language/asm/nasm/mod.rs | 7 +- src/core/build/language/cxx/qt.rs | 3 +- src/core/build/language/llvm_ir/mod.rs | 8 +- src/core/build/steps.rs | 3 +- src/core/build_config.rs | 75 +- src/core/deps/common.rs | 2 + src/core/deps/lock.rs | 3 +- src/core/deps/mod.rs | 74 +- src/core/registry.rs | 4 +- src/core/runner.rs | 3 +- src/core/vcs/git.rs | 3 +- src/main.rs | 15 +- src/platform/mod.rs | 40 +- src/prelude.rs | 1 + src/utils/build.rs | 10 +- src/utils/{text.rs => cli_styles.rs} | 50 +- src/utils/git.rs | 3 +- src/utils/help.rs | 124 +++ src/utils/log.rs | 40 +- src/utils/mod.rs | 4 +- src/utils/output.rs | 535 +++++++++++++ tests/cli_deps.rs | 68 ++ tests/cli_new.rs | 21 + tests/cli_test.rs | 11 +- 63 files changed, 2619 insertions(+), 1317 deletions(-) delete mode 100755 dcrup.cmd mode change 100755 => 100644 dcrup.sh create mode 100644 docs/reference/cli-output.mdx create mode 100644 src/prelude.rs rename src/utils/{text.rs => cli_styles.rs} (54%) create mode 100644 src/utils/help.rs create mode 100644 src/utils/output.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cacb427..e0abac9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1191,6 +1191,7 @@ jobs: - name: Update Formula/dcr.rb run: | VERSION="${{ needs.setup.outputs.version }}" + cat > Formula/dcr.rb < "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 \ No newline at end of file + --region auto diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd6fd8..eb0f7d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 19e43e3..0384db0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 44a2809..cec3e4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 \ No newline at end of file +debug = false diff --git a/dcrup.cmd b/dcrup.cmd deleted file mode 100755 index e601f29..0000000 --- a/dcrup.cmd +++ /dev/null @@ -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 diff --git a/dcrup.ps1 b/dcrup.ps1 index 094b9a9..6922d82 100644 --- a/dcrup.ps1 +++ b/dcrup.ps1 @@ -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 |--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 } diff --git a/dcrup.sh b/dcrup.sh old mode 100755 new mode 100644 index 7f0bc7a..1d70fa6 --- a/dcrup.sh +++ b/dcrup.sh @@ -6,6 +6,7 @@ DCRUP_BIN="${DCRUP_BIN:-$DCRUP_HOME/bin}" DCRUP_TC="${DCRUP_HOME}/toolchains" DCRUP_META="${DCRUP_HOME}/meta" REPO_URL="${DCRUP_REPO:-https://github.com/dexoron/dcr}" +DCRUP_INSTALL_URL="${DCRUP_INSTALL_URL:-https://sh.dcr-tool.ru}" DCRUP_MIRROR="${DCRUP_MIRROR:-auto}" API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest" API_ALL="https://api.github.com/repos/dexoron/dcr/releases" @@ -17,13 +18,16 @@ YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' -log() { printf "${CYAN}[dcrup]${NC} %s\n" "$*"; } -ok() { printf "${GREEN}[ok]${NC} %s\n" "$*"; } +log() { printf "${CYAN}[dcrup]${NC} %s\n" "$*"; } +ok() { printf "${GREEN}[ok]${NC} %s\n" "$*"; } warn() { printf "${YELLOW}[warn]${NC} %s\n" "$*"; } -die() { printf "${RED}[error]${NC} %s\n" "$*" >&2; exit 1; } +die() { + printf "${RED}[error]${NC} %s\n" "$*" >&2 + exit 1 +} usage() { - cat <<'EOF' + cat <<'EOF' dcrup — install and switch DCR versions Usage: @@ -73,596 +77,624 @@ EOF } need_cmd() { - command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" } detect_linux_libc() { - local pref="${DCRUP_LIBC:-gnu}" - case "$pref" in - gnu|glibc) echo "gnu" ;; - musl) echo "musl" ;; - auto) - if command -v ldd >/dev/null 2>&1; then - if ldd --version 2>&1 | grep -qi musl; then - echo "musl" - return 0 - fi - fi - if [[ -e /lib/ld-musl-x86_64.so.1 || -e /lib/ld-musl-aarch64.so.1 ]]; then - echo "musl" - return 0 - fi - if command -v getconf >/dev/null 2>&1 && getconf GNU_LIBC_VERSION >/dev/null 2>&1; then - echo "gnu" - return 0 - fi - echo "gnu" - ;; - *) die "invalid DCRUP_LIBC / --libc: $pref (use gnu|musl|auto)" ;; - esac + local pref="${DCRUP_LIBC:-gnu}" + case "$pref" in + gnu | glibc) echo "gnu" ;; + musl) echo "musl" ;; + auto) + if command -v ldd >/dev/null 2>&1; then + if ldd --version 2>&1 | grep -qi musl; then + echo "musl" + return 0 + fi + fi + if [[ -e /lib/ld-musl-x86_64.so.1 || -e /lib/ld-musl-aarch64.so.1 ]]; then + echo "musl" + return 0 + fi + if command -v getconf >/dev/null 2>&1 && getconf GNU_LIBC_VERSION >/dev/null 2>&1; then + echo "gnu" + return 0 + fi + echo "gnu" + ;; + *) die "invalid DCRUP_LIBC / --libc: $pref (use gnu|musl|auto)" ;; + esac } detect_triple() { - local os arch libc - os="$(uname -s)" - arch="$(uname -m)" - case "$os:$arch" in - Linux:x86_64) - libc="$(detect_linux_libc)" - if [[ "$libc" == "musl" ]]; then - echo "x86_64-unknown-linux-musl" - else - echo "x86_64-unknown-linux-gnu" - fi - ;; - Linux:aarch64|Linux:arm64) - libc="$(detect_linux_libc)" - if [[ "$libc" == "musl" ]]; then - echo "aarch64-unknown-linux-musl" - else - echo "aarch64-unknown-linux-gnu" - fi - ;; - Linux:i686|Linux:i386) - libc="$(detect_linux_libc)" - if [[ "$libc" == "musl" ]]; then - echo "i686-unknown-linux-musl" - else - echo "i686-unknown-linux-gnu" - fi - ;; - Linux:armv7l|Linux:armv7) - libc="$(detect_linux_libc)" - if [[ "$libc" == "musl" ]]; then - echo "armv7-unknown-linux-musleabihf" - else - echo "armv7-unknown-linux-gnueabihf" - fi - ;; - Linux:riscv64) - libc="$(detect_linux_libc)" - if [[ "$libc" == "musl" ]]; then - echo "riscv64gc-unknown-linux-musl" - else - echo "riscv64gc-unknown-linux-gnu" - fi - ;; - Darwin:x86_64) echo "x86_64-apple-darwin" ;; - Darwin:arm64|Darwin:aarch64) echo "aarch64-apple-darwin" ;; - FreeBSD:x86_64|FreeBSD:amd64) echo "x86_64-unknown-freebsd" ;; - FreeBSD:aarch64|FreeBSD:arm64) echo "aarch64-unknown-freebsd" ;; - OpenBSD:x86_64|OpenBSD:amd64) echo "x86_64-unknown-openbsd" ;; - NetBSD:x86_64|NetBSD:amd64) echo "x86_64-unknown-netbsd" ;; - MINGW*|MSYS*|CYGWIN*) - case "$arch" in - x86_64|amd64) echo "x86_64-pc-windows-gnu" ;; - aarch64|arm64) echo "aarch64-pc-windows-gnullvm" ;; - i686|i386) echo "i686-pc-windows-gnu" ;; - *) die "unsupported Windows arch under MSYS: $arch" ;; - esac - ;; - *) die "unsupported platform: $os/$arch" ;; + local os arch libc + os="$(uname -s)" + arch="$(uname -m)" + case "$os:$arch" in + Linux:x86_64) + libc="$(detect_linux_libc)" + if [[ "$libc" == "musl" ]]; then + echo "x86_64-unknown-linux-musl" + else + echo "x86_64-unknown-linux-gnu" + fi + ;; + Linux:aarch64 | Linux:arm64) + libc="$(detect_linux_libc)" + if [[ "$libc" == "musl" ]]; then + echo "aarch64-unknown-linux-musl" + else + echo "aarch64-unknown-linux-gnu" + fi + ;; + Linux:i686 | Linux:i386) + libc="$(detect_linux_libc)" + if [[ "$libc" == "musl" ]]; then + echo "i686-unknown-linux-musl" + else + echo "i686-unknown-linux-gnu" + fi + ;; + Linux:armv7l | Linux:armv7) + libc="$(detect_linux_libc)" + if [[ "$libc" == "musl" ]]; then + echo "armv7-unknown-linux-musleabihf" + else + echo "armv7-unknown-linux-gnueabihf" + fi + ;; + Linux:riscv64) + libc="$(detect_linux_libc)" + if [[ "$libc" == "musl" ]]; then + echo "riscv64gc-unknown-linux-musl" + else + echo "riscv64gc-unknown-linux-gnu" + fi + ;; + Darwin:x86_64) echo "x86_64-apple-darwin" ;; + Darwin:arm64 | Darwin:aarch64) echo "aarch64-apple-darwin" ;; + FreeBSD:x86_64 | FreeBSD:amd64) echo "x86_64-unknown-freebsd" ;; + FreeBSD:aarch64 | FreeBSD:arm64) echo "aarch64-unknown-freebsd" ;; + OpenBSD:x86_64 | OpenBSD:amd64) echo "x86_64-unknown-openbsd" ;; + NetBSD:x86_64 | NetBSD:amd64) echo "x86_64-unknown-netbsd" ;; + MINGW* | MSYS* | CYGWIN*) + case "$arch" in + x86_64 | amd64) echo "x86_64-pc-windows-gnu" ;; + aarch64 | arm64) echo "aarch64-pc-windows-gnullvm" ;; + i686 | i386) echo "i686-pc-windows-gnu" ;; + *) die "unsupported Windows arch under MSYS: $arch" ;; esac + ;; + *) die "unsupported platform: $os/$arch" ;; + esac } normalize_version() { - local v="$1" - v="${v#v}" - printf '%s' "$v" + local v="$1" + v="${v#v}" + printf '%s' "$v" } parse_spec() { - local raw="$1" - SPEC_RAW="$raw" - SPEC_CHANNEL="" - SPEC_VERSION="" - SPEC_FLOATING=0 + local raw="$1" + SPEC_RAW="$raw" + SPEC_CHANNEL="" + SPEC_VERSION="" + SPEC_FLOATING=0 - case "$raw" in - "") die "missing version/channel spec" ;; - stable|dev|night) - SPEC_CHANNEL="$raw" - SPEC_VERSION="" - SPEC_FLOATING=1 - ;; - *@*) - SPEC_VERSION="$(normalize_version "${raw%@*}")" - SPEC_CHANNEL="${raw#*@}" - case "$SPEC_CHANNEL" in - stable|dev) ;; - night) die "cannot pin a version on night (use: dcrup install night)" ;; - *) die "unknown channel in spec: $SPEC_CHANNEL (use stable|dev)" ;; - esac - [[ -n "$SPEC_VERSION" ]] || die "empty version in spec: $raw" - ;; - *) - SPEC_VERSION="$(normalize_version "$raw")" - SPEC_CHANNEL="stable" - [[ -n "$SPEC_VERSION" ]] || die "invalid spec: $raw" - ;; + case "$raw" in + "") die "missing version/channel spec" ;; + stable | dev | night) + SPEC_CHANNEL="$raw" + SPEC_VERSION="" + SPEC_FLOATING=1 + ;; + *@*) + SPEC_VERSION="$(normalize_version "${raw%@*}")" + SPEC_CHANNEL="${raw#*@}" + case "$SPEC_CHANNEL" in + stable | dev) ;; + night) die "cannot pin a version on night (use: dcrup install night)" ;; + *) die "unknown channel in spec: $SPEC_CHANNEL (use stable|dev)" ;; esac + [[ -n "$SPEC_VERSION" ]] || die "empty version in spec: $raw" + ;; + *) + SPEC_VERSION="$(normalize_version "$raw")" + SPEC_CHANNEL="stable" + [[ -n "$SPEC_VERSION" ]] || die "invalid spec: $raw" + ;; + esac } toolchain_id() { - if [[ "$SPEC_CHANNEL" == "night" ]]; then - local sha="${1:-unknown}" - echo "night-${sha}" - elif [[ "$SPEC_FLOATING" -eq 1 ]]; then - local ver="${1:-latest}" - echo "${SPEC_CHANNEL}-${ver}" - else - echo "${SPEC_VERSION}-${SPEC_CHANNEL}" - fi + if [[ "$SPEC_CHANNEL" == "night" ]]; then + local sha="${1:-unknown}" + echo "night-${sha}" + elif [[ "$SPEC_FLOATING" -eq 1 ]]; then + local ver="${1:-latest}" + echo "${SPEC_CHANNEL}-${ver}" + else + echo "${SPEC_VERSION}-${SPEC_CHANNEL}" + fi } ensure_dirs() { - mkdir -p "$DCRUP_TC" "$DCRUP_BIN" "$DCRUP_META" + mkdir -p "$DCRUP_TC" "$DCRUP_BIN" "$DCRUP_META" } active_file() { echo "$DCRUP_META/active"; } read_active() { - if [[ -f "$(active_file)" ]]; then - cat "$(active_file)" - fi + if [[ -f "$(active_file)" ]]; then + cat "$(active_file)" + fi } write_active() { - printf '%s\n' "$1" >"$(active_file)" + printf '%s\n' "$1" >"$(active_file)" } link_default() { - local id="$1" - local src="$DCRUP_TC/$id/dcr" - [[ -x "$src" ]] || die "toolchain not installed: $id" - ln -sfn "$src" "$DCRUP_BIN/dcr" - write_active "$id" - ok "default → $id ($DCRUP_BIN/dcr)" + local id="$1" + local src="$DCRUP_TC/$id/dcr" + [[ -x "$src" ]] || die "toolchain not installed: $id" + # Resolve the source so a relative DCRUP_HOME cannot create a broken link. + src="$(cd "$(dirname "$src")" && pwd)/$(basename "$src")" + ln -sfn "$src" "$DCRUP_BIN/dcr" + write_active "$id" + ok "default → $id ($DCRUP_BIN/dcr)" } fetch_json() { - need_cmd curl - curl -fsSL "$1" + need_cmd curl + curl -fsSL "$1" } json_tag() { - printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 + printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 } set_mirror() { - case "$1" in - auto|cf|gh|sf|ya) DCRUP_MIRROR="$1" ;; - *) die "invalid --mirror: $1 (use auto|cf|gh|sf|ya)" ;; - esac + case "$1" in + auto | cf | gh | sf | ya) DCRUP_MIRROR="$1" ;; + *) die "invalid --mirror: $1 (use auto|cf|gh|sf|ya)" ;; + esac } mirror_url() { - local mirror="$1" tag="$2" asset="$3" - case "$mirror" in - cf) printf 'https://cdn-global.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; - gh) printf 'https://github.com/dexoron/dcr/releases/download/%s/%s' "$tag" "$asset" ;; - sf) printf 'https://sourceforge.net/projects/dcr-tool/files/%s/%s/download' "$tag" "$asset" ;; - ya) printf 'https://cdn-ru.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; - *) return 1 ;; - esac + local mirror="$1" tag="$2" asset="$3" + case "$mirror" in + cf) printf 'https://cdn-global.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; + gh) printf 'https://github.com/dexoron/dcr/releases/download/%s/%s' "$tag" "$asset" ;; + sf) printf 'https://sourceforge.net/projects/dcr-tool/files/%s/%s/download' "$tag" "$asset" ;; + ya) printf 'https://cdn-ru.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; + *) return 1 ;; + esac } mirror_chain() { echo "cf gh sf ya"; } mirrors_to_try() { - case "$DCRUP_MIRROR" in - auto) mirror_chain ;; - cf|gh|sf|ya) echo "$DCRUP_MIRROR" ;; - *) die "invalid mirror: $DCRUP_MIRROR (use auto|cf|gh|sf|ya)" ;; - esac + case "$DCRUP_MIRROR" in + auto) mirror_chain ;; + cf | gh | sf | ya) echo "$DCRUP_MIRROR" ;; + *) die "invalid mirror: $DCRUP_MIRROR (use auto|cf|gh|sf|ya)" ;; + esac } download_from_mirrors() { - local tag="$1" asset="$2" dest="$3" - local tmp="$dest/dcr.download" m url size - USED_MIRROR="" - for m in $(mirrors_to_try); do - url="$(mirror_url "$m" "$tag" "$asset")" - log "trying mirror $m …" - rm -f "$tmp" - if curl -fL --retry 2 --retry-delay 1 --connect-timeout 10 --max-time 180 \ - -A "dcrup" "$url" -o "$tmp"; then - size="$(wc -c <"$tmp" | tr -d ' ')" - if [[ "${size:-0}" -gt 100000 ]]; then - mv "$tmp" "$dest/dcr" - USED_MIRROR="$m" - return 0 - fi - warn "mirror $m returned a too-small file (${size:-0} bytes)" - else - warn "mirror $m failed" - fi - rm -f "$tmp" - done - return 1 + local tag="$1" asset="$2" dest="$3" + local tmp="$dest/dcr.download" m url size + USED_MIRROR="" + for m in $(mirrors_to_try); do + url="$(mirror_url "$m" "$tag" "$asset")" + log "trying mirror $m …" + rm -f "$tmp" + if curl -fL --retry 2 --retry-delay 1 --connect-timeout 10 --max-time 180 \ + -A "dcrup" "$url" -o "$tmp"; then + size="$(wc -c <"$tmp" | tr -d ' ')" + if [[ "${size:-0}" -gt 100000 ]]; then + mv "$tmp" "$dest/dcr" + USED_MIRROR="$m" + return 0 + fi + warn "mirror $m returned a too-small file (${size:-0} bytes)" + else + warn "mirror $m failed" + fi + rm -f "$tmp" + done + return 1 } latest_stable_json() { - fetch_json "$API_LATEST" + fetch_json "$API_LATEST" } latest_dev_json() { - need_cmd curl - local json result - json="$(fetch_json "$API_ALL")" - if command -v jq >/dev/null 2>&1; then - result="$(printf '%s' "$json" | jq -e -c '[.[] | select(.prerelease == true)][0]')" || true - elif command -v python3 >/dev/null 2>&1; then - result="$(printf '%s' "$json" | python3 -c ' + need_cmd curl + local json result + json="$(fetch_json "$API_ALL")" + if command -v jq >/dev/null 2>&1; then + result="$(printf '%s' "$json" | jq -e -c '[.[] | select(.prerelease == true)][0]')" || true + elif command -v python3 >/dev/null 2>&1; then + result="$(printf '%s' "$json" | python3 -c ' import sys, json rels = json.load(sys.stdin) pre = [r for r in rels if r.get("prerelease")] print(json.dumps(pre[0]) if pre else "", end="") ')" - else - die "dev channel needs jq or python3 to parse GitHub API" - fi - [[ -n "$result" && "$result" != "null" ]] || die "no prerelease (dev) found" - printf '%s' "$result" + else + die "dev channel needs jq or python3 to parse GitHub API" + fi + [[ -n "$result" && "$result" != "null" ]] || die "no prerelease (dev) found" + printf '%s' "$result" } release_json_for_spec() { - if [[ "$SPEC_FLOATING" -eq 1 ]]; then - case "$SPEC_CHANNEL" in - stable) latest_stable_json ;; - dev) latest_dev_json ;; - night) die "night has no GitHub release binary" ;; - esac - else - fetch_json "https://api.github.com/repos/dexoron/dcr/releases/tags/v${SPEC_VERSION}" - fi + if [[ "$SPEC_FLOATING" -eq 1 ]]; then + case "$SPEC_CHANNEL" in + stable) latest_stable_json ;; + dev) latest_dev_json ;; + night) die "night has no GitHub release binary" ;; + esac + else + fetch_json "https://api.github.com/repos/dexoron/dcr/releases/tags/v${SPEC_VERSION}" + fi } install_prebuilt() { - local triple asset tag version id dest mirror - need_cmd curl - triple="$(detect_triple)" - json="$(release_json_for_spec)" - tag="$(json_tag "$json")" - [[ -n "$tag" ]] || die "failed to parse release tag" - version="$(normalize_version "$tag")" - asset="dcr-${triple}-${version}" - case "$triple" in - *windows*) asset="${asset}.exe" ;; - esac + local triple asset tag version id dest mirror + need_cmd curl + triple="$(detect_triple)" + json="$(release_json_for_spec)" + tag="$(json_tag "$json")" + [[ -n "$tag" ]] || die "failed to parse release tag" + version="$(normalize_version "$tag")" + asset="dcr-${triple}-${version}" + case "$triple" in + *windows*) asset="${asset}.exe" ;; + esac - id="$(toolchain_id "$version")" - dest="$DCRUP_TC/$id" - if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then - ok "already installed: $id" - link_default "$id" - return 0 - fi - - mkdir -p "$dest" - log "downloading $asset (mirror=$DCRUP_MIRROR) …" - download_from_mirrors "$tag" "$asset" "$dest" \ - || die "no mirror reachable for $asset (try: --mirror cf|gh|sf|ya)" - mirror="$USED_MIRROR" - log "mirror → $mirror" - chmod +x "$dest/dcr" - printf '%s\n' \ - "channel=$SPEC_CHANNEL" \ - "version=$version" \ - "tag=$tag" \ - "source=release" \ - "triple=$triple" \ - "mirror=$mirror" \ - "libc=${DCRUP_LIBC:-gnu}" \ - >"$dest/dcrup-meta" - ok "installed prebuilt $id ($triple via $mirror)" + id="$(toolchain_id "$version")" + dest="$DCRUP_TC/$id" + if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then + ok "already installed: $id" link_default "$id" + return 0 + fi + + mkdir -p "$dest" + log "downloading $asset (mirror=$DCRUP_MIRROR) …" + download_from_mirrors "$tag" "$asset" "$dest" || + die "no mirror reachable for $asset (try: --mirror cf|gh|sf|ya)" + mirror="$USED_MIRROR" + log "mirror → $mirror" + chmod +x "$dest/dcr" + printf '%s\n' \ + "channel=$SPEC_CHANNEL" \ + "version=$version" \ + "tag=$tag" \ + "source=release" \ + "triple=$triple" \ + "mirror=$mirror" \ + "libc=${DCRUP_LIBC:-gnu}" \ + >"$dest/dcrup-meta" + ok "installed prebuilt $id ($triple via $mirror)" + link_default "$id" } install_build() { - local tmp id dest sha version branch_or_tag - need_cmd git - need_cmd cargo + local tmp id dest sha version branch_or_tag + need_cmd git + need_cmd cargo - tmp="$(mktemp -d "${TMPDIR:-/tmp}/dcrup.XXXXXX")" - cleanup_tmp() { rm -rf "$tmp"; } - trap cleanup_tmp EXIT + tmp="$(mktemp -d "${TMPDIR:-/tmp}/dcrup.XXXXXX")" + cleanup_tmp() { rm -rf "$tmp"; } + trap cleanup_tmp EXIT - if [[ "$SPEC_CHANNEL" == "night" ]]; then - log "cloning $REPO_URL (branch dev) for night …" - git clone --depth 1 --branch dev "$REPO_URL" "$tmp/src" 2>/dev/null \ - || git clone --depth 1 "$REPO_URL" "$tmp/src" - sha="$(git -C "$tmp/src" rev-parse --short HEAD)" - version="night" - id="$(toolchain_id "$sha")" - elif [[ "$SPEC_FLOATING" -eq 1 ]]; then - local json tag - json="$(release_json_for_spec)" - tag="$(json_tag "$json")" - [[ -n "$tag" ]] || die "failed to resolve floating channel tag" - version="$(normalize_version "$tag")" - id="$(toolchain_id "$version")" - log "cloning $REPO_URL @ $tag …" - git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null \ - || { - git clone --depth 1 "$REPO_URL" "$tmp/src" - git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}" - git -C "$tmp/src" checkout "$tag" - } - sha="$(git -C "$tmp/src" rev-parse --short HEAD)" - else - version="$SPEC_VERSION" - id="$(toolchain_id)" - local tag="v${SPEC_VERSION}" - log "cloning $REPO_URL @ $tag …" - git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null \ - || { - git clone --depth 1 "$REPO_URL" "$tmp/src" - git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}" - git -C "$tmp/src" checkout "$tag" - } - sha="$(git -C "$tmp/src" rev-parse --short HEAD)" - fi + if [[ "$SPEC_CHANNEL" == "night" ]]; then + log "cloning $REPO_URL (branch dev) for night …" + git clone --depth 1 --branch dev "$REPO_URL" "$tmp/src" 2>/dev/null || + git clone --depth 1 "$REPO_URL" "$tmp/src" + sha="$(git -C "$tmp/src" rev-parse --short HEAD)" + version="night" + id="$(toolchain_id "$sha")" + elif [[ "$SPEC_FLOATING" -eq 1 ]]; then + local json tag + json="$(release_json_for_spec)" + tag="$(json_tag "$json")" + [[ -n "$tag" ]] || die "failed to resolve floating channel tag" + version="$(normalize_version "$tag")" + id="$(toolchain_id "$version")" + log "cloning $REPO_URL @ $tag …" + git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null || + { + git clone --depth 1 "$REPO_URL" "$tmp/src" + git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}" + git -C "$tmp/src" checkout "$tag" + } + sha="$(git -C "$tmp/src" rev-parse --short HEAD)" + else + version="$SPEC_VERSION" + id="$(toolchain_id)" + local tag="v${SPEC_VERSION}" + log "cloning $REPO_URL @ $tag …" + git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null || + { + git clone --depth 1 "$REPO_URL" "$tmp/src" + git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}" + git -C "$tmp/src" checkout "$tag" + } + sha="$(git -C "$tmp/src" rev-parse --short HEAD)" + fi - dest="$DCRUP_TC/$id" - if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then - ok "already installed: $id" - link_default "$id" - return 0 - fi - - log "building release (features: $FEATURES) …" - ( - cd "$tmp/src" - # shellcheck disable=SC2086 - cargo build --release --features $FEATURES - ) - mkdir -p "$dest" - cp "$tmp/src/target/release/dcr" "$dest/dcr" - chmod +x "$dest/dcr" - printf '%s\n' "channel=$SPEC_CHANNEL" "version=$version" "git=$sha" "source=build" "features=$FEATURES" >"$dest/dcrup-meta" - ok "installed build $id" + dest="$DCRUP_TC/$id" + if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then + ok "already installed: $id" link_default "$id" - trap - EXIT - cleanup_tmp + return 0 + fi + + log "building release (features: $FEATURES) …" + ( + cd "$tmp/src" + # shellcheck disable=SC2086 + cargo build --release --features $FEATURES + ) + mkdir -p "$dest" + cp "$tmp/src/target/release/dcr" "$dest/dcr" + chmod +x "$dest/dcr" + printf '%s\n' "channel=$SPEC_CHANNEL" "version=$version" "git=$sha" "source=build" "features=$FEATURES" >"$dest/dcrup-meta" + ok "installed build $id" + link_default "$id" + trap - EXIT + cleanup_tmp } cmd_install() { - local mode="release" FORCE=0 - local spec="" - local libc_cli="" - while [[ $# -gt 0 ]]; do - case "$1" in - --build) mode="build"; shift ;; - --release) mode="release"; shift ;; - --force) FORCE=1; shift ;; - --libc) - [[ $# -ge 2 ]] || die "--libc needs gnu|musl|auto" - libc_cli="$2" - shift 2 - ;; - --libc=*) - libc_cli="${1#--libc=}" - shift - ;; - --mirror) - [[ $# -ge 2 ]] || die "--mirror needs auto|cf|gh|sf|ya" - set_mirror "$2" - shift 2 - ;; - --mirror=*) - set_mirror "${1#--mirror=}" - shift - ;; - -h|--help) usage; exit 0 ;; - -*) die "unknown flag: $1" ;; - *) - [[ -z "$spec" ]] || die "unexpected argument: $1" - spec="$1" - shift - ;; - esac - done - [[ -n "$spec" ]] || die "usage: dcrup install [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]" - parse_spec "$spec" - ensure_dirs + local mode="release" FORCE=0 + local spec="" + local libc_cli="" + while [[ $# -gt 0 ]]; do + case "$1" in + --build) + mode="build" + shift + ;; + --release) + mode="release" + shift + ;; + --force) + FORCE=1 + shift + ;; + --libc) + [[ $# -ge 2 ]] || die "--libc needs gnu|musl|auto" + libc_cli="$2" + shift 2 + ;; + --libc=*) + libc_cli="${1#--libc=}" + shift + ;; + --mirror) + [[ $# -ge 2 ]] || die "--mirror needs auto|cf|gh|sf|ya" + set_mirror "$2" + shift 2 + ;; + --mirror=*) + set_mirror "${1#--mirror=}" + shift + ;; + -h | --help) + usage + exit 0 + ;; + -*) die "unknown flag: $1" ;; + *) + [[ -z "$spec" ]] || die "unexpected argument: $1" + spec="$1" + shift + ;; + esac + done + [[ -n "$spec" ]] || die "usage: dcrup install [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]" + parse_spec "$spec" + ensure_dirs - if [[ -n "$libc_cli" ]]; then - case "$libc_cli" in - gnu|glibc|musl|auto) DCRUP_LIBC="$libc_cli" ;; - *) die "invalid --libc: $libc_cli (use gnu|musl|auto)" ;; - esac - fi - export DCRUP_LIBC="${DCRUP_LIBC:-gnu}" + if [[ -n "$libc_cli" ]]; then + case "$libc_cli" in + gnu | glibc | musl | auto) DCRUP_LIBC="$libc_cli" ;; + *) die "invalid --libc: $libc_cli (use gnu|musl|auto)" ;; + esac + fi + export DCRUP_LIBC="${DCRUP_LIBC:-gnu}" - if [[ "$SPEC_CHANNEL" == "night" ]]; then - mode="build" - fi + if [[ "$SPEC_CHANNEL" == "night" ]]; then + mode="build" + fi - export FORCE - log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC mirror=$DCRUP_MIRROR" - if [[ "$mode" == "build" ]]; then - install_build - else - install_prebuilt - fi - check_path + export FORCE + log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC mirror=$DCRUP_MIRROR" + if [[ "$mode" == "build" ]]; then + install_build + else + install_prebuilt + fi + check_path } cmd_default() { - [[ $# -eq 1 ]] || die "usage: dcrup default " - parse_spec "$1" - ensure_dirs - local id - if [[ "$SPEC_CHANNEL" == "night" || "$SPEC_FLOATING" -eq 1 ]]; then - id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)" - [[ -n "$id" ]] || die "no installed toolchain for channel $SPEC_CHANNEL (run: dcrup install $SPEC_CHANNEL)" - else - id="$(toolchain_id)" - [[ -d "$DCRUP_TC/$id" ]] || die "not installed: $id (run: dcrup install $1)" - fi - link_default "$id" + [[ $# -eq 1 ]] || die "usage: dcrup default " + parse_spec "$1" + ensure_dirs + local id + if [[ "$SPEC_CHANNEL" == "night" || "$SPEC_FLOATING" -eq 1 ]]; then + id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)" + [[ -n "$id" ]] || die "no installed toolchain for channel $SPEC_CHANNEL (run: dcrup install $SPEC_CHANNEL)" + else + id="$(toolchain_id)" + [[ -d "$DCRUP_TC/$id" ]] || die "not installed: $id (run: dcrup install $1)" + fi + link_default "$id" } cmd_update() { - ensure_dirs - local active - active="$(read_active)" - [[ -n "$active" ]] || die "no active toolchain (run: dcrup install stable)" - case "$active" in - night-*) - log "updating night (rebuild HEAD) …" - FORCE=1 cmd_install night --build --force - ;; - stable-*|dev-*) - local ch="${active%%-*}" - log "updating floating channel $ch …" - FORCE=1 cmd_install "$ch" --force - ;; - *-stable|*-dev) - ok "pinned toolchain $active — not floating; install a newer version explicitly" - ;; - *) - warn "unknown active id $active; try: dcrup install stable" - ;; - esac + ensure_dirs + local active + active="$(read_active)" + [[ -n "$active" ]] || die "no active toolchain (run: dcrup install stable)" + case "$active" in + night-*) + log "updating night (rebuild HEAD) …" + FORCE=1 cmd_install night --build --force + ;; + stable-* | dev-*) + local ch="${active%%-*}" + log "updating floating channel $ch …" + FORCE=1 cmd_install "$ch" --force + ;; + *-stable | *-dev) + ok "pinned toolchain $active — not floating; install a newer version explicitly" + ;; + *) + warn "unknown active id $active; try: dcrup install stable" + ;; + esac } cmd_list() { - ensure_dirs - local active - active="$(read_active)" - if [[ ! -d "$DCRUP_TC" ]] || [[ -z "$(ls -A "$DCRUP_TC" 2>/dev/null || true)" ]]; then - warn "no toolchains installed" - return 0 + ensure_dirs + local active + active="$(read_active)" + if [[ ! -d "$DCRUP_TC" ]] || [[ -z "$(ls -A "$DCRUP_TC" 2>/dev/null || true)" ]]; then + warn "no toolchains installed" + return 0 + fi + local d base + for d in "$DCRUP_TC"/*; do + [[ -d "$d" ]] || continue + base="$(basename "$d")" + if [[ "$base" == "$active" ]]; then + printf ' * %s\n' "$base" + else + printf ' %s\n' "$base" fi - local d base - for d in "$DCRUP_TC"/*; do - [[ -d "$d" ]] || continue - base="$(basename "$d")" - if [[ "$base" == "$active" ]]; then - printf ' * %s\n' "$base" - else - printf ' %s\n' "$base" - fi - done + done } cmd_show() { - ensure_dirs - local active - active="$(read_active)" - echo "DCRUP_HOME=$DCRUP_HOME" - echo "DCRUP_BIN=$DCRUP_BIN" - echo "active=${active:-none}" - if [[ -n "$active" && -f "$DCRUP_TC/$active/dcrup-meta" ]]; then - echo "--- meta ---" - cat "$DCRUP_TC/$active/dcrup-meta" - fi - if [[ -x "$DCRUP_BIN/dcr" ]]; then - echo "--- dcr --version ---" - "$DCRUP_BIN/dcr" --version 2>/dev/null || true - fi + ensure_dirs + local active + active="$(read_active)" + echo "DCRUP_HOME=$DCRUP_HOME" + echo "DCRUP_BIN=$DCRUP_BIN" + echo "active=${active:-none}" + if [[ -n "$active" && -f "$DCRUP_TC/$active/dcrup-meta" ]]; then + echo "--- meta ---" + cat "$DCRUP_TC/$active/dcrup-meta" + fi + if [[ -x "$DCRUP_BIN/dcr" ]]; then + echo "--- dcr --version ---" + "$DCRUP_BIN/dcr" --version 2>/dev/null || true + fi } cmd_which() { - if [[ -L "$DCRUP_BIN/dcr" || -e "$DCRUP_BIN/dcr" ]]; then - readlink -f "$DCRUP_BIN/dcr" 2>/dev/null || readlink "$DCRUP_BIN/dcr" 2>/dev/null || echo "$DCRUP_BIN/dcr" - else - die "dcr not linked (run: dcrup install stable)" - fi + if [[ -L "$DCRUP_BIN/dcr" || -e "$DCRUP_BIN/dcr" ]]; then + readlink -f "$DCRUP_BIN/dcr" 2>/dev/null || readlink "$DCRUP_BIN/dcr" 2>/dev/null || echo "$DCRUP_BIN/dcr" + else + die "dcr not linked (run: dcrup install stable)" + fi } cmd_uninstall() { - ensure_dirs - if [[ "${1:-}" == "--all" ]]; then - rm -rf "$DCRUP_TC" - rm -f "$DCRUP_BIN/dcr" "$(active_file)" - ok "removed all toolchains" - return 0 - fi - [[ $# -eq 1 ]] || die "usage: dcrup uninstall |--all" - parse_spec "$1" - local id - if [[ "$SPEC_FLOATING" -eq 1 || "$SPEC_CHANNEL" == "night" ]]; then - id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)" - else - id="$(toolchain_id)" - fi - [[ -n "$id" && -d "$DCRUP_TC/$id" ]] || die "not installed: $1" - rm -rf "$DCRUP_TC/$id" - if [[ "$(read_active)" == "$id" ]]; then - rm -f "$DCRUP_BIN/dcr" "$(active_file)" - warn "removed active toolchain; run: dcrup install stable" - fi - ok "removed $id" + ensure_dirs + if [[ "${1:-}" == "--all" ]]; then + rm -rf "$DCRUP_TC" + rm -f "$DCRUP_BIN/dcr" "$(active_file)" + ok "removed all toolchains" + return 0 + fi + [[ $# -eq 1 ]] || die "usage: dcrup uninstall |--all" + parse_spec "$1" + local id + if [[ "$SPEC_FLOATING" -eq 1 || "$SPEC_CHANNEL" == "night" ]]; then + id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)" + else + id="$(toolchain_id)" + fi + [[ -n "$id" && -d "$DCRUP_TC/$id" ]] || die "not installed: $1" + rm -rf "$DCRUP_TC/$id" + if [[ "$(read_active)" == "$id" ]]; then + rm -f "$DCRUP_BIN/dcr" "$(active_file)" + warn "removed active toolchain; run: dcrup install stable" + fi + ok "removed $id" } check_path() { - case ":$PATH:" in - *":$DCRUP_BIN:"*) ;; - *) - warn "$DCRUP_BIN is not in PATH" - echo "Add to ~/.bashrc / ~/.zshrc / ~/.profile:" - echo " export PATH=\"$DCRUP_BIN:\$PATH\"" - ;; - esac + case ":$PATH:" in + *":$DCRUP_BIN:"*) ;; + *) + warn "$DCRUP_BIN is not in PATH" + echo "Add to ~/.bashrc / ~/.zshrc / ~/.profile:" + echo " export PATH=\"$DCRUP_BIN:\$PATH\"" + ;; + esac } cmd_self_install() { - local dest="$DCRUP_BIN" - while [[ $# -gt 0 ]]; do - case "$1" in - --to) dest="$2"; shift 2 ;; - *) die "usage: dcrup self-install [--to DIR]" ;; - esac - done - ensure_dirs - mkdir -p "$dest" - local self - self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")" - cp "$self" "$dest/dcrup" - chmod +x "$dest/dcrup" - ok "dcrup installed to $dest/dcrup" - check_path - echo "Then: dcrup install stable" + local dest="$DCRUP_BIN" + while [[ $# -gt 0 ]]; do + case "$1" in + --to) + dest="$2" + shift 2 + ;; + *) die "usage: dcrup self-install [--to DIR]" ;; + esac + done + ensure_dirs + mkdir -p "$dest" + local self + self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || true)" + if [[ -z "$self" || ! -f "$self" ]]; then + # When invoked as `curl ... | sh`, the shell has no script file to copy. + # Fetch the same trusted installer URL and install that exact script. + need_cmd curl + self="$(mktemp "${TMPDIR:-/tmp}/dcrup.XXXXXX")" + curl -fsSL --proto '=https' --tlsv1.2 "$DCRUP_INSTALL_URL" -o "$self" || { + rm -f "$self" + die "unable to download installer for self-install" + } + trap 'rm -f "$self"' EXIT + fi + cp "$self" "$dest/dcrup" + chmod +x "$dest/dcrup" + ok "dcrup installed to $dest/dcrup" + check_path + echo "Then: dcrup install stable" } main() { - local cmd="${1:-}" - shift || true - case "$cmd" in - ""|-h|--help|help) usage ;; - install) cmd_install "$@" ;; - default) cmd_default "$@" ;; - update) cmd_update "$@" ;; - list) cmd_list "$@" ;; - show) cmd_show "$@" ;; - which) cmd_which "$@" ;; - uninstall) cmd_uninstall "$@" ;; - self-install) cmd_self_install "$@" ;; - *) die "unknown command: $cmd (try: dcrup help)" ;; - esac + local cmd="${1:-}" + shift || true + case "$cmd" in + "" | -h | --help | help) usage ;; + install) cmd_install "$@" ;; + default) cmd_default "$@" ;; + update) cmd_update "$@" ;; + list) cmd_list "$@" ;; + show) cmd_show "$@" ;; + which) cmd_which "$@" ;; + uninstall) cmd_uninstall "$@" ;; + self-install) cmd_self_install "$@" ;; + *) die "unknown command: $cmd (try: dcrup help)" ;; + esac } main "$@" diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 48aa30b..86bbc01 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -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`. diff --git a/docs/commands/dependency-commands.mdx b/docs/commands/dependency-commands.mdx index 64150fb..8ee6bb1 100644 --- a/docs/commands/dependency-commands.mdx +++ b/docs/commands/dependency-commands.mdx @@ -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//` - `gitlab:` — expands to `https://gitlab.com//` +- `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. diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 9b9a26f..6aaa409 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -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//dcr` and `~/.dcr/bin/dcr` → active version. diff --git a/docs/reference/cli-output.mdx b/docs/reference/cli-output.mdx new file mode 100644 index 0000000..48c9e0b --- /dev/null +++ b/docs/reference/cli-output.mdx @@ -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. diff --git a/docs/reference/dcr-toml.mdx b/docs/reference/dcr-toml.mdx index ac0a45d..2c07f63 100644 --- a/docs/reference/dcr-toml.mdx +++ b/docs/reference/dcr-toml.mdx @@ -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 diff --git a/docs/reference/dependencies.mdx b/docs/reference/dependencies.mdx index f2a696d..cdc04f0 100644 --- a/docs/reference/dependencies.mdx +++ b/docs/reference/dependencies.mdx @@ -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. diff --git a/man/man1/dcr-add.1 b/man/man1/dcr-add.1 index e3899a2..a7e07b8 100644 --- a/man/man1/dcr-add.1 +++ b/man/man1/dcr-add.1 @@ -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 from registry. .SH OPTIONS diff --git a/man/man1/dcr.1 b/man/man1/dcr.1 index a5bba7b..fcd5ec0 100644 --- a/man/man1/dcr.1 +++ b/man/man1/dcr.1 @@ -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\fR Version from registry (e.g. \fB1.2.3\fR). .SH GEN SUBCOMMANDS diff --git a/src/cli/add.rs b/src/cli/add.rs index 72e2b0a..16b57e2 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -15,17 +15,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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, /// Version string resolved from the package registry when no explicit source is given. pub version_from_registry: Option, + /// Package names resolved through pkg-config. + pub pkg_config: Option>, } /// 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 [--branch ] [--tag ] [--rev ]", - 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 from registry"); - println!(); - printc("OPTIONS:", BOLD_GREEN); - println!(" --branch Use a specific branch"); - println!(" --tag Use a specific tag"); - println!(" --rev Use a specific commit hash"); + let mut help = crate::utils::help::HelpBuilder::new("dcr add"); + help.section("USAGE") + .example("dcr add [--branch ] [--tag ] [--rev ]"); + 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 from registry"); + help.section("OPTIONS") + .item("--branch ", "Use a specific branch") + .item("--tag ", "Use a specific tag") + .item("--rev ", "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 { if args.is_empty() { - error("Usage: dcr add [--branch | --tag | --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 [--branch | --tag | --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 { 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 { 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 { ) }) .map_err(|(msg, code)| { - error(&msg); + error!("{msg}"); code })?; return Ok(AddArgs { @@ -221,10 +225,11 @@ fn parse_add_args(args: &[String]) -> Result { 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 { "--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 { 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::>(); + 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 { { 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 { tag, rev, version_from_registry: None, + pkg_config, }) } diff --git a/src/cli/build.rs b/src/cli/build.rs index 2975bfe..00f2161 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -15,6 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 ] [--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 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 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 ", "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 ", "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 } } diff --git a/src/cli/clean.rs b/src/cli/clean.rs index 2c02b95..8dad090 100644 --- a/src/cli/clean.rs +++ b/src/cli/clean.rs @@ -15,15 +15,14 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 ] [--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 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 ] [--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 ", "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")?; diff --git a/src/cli/flag_update.rs b/src/cli/flag_update.rs index 8e0fa0d..b7b55c6 100644 --- a/src/cli/flag_update.rs +++ b/src/cli/flag_update.rs @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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(¤t_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(¤t_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 } diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 257eddf..c063fcd 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -15,8 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 { 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 { 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 { 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 { // 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); } diff --git a/src/cli/fmt.rs b/src/cli/fmt.rs index 0f91e7c..6c01a5e 100644 --- a/src/cli/fmt.rs +++ b/src/cli/fmt.rs @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 } } diff --git a/src/cli/gen.rs b/src/cli/gen.rs index d75d0ca..d7cb38d 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -15,6 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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, lib_dirs: Vec, libs: Vec, + cflags: Vec, + ldflags: Vec, } -/// 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 ", 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 "); + 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 ", 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 "); + 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 = 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, 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, 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::>(); + 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/.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); } }; diff --git a/src/cli/help.rs b/src/cli/help.rs index 2a973de..f9221d3 100644 --- a/src/cli/help.rs +++ b/src/cli/help.rs @@ -15,8 +15,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 [options]", BOLD_CYAN); - println!(); - printc("COMMANDS:", BOLD_GREEN); - println!(" new 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 ... 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 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 --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 [options]"); + help.section("COMMANDS") + .item("new ", "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 ...", "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 ", "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 --help' for command-specific help."); + help.render(); 0 } diff --git a/src/cli/init.rs b/src/cli/init.rs index 0c0537c..6363222 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -15,15 +15,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 ]", 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 ]"); + 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 } diff --git a/src/cli/lint.rs b/src/cli/lint.rs index b47b2ec..eaf4b04 100644 --- a/src/cli/lint.rs +++ b/src/cli/lint.rs @@ -15,12 +15,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 diff --git a/src/cli/new.rs b/src/cli/new.rs index 3592a39..d66ed92 100644 --- a/src/cli/new.rs +++ b/src/cli/new.rs @@ -15,15 +15,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 [--vcs ]", 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 [--vcs ]"); + 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 } diff --git a/src/cli/run.rs b/src/cli/run.rs index fd11bf6..7597003 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -15,17 +15,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 ] [--force] [--clean] [--verbose] [-- ...]", - 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 Cross-compile for the given target"); - println!(" --force Force a full rebuild"); - println!(" --clean Clean before building"); - println!(" --verbose Print detailed build output"); - println!(" -- ... Arguments forwarded to the binary"); + let mut help = crate::utils::help::HelpBuilder::new("dcr run"); + help.section("USAGE").example("dcr run [--debug | --release] [--target ] [--force] [--clean] [--verbose] [-- ...]"); + 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 ", "Cross-compile for the given target") + .item("--force", "Force a full rebuild") + .item("--clean", "Clean before building") + .item("--verbose", "Print detailed build output") + .item("-- ...", "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, diff --git a/src/cli/setup.rs b/src/cli/setup.rs index c4b7842..e217240 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -15,32 +15,31 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 } } diff --git a/src/cli/test.rs b/src/cli/test.rs index f536d1f..19807d8 100644 --- a/src/cli/test.rs +++ b/src/cli/test.rs @@ -15,6 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 { 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 { 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 { 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 { 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 { 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 { 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 { } 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); - } -} diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 4c24517..8909831 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -15,9 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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::>() + .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 diff --git a/src/core/build/builder/artifact.rs b/src/core/build/builder/artifact.rs index afedf3a..6da9f66 100644 --- a/src/core/build/builder/artifact.rs +++ b/src/core/build/builder/artifact.rs @@ -15,8 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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}; diff --git a/src/core/build/builder/cc_common.rs b/src/core/build/builder/cc_common.rs index 963e120..42c1221 100644 --- a/src/core/build/builder/cc_common.rs +++ b/src/core/build/builder/cc_common.rs @@ -15,9 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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}; diff --git a/src/core/build/builder/msvc/mod.rs b/src/core/build/builder/msvc/mod.rs index b0d6dc4..ed4b2a9 100644 --- a/src/core/build/builder/msvc/mod.rs +++ b/src/core/build/builder/msvc/mod.rs @@ -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 { diff --git a/src/core/build/cache.rs b/src/core/build/cache.rs index d1184db..ef37bd3 100644 --- a/src/core/build/cache.rs +++ b/src/core/build/cache.rs @@ -15,8 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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}; diff --git a/src/core/build/engine.rs b/src/core/build/engine.rs index 8facd20..4833ab8 100644 --- a/src/core/build/engine.rs +++ b/src/core/build/engine.rs @@ -769,8 +769,10 @@ fn build_project_at_inner( )?; } - let (resolved_cflags, resolved_ldflags) = + let (mut resolved_cflags, mut resolved_ldflags) = resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?; + resolved_cflags.extend(resolved.cflags.clone()); + resolved_ldflags.extend(resolved.ldflags.clone()); let mut combined_excludes = Vec::new(); for dir in exclude_dirs { combined_excludes.push(dir.clone()); @@ -1011,6 +1013,8 @@ fn extend_transitive_link_deps( consumer_deps.include_dirs.extend(deps.include_dirs); consumer_deps.lib_dirs.extend(deps.lib_dirs); consumer_deps.libs.extend(deps.libs); + consumer_deps.cflags.extend(deps.cflags); + consumer_deps.ldflags.extend(deps.ldflags); for child_root in deps.package_roots { extend_transitive_link_deps( Path::new(&child_root), diff --git a/src/core/build/language/asm/common.rs b/src/core/build/language/asm/common.rs index 1a1cb97..81a3921 100644 --- a/src/core/build/language/asm/common.rs +++ b/src/core/build/language/asm/common.rs @@ -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. /// diff --git a/src/core/build/language/asm/fasm/mod.rs b/src/core/build/language/asm/fasm/mod.rs index ff928ec..c4d3994 100644 --- a/src/core/build/language/asm/fasm/mod.rs +++ b/src/core/build/language/asm/fasm/mod.rs @@ -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. /// diff --git a/src/core/build/language/asm/gas/mod.rs b/src/core/build/language/asm/gas/mod.rs index 6aab0bb..0bae39c 100644 --- a/src/core/build/language/asm/gas/mod.rs +++ b/src/core/build/language/asm/gas/mod.rs @@ -15,12 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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. /// diff --git a/src/core/build/language/asm/masm/mod.rs b/src/core/build/language/asm/masm/mod.rs index 239092e..14e24e5 100644 --- a/src/core/build/language/asm/masm/mod.rs +++ b/src/core/build/language/asm/masm/mod.rs @@ -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`. /// diff --git a/src/core/build/language/asm/nasm/mod.rs b/src/core/build/language/asm/nasm/mod.rs index 10aedfd..6f1ff60 100644 --- a/src/core/build/language/asm/nasm/mod.rs +++ b/src/core/build/language/asm/nasm/mod.rs @@ -16,12 +16,9 @@ // along with this program. If not, see . /// 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. /// diff --git a/src/core/build/language/cxx/qt.rs b/src/core/build/language/cxx/qt.rs index aa509ed..e2dfb7c 100644 --- a/src/core/build/language/cxx/qt.rs +++ b/src/core/build/language/cxx/qt.rs @@ -15,8 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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}; diff --git a/src/core/build/language/llvm_ir/mod.rs b/src/core/build/language/llvm_ir/mod.rs index 0b121dc..eb04806 100644 --- a/src/core/build/language/llvm_ir/mod.rs +++ b/src/core/build/language/llvm_ir/mod.rs @@ -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; diff --git a/src/core/build/steps.rs b/src/core/build/steps.rs index c8a6ed9..b72f7d6 100644 --- a/src/core/build/steps.rs +++ b/src/core/build/steps.rs @@ -15,8 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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; diff --git a/src/core/build_config.rs b/src/core/build_config.rs index 8144a24..58efa14 100644 --- a/src/core/build_config.rs +++ b/src/core/build_config.rs @@ -15,12 +15,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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::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 { 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 { + 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"); diff --git a/src/core/deps/common.rs b/src/core/deps/common.rs index 76fa078..18a98c2 100644 --- a/src/core/deps/common.rs +++ b/src/core/deps/common.rs @@ -4,5 +4,7 @@ pub struct ResolvedDeps { pub include_dirs: Vec, pub lib_dirs: Vec, pub libs: Vec, + pub cflags: Vec, + pub ldflags: Vec, pub package_roots: Vec, } diff --git a/src/core/deps/lock.rs b/src/core/deps/lock.rs index c404875..2ff50fc 100644 --- a/src/core/deps/lock.rs +++ b/src/core/deps/lock.rs @@ -15,8 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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)] diff --git a/src/core/deps/mod.rs b/src/core/deps/mod.rs index 71f1ba7..78e8269 100644 --- a/src/core/deps/mod.rs +++ b/src/core/deps/mod.rs @@ -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> { 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>, 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::, _>>()? + .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() { diff --git a/src/core/registry.rs b/src/core/registry.rs index 31d6bc5..7f27ab9 100644 --- a/src/core/registry.rs +++ b/src/core/registry.rs @@ -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)] diff --git a/src/core/runner.rs b/src/core/runner.rs index 7d6255f..47e8bf2 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -16,8 +16,7 @@ // along with this program. If not, see . 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`. /// diff --git a/src/core/vcs/git.rs b/src/core/vcs/git.rs index b6f9bde..6736717 100644 --- a/src/core/vcs/git.rs +++ b/src/core/vcs/git.rs @@ -15,8 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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. /// diff --git a/src/main.rs b/src/main.rs index 0254792..d30969b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,20 +14,29 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 = 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 } }; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index b7c6ff8..3348739 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -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) } } diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..a1f87fc --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1 @@ +pub use log::{error, warn}; diff --git a/src/utils/build.rs b/src/utils/build.rs index 8b716ec..d50e7f9 100644 --- a/src/utils/build.rs +++ b/src/utils/build.rs @@ -15,8 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +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 } } diff --git a/src/utils/text.rs b/src/utils/cli_styles.rs similarity index 54% rename from src/utils/text.rs rename to src/utils/cli_styles.rs index 74c4a18..5eabd84 100644 --- a/src/utils/text.rs +++ b/src/utils/cli_styles.rs @@ -15,39 +15,33 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#[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; diff --git a/src/utils/git.rs b/src/utils/git.rs index fb0d790..249f97b 100644 --- a/src/utils/git.rs +++ b/src/utils/git.rs @@ -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. /// diff --git a/src/utils/help.rs b/src/utils/help.rs new file mode 100644 index 0000000..cce37d9 --- /dev/null +++ b/src/utils/help.rs @@ -0,0 +1,124 @@ +use super::cli_styles::{BOLD_CYAN, BOLD_GREEN, Colorize}; +use super::output; + +pub struct HelpBuilder { + title: String, + description: Option, + sections: Vec
, + tip: Option, +} + +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) -> Self { + Self { + title: title.into(), + description: None, + sections: Vec::new(), + tip: None, + } + } + pub fn description(&mut self, text: impl Into) -> &mut Self { + self.description = Some(text.into()); + self + } + + /// Add an example to a dedicated `EXAMPLES` section. + pub fn example(&mut self, text: impl Into) -> &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) -> &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) -> 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) -> &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::>() })).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: { + pub fn item(self, name: impl Into, description: impl Into) -> Self { + self.help.sections[self.index] + .entries + .push((name.into(), description.into())); + self + } + pub fn example(self, command: impl Into) -> Self { + self.item(command, "") + } +} diff --git a/src/utils/log.rs b/src/utils/log.rs index 5726cb8..068e44f 100644 --- a/src/utils/log.rs +++ b/src/utils/log.rs @@ -15,17 +15,37 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 + }); } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f4c0ee2..53312e1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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; diff --git a/src/utils/output.rs b/src/utils/output.rs new file mode 100644 index 0000000..e9eda02 --- /dev/null +++ b/src/utils/output.rs @@ -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, +} + +static OPTIONS: OnceLock = OnceLock::new(); + +/// Consume global options without touching arguments after `--`. +pub fn configure(args: Vec) -> Result, 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, +} + +#[allow(dead_code)] +pub fn ui() -> Ui { + Ui { lines: Vec::new() } +} + +pub fn say(message: impl AsRef) { + write_stdout(&format!("{}\n", message.as_ref())); +} +pub fn status(action: impl AsRef, subject: impl AsRef) { + emit( + "status", + &format!("{} {}", action.as_ref(), subject.as_ref()), + ); +} +pub fn success(message: impl AsRef) { + emit("success", message.as_ref()); +} +pub fn warning(message: impl AsRef) { + emit("warning", message.as_ref()); +} +pub fn info(message: impl AsRef) { + emit("info", message.as_ref()); +} +pub fn error(message: impl AsRef) { + emit("error", message.as_ref()); +} +pub fn created(subject: impl AsRef) { + emit("success", &format!("Created {}", subject.as_ref())); +} +pub fn removed(subject: impl AsRef) { + 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, subject: impl Into) -> Self { + self.lines + .push(format!("{} {}", action.into(), subject.into())); + self + } + #[allow(dead_code)] + pub fn detail(mut self, detail: impl Into) -> 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) -> 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"); + } +} diff --git a/tests/cli_deps.rs b/tests/cli_deps.rs index 297e5ba..9a5a44a 100644 --- a/tests/cli_deps.rs +++ b/tests/cli_deps.rs @@ -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 \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 { diff --git a/tests/cli_new.rs b/tests/cli_new.rs index ddf07ea..b6f683c 100644 --- a/tests/cli_new.rs +++ b/tests/cli_new.rs @@ -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" + ); +} diff --git a/tests/cli_test.rs b/tests/cli_test.rs index 774ab7f..a2e9389 100644 --- a/tests/cli_test.rs +++ b/tests/cli_test.rs @@ -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 );