17 Commits

Author SHA1 Message Date
9d030cba0c fix(build): skip linking/archiving for custom and none project kinds 2026-07-18 20:21:41 +08:00
9eaa4635d5 refactor: modularize build core engine, update workspaces, cache, and test suite 2026-07-18 20:02:12 +08:00
9326e23a53 feat: add native Qt support and dcr lint command; refactor architecture and integration tests 2026-06-17 12:39:22 +08:00
21932ac94a feat(build): add freestanding, lto, strip, panic-abort, and codegen-units support in dcr.toml 2026-06-16 16:42:56 +08:00
3d596d7280 Update install docs 2026-06-15 22:35:38 +08:00
5cb4473bd1 Update README.md 2026-06-15 22:35:06 +08:00
33f955d4e3 refactor(updater): migrate from reqwest to ureq for lightweight builds 2026-06-13 16:15:24 +08:00
838be72dd2 refactor(deps): replace git2 with system git and add vcs support 2026-06-13 15:28:14 +08:00
39fad31c4f Merge pull request #18 from dexoron/master
v0.7.2
2026-06-07 22:31:43 +08:00
c8ef38a99b release: bump version to 0.7.2 and update changelog 2026-06-07 21:41:02 +08:00
8e0c535cfd fix: improve default target triple generation for various OS environments 2026-06-07 21:29:55 +08:00
de88c160ad Merge pull request #16 from XiaoPengYouCode/codex/fix-macos-host-target-link 2026-06-07 21:21:53 +08:00
4daca925a0 Merge pull request #17 from dexoron/master
master -> dev
2026-06-07 21:17:14 +08:00
595b7ac105 Merge pull request #15 from XiaoPengYouCode/codex/fix-readme-badges 2026-06-07 21:13:10 +08:00
XiaoPengYouCode
11ae3762d0 fix: use host macOS target for default builds 2026-06-07 05:41:07 +08:00
XiaoPengYouCode
4c439317e3 docs: fix README badge rendering 2026-06-07 05:09:38 +08:00
0aa3f87f99 feat: temporarily disable Snap Store publishing and update README instructions 2026-06-06 22:18:03 +08:00
87 changed files with 6017 additions and 4744 deletions

View File

@@ -41,10 +41,6 @@ on:
description: "Publish to crates.io"
type: boolean
default: false
run_snap:
description: "Publish to Snap Store (dcrup)"
type: boolean
default: false
run_brew:
description: "Update Homebrew tap"
type: boolean
@@ -97,20 +93,22 @@ jobs:
set -euo pipefail
tag="${{ needs.setup.outputs.version }}"
notes_file=".github/release-notes.md"
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then
# Strip -dev suffix to match base version in CHANGELOG
changelog_tag="${tag%-dev}"
if grep -q "^## \\[${changelog_tag}\\]" CHANGELOG.md; then
# Extract release name from header: ## [0.7.1] - 2026-06-02 "Title"
release_name=$(awk -v ver="$tag" '
$0 ~ "^## \\[" ver "\\]" {
release_name=$(awk -v search_ver="$changelog_tag" -v display_ver="$tag" '
$0 ~ "^## \\[" search_ver "\\]" {
match($0, /"([^"]+)"/, arr)
if (arr[1] != "") {
print "v" ver " - " arr[1]
print "v" display_ver " - " arr[1]
} else {
print "v" ver
print "v" display_ver
}
}
' CHANGELOG.md)
echo "release_name=${release_name}" >> "$GITHUB_OUTPUT"
awk -v ver="$tag" '
awk -v ver="$changelog_tag" '
$0 ~ "^## \\[" ver "\\]" {found=1; next}
found && $0 ~ "^## \\[" {exit}
found {print}
@@ -1248,34 +1246,11 @@ jobs:
git commit -m "dcr ${{ needs.setup.outputs.version }}"
git push
# ── Snap Store (dcrup) ─────────────────────────────────────────────────────
publish-snap:
if: |
(github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_snap == 'true')
needs: [setup, build-linux-packages]
runs-on: ubuntu-latest
steps:
- name: Download Built Packages
uses: actions/download-artifact@v4
with:
name: snap-package
path: dist-packages
- name: Resolve Snap File
id: snap-file
run: |
snap_file=$(ls dist-packages/*.snap 2>/dev/null | head -1)
if [ -z "$snap_file" ]; then
echo "No .snap file found in dist-packages/"
exit 1
fi
echo "snap=$snap_file" >> "$GITHUB_OUTPUT"
- name: Publish to Snap Store
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
with:
snap: ${{ steps.snap-file.outputs.snap }}
release: stable
# ── Snap Store (dcrup) — temporarily disabled ────────────────────────────
# Classic confinement was denied for this project (too new).
# Snap artifact is still built and attached to the release via
# publish-dexoron-packages. Users can install manually:
# sudo snap install --classic --dangerous dcrup_*.snap
#
# publish-snap:
# if: ...

4
.gitignore vendored
View File

@@ -5,4 +5,6 @@ sandbox/**
.cargo/
.env
personal/
personal/
.*
!.gitignore

View File

@@ -1,5 +1,169 @@
# Changelog
## [0.8.0] - 2026-07-18 "Модульный Движок Сборки, Новая Архитектура Ядра и Улучшение Workspaces / Modular Build Engine, New Core Architecture, & Workspace Improvements"
### RU
**Добавлено:**
- **Выделенное ядро сборки (Build Engine extraction)** — вся логика оркестрации компиляции и сборки полностью вынесена из интерфейсного модуля `src/cli/build.rs` в изолированный, переиспользуемый модуль ядра `src/core/build/engine.rs`.
- **Полиморфная модель языков (Трейт `Language`)** — введение абстрактного трейта `Language` (`src/core/build/language/mod.rs`). Логика поиска файлов, сопоставления расширений и компиляторов распределена по выделенным языковым бэкендам для C, C++ (включая шаги генерации Qt), LLVM IR и ассемблера (GAS, NASM, FASM, MASM).
- **Абстракция сборщиков (Трейт `Builder`)** — введение трейта `Builder` для полиморфного управления процессами компиляции (`src/core/build/builder/mod.rs`). Логика вызова компиляторов семейства GCC/Clang вынесена в общий модуль `builder/cc_common.rs`, а MSVC — в `builder/msvc/`.
- **Поязыковая конфигурация (`[build.<lang>]`)** — добавлена поддержка индивидуальных настроек для каждого языка в `dcr.toml` (например, `[build.c]`, `[build.cpp]`, `[build.asm]`), позволяя независимо настраивать компилятор, стандарт и флаги сборки.
- **Интеграция путей зависимостей в Workspaces** — сборщик теперь автоматически инжектирует пути заголовков (включая исходники `src/` членов, папки `include/` и директорию `target/include`) и библиотек (`target/lib`, а также специфичные для целевой платформы папки сборки) зависимых членов workspace при сборке.
- **Система событий `BuildReporter`** — весь вывод сборщика переведён на событийную модель (`src/core/build/report.rs`), что делает движок пригодным для программного встраивания (например, в IDE) без необходимости перехвата stdout/stderr.
- **Поддержка отмены сборки** — добавлен потокобезопасный токен отмены (`Arc<AtomicBool>`), позволяющий безопасно прервать выполнение компиляции на любом этапе.
- **Сохранение кастомных ключей TOML** — парсер `dcr.toml` переведен на `toml_edit`, что гарантирует сохранение всех неизвестных/пользовательских ключей, комментариев и форматирования при перезаписи файла (например, через `dcr add`).
- **Разрешение заголовков для локальных path-зависимостей** — исправлена проблема, при которой потребители не могли найти заголовочные файлы статических/динамических библиотек, лежащие в упакованной папке `target/include` локальных path-зависимостей.
- **Новая система интеграционных тестов** — старый громоздкий файл `tests/cli_basic.rs` разделен на изолированные и структурированные интеграционные тесты для каждой команды и фичи: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
**Изменено:**
- **Дедупликация генерации артефактов** — логика линковки бинарных файлов (ELF, PE, EXE), динамических библиотек (`.so`, `.dll`, `.dylib`) и архивации статических библиотек (`.a`, `.lib`) полностью перенесена в выделенный модуль `src/core/build/builder/artifact.rs`.
- **Запуск членов workspace из корня** — команда `dcr run` при вызове из корня workspace-only проекта теперь запускает сборку выбранного пакета в контексте workspace (через флаг `--workspace`), исключая изолированные ошибки сборки.
- **Отказ от глобального состояния** — все операции сборки изолированы внутри структуры `BuildContext`, обеспечивая многопоточную безопасность сборщика.
- **Вынос логики кэширования** — проверка инкрементальных изменений, отслеживание заголовков (`#include`) и вычисление хэшей сборки перенесено в изолированный модуль `src/core/build/cache.rs`.
- **Модуляризация кастомных шагов** — обработка этапа `build.steps` для препроцессоров вынесена в `src/core/build/steps.rs`.
### EN
**Added:**
- **Dedicated Build Engine (`src/core/build/engine.rs`)** — decoupled the build orchestration logic completely from the CLI front-end wrapper in `src/cli/build.rs` to a reusable, decoupled core build engine.
- **Polymorphic Language Model (`Language` trait)** — introduced the abstract `Language` trait (`src/core/build/language/mod.rs`). File scanning, compiler resolution, and flags handling are now encapsulated in dedicated language modules for C, C++ (including Qt code generation), LLVM IR, and ASM (GAS, NASM, FASM, MASM).
- **Polymorphic Compilation Dispatch (`Builder` trait)** — introduced the `Builder` trait to generalize builder invocations (`src/core/build/builder/mod.rs`). Consolidated GCC/Clang logic into `builder/cc_common.rs` and Microsoft Visual C++ logic into `builder/msvc/`.
- **Per-Language Configuration overrides (`[build.<lang>]`)** — introduced support for configuring language-specific compiler, standard, and flags (e.g. `[build.c]`, `[build.cpp]`, `[build.asm]`) independently in `dcr.toml`.
- **Automatic Workspace Dependency Injection** — during workspace builds, include/lib paths of dependent workspace members (including their source headers, local `include/` folders, and build target `target/include` / `target/lib` folders) are automatically resolved and injected.
- **`BuildReporter` Event System** — decoupled build orchestration output into a structured event-driven reporter model (`src/core/build/report.rs`), making DCR suitable for library embedding and IDE integrations without stderr capturing.
- **Build Cancellation Support** — introduced thread-safe cancellation tokens (`Arc<AtomicBool>`) allowing clients to safely abort compile runs mid-execution.
- **TOML Formatting & Custom Keys Preservation** — migrated the config editor to `toml_edit`, preserving all unknown/user-defined TOML keys, structures, and comments during file write operations (e.g., in `dcr add`).
- **Path Dependencies Target Include Resolution** — local path dependencies configured via tables now correctly expose headers built and packaged into their `target/include` folder to consumer packages.
- **Modular Integration Test Suite** — refactored the monolith `tests/cli_basic.rs` file into clean, specialized integration tests: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
**Changed:**
- **Consolidated Linking and Archiving** — artifact generation (linking executables/shared libraries and archiving static libraries) consolidated into a single `src/core/build/builder/artifact.rs` module.
- **Workspace-Aware member execution** — `dcr run` executed at the workspace root of a `workspace_only` project now triggers the member build inside the workspace context using the `--workspace` parameter, preventing standalone build issues.
- **Elimination of Global State** — removed global variables and states from the build core, encapsulating build logic inside isolated `BuildContext` structs for thread safety.
- **Build Cache Separation** — relocated mtime tracking, incremental caching, and header-dependency checking logic to `src/core/build/cache.rs`.
- **Modularized Pre-build Steps** — decoupled generator scripts and pre-build commands parsing into `src/core/build/steps.rs`.
## [0.7.4] - 2026-06-17 "Qt Поддержка, Linting и Рефакторинг Архитектуры / Qt Support, Linting, & Architecture Refactor"
### RU
**Добавлено:**
- **Нативная поддержка Qt** — добавлена автоматическая обработка мета-объектов (MOC, UIC, RCC) при включении `build.qt = true` в `dcr.toml`.
- **Команда `dcr lint`** — статический анализ C/C++ через `clang-tidy`. Поддерживает `--fix` для автоматического исправления. Уважает `build.roots` и `build.src_disable` из конфига.
- **Три новых ассемблерных бэкенда**:
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
- **LLVM IR** (`compiler = "llc"`) — компиляция `.ll` → объектный файл.
- **Общий ASM-пайплайн** — линковка и архивирование вынесены в `core/builder/asm.rs`. Новый ассемблерный бэкенд теперь пишется за ~30 строк.
- **Утилиты для файловой системы** — `to_hex()` и `home_dir()` вынесены в `utils/fs.rs`.
- **Утилиты для сборки** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` вынесены в `utils/build.rs`.
- **Утилиты для бэкендов** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` добавлены в `core/builder/common.rs`.
- **Полноценная автоматизация Bare-Metal / Freestanding** — добавлен конфигурационный флаг `build.freestanding = true`. При его включении (или при обнаружении bare-metal таргета) DCR автоматически инжектирует флаг `-ffreestanding` на этапе компиляции, а также `-nostdlib` и `-static` на этапе линковки.
- **Оптимизация артефактов (LTO и Strip)** — в `dcr.toml` добавлены параметры `build.lto` (авто-инжекция флага `-flto` для компилятора и линкера) и `build.strip` (автоматическое отсекание отладочной информации через флаг `-s` линкера).
- **Контроль многопоточности сборки** — добавлен параметр `build.codegen-units`, позволяющий жестко ограничить максимальное количество параллельных потоков сборки для кастомного движка `parallel_build`.
- **Управление паникой (Panic Abort)** — добавлен параметр `build.panic = "abort"`. Для C++ проектов он автоматически отключает генерацию таблиц раскрутки стека и исключений через флаги `-fno-exceptions`, `-fno-unwind-tables` и `-fno-asynchronous-unwind-tables`.
**Изменено:**
- **Интеллектуальные дефолтные флаги (DCR Defaults)** — генерация дефолтных флагов оптимизации (`-O3`/`-O0`), отладки (`-g`) и предупреждений (`-Wall -Wextra`) теперь автоматически отключается для bare-metal и freestanding целей, если пользователь переопределил `build.cflags`.
- **Глобальный вынос bare-metal детектора** — функция `is_bare_metal_target` вынесена в общие утилиты `src/utils/build.rs` для сквозного использования в конвейере сборки.
- **Рефакторинг архитектуры**:
- `src/config.rs` переименован в `src/templates.rs`.
- Начата консолидация конфигурационной логики и оркестрации сборки.
### EN
**Added:**
- **Native Qt support** — automatic meta-object handling (MOC, UIC, RCC) added when `build.qt = true` is set in `dcr.toml`.
- **`dcr lint` command** — C/C++ static analysis via `clang-tidy`. Supports `--fix` for automatic fixes. Respects `build.roots` and `build.src_disable` from config.
- **Three new assembler backends**:
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
- **LLVM IR** (`compiler = "llc"`) — compiles `.ll` files via `llc -filetype=obj`.
- **Shared ASM pipeline** — linking and archiving logic consolidated into `core/builder/asm.rs`. Adding a new assembler backend now takes ~30 lines.
- **Filesystem utilities** — `to_hex()` and `home_dir()` extracted to `utils/fs.rs`.
- **Build utilities** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` extracted to `utils/build.rs`.
- **Backend helpers** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` added to `core/builder/common.rs`.
- **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking.
- **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`.
- **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool.
- **Panic Behavior Management** — added `build.panic = "abort"` support. For C++ targets, it automatically strips exception handling and unwind tables via `-fno-exceptions`, `-fno-unwind-tables`, and `-fno-asynchronous-unwind-tables`.
**Changed:**
- **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden.
- **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core.
- **Architectural Refactoring**:
- Renamed `src/config.rs` to `src/templates.rs`.
- Started consolidation of configuration and build orchestration logic.
## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS"
### RU
**Добавлено:**
- **Параметр `--vcs` для команд `new` и `init`** — добавлена возможность явно указывать систему контроля версий через `--vcs <git|none>`.
- **Интеграция Git-информации в `--version`** — утилита теперь выводит текущий короткий хэш коммита и суффикс `-dirty`, если в репозитории DCR есть незакоммиченные изменения.
- **Автоматическая генерация `.gitignore`** — при инициализации Git-репозитория автоматически создается файл `.gitignore` с исключением директории `/target`.
- **Защита от вложенных репозиториев** — автоматическая инициализация Git теперь пропускается, если `dcr new` или `dcr init` запускаются внутри уже существующего Git-репозитория.
- **Тесты для верификации VCS** — добавлены интеграционные тесты `new_vcs_options_work` и `init_vcs_options_work` для проверки логики инициализации репозиториев.
**Изменено:**
- **Полный отказ от библиотеки `git2`** — работа с Git (клонирование зависимостей, инициализация VCS, проверка статуса) переведена на вызовы системного `git` через `Command`. Это облегчает сборку проекта и убирает необходимость в фичах `vendored` и `openssl`.
- **Миграция с `reqwest` на `ureq`** — в модуле обновления (`flag_update.rs`) тяжелый `reqwest` заменен на легковесный синхронный клиент `ureq 2.10`, что уменьшило оверхед и размер итогового бинарника.
### EN
**Added:**
- **`--vcs` option for `new` and `init` commands** — introduced explicit version control system selection via `--vcs <git|none>`.
- **Git metadata integration in `--version`** — the CLI now appends the current short commit hash and a `-dirty` suffix if there are uncommitted changes in the DCR repository.
- **Automatic `.gitignore` generation** — Git repository initialization now automatically writes a `.gitignore` file excluding the `/target` directory.
- **Nested repository prevention** — automatic Git initialization is now skipped if `dcr new` or `dcr init` is executed inside an already existing Git repository.
- **VCS verification tests** — added `new_vcs_options_work` and `init_vcs_options_work` integration tests to ensure reliable repository behavior.
**Changed:**
- **Complete removal of `git2` dependency** — all Git actions (dependency fetching, VCS setup, status checks) are now delegated to the system `git` executable via `Command`. This simplifies compilation and drops the need for `vendored` and `openssl` features.
- **Migrated from `reqwest` to `ureq`** — replaced the heavy `reqwest` crate in `flag_update.rs` with the lightweight synchronous `ureq 2.10` client, reducing overhead and the final binary size.
## [0.7.2] - 2026-06-07 "Исправление Таргетов и Линковки / Target & Linker Fixes"
### RU
**Добавлено:**
- Учтён `target_env` в `default_target_triple()` — Linux (gnu/musl) и Windows (msvc/gnu) теперь используют правильное окружение, а не хардкод.
**Исправлено:**
- **Сборка на macOS Apple Silicon падала с `_main` undefined** — `--target=` теперь передаётся и в ldflags, не только в cflags. `default_target_triple()` на macOS использует `std::env::consts::ARCH` вместо хардкода `x86_64`.
- **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
**Изменено:**
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
### EN
**Added:**
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
**Fixed:**
- **macOS Apple Silicon builds failed with `_main` undefined** — `--target=` is now passed to ldflags, not just cflags. `default_target_triple()` uses `std::env::consts::ARCH` on macOS instead of hardcoded `x86_64`.
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
**Changed:**
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean.
## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
### RU
@@ -12,7 +176,7 @@
- Оптимизация сборки: `opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, `strip = true`.
- Поддержка новых архитектур (i686, armv7, riscv64gc, aarch64) в рабочих процессах и установочных скриптах.
- Сборка пакетов `deb` и `rpm` для всех 5 архитектур Linux через `cargo-zigbuild`.
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и публикация Snap в CI.
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
**Изменено:**
- Функция `validate_package_name` стала публичной.
@@ -20,6 +184,15 @@
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
- Отключена архитектура Windows 32-bit (GNU) из-за проблем со сборкой.
**Исправлено:**
- **`dcr new <invalid-name>` создаёт директорию, затем падает** — валидация теперь происходит до операций с файлами.
- **`dcr init` создаёт dcr.toml, затем падает при невалидном имени директории** — то же исправление.
- **Man-страницы отсутствовали после установки** — теперь устанавливаются через скрипты установки, пакетные менеджеры и Homebrew.
- **Ложное срабатывание на букву диска в Windows** — в парсинге файлов зависимостей.
- **CRLF `\r\n` ломает парсер зависимостей** — теперь обрабатываются смешанные окончания строк.
- **Новая строка после обратного слеша не поглощалась** — в обработке escape-последовательностей `parse_d_file`.
### EN
**Added:**
@@ -30,7 +203,7 @@
- Release profile optimizations (`opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, `strip = true`).
- Linux i686, armv7, riscv64gc, aarch64 architecture support in release workflows and installation scripts.
- Multi-arch `deb` and `rpm` package builds for all 5 Linux architectures via `cargo-zigbuild`.
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and automated Snapcraft publishing in CI.
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
**Changed:**
- `validate_package_name` made public and callable from CLI commands directly.

1071
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
[package]
name = "dcr"
version = "0.7.1"
version = "0.8.0"
edition = "2024"
build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
license = "GPL-3.0-or-later"
repository = "https://github.com/dexoron/dcr"
documentation = "https://dcr.dexoron.su/docs"
homepage = "https://dcr.dexoron.su"
documentation = "https://dcr-tool.ru/docs"
homepage = "https://dcr-tool.ru"
[package.metadata.binstall]
pkg-fmt = "bin"
@@ -61,29 +61,15 @@ assets = [
[dependencies]
glob = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
ureq = { version = "2.10", default-features = false, features = ["json", "native-certs"] }
serde = { version = "1", features = ["derive"] }
self-replace = "1"
sha2 = "0.10"
toml = "1.0.3"
toml_edit = "0.22"
serde_json = "1"
ctrlc = "3"
[features]
default = ["vendored"]
vendored = [
"git2/vendored-libgit2",
"git2/vendored-openssl",
"openssl/vendored",
]
[target.'cfg(not(windows))'.dependencies]
git2 = "0.20.4"
openssl = "0.10"
[target.'cfg(windows)'.dependencies]
git2 = "0.20.4"
[profile.release]
opt-level = "z"
lto = "thin"

View File

@@ -1,22 +1,21 @@
<div align="center">
<h1 align="center">DCR — Dexoron Cargo Realization</h1>
# DCR — Dexoron Cargo Realization
<p align="center"><strong>A Cargo-style build tool for C/C++ projects</strong></p>
**A Cargo-style build tool for C/C++ projects**
<p align="center">
<a href="https://github.com/dexoron/dcr/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/dexoron/dcr/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://github.com/dexoron/dcr/releases/latest"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/dexoron/dcr"></a>
<a href="https://github.com/dexoron/dcr/releases"><img alt="Platform" src="https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey"></a>
<br>
<a href="https://aur.archlinux.org/packages/dcr"><img alt="AUR" src="https://img.shields.io/aur/version/dcr"></a>
<a href="https://crates.io/crates/dcr"><img alt="Crates.io" src="https://img.shields.io/crates/v/dcr"></a>
<a href="https://github.com/dexoron/homebrew-dexoron"><img alt="Homebrew" src="https://img.shields.io/badge/homebrew-dexoron%2Fdexoron-orange"></a>
<br>
<a href="https://github.com/dexoron/dcr/stargazers"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/dexoron/dcr?style=flat"></a>
<a href="LICENSE"><img alt="License: GPL-3.0" src="https://img.shields.io/badge/License-GPL--3.0-blue.svg"></a>
</p>
[![CI](https://github.com/dexoron/dcr/actions/workflows/ci.yml/badge.svg)](https://github.com/dexoron/dcr/actions/workflows/ci.yml)
[![GitHub Release](https://img.shields.io/github/v/release/dexoron/dcr)](https://github.com/dexoron/dcr/releases/latest)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/dexoron/dcr/releases)
<br/>
[![AUR](https://img.shields.io/aur/version/dcr)](https://aur.archlinux.org/packages/dcr)
[![Crates.io](https://img.shields.io/crates/v/dcr)](https://crates.io/crates/dcr)
[![Homebrew](https://img.shields.io/badge/homebrew-dexoron%2Fdexoron-orange)](https://github.com/dexoron/homebrew-dexoron)
[![Snap](https://img.shields.io/badge/snap-dcrup-orange)](https://snapcraft.io/dcrup)
<br/>
[![GitHub Stars](https://img.shields.io/github/stars/dexoron/dcr?style=flat)](https://github.com/dexoron/dcr/stargazers)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](LICENSE)
</div>
<!-- Snap badge removed: classic confinement was denied for this project. -->
---
@@ -38,7 +37,7 @@
<td><b>Linux (Dexoron Packages)</b></td>
<td>
See https://dcr.dexoron.su for settings youre packages manager
See https://pkg.dexoron.su for settings youre packages manager
**Arch Linux:**
@@ -127,8 +126,11 @@ curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
<td><b>Snap (Linux)</b></td>
<td>
> Snap Store publishing temporarily disabled — see [#51671](https://forum.snapcraft.io/t/classic-confinement-request-for-dcrup/51671).
> Download the `.snap` from [GitHub Releases](https://github.com/dexoron/dcr/releases/latest) and install manually:
```sh
sudo snap install dcrup
sudo snap install --classic --dangerous dcrup_*.snap
```
</td>

60
TODO.md
View File

@@ -2,38 +2,35 @@
Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers.
## 🔥 High Priority (Next Releases)
**Legend:** `[ ]` — not implemented | `[-]` — partially implemented | `[x]` — implemented
- [x] **Git dependencies support**
`git = "https://github.com/user/repo.git#branch"` and `git = "..."@tag`
- [ ] **Conan and vcpkg integration**
Automatic download, installation and linking of packages
- [ ] **Full freestanding / embedded support**
`-nostdlib`, linker scripts, bare-metal targets (i686-elf, aarch64-none-elf, etc.)
- [ ] **EFI support**
Dedicated platform configuration for EFI targets
- [ ] **Custom project name**
Override binary name in `dcr.toml`
- [ ] **Custom build steps / targets**
Ability to define custom pre-build, post-build, codegen steps in `dcr.toml`
- [ ] **Improved mixed C/C++/ASM projects**
Better handling of `.S` and `.asm` files with correct flags
- [ ] **dcr publish + official package registry**
Publish and fetch your own libraries easily
## 🔥 High Priority
- [ ] **CLI Output Refactoring & Cleanup**
- [ ] Move all terminal strings to a dedicated module (`src/ui/messages.rs`)
- [ ] Standardize logging formats (`error:`, `warning:`, `status:`)
- [x] Add build execution time statistics to the final output (e.g., `Finished in 4.23s`)
- [ ] **Internationalization (i18n) / Localization**
- [ ] Integrate a lightweight localization engine (`fluent` or `gettext` pure-Rust port)
- [ ] Create initial translation catalogs (`en.ftl`, `ru.ftl`)
- [x] **Self-Update Stability Validation**
- [x] Self-update implemented via `self-replace` + `ureq` in `flag_update.rs`
- [-] **Freestanding / Embedded Automation**
- *`-ffreestanding` injected at compile time, `-nostdlib -static` at link time for bare-metal targets or `build.freestanding = true`. Config option `build.freestanding` (bool) added.*
- [ ] **Conan and vcpkg integration** Automatic download, installation and linking of packages
## ✅ Medium Priority
- [ ] **Static analysis** (`dcr check`)
Integration with clang-tidy, cppcheck, include-what-you-use
- [ ] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
- [x] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
*`dcr fmt` (clang-format) and `dcr lint` (clang-tidy) are implemented*
- [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.)
- [ ] **Better cross-compilation experience**
Ready-to-use toolchains + automatic download
- [ ] **Linker script support** + direct linker control
- [ ] **Workspaces improvements**
Per-member include directories, selective builds, better dependency graph
- [ ] **Build-time code generation**
Support for protobuf, flatbuffers, Qt moc, custom generators
- [-] **Workspaces improvements** Per-member include directories, selective builds, better dependency graph
*Basic workspaces with dep ordering + topological sort work; advanced features missing*
- [-] **Build-time code generation** Support for protobuf, flatbuffers, Qt moc, custom generators
*Generic `build.steps` machinery + Qt toolchain (uic/moc/rcc) work; no dedicated protobuf/flatbuffers generators*
- [-] **dcr publish + official package registry** *Registry consumption (fetch, resolve, link) works; `dcr publish` command / local packing missing*
## 📌 Low Priority / Nice to Have
@@ -57,13 +54,12 @@ Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers
---
**Currently in progress:**
- ...
- UI/UX terminal output unification and benchmarking metrics.
**Completed in recent releases:**
- Workspaces
- Cross-compilation
- Extreme binary size optimization (-50% weight reduction via `ureq` + `native-certs`)
- Complete removal of `git2` and `openssl` in favor of system Git execution
- Workspaces & Cross-compilation (`--target <triple>`)
- IDE config generation (VS Code, CLion, compile_commands.json)
- Self-update (`dcr --update`)
- Header tracking
- EFI support & Linker script support (`build.ldscript`)
- Mixed C/C++/ASM compilation with custom `std::thread::scope` work-stealing engine

View File

@@ -18,4 +18,37 @@
fn main() {
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
println!("cargo:rustc-env=DCR_TARGET={target}");
// Let's try to retrieve the commit hash for the version
let commit = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
});
// Check if there are any uncommitted changes in the repository
let is_dirty = std::process::Command::new("git")
.args(["status", "--porcelain"])
.output()
.ok()
.map(|o| o.status.success() && !o.stdout.is_empty())
.unwrap_or(false);
if let Some(commit_hash) = commit {
let dirty_suffix = if is_dirty { "-dirty" } else { "" };
println!(
"cargo:rustc-env=DCR_GIT_INFO= (git {}{})",
commit_hash, dirty_suffix
);
} else {
println!("cargo:rustc-env=DCR_GIT_INFO=");
}
}

View File

@@ -12,8 +12,12 @@ Only ASCII letters, digits, underscores `_` and hyphens `-` are allowed in the p
```bash
dcr new my-app
dcr new my-app --vcs none
```
Flags:
- `--vcs <git|none>` — Initialize version control system (defaults to `git` if available, or `none`).
Generates:
- `dcr.toml` with basic fields
- `src/main.c` with a `main` template
@@ -23,8 +27,11 @@ Generates:
Initializes a DCR project in the current (empty) directory.
```bash
mkdir app && cd app
dcr init
dcr init --vcs none
```
Flags:
- `--vcs <git|none>` — Initialize version control system (defaults to `git` if available, or `none`).
The project name is taken from the current directory name. The directory name must follow the same naming rules as `dcr new`.

View File

@@ -42,3 +42,18 @@ dcr fmt
Processes: `src/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}` and `tests/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}`.
Uses `.clang-format` at the project root (if present), otherwise default clang-format style.
## `dcr lint`
Runs `clang-tidy` on C/C++ source files for static analysis.
```bash
dcr lint # show diagnostics
dcr lint --fix # apply fixes automatically
dcr lint --help # detailed help
```
Processes: `src/**/*.{c,cpp,cxx,cc}` and `tests/**/*.{c,cpp,cxx,cc}`.
Without `--fix`, clang-tidy reports warnings and errors without modifying files.
With `--fix`, clang-tidy applies automatic suggestions in place.

View File

@@ -15,9 +15,9 @@ dcr setup
If `~/.dcr/config.toml` is not found, DCR returns an error. Create it manually:
```toml
[[registry]]
name = "default"
[registry.default]
url = "https://index.dcr.pm"
priority = 1
```
## `dcr --help`

View File

@@ -13,9 +13,9 @@ error: registry not found
**Solution:** Make sure `~/.dcr/config.toml` exists:
```toml
[[registry]]
name = "default"
[registry.default]
url = "https://index.dcr.pm"
priority = 1
```
And check `DCR_INDEX_PATH`:

View File

@@ -6,7 +6,7 @@ sidebar_label: Installation
## Linux (Dexoron Packages)
Configure your package manager at https://dcr.dexoron.su and install:
Configure your package manager at https://pkg.dexoron.su and install:
**Arch Linux**
```sh
@@ -100,7 +100,7 @@ man dcr-build
To use the registry, create `~/.dcr/config.toml`:
```toml
[[registry]]
name = "default"
[registry.default]
url = "https://index.dcr.pm"
priority = 1
```

View File

@@ -10,36 +10,23 @@ DCR itself is [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html
> **Note:** DCR is a build tool, not a library. The GPL applies only to DCR's own source code. Projects built with DCR are not subject to DCR's license — their licensing is determined solely by their own code and dependencies.
## Vendored libraries
DCR statically links the following C libraries through its vendored Rust dependencies:
| Library | License | Bundled by |
|---|---|---|
| [libgit2](https://libgit2.org) | MIT OR Apache-2.0 | `git2` / `libgit2-sys` |
| [libssh2](https://libssh2.org) | MIT OR Apache-2.0 | `libgit2-sys` |
| [zlib](https://zlib.net) | MIT OR Apache-2.0 | `libgit2-sys` |
| [OpenSSL](https://openssl.org) | Apache-2.0 | `openssl-sys` / `openssl-src` |
| [ring](https://github.com/briansmith/ring) | Apache-2.0 AND ISC | `rustls` |
## Rust dependencies
DCR is written in Rust. The following notable Rust crates are statically linked:
| Crate | License |
|---|---|
| [`reqwest`](https://crates.io/crates/reqwest) | MIT OR Apache-2.0 |
| [`rustls`](https://crates.io/crates/rustls) | Apache-2.0 OR ISC OR MIT |
| [`ureq`](https://crates.io/crates/ureq) | MIT OR Apache-2.0 |
| [`serde`](https://crates.io/crates/serde) | MIT OR Apache-2.0 |
| [`toml`](https://crates.io/crates/toml) | MIT OR Apache-2.0 |
| [`toml_edit`](https://crates.io/crates/toml_edit) | MIT OR Apache-2.0 |
| [`serde_json`](https://crates.io/crates/serde_json) | MIT OR Apache-2.0 |
| [`sha2`](https://crates.io/crates/sha2) | MIT OR Apache-2.0 |
| [`git2`](https://crates.io/crates/git2) | MIT OR Apache-2.0 |
| [`openssl`](https://crates.io/crates/openssl) | Apache-2.0 |
| [`tokio`](https://crates.io/crates/tokio) | MIT |
| [`hyper`](https://crates.io/crates/hyper) | MIT |
| [`glob`](https://crates.io/crates/glob) | MIT OR Apache-2.0 |
| [`self-replace`](https://crates.io/crates/self-replace) | MIT OR Apache-2.0 |
| [`ctrlc`](https://crates.io/crates/ctrlc) | MIT OR Apache-2.0 |
Full dependency tree (143 packages) is available in [`Cargo.lock`](https://github.com/dexoron/dcr/blob/main/Cargo.lock). The overwhelming majority are dual-licensed under MIT OR Apache-2.0.
Full dependency tree is available in [`Cargo.lock`](https://github.com/dexoron/dcr/blob/main/Cargo.lock). The overwhelming majority are dual-licensed under MIT OR Apache-2.0.
## Additional credits

View File

@@ -34,6 +34,7 @@ type = "none"
inherit = true
language = "c"
standard = "c11"
workspace_only = true
[workspace.lib-core]
path = "lib-core"

View File

@@ -32,11 +32,37 @@ Set `inherit = false` to disable array inheritance (only profile's own arrays ar
## Built-in profiles
| Profile | Default cflags |
The default flags for each profile are composed from three config fields:
| Field | debug default | release default |
|-------|--------------|----------------|
| `opt_level` | `"0"` | `"3"` |
| `debug` | `true` | `false` |
| `warnings` | `["all", "extra"]` | `[]` |
Which produce the equivalent compiler flags:
| Profile | Effective flags |
|---------|---------------|
| `debug` | `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` |
| `release` | `-O3 -DNDEBUG` |
Additional build options can be toggled per-profile:
```toml
[build.release]
opt_level = "z"
lto = true
strip = true
panic = "abort"
codegen-units = "1"
[build.debug]
opt_level = "1"
debug = false
warnings = ["all", "error"]
```
## Target-specific profiles
```toml

View File

@@ -6,30 +6,60 @@ sidebar_label: Build system
## Compiler Backends
DCR supports 4 compilation backends. Selection is automatic based on file extension.
DCR supports 7 compilation backends. Selection is automatic based on file extension and `build.compiler` value.
| Backend | Files | When used |
|---------|-------|-----------|
| `unix_cc` | `.c`, `.cpp`, `.cxx`, `.cc`, `.S` | gcc/clang on Linux/macOS/BSD |
| `msvc` | `.c`, `.cpp`, `.cxx`, `.cc` | Windows (cl, clang-cl) |
| `gas` | `.s` | ARM/ARM64 assembler (no preprocessor) |
| `nasm` | `.asm` | x86/x86_64 assembler |
| `nasm` | `.asm`, `.s` | x86/x86_64 NASM assembler |
| `masm` | `.asm` | MASM (ml/ml64) on Windows |
| `fasm` | `.asm`, `.fasm` | Flat Assembler |
| `llvm_ir` | `.ll` | LLVM IR via `llc -filetype=obj` |
## Qt Support
DCR provides native Qt support for automatic meta-object handling (MOC, UIC, RCC).
Enable it by setting `build.qt = true` in `dcr.toml`. DCR will automatically detect Qt-related files (`.ui`, `.qrc`, `.h` with `Q_OBJECT`) and process them.
```toml
[build]
qt = true
```
*Note: Requires `qt6` modules (Core, Widgets, Gui, Svg) installed via `pkg-config`.*
Advanced customization is still possible via `build.steps` if special handling is needed:
```toml
[build.steps]
moc = "moc {in} -o {out}"
```
### Unix CC
- Compiler resolved via `resolve_compiler()`: `DCR_COMPILER` > `DCR_CC` > `[toolchain]` > `build.compiler` > `PATH`
- Supports `.d` files for header dependency tracking
- Flags: `-std=`, `-MMD -MF`, `-c -o`, `-I`, `-L`, `-l`
- Conditional flags based on config:
- `freestanding` or bare-metal target: `-ffreestanding` (compile), `-nostdlib -static` (link)
- `lto`: `-flto` (compile + link)
- `panic = "abort"`: `-fno-exceptions` (C++ only), `-fno-unwind-tables`, `-fno-asynchronous-unwind-tables`
### MSVC
- Supports cl.exe and clang-cl.exe
- Flags: `/std:`, `/Fo:`, `/Fe:`, `/I`, `/link`
### GAS / NASM
### GAS / NASM / MASM / FASM / LLVM-IR
- GAS: `-I`, `-c -o`, `--defsym`
- NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32)
- MASM: `/nologo /c /Fo<obj> <source>`
- FASM: `<source> <output>` (output path is the object file)
- LLVM IR: `-filetype=obj <source> -o <output>`
## Incremental Builds
@@ -44,7 +74,7 @@ Three levels of incrementality:
- `thread::scope` for thread pool
- Atomic task queue (`AtomicU64`)
- Mutex on stdout (`OUTPUT_MUTEX`)
- Thread count = `available_parallelism()`
- Thread count = `available_parallelism()`, capped by `build.codegen-units` if set
## Build Kinds

View File

@@ -37,9 +37,19 @@ target = "aarch64-unknown-linux-gnu"
# Auto: cflags += ["--target=aarch64-unknown-linux-gnu"]
```
## Bare-Metal
## Bare-Metal / Freestanding
For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR default flags are disabled (no system include paths, no `-l` libc).
For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR automatically:
1. **Disables default flags** — no system include paths, no `-l` libc
2. **Injects `-ffreestanding`** at compile time and `-nostdlib -static` at link time
You can also enable freestanding mode explicitly:
```toml
[build]
freestanding = true
```
```bash
dcr build --target aarch64-none-elf

View File

@@ -41,7 +41,7 @@ Main project configuration file. Located at the project root.
|-------|----------|-------------|
| `name` | yes | Project name |
| `version` | yes | Semantic version |
| `type` | yes | `app`, `lib`, `none` |
| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) |
| `license` | no | SPDX license identifier |
| `author` | no | Author |
@@ -56,15 +56,19 @@ author = "John Doe"
## [build]
...
- `build.qt` — (bool) Enable automatic Qt meta-object handling (MOC, UIC, RCC). Requires `qt6` modules installed via `pkg-config`.
| Field | Default | Description |
|-------|---------|-------------|
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` |
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` (optional, defaults to `"c"`) |
| `standard` | `"c11"` | C standard (`c11`, `c17`, `c23`) |
| `cxx_standard` | `"c++20"` | C++ standard (`c++17`, `c++20`, `c++23`) |
| `compiler` | `"clang"` | Preferred compiler |
| `cxx_standard` | — | C++ standard (`c++17`, `c++20`, `c++23`) |
| `compiler` | `"clang"` | Preferred compiler (optional, defaults to `"clang"`) |
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` |
| `target` | host | Target triple for cross-compilation |
| `platform` | `"native"` | `native`, `efi`, `freestanding` |
| `platform` | `"native"` | `native`, `efi` |
| `cflags` | `[]` | Additional C/C++/ASM flags |
| `ldflags` | `[]` | Additional linker flags |
| `filename` | `""` | Custom output file name |
@@ -76,13 +80,46 @@ author = "John Doe"
| `inherit` | `false` | Inherit build from workspace root |
| `clean` | `[]` | Glob patterns for custom clean paths |
| `out_dir` | `""` | Custom output directory |
| `workspace_only` | `false` | Workspace-only, not built standalone |
| `workspace_only` | `false` | Workspace-only, not built standalone (no `language`/`compiler` required) |
| `freestanding` | `false` | Compile in freestanding mode (`-ffreestanding` + `-nostdlib -static`) |
| `opt_level` | — | Optimization level: `0`-`3`, `"s"`, `"z"` (derived from profile if omitted) |
| `debug` | profile-based | Emit debug symbols (`-g`): `true` in debug, `false` in release |
| `lto` | `false` | Link-time optimization (`-flto` for both compiler and linker) |
| `strip` | `false` | Strip symbols from output (`-s` in ldflags) |
| `warnings` | `[]` | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`); engine adds `-Wall -Wextra` in debug if empty |
| `panic` | `""` | Panic strategy: `"abort"` disables exceptions and unwind tables |
| `codegen-units` | `""` | Max parallel compilation jobs (`"0"` = auto) |
| `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
- `ldscript` — linker script path
- `build.steps` / `build.post_steps` — codegen steps
## Per-language overrides: `[build.c]`, `[build.cxx]`, `[build.asm]`
Each language can have its own table that overrides the flat `[build]` settings:
```toml
[build]
compiler = "clang"
standard = "c11"
[build.c]
standard = "c23"
compiler = "gcc"
[build.cxx]
standard = "c++23"
compiler = "g++"
[build.asm]
compiler = "nasm"
flags = ["-felf64"]
```
The flat `[build]` acts as fallback; per-language tables take precedence for their language.
Example:
```toml
@@ -93,6 +130,11 @@ cxx_standard = "c++23"
compiler = "clang"
kind = "sharedlib"
cflags = ["-Wall", "-Wextra"]
opt_level = "z"
lto = true
strip = true
panic = "abort"
codegen-units = "2"
```
## [toolchain]

View File

@@ -41,24 +41,22 @@ DCR uses a package registry for dependency lookup by name.
```toml
# ~/.dcr/config.toml
[[registry]]
name = "default"
[registry.default]
url = "https://index.dcr.pm"
priority = 1
```
Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrides the path to `index.json`.
## Git dependencies
DCR clones git repositories via `git2`:
Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying branch/tag/rev for git sources:
- `branch` — switch to a branch
- `tag` — switch to a tag
- `rev` — switch to a specific commit
- `features` — feature flags (parsed, but does not affect build)
Cache path: `~/.dcr/git/<hash>/`.
## Path dependencies
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.

View File

@@ -65,3 +65,21 @@ standard = "c17"
# member inherits language and standard
```
## Workspace-only root (no build of its own)
A workspace root can set `workspace_only = true` — it won't be built itself, and doesn't need `language` or `compiler`:
```toml
[package]
name = "my-workspace"
version = "0.1.0"
[build]
workspace_only = true
kind = "bin"
[workspace]
lib-core = { path = "lib-core" }
app = { path = "app", deps = ["lib-core"] }
```

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::deps::register;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
@@ -45,7 +45,6 @@ pub fn add(args: &[String]) -> i32 {
println!(" Adds a dependency to the project.");
println!();
printc("SOURCES:", BOLD_GREEN);
println!(" user/repo GitHub shorthand");
println!(" github:user/repo GitHub repository");
println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository");
@@ -140,7 +139,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:");
error(" user/repo -> github.com/user/repo");
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");

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::flags;
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::parse_version_info;
use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
@@ -106,7 +105,7 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
}
if arg.starts_with("--") {
let candidate = arg.trim_start_matches("--").to_string();
if flags(&candidate).is_some() {
if !default_profile_flags(&candidate).is_empty() {
if profile.is_some() {
return Err("Duplicate profile flag".to_string());
}
@@ -136,28 +135,7 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.or_else(|| {
Some(if cfg!(target_os = "linux") {
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
format!(
"{}-unknown-{}",
std::env::consts::ARCH,
std::env::consts::OS
)
} else {
"unknown".to_string()
})
});
.or_else(|| Some(default_target_triple()));
if flags.all
&& let Some(workspace) = parse_workspace(
@@ -248,7 +226,7 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
.filter_map(|item| item.as_str())
.map(|s| {
let value = s.replace("{profile}", profile);
substitute_version_vars(&value, config)
substitute_clean_vars(&value, config)
})
.collect::<Vec<String>>(),
None => Vec::new(),
@@ -269,17 +247,11 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
Ok(())
}
fn substitute_version_vars(value: &str, config: &Config) -> String {
fn substitute_clean_vars(value: &str, config: &Config) -> String {
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("");
let info = parse_version_info(version);
value
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
crate::utils::build::substitute_version_vars(value, &info)
}

View File

@@ -17,9 +17,9 @@
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use reqwest::blocking::Client;
use serde::Deserialize;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use std::process::Command;
@@ -72,15 +72,8 @@ pub fn flag_update(args: &[String]) -> i32 {
let current_version = env!("CARGO_PKG_VERSION");
let target = env!("DCR_TARGET");
let client = match Client::builder().user_agent("dcr-updater").build() {
Ok(client) => client,
Err(_) => {
error("Failed to initialize HTTP client");
return 1;
}
};
let release = match fetch_latest_release(&client) {
let release = match fetch_latest_release() {
Ok(release) => release,
Err(err) => {
error(&format!("Failed to check for updates: {err}"));
@@ -104,7 +97,7 @@ pub fn flag_update(args: &[String]) -> i32 {
return 1;
};
let bytes = match download_asset(&client, &asset.browser_download_url) {
let bytes = match download_asset(&asset.browser_download_url) {
Ok(bytes) => bytes,
Err(err) => {
error(&format!("Failed to download update: {err}"));
@@ -131,35 +124,35 @@ pub fn flag_update(args: &[String]) -> i32 {
0
}
fn fetch_latest_release(client: &Client) -> Result<Release, String> {
let response = client
.get(LATEST_RELEASE_URL)
.send()
.map_err(|_| "GitHub API request failed".to_string())?;
if !response.status().is_success() {
return Err(format!("GitHub API returned status {}", response.status()));
}
fn fetch_latest_release() -> Result<Release, String> {
let response = ureq::get(LATEST_RELEASE_URL)
.set("User-Agent", "dcr-updater")
.call()
.map_err(|e| match e {
ureq::Error::Status(code, _) => format!("GitHub API returned status {code}"),
ureq::Error::Transport(e) => format!("GitHub API request failed: {e}"),
})?;
response
.json::<Release>()
.into_json::<Release>()
.map_err(|_| "GitHub API response has an unexpected format".to_string())
}
fn download_asset(client: &Client, url: &str) -> Result<Vec<u8>, String> {
let response = client
.get(url)
.send()
.map_err(|_| "Download request failed".to_string())?;
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
let response = ureq::get(url)
.set("User-Agent", "dcr-updater")
.call()
.map_err(|e| match e {
ureq::Error::Status(code, _) => format!("Download returned status {code}"),
ureq::Error::Transport(e) => format!("Download request failed: {e}"),
})?;
if !response.status().is_success() {
return Err(format!("Download returned status {}", response.status()));
}
response
.bytes()
.map(|bytes| bytes.to_vec())
.map_err(|_| "Failed to read downloaded data".to_string())
let mut reader = response.into_reader();
let mut data = Vec::new();
reader
.read_to_end(&mut data)
.map_err(|_| "Failed to read downloaded data".to_string())?;
Ok(data)
}
fn asset_candidates(target: &str) -> Vec<String> {

View File

@@ -15,15 +15,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::collect_sources;
use crate::core::builder::common;
use crate::core::config::Config;
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::collect_sources;
use crate::core::build::common;
use crate::core::build_config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::{
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,
get_list_with_profile, get_string_with_profile, normalize_target_os, resolve_compiler,
resolve_pkg_config_flags_lossy,
get_list_with_profile, get_string_with_profile, normalize_kind, normalize_platform,
normalize_target, resolve_compiler, resolve_pkg_config_flags_lossy,
};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
@@ -366,7 +366,13 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
platform: normalize_platform(&platform),
linker: resolved_linker.as_deref(),
archiver: resolved_archiver.as_deref(),
moc: None,
uic: None,
rcc: None,
package_type: None,
freestanding: false,
panic_abort: false,
codegen_units: 0,
source_roots: &source_roots,
exclude_dirs: &combined_excludes,
include_paths: &combined_includes,
@@ -378,6 +384,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
output_filename: None,
output_extension: None,
verbose: false,
qt: false,
};
let sources = collect_sources(&ctx).map_err(|e| format!("Failed to collect sources: {e}"))?;
@@ -603,7 +610,7 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
cmd.push("-c".to_string());
// ASM x flag — must be before source file
if let Some(flag) = asm_lang_flag(source) {
if let Some(flag) = common::asm_lang_flag(source) {
cmd.push("-x".to_string());
cmd.push(flag.to_string());
}
@@ -681,15 +688,6 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
cmd
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
fn strip_src_prefix(p: &Path) -> PathBuf {
// Try to strip leading ./src or src
let s = p.to_string_lossy();
@@ -1238,25 +1236,6 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
Ok((root, profile))
}
fn normalize_target(s: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(s.trim());
if trimmed.is_empty() {
None
} else {
Some(format!("target/{trimmed}/{profile}"))
}
}
fn normalize_kind(s: &str) -> &str {
let t = s.trim();
if t.is_empty() { "bin" } else { t }
}
fn normalize_platform(s: &str) -> Option<&str> {
let t = s.trim();
if t.is_empty() { None } else { Some(t) }
}
fn json_str(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 2);
result.push('"');

View File

@@ -16,7 +16,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C;
use crate::core::config::{Config, validate_package_name};
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};
@@ -27,7 +28,7 @@ use toml::Value;
pub fn init(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr init", BOLD_CYAN);
printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Initializes the current directory as a DCR project.");
@@ -35,7 +36,25 @@ pub fn init(args: &[String]) -> i32 {
return 0;
}
if !args.is_empty() {
let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--vcs" {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
vcs_str = Some(stripped.to_string());
} else {
clean_args.push(arg.clone());
}
}
if !clean_args.is_empty() {
warn("Command does not support additional arguments");
return 1;
}
@@ -106,6 +125,40 @@ pub fn init(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN)
);
// settings VCS
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
return 1;
}
}
} else if let Ok(cwd_path) = std::env::current_dir()
&& crate::core::vcs::find_existing_vcs(&cwd_path).is_some()
{
vcs_kind = VcsKind::None;
}
if vcs_kind == VcsKind::Git {
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));
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!(
"Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)

189
src/cli/lint.rs Normal file
View File

@@ -0,0 +1,189 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::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;
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");
return 0;
}
let do_fix = args.iter().any(|a| a == "--fix");
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(r)) => r,
Ok(None) => {
error("dcr.toml not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
return 1;
}
};
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}"));
return 1;
}
};
let profile = "debug";
let build_roots = get_list_with_profile(&config, "roots", profile);
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
let mut source_roots: Vec<String> = Vec::new();
for raw in &build_roots {
let p = Path::new(raw);
if p.is_absolute() {
source_roots.push(raw.clone());
} else {
source_roots.push(root.join(p).to_string_lossy().to_string());
}
}
if !src_disable && source_roots.is_empty() {
source_roots.push(root.join("src").to_string_lossy().to_string());
}
let cxx_extensions = ["c", "cpp", "cxx", "cc"];
let exclude_dirs: Vec<PathBuf> = Vec::new();
let include_paths: Vec<String> = Vec::new();
let roots: Vec<PathBuf> = source_roots.iter().map(PathBuf::from).collect();
let mut files =
match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) {
Ok(f) => f,
Err(e) => {
error(&format!("Failed to collect source files: {e}"));
return 1;
}
};
let tests_dir = root.join("tests");
if tests_dir.is_dir() {
let test_roots = vec![tests_dir];
if let Ok(mut test_files) =
common::collect_sources(&test_roots, &cxx_extensions, &exclude_dirs, &include_paths)
{
files.append(&mut test_files);
}
}
files.sort();
if files.is_empty() {
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
return 0;
}
println!(
" {} {} files {}",
colored("Linting", BOLD_GREEN),
files.len(),
if do_fix { "(with --fix)" } else { "" }
);
let mut total_issues = 0u32;
let mut failed = 0u32;
for file in &files {
let mut cmd = Command::new("clang-tidy");
if do_fix {
cmd.arg("--fix");
}
cmd.arg("--quiet");
cmd.arg(file);
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
error(&format!("Failed to execute clang-tidy: {}", e));
return 1;
}
};
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}{}", stdout, stderr);
let issues: Vec<&str> = combined
.lines()
.filter(|l| l.contains("warning:") || l.contains("error:"))
.collect();
let issue_count = issues.len();
if issue_count > 0 {
total_issues += issue_count as u32;
failed += 1;
print!(" {} ", colored("×", BOLD_YELLOW));
println!("{} ({} issues)", file, issue_count);
for issue in &issues {
println!(" {}", issue);
}
} else {
print!(" {} ", colored("✓", BOLD_GREEN));
println!("{}", file);
}
}
let total = files.len();
let passed = total - failed as usize;
if failed == 0 {
println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
total
);
} else {
println!(
" {} {}/{} files passed, {} issues found",
colored("Lint", BOLD_YELLOW),
passed,
total,
total_issues
);
}
0
}

View File

@@ -24,6 +24,7 @@ pub mod fmt;
pub mod r#gen;
pub mod help;
pub mod init;
pub mod lint;
pub mod new;
pub mod run;
pub mod setup;

View File

@@ -16,7 +16,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C;
use crate::core::config::{Config, validate_package_name};
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};
@@ -27,7 +28,7 @@ use toml::Value;
pub fn new(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr new <name>", BOLD_CYAN);
printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Creates a new C/C++ project with the given name.");
@@ -37,16 +38,34 @@ pub fn new(args: &[String]) -> i32 {
let items = check_dir(None).unwrap_or_default();
if args.is_empty() {
let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--vcs" {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
vcs_str = Some(stripped.to_string());
} else {
clean_args.push(arg.clone());
}
}
if clean_args.is_empty() {
error("Project name not specified");
return 1;
}
if args.len() > 1 {
if clean_args.len() > 1 {
warn("Command does not support additional arguments");
return 1;
}
let project_name = &args[0];
let project_name = &clean_args[0];
println!(
"Creating a Project `{}`...",
colored(project_name, BOLD_CYAN)
@@ -127,6 +146,40 @@ pub fn new(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN)
);
// settings VCS
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
return 1;
}
}
} else if let Ok(cwd) = std::env::current_dir()
&& crate::core::vcs::find_existing_vcs(&cwd).is_some()
{
vcs_kind = VcsKind::None;
}
if vcs_kind == VcsKind::Git {
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));
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!(
"Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN)

View File

@@ -17,9 +17,11 @@
use crate::cli::build::build;
use crate::cli::flags::parse_build_run_flags;
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::runner::run_binary;
use crate::utils::build::{normalize_target_os, parse_version_info};
use crate::utils::build::{
default_target_triple, 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;
@@ -178,27 +180,7 @@ fn run_project(
// If no target specified, use default host target for target-specific config
let mut target = flags.target.clone();
if target.is_none() {
let default_target = if cfg!(target_os = "linux") {
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
format!(
"{}-unknown-{}",
std::env::consts::ARCH,
std::env::consts::OS
)
} else {
"unknown".to_string()
};
target = Some(default_target);
target = Some(default_target_triple());
}
let build_kind = config
@@ -215,11 +197,11 @@ fn run_project(
match workspace_root {
Some(wr) => target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile))
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile))
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
None => target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)),
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)),
}
};
@@ -236,7 +218,14 @@ fn run_project(
return Err("Cannot run library build".to_string());
}
let build_status = build(&args_for_build(flags));
let build_status = if let Some(wroot) = workspace_root {
with_dir(wroot, || {
Ok(build(&args_for_workspace_build(flags, project_name)))
})
.unwrap_or(1)
} else {
build(&args_for_build(flags))
};
let bin_path = crate::platform::bin_path(
&flags.profile,
project_name,
@@ -284,6 +273,36 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
args.push("--workspace".to_string());
args.push(name.clone());
}
if flags.force {
args.push("--force".to_string());
}
if flags.clean {
args.push("--clean".to_string());
}
if flags.verbose {
args.push("--verbose".to_string());
}
args
}
fn args_for_workspace_build(
flags: &crate::cli::flags::BuildRunFlags,
member_name: &str,
) -> Vec<String> {
let mut args = Vec::new();
args.push(format!("--{}", flags.profile));
if let Some(ref target) = flags.target {
args.push("--target".to_string());
args.push(target.clone());
}
args.push("--workspace".to_string());
args.push(member_name.to_string());
if flags.force {
args.push("--force".to_string());
}
if flags.clean {
args.push("--clean".to_string());
}
if flags.verbose {
args.push("--verbose".to_string());
}
@@ -305,11 +324,5 @@ fn run_shell(cmd: &str) -> i32 {
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
let info = parse_version_info(version);
cmd.replace("{profile}", profile)
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
substitute_vars(cmd, &info, profile, "")
}

View File

@@ -17,7 +17,7 @@
use crate::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::build::{
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::collections::HashSet;

View File

@@ -97,16 +97,6 @@ int main(int argc, char **argv) {
"#;
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
match profile {
"release" => Some(&["-O3", "-DNDEBUG"]),
"debug" => Some(&[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
]),
_ => None,
}
let f = crate::utils::build::default_profile_flags(profile);
if f.is_empty() { None } else { Some(f) }
}

View File

@@ -0,0 +1,126 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::platform;
use crate::utils::build::is_bare_metal_target;
use std::process::Command;
use std::time::Instant;
pub(crate) fn archive_static(
ctx: &BuildContext,
objects: &[String],
start_time: Instant,
) -> Result<f64, String> {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(objects, &lib_path) {
return Ok(common::elapsed_secs(start_time));
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in objects {
cmd.arg(obj);
}
run(ctx, cmd, start_time)
}
pub(crate) fn link_binary(
ctx: &BuildContext,
objects: &[String],
default_linker: &str,
start_time: Instant,
) -> Result<f64, String> {
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
if ctx.kind == "efi" {
cmd.arg("-shared");
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
} else if ctx.freestanding || is_bare_metal_target(ctx.target) {
cmd.arg("-nostdlib");
cmd.arg("-static");
}
for obj in objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "efi" {
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
cmd.arg("-o").arg(out_path);
run(ctx, cmd, start_time)
}
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
if output.status.success() {
Ok(common::elapsed_secs(start_time))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
}
}

View File

@@ -0,0 +1,213 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::utils::build::is_bare_metal_target;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let qt_include_path = if ctx.qt {
crate::core::build::language::cxx::qt::process_qt(ctx, &obj_dir)?
} else {
None
};
let mut all_source_roots = ctx.source_roots.to_vec();
if let Some(qt_path) = &qt_include_path {
all_source_roots.push(qt_path.clone());
}
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
&all_source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?;
if ctx.kind == "none" || ctx.kind == "custom" {
return Ok(common::elapsed_secs(start_time));
}
if ctx.kind == "staticlib" {
return artifact::archive_static(ctx, &objects, start_time);
}
artifact::link_binary(ctx, &objects, compiler, start_time)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::build::common::source_extensions(language)
}
fn default_flags(profile: &str) -> &'static [&'static str] {
crate::utils::build::default_profile_flags(profile)
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
qt_include_dir: Option<PathBuf>,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(
sources.len(),
|i| {
build_object(
compiler,
&sources[i],
&objects[i],
ctx,
qt_include_dir.as_deref(),
)
},
ctx.codegen_units,
)?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
qt_include_dir: Option<&Path>,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("-c");
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
cmd.arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if (ctx.freestanding || is_bare_metal_target(ctx.target))
&& ctx.language.to_lowercase() != "asm"
{
cmd.arg("-ffreestanding");
}
if ctx.panic_abort && ctx.language.to_lowercase() != "asm" {
if ctx.language.contains("++")
|| ctx.language.contains("cpp")
|| ctx.language.contains("cxx")
{
cmd.arg("-fno-exceptions");
}
cmd.arg("-fno-unwind-tables");
cmd.arg("-fno-asynchronous-unwind-tables");
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp && !ctx.cxx_standard.is_empty() {
cmd.arg(format!("-std={}", ctx.cxx_standard));
} else if !is_cpp {
cmd.arg(format!("-std={}", ctx.standard));
}
}
let use_dcr_defaults =
ctx.cflags.is_empty() && !(ctx.freestanding || is_bare_metal_target(ctx.target));
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
if let Some(path) = qt_include_dir {
cmd.arg(format!("-I{}", path.display()));
}
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
crate::core::build::common::asm_lang_flag(source)
}

View File

@@ -0,0 +1,234 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod artifact;
pub mod cc_common;
pub mod msvc;
use crate::core::build::language::asm::{fasm, gas, masm, nasm};
use crate::core::build::language::llvm_ir;
pub struct BuildContext<'a> {
pub profile: &'a str,
pub project_name: &'a str,
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
pub cxx_standard: &'a str,
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub moc: Option<&'a str>,
pub uic: Option<&'a str>,
pub rcc: Option<&'a str>,
pub package_type: Option<&'a str>,
pub freestanding: bool,
pub panic_abort: bool,
pub codegen_units: usize,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String],
pub lib_dirs: &'a [String],
pub libs: &'a [String],
pub cflags: &'a [String],
pub ldflags: &'a [String],
pub output_filename: Option<&'a str>,
pub output_extension: Option<&'a str>,
pub verbose: bool,
pub qt: bool,
}
pub trait Builder {
#[allow(dead_code)]
fn id(&self) -> &'static str;
fn build(&self, ctx: &BuildContext) -> Result<f64, String>;
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String>;
}
struct Cc;
struct Msvc;
struct Nasm;
struct Gas;
struct Masm;
struct Fasm;
struct Llc;
impl Builder for Cc {
fn id(&self) -> &'static str {
"cc"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
cc_common::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
cc_common::collect_sources(ctx)
}
}
impl Builder for Msvc {
fn id(&self) -> &'static str {
"msvc"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
msvc::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
msvc::collect_sources(ctx)
}
}
impl Builder for Nasm {
fn id(&self) -> &'static str {
"nasm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
nasm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
nasm::collect_sources(ctx)
}
}
impl Builder for Gas {
fn id(&self) -> &'static str {
"gas"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
gas::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
gas::collect_sources(ctx)
}
}
impl Builder for Masm {
fn id(&self) -> &'static str {
"masm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
masm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
masm::collect_sources(ctx)
}
}
impl Builder for Fasm {
fn id(&self) -> &'static str {
"fasm"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
fasm::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
fasm::collect_sources(ctx)
}
}
impl Builder for Llc {
fn id(&self) -> &'static str {
"llvm_ir"
}
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
llvm_ir::build(ctx)
}
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
llvm_ir::collect_sources(ctx)
}
}
pub fn builder_for(compiler: &str) -> &'static dyn Builder {
let c = compiler.to_lowercase();
if c.contains("clang-cl") {
return &Msvc;
}
if c == "as" || c.contains("gas") {
return &Gas;
}
if c.contains("llc") {
return &Llc;
}
if c.contains("fasm") {
return &Fasm;
}
if c.contains("nasm") {
return &Nasm;
}
if c == "ml" || c == "ml64" || c.contains("masm") {
return &Masm;
}
if c == "cl" || c.contains("msvc") {
return &Msvc;
}
&Cc
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if !check_compiler_exists(ctx.compiler) {
return Err(format!(
"Compiler not found: {}. Make sure it is installed and available in PATH.",
ctx.compiler
));
}
builder_for(ctx.compiler).build(ctx)
}
fn check_compiler_exists(compiler: &str) -> bool {
let name = if compiler.is_empty() {
"cc"
} else {
match compiler.to_lowercase().as_str() {
"gas" | "gnu-as" => "as",
"nasm" => "nasm",
"fasm" | "fasm64" => "fasm",
"masm" | "ml" | "ml64" => "ml",
_ => compiler,
}
};
std::process::Command::new(name)
.arg("--version")
.output()
.is_ok()
}
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
builder_for(ctx.compiler).collect_sources(ctx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_resolves_by_compiler_name() {
assert_eq!(builder_for("gcc").id(), "cc");
assert_eq!(builder_for("clang").id(), "cc");
assert_eq!(builder_for("clang++").id(), "cc");
assert_eq!(builder_for("").id(), "cc");
assert_eq!(builder_for("nasm").id(), "nasm");
assert_eq!(builder_for("as").id(), "gas");
assert_eq!(builder_for("llc-18").id(), "llvm_ir");
assert_eq!(builder_for("fasm").id(), "fasm");
assert_eq!(builder_for("ml64").id(), "masm");
assert_eq!(builder_for("cl").id(), "msvc");
assert_eq!(builder_for("clang-cl").id(), "msvc");
}
}

View File

@@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::platform;
use std::fs;
use std::path::Path;
@@ -41,10 +41,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
if ctx.kind == "none" || ctx.kind == "custom" {
return Ok(common::elapsed_secs(start_time));
}
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
@@ -57,7 +61,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
@@ -123,17 +127,17 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
cmd.arg(format!("/Fe:{out_path}"));
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
@@ -151,21 +155,8 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => {}
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::build::common::source_extensions(language)
}
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
@@ -228,9 +219,11 @@ fn build_objects(
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
common::parallel_build(
sources.len(),
|i| build_object(compiler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects)
}

254
src/core/build/cache.rs Normal file
View File

@@ -0,0 +1,254 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) fn compute_build_fingerprint(
ctx: &BuildContext,
sources: &[String],
headers: &[PathBuf],
lib_files: &[PathBuf],
) -> Result<String, String> {
let mut hasher = Sha256::new();
hasher.update(ctx.profile.as_bytes());
hasher.update(ctx.project_name.as_bytes());
hasher.update(ctx.compiler.as_bytes());
hasher.update(ctx.language.as_bytes());
hasher.update(ctx.standard.as_bytes());
hasher.update(ctx.kind.as_bytes());
if let Some(v) = ctx.target_dir {
hasher.update(v.as_bytes());
}
if let Some(v) = ctx.platform {
hasher.update(v.as_bytes());
}
if let Some(v) = ctx.linker {
hasher.update(v.as_bytes());
}
if let Some(v) = ctx.archiver {
hasher.update(v.as_bytes());
}
for value in ctx.include_dirs {
hasher.update(value.as_bytes());
}
for value in ctx.lib_dirs {
hasher.update(value.as_bytes());
}
for value in ctx.libs {
hasher.update(value.as_bytes());
}
for value in ctx.cflags {
hasher.update(value.as_bytes());
}
for value in ctx.ldflags {
hasher.update(value.as_bytes());
}
let toml =
fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?;
hasher.update(toml.as_bytes());
if let Ok(lock) = fs::read_to_string("dcr.lock") {
hasher.update(lock.as_bytes());
}
for source in sources {
let path = Path::new(source);
update_hasher_with_file(&mut hasher, path)?;
}
for header in headers {
update_hasher_with_file(&mut hasher, header)?;
}
for lib in lib_files {
update_hasher_with_file(&mut hasher, lib)?;
}
Ok(to_hex(&hasher.finalize()))
}
pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
let output = build_output_path(ctx);
if !Path::new(&output).is_file() {
return false;
}
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
let cached = fs::read_to_string(cache_path).unwrap_or_default();
cached.trim() == fingerprint
}
pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> {
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
if let Some(parent) = cache_path.parent() {
fs::create_dir_all(parent).map_err(|err| format!("Failed to create cache dir: {err}"))?;
}
fs::write(cache_path, format!("{fingerprint}\n"))
.map_err(|err| format!("Failed to write cache: {err}"))
}
fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
match target_dir {
Some(dir) => Path::new(dir).join(".dcr-build.hash"),
None => Path::new("./target").join(profile).join(".dcr-build.hash"),
}
}
fn build_output_path(ctx: &BuildContext) -> String {
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
if ctx.kind == "staticlib" {
return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "sharedlib" {
return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "efi" {
return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "elf" {
return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir);
}
crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
}
pub(crate) fn collect_header_files(
ctx: &BuildContext,
project_root: &Path,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
let mut roots = Vec::new();
if ctx.source_roots.is_empty() {
roots.push(project_root.join("src"));
} else {
roots.extend(ctx.source_roots.iter().cloned());
}
let abs_root = project_root
.canonicalize()
.unwrap_or_else(|_| project_root.to_path_buf());
for dir in ctx.include_dirs {
let p = Path::new(dir).to_path_buf();
if let Ok(abs_p) = p.canonicalize() {
if abs_p.starts_with(&abs_root) {
roots.push(abs_p);
}
} else if p.starts_with(&abs_root) {
roots.push(p);
}
}
for root in roots {
if !root.exists() {
continue;
}
collect_header_files_rec(&root, &mut out, ctx.exclude_dirs, ctx.include_paths)?;
}
out.sort();
out.dedup();
Ok(out)
}
fn collect_header_files_rec(
dir: &Path,
out: &mut Vec<PathBuf>,
exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<(), String> {
if common::is_excluded(dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(());
}
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir error: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if common::is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
continue;
}
collect_header_files_rec(&path, out, exclude_dirs, include_paths)?;
continue;
}
if !path.is_file() {
continue;
}
if common::is_excluded(&path, exclude_dirs, include_paths) {
continue;
}
if is_header_file(&path) {
out.push(path);
}
}
Ok(())
}
fn is_header_file(path: &Path) -> bool {
let ext = path.extension().and_then(|v| v.to_str()).unwrap_or("");
matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc")
}
pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
let mut out = Vec::new();
for dir in ctx.lib_dirs {
let dir_path = Path::new(dir);
for lib in ctx.libs {
for candidate in lib_candidates(lib) {
let path = dir_path.join(candidate);
if path.is_file() {
out.push(path);
}
}
}
}
out
}
fn lib_candidates(name: &str) -> Vec<String> {
if cfg!(target_os = "windows") {
return vec![format!("{name}.lib")];
}
if cfg!(target_os = "macos") {
return vec![
format!("lib{name}.a"),
format!("lib{name}.dylib"),
format!("lib{name}.so"),
];
}
vec![
format!("lib{name}.a"),
format!("lib{name}.so"),
format!("lib{name}.so.0"),
]
}
fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> {
hasher.update(path.to_string_lossy().as_bytes());
let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?;
hasher.update(meta.len().to_le_bytes());
if let Ok(modified) = meta.modified()
&& let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH)
{
hasher.update(duration.as_nanos().to_le_bytes());
}
Ok(())
}
fn to_hex(bytes: &[u8]) -> String {
crate::utils::fs::to_hex(bytes)
}

View File

@@ -19,6 +19,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
@@ -57,9 +58,10 @@ fn collect_sources_rec(
if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
let entries = fs::read_dir(&full_dir)
.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let entry = entry.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
@@ -164,13 +166,44 @@ pub fn has_glob_magic(value: &str) -> bool {
value.chars().any(|c| matches!(c, '*' | '?' | '['))
}
pub fn parallel_build<F>(total_tasks: usize, task_fn: F) -> Result<(), String>
pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
pub fn source_extensions(language: &str) -> Vec<&'static str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
if let Some(lang) = crate::core::build::language::language_for_token(part) {
out.extend(lang.extensions().iter().copied());
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
}
pub fn elapsed_secs(start: Instant) -> f64 {
((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
}
pub fn parallel_build<F>(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String>
where
F: Fn(usize) -> Result<(), String> + Sync + Send,
{
let num_threads = std::thread::available_parallelism()
let available = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let num_threads = if max_jobs > 0 {
max_jobs.min(available)
} else {
available
};
let counter = std::sync::atomic::AtomicUsize::new(0);
let err_msg = std::sync::Mutex::new(None);
@@ -295,7 +328,7 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
return true;
}
} else {
return true; // Missing dependency triggers rebuild
return true;
}
}
}
@@ -501,7 +534,6 @@ mod tests {
let src = dir.join("test.c");
let obj = dir.join("test.o");
fs::write(&src, "int main() {}").unwrap();
// Sleep briefly to ensure mtime difference
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap();
assert!(!needs_rebuild(
@@ -791,4 +823,44 @@ mod tests {
"changed header should trigger rebuild"
);
}
#[test]
fn asm_lang_flag_returns_correct_flag() {
assert_eq!(asm_lang_flag("foo.S"), Some("assembler-with-cpp"));
assert_eq!(asm_lang_flag("bar.s"), Some("assembler"));
assert_eq!(asm_lang_flag("baz.asm"), Some("assembler"));
assert_eq!(asm_lang_flag("main.c"), None);
}
#[test]
fn source_extensions_parses_language_string() {
let c = source_extensions("c");
assert!(c.contains(&"c"));
assert!(!c.contains(&"cpp"));
let cpp = source_extensions("c++");
assert!(cpp.contains(&"cpp"));
assert!(cpp.contains(&"cxx"));
assert!(cpp.contains(&"cc"));
let asm = source_extensions("asm");
assert!(asm.contains(&"s"));
assert!(asm.contains(&"S"));
assert!(asm.contains(&"asm"));
let mixed = source_extensions("c, c++");
assert!(mixed.contains(&"c"));
assert!(mixed.contains(&"cpp"));
let unknown = source_extensions("rust");
assert_eq!(unknown, vec!["c"]);
}
#[test]
fn elapsed_secs_returns_positive() {
let start = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_millis(50));
let elapsed = elapsed_secs(start);
assert!(elapsed > 0.0);
}
}

1084
src/core/build/engine.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use std::path::Path;
use std::time::Instant;
pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
flags
.iter()
.filter(|f| {
let f = f.as_str();
f.starts_with("-I") || f.starts_with("-i") || f == "-g"
})
.cloned()
.collect()
}
pub fn build_assembly<F>(
ctx: &BuildContext,
backend_name: &str,
default_assembler: &str,
extensions: &[&str],
build_object: F,
) -> Result<f64, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err(format!(
"{backend_name} backend requires build.language = \"asm\""
));
}
let assembler = if ctx.compiler.is_empty() {
default_assembler
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = common::collect_sources(
ctx.source_roots,
extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?;
if ctx.kind == "none" || ctx.kind == "custom" {
return Ok(common::elapsed_secs(start_time));
}
if ctx.kind == "staticlib" {
return artifact::archive_static(ctx, &objects, start_time);
}
artifact::link_binary(ctx, &objects, "cc", start_time)
}
fn build_objects_common<F>(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
build_object: &F,
) -> Result<Vec<String>, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, "o"))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(assembler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects)
}

View File

@@ -0,0 +1,47 @@
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;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
&["asm", "fasm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg(obj_path);
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,73 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
// Use full path to as
let as_path = std::process::Command::new("which")
.arg("as")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "as".to_string());
asm::build_assembly(ctx, "GAS", &as_path, &["s"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
&["s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(obj_path);
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,51 @@
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;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "MASM", "ml", &["asm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
&["asm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("/nologo")
.arg("/c")
.arg("/Fo")
.arg(obj_path)
.arg(source);
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,38 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod common;
pub mod fasm;
pub mod gas;
pub mod masm;
pub mod nasm;
use crate::core::build::language::Language;
pub struct Asm;
impl Language for Asm {
fn id(&self) -> &'static str {
"asm"
}
fn extensions(&self) -> &'static [&'static str] {
&["s", "S", "asm"]
}
fn matches_token(&self, token: &str) -> bool {
token.eq_ignore_ascii_case("asm")
}
}

View File

@@ -0,0 +1,107 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed)
common::collect_sources(
ctx.source_roots,
&["asm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let format = nasm_format(ctx.platform);
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(obj_path);
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {
let p = p.to_lowercase().replace('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
#[cfg(target_os = "macos")]
{
return "macho32";
}
#[cfg(target_os = "windows")]
{
return "win32";
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return "elf32";
}
}
}
#[cfg(target_os = "linux")]
{
"elf64"
}
#[cfg(target_os = "macos")]
{
"macho64"
}
#[cfg(target_os = "windows")]
{
"win64"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"elf64"
}
}

View File

@@ -0,0 +1,32 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::language::Language;
pub struct C;
impl Language for C {
fn id(&self) -> &'static str {
"c"
}
fn extensions(&self) -> &'static [&'static str] {
&["c"]
}
fn matches_token(&self, token: &str) -> bool {
token.eq_ignore_ascii_case("c")
}
}

View File

@@ -0,0 +1,34 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod qt;
use crate::core::build::language::Language;
pub struct Cxx;
impl Language for Cxx {
fn id(&self) -> &'static str {
"cxx"
}
fn extensions(&self) -> &'static [&'static str] {
&["cpp", "cxx", "cc"]
}
fn matches_token(&self, token: &str) -> bool {
matches!(token.to_lowercase().as_str(), "c++" | "cpp" | "cxx")
}
}

View File

@@ -0,0 +1,137 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::utils::build::run_pkg_config;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf>, String> {
let qt_dir = build_dir.join("qt");
fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?;
let extensions = vec!["ui", "qrc", "h", "hpp", "hxx"];
let files = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let mut qt_moc_args = Vec::new();
let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"];
for module in qt_modules {
if let Ok(cflags) = run_pkg_config(module, "--cflags") {
for flag in cflags.split_whitespace() {
if flag.starts_with("-I") || flag.starts_with("-D") {
qt_moc_args.push(flag.to_string());
}
}
}
}
let mut generated_any = false;
for file in files {
let path = Path::new(&file);
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"ui" => {
if let Some(uic) = ctx.uic {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("ui_{out_name}.h"));
run_command(
uic,
&[path.to_str().unwrap(), "-o", out_file.to_str().unwrap()],
ctx.verbose,
)?;
generated_any = true;
}
}
"qrc" => {
if let Some(rcc) = ctx.rcc {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("qrc_{out_name}.cpp"));
run_command(
rcc,
&[
path.to_str().unwrap(),
"-name",
out_name,
"-o",
out_file.to_str().unwrap(),
],
ctx.verbose,
)?;
generated_any = true;
}
}
"h" | "hpp" | "hxx" => {
if let Some(moc) = ctx.moc {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
if content.contains("Q_OBJECT") {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("moc_{out_name}.cpp"));
let mut moc_args = Vec::new();
for arg in &qt_moc_args {
moc_args.push(arg.as_str());
}
for inc in ctx.include_paths.iter().chain(ctx.include_dirs.iter()) {
moc_args.push("-I");
moc_args.push(inc);
}
moc_args.push(path.to_str().unwrap());
moc_args.push("-o");
moc_args.push(out_file.to_str().unwrap());
run_command(moc, &moc_args, ctx.verbose)?;
generated_any = true;
}
}
}
_ => {}
}
}
if generated_any {
Ok(Some(qt_dir))
} else {
Ok(None)
}
}
fn run_command(cmd_path: &str, args: &[&str], verbose: bool) -> Result<(), String> {
let mut cmd = Command::new(cmd_path);
cmd.args(args);
if verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(s) if s.success() => Ok(()),
Ok(s) => Err(format!("Command {} failed with status {}", cmd_path, s)),
Err(e) => Err(format!("Failed to execute {}: {}", cmd_path, e)),
}
}

View File

@@ -0,0 +1,58 @@
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;
pub struct LlvmIr;
impl Language for LlvmIr {
fn id(&self) -> &'static str {
"llvm_ir"
}
fn extensions(&self) -> &'static [&'static str] {
&["ll"]
}
fn matches_token(&self, token: &str) -> bool {
matches!(token.to_lowercase().as_str(), "llvm_ir" | "llvm-ir" | "ll")
}
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "LLVM-IR", "llc", &["ll"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
&["ll"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-filetype=obj").arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -0,0 +1,57 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod asm;
pub mod c;
pub mod cxx;
pub mod llvm_ir;
pub trait Language {
fn id(&self) -> &'static str;
fn extensions(&self) -> &'static [&'static str];
fn matches_token(&self, token: &str) -> bool;
}
pub fn languages() -> [&'static dyn Language; 4] {
[&c::C, &cxx::Cxx, &asm::Asm, &llvm_ir::LlvmIr]
}
pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
languages().into_iter().find(|l| l.matches_token(token))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokens_resolve_to_languages() {
assert_eq!(language_for_token("c").unwrap().id(), "c");
assert_eq!(language_for_token("c++").unwrap().id(), "cxx");
assert_eq!(language_for_token("cpp").unwrap().id(), "cxx");
assert_eq!(language_for_token("cxx").unwrap().id(), "cxx");
assert_eq!(language_for_token("asm").unwrap().id(), "asm");
assert!(language_for_token("rust").is_none());
}
#[test]
fn languages_expose_expected_extensions() {
assert_eq!(c::C.extensions(), &["c"]);
assert_eq!(cxx::Cxx.extensions(), &["cpp", "cxx", "cc"]);
assert_eq!(asm::Asm.extensions(), &["s", "S", "asm"]);
}
}

29
src/core/build/mod.rs Normal file
View File

@@ -0,0 +1,29 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod builder;
pub mod cache;
pub mod common;
pub mod engine;
pub mod language;
pub mod report;
pub mod resolve;
pub mod steps;
pub use engine::run_build;
pub use report::{BuildEvent, BuildReporter, BuildRequest};
pub use resolve::get_build_string_with_profile;

83
src/core/build/report.rs Normal file
View File

@@ -0,0 +1,83 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// The reporter decouples the build engine from any particular frontend: the CLI
// renders events as coloured lines, a TUI/GUI would push them into a widget and
// MCP would serialise them to JSON. The engine only ever calls `on_event`.
use std::sync::{Arc, atomic::AtomicBool};
pub struct BuildRequest {
pub profile: String,
pub target: Option<String>,
pub force: bool,
pub verbose: bool,
pub workspace: Option<String>,
pub cancel: Arc<AtomicBool>,
}
#[allow(dead_code)]
pub struct BuildOutcome {
pub secs: f64,
}
pub struct BuildError {
pub message: String,
}
impl From<String> for BuildError {
fn from(message: String) -> Self {
BuildError { message }
}
}
pub enum BuildEvent<'a> {
TargetStart {
index: usize,
total: usize,
target: &'a str,
},
ProjectStart {
name: &'a str,
profile: &'a str,
target: &'a str,
},
DepBuilding {
name: &'a str,
version: &'a str,
},
DepReady {
name: &'a str,
version: &'a str,
rebuilt: bool,
},
Compiling {
name: &'a str,
version: &'a str,
},
CompilerOutput {
stream: &'a str,
text: &'a str,
},
Finished {
secs: f64,
},
}
pub trait BuildReporter {
fn on_event(&mut self, event: BuildEvent<'_>);
}

232
src/core/build/resolve.rs Normal file
View File

@@ -0,0 +1,232 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build_config::Config;
use crate::utils::build::{get_config_str, normalize_target_os, profile_table};
pub(crate) fn get_config_value_raw(
config: &Config,
section: &str,
field: &str,
profile: &str,
target: Option<&str>,
) -> Option<toml::Value> {
// Order: target.profile, profile.target, target, profile, base
let keys = [
target.map(|t| {
format!(
"{}.{}.{}.{}",
section,
normalize_target_os(t),
profile,
field
)
}),
target.map(|t| {
format!(
"{}.{}.{}.{}",
section,
profile,
normalize_target_os(t),
field
)
}),
target.map(|t| format!("{}.{}.{}", section, normalize_target_os(t), field)),
Some(format!("{}.{}.{}", section, profile, field)),
Some(format!("{}.{}", section, field)),
];
for key in keys.into_iter().flatten() {
if let Some(val) = config.get(&key) {
return Some(val.clone());
}
}
None
}
pub(crate) fn get_inherit(
config: &Config,
section: &str,
profile: &str,
target: Option<&str>,
) -> bool {
get_config_value_raw(config, section, "inherit", profile, target)
.and_then(|v| v.as_bool())
.unwrap_or(true)
}
pub(crate) fn get_config_value(
config: &Config,
section: &str,
field: &str,
profile: &str,
target: Option<&str>,
) -> Option<String> {
get_config_value_raw(config, section, field, profile, target)
.and_then(|v| v.as_str().map(|s| s.trim().to_string()))
.filter(|s| !s.is_empty())
}
pub(crate) fn get_string_with_profile_and_target(
config: &Config,
field: &str,
profile: &str,
target: Option<&str>,
) -> String {
if get_inherit(config, "build", profile, target) {
get_config_value(config, "build", field, profile, target)
.unwrap_or_else(|| get_config_str(config, &format!("build.{field}")))
} else {
get_config_value(config, "build", field, profile, target).unwrap_or_default()
}
}
pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
get_string_with_profile_and_target(config, field, profile, None)
}
pub(crate) fn get_lang_string(
config: &Config,
lang: &str,
field: &str,
profile: &str,
target: Option<&str>,
) -> Option<String> {
get_config_value(config, &format!("build.{lang}"), field, profile, target)
}
pub(crate) fn get_lang_list(
config: &Config,
lang: &str,
field: &str,
profile: &str,
target: Option<&str>,
) -> Result<Vec<String>, String> {
match get_config_value_raw(config, &format!("build.{lang}"), field, profile, target) {
Some(val) => parse_string_array(&val, field),
None => Ok(Vec::new()),
}
}
pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
let arr = value
.as_array()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
let mut out = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
out.push(s.to_string());
}
Ok(out)
}
pub(crate) fn get_list_with_profile_and_target(
config: &Config,
field: &str,
profile: &str,
target: Option<&str>,
) -> Result<Vec<String>, String> {
let inherit = get_inherit(config, "build", profile, target);
let mut out = if inherit {
get_config_list(config, &format!("build.{field}"))?
} else {
Vec::new()
};
if let Some(val) = get_config_value_raw(config, "build", field, profile, target) {
if let Some(_arr) = val.as_array() {
let is_base = inherit
&& config
.get(&format!("build.{field}"))
.map(|v| v == &val)
.unwrap_or(false);
if !is_base {
let custom = parse_string_array(&val, &format!("build.{field}"))?;
if inherit {
out.extend(custom);
} else {
out = custom;
}
}
}
} else if inherit {
if let Some(table) = profile_table(config, profile)
&& let Some(value) = table.get(field)
{
let mut extra = parse_string_array(value, &format!("build.{profile}.{field}"))?;
out.append(&mut extra);
}
if let Some(t) = target {
let normalized_t = normalize_target_os(t);
if let Some(table) = profile_table(config, normalized_t)
&& let Some(value) = table.get(field)
{
let mut extra =
parse_string_array(value, &format!("build.{normalized_t}.{field}"))?;
out.append(&mut extra);
}
}
}
Ok(out)
}
pub(crate) fn get_list_with_profile(
config: &Config,
field: &str,
profile: &str,
) -> Result<Vec<String>, String> {
get_list_with_profile_and_target(config, field, profile, None)
}
pub(crate) fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
get_list_with_profile(config, "targets", profile)
}
pub(crate) fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
let value = match config.get(key) {
Some(v) => v,
None => return Ok(Vec::new()),
};
let arr = value
.as_array()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
let mut out = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
out.push(s.to_string());
}
Ok(out)
}
#[cfg(test)]
mod tests {
use crate::utils::build::normalize_target_os;
#[test]
fn test_normalize_target_os() {
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
assert_eq!(
normalize_target_os("x86_64-unknown-linux-gnu"),
"x86_64-unknown-linux-gnu"
);
assert_eq!(normalize_target_os("unknown"), "unknown");
}
}

398
src/core/build/steps.rs Normal file
View File

@@ -0,0 +1,398 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::engine::ToolchainExecs;
use crate::core::build_config::Config;
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
use glob::glob;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub(crate) struct BuildStep {
name: String,
input: String,
output: String,
cmd: String,
}
pub(crate) struct StepVars<'a> {
pub(crate) profile: &'a str,
pub(crate) version: &'a str,
pub(crate) version_major: &'a str,
pub(crate) version_minor: &'a str,
pub(crate) version_patch: &'a str,
pub(crate) version_suffix: &'a str,
pub(crate) version_suffix_dash: &'a str,
}
fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<BuildStep>, String> {
let arr = value
.as_array()
.ok_or_else(|| format!("{key} must be an array"))?;
let mut out = Vec::new();
for item in arr {
let tbl = item
.as_table()
.ok_or_else(|| format!("{key} entries must be tables"))?;
let name = tbl
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let input = tbl
.get("in")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let output = tbl
.get("out")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let cmd = tbl
.get("cmd")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
if name.is_empty() || input.is_empty() || output.is_empty() || cmd.is_empty() {
return Err(format!("{key} entries must include name, in, out, cmd"));
}
out.push(BuildStep {
name,
input,
output,
cmd,
});
}
Ok(out)
}
pub(crate) fn get_build_steps_with_profile(
config: &Config,
field: &str,
profile: &str,
) -> Result<Vec<BuildStep>, String> {
if let Some(table) = profile_table(config, profile)
&& let Some(value) = table.get(field)
{
return get_build_steps_from_value(value, &format!("build.{profile}.{field}"));
}
get_build_steps(config, &format!("build.{field}"))
}
fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String> {
let value = match config.get(key) {
Some(v) => v,
None => return Ok(Vec::new()),
};
get_build_steps_from_value(value, key)
}
use crate::core::build::report::{BuildEvent, BuildReporter};
use std::sync::{Arc, atomic::AtomicBool};
pub(crate) fn run_build_steps(
steps: &[BuildStep],
tools: &ToolchainExecs,
step_flags: &str,
vars: &StepVars,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
) -> Result<(), String> {
for step in steps {
if cancel.load(std::sync::atomic::Ordering::SeqCst) {
return Err("Build interrupted".to_string());
}
run_build_step(step, tools, step_flags, vars, cancel, rep)?;
}
Ok(())
}
fn run_build_step(
step: &BuildStep,
tools: &ToolchainExecs,
step_flags: &str,
vars: &StepVars,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
) -> Result<(), String> {
let input_pattern = expand_step_value(&step.input, "", vars);
let inputs = expand_glob(&input_pattern)?;
if inputs.is_empty() {
return Ok(());
}
let needs_stem = step.output.contains("{stem}");
if inputs.len() > 1 && !needs_stem {
return Err(format!(
"build.steps '{}' output must include {{stem}} for multiple inputs",
step.name
));
}
for input in inputs {
if !input.is_file() {
continue;
}
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
if !should_run_step(&input, &out_path) {
continue;
}
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)
.map_err(|err| format!("Failed to create step output dir: {err}"))?;
}
let cmd = substitute_step_cmd(&step.cmd, &input, &out_path, tools, step_flags, stem, vars);
let status = run_shell_command(&cmd, cancel, rep)
.map_err(|err| format!("Failed to run step '{}': {err}", step.name))?;
if !status.success() {
return Err(format!("Step '{}' failed", step.name));
}
}
Ok(())
}
pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
for pattern in patterns {
for path in expand_glob(pattern)? {
if path.is_file() {
let _ = fs::remove_file(&path);
}
}
}
Ok(())
}
pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?;
for entry in entries {
let path = entry.map_err(|err| format!("glob error: {err}"))?;
out.push(path);
}
Ok(out)
}
pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> {
for pattern in patterns {
let expanded = expand_step_value(pattern, "", vars);
let matches = expand_glob(&expanded)?;
if matches.is_empty() {
return Err(format!("Expected artifact not found: {expanded}"));
}
}
Ok(())
}
fn should_run_step(input: &Path, output: &Path) -> bool {
let in_time = fs::metadata(input).and_then(|m| m.modified());
let out_time = fs::metadata(output).and_then(|m| m.modified());
match (in_time, out_time) {
(Ok(i), Ok(o)) => i > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}
fn substitute_step_cmd(
template: &str,
input: &Path,
output: &Path,
tools: &ToolchainExecs,
step_flags: &str,
stem: &str,
vars: &StepVars,
) -> String {
let info = make_version_info(vars);
let s = substitute_vars(template, &info, vars.profile, "");
s.replace("{in}", &input.to_string_lossy())
.replace("{out}", &output.to_string_lossy())
.replace("{uic}", &tools.uic)
.replace("{moc}", &tools.moc)
.replace("{rcc}", &tools.rcc)
.replace("{cflags}", step_flags)
.replace("{stem}", stem)
}
pub(crate) fn build_step_flags(
cflags: &[String],
include_dirs: &[String],
compiler: &str,
) -> String {
let mut out = Vec::new();
let msvc_style = is_msvc_compiler(compiler) || cflags.iter().any(|f| f.starts_with('/'));
for flag in cflags {
if flag.starts_with("-I") || flag.starts_with("-D") {
out.push(flag.clone());
}
if flag.starts_with("/I") || flag.starts_with("/D") {
out.push(flag.clone());
}
if msvc_style && flag.starts_with("-D") {
out.push(format!("/D{}", flag.trim_start_matches("-D")));
}
}
for dir in include_dirs {
out.push(format!("-I{dir}"));
if msvc_style {
out.push(format!("/I{dir}"));
}
}
out.sort();
out.dedup();
out.into_iter()
.map(quote_step_arg)
.collect::<Vec<_>>()
.join(" ")
}
fn quote_step_arg(arg: String) -> String {
if !arg.chars().any(|c| c.is_whitespace() || c == '"') {
return arg;
}
let escaped = arg.replace('"', "\\\"");
format!("\"{escaped}\"")
}
fn is_msvc_compiler(compiler: &str) -> bool {
let lower = compiler.to_lowercase();
lower.contains("cl.exe")
|| lower == "cl"
|| lower.contains("clang-cl")
|| lower.contains("msvc")
}
fn run_shell_command(
cmd: &str,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
) -> Result<std::process::ExitStatus, std::io::Error> {
let mut child = if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.arg("/C")
.arg(cmd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
} else {
std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
}?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
use std::io::{BufRead, BufReader};
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
let cancel_clone = cancel.clone();
let tx_stdout = tx.clone();
let stdout_thread = std::thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines() {
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
if let Ok(line) = line {
let _ = tx_stdout.send(("stdout", line));
}
}
});
let cancel_clone = cancel.clone();
let tx_stderr = tx.clone();
let stderr_thread = std::thread::spawn(move || {
let reader = BufReader::new(stderr);
for line in reader.lines() {
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
if let Ok(line) = line {
let _ = tx_stderr.send(("stderr", line));
}
}
});
let status = child.wait()?;
let _ = stdout_thread.join();
let _ = stderr_thread.join();
drop(tx);
while let Ok((stream, text)) = rx.recv() {
rep.on_event(BuildEvent::CompilerOutput {
stream,
text: &text,
});
}
Ok(status)
}
pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, String> {
for step in steps {
let input_pattern = expand_step_value(&step.input, "", vars);
let inputs = expand_glob(&input_pattern)?;
if inputs.is_empty() {
continue;
}
let needs_stem = step.output.contains("{stem}");
if inputs.len() > 1 && !needs_stem {
return Err(format!(
"build.steps '{}' output must include {{stem}} for multiple inputs",
step.name
));
}
for input in inputs {
if !input.is_file() {
continue;
}
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
if should_run_step(&input, &out_path) {
return Ok(true);
}
}
}
Ok(false)
}
fn make_version_info(vars: &StepVars) -> VersionInfo {
VersionInfo {
full: vars.version.to_string(),
major: vars.version_major.to_string(),
minor: vars.version_minor.to_string(),
patch: vars.version_patch.to_string(),
suffix: vars.version_suffix.to_string(),
suffix_dash: vars.version_suffix_dash.to_string(),
}
}
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
let info = make_version_info(vars);
let s = substitute_vars(template, &info, vars.profile, "");
s.replace("{stem}", stem)
}

View File

@@ -21,6 +21,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
use toml::map::Map;
use toml_edit::{DocumentMut, Item, Table};
const DEFAULT_VERSION: &str = "0.1.0";
const DEFAULT_LANGUAGE: &str = "c";
@@ -42,6 +43,7 @@ pub enum ConfigError {
Io(std::io::Error),
TomlDe(toml::de::Error),
TomlSer(toml::ser::Error),
TomlEdit(toml_edit::TomlError),
Invalid(String),
}
@@ -51,6 +53,7 @@ impl std::fmt::Display for ConfigError {
ConfigError::Io(err) => write!(f, "I/O error: {err}"),
ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"),
ConfigError::TomlSer(err) => write!(f, "TOML serialize error: {err}"),
ConfigError::TomlEdit(err) => write!(f, "TOML parse error: {err}"),
ConfigError::Invalid(msg) => write!(f, "Invalid config: {msg}"),
}
}
@@ -80,6 +83,10 @@ pub struct Config {
path: PathBuf,
data: Value,
typed: DcrConfig,
/// Editable document mirror of `data`, used for serialization. It preserves
/// comments, formatting, and any keys not modeled by the typed/raw views, so
/// `save()` never drops unknown sections (e.g. `[run]`, `[[build.post_steps]]`).
doc: DocumentMut,
}
#[allow(dead_code)]
@@ -105,14 +112,24 @@ pub struct PackageConfig {
pub pkg_type: Option<String>,
}
fn default_language() -> LanguageConfig {
LanguageConfig::One(DEFAULT_LANGUAGE.to_string())
}
fn default_compiler() -> String {
DEFAULT_COMPILER.to_string()
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct BuildConfig {
#[serde(default = "default_language")]
pub language: LanguageConfig,
#[serde(default)]
pub standard: Option<String>,
#[serde(default)]
pub cxx_standard: Option<String>,
#[serde(default = "default_compiler")]
pub compiler: String,
#[serde(default)]
pub kind: Option<String>,
@@ -138,6 +155,8 @@ pub struct BuildConfig {
pub src_disable: Option<bool>,
#[serde(default)]
pub workspace_only: bool,
#[serde(default)]
pub qt: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
@@ -180,20 +199,29 @@ pub struct WorkspaceMemberConfig {
pub path: String,
#[serde(default)]
pub deps: Vec<String>,
#[serde(default)]
pub main: Option<bool>,
}
impl Config {
pub fn new(path: &str) -> Result<Self, ConfigError> {
let path = PathBuf::from(path);
let data = if path.exists() {
read_toml(&path)?
let (data, doc) = if path.exists() {
load_parts(&path)?
} else {
let default_value = default_toml()?;
write_toml(&path, &default_value)?;
default_value
let content = default_toml_text();
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
fs::write(&path, doc.to_string())?;
let data: Value = toml::from_str(&content)?;
(data, doc)
};
let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
let cfg = Self {
path,
data,
typed,
doc,
};
cfg.validate()?;
Ok(cfg)
}
@@ -203,9 +231,14 @@ impl Config {
if !path.exists() {
return Err(ConfigError::Invalid("dcr.toml not found".into()));
}
let data = read_toml(&path)?;
let (data, doc) = load_parts(&path)?;
let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
let cfg = Self {
path,
data,
typed,
doc,
};
cfg.validate()?;
Ok(cfg)
}
@@ -345,6 +378,13 @@ impl Config {
.as_ref()
.ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?;
// For workspace_only projects, [build] section is optional
// and language/compiler are not required
if build.workspace_only {
self.validate_workspace()?;
return Ok(());
}
if pkg.name.trim().is_empty() {
return Err(ConfigError::Invalid("package.name is empty".into()));
}
@@ -419,22 +459,35 @@ impl Config {
}
pub fn save(&self) -> Result<(), ConfigError> {
write_toml(&self.path, &self.data)
fs::write(&self.path, self.doc.to_string())?;
Ok(())
}
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
let parts: Vec<&str> = key.split('.').collect();
let previous = self.data.clone();
set_path(&mut self.data, &parts, value)?;
let previous_data = self.data.clone();
let previous_doc = self.doc.clone();
if let Err(err) = set_doc_path(&mut self.doc, &parts, &value) {
self.doc = previous_doc;
return Err(err);
}
if let Err(err) = set_path(&mut self.data, &parts, value) {
self.data = previous_data;
self.doc = previous_doc;
return Err(err);
}
self.typed = match parse_typed_config(&self.data) {
Ok(typed) => typed,
Err(err) => {
self.data = previous;
self.data = previous_data;
self.doc = previous_doc;
return Err(err);
}
};
if let Err(err) = self.validate() {
self.data = previous;
self.data = previous_data;
self.doc = previous_doc;
self.typed = parse_typed_config(&self.data)?;
return Err(err);
}
@@ -656,235 +709,119 @@ impl Config {
}
}
fn read_toml(path: &Path) -> Result<Value, ConfigError> {
fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
let content = fs::read_to_string(path)?;
Ok(toml::from_str(&content)?)
let data: Value = toml::from_str(&content)?;
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
Ok((data, doc))
}
fn write_toml(path: &Path, value: &Value) -> Result<(), ConfigError> {
let content = format_toml(value)?;
fs::write(path, content)?;
Ok(())
}
fn default_toml() -> Result<Value, ConfigError> {
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()))
.unwrap_or_else(|| "project".to_string());
let mut package = Map::new();
package.insert("name".to_string(), Value::String(name));
package.insert(
"version".to_string(),
Value::String(DEFAULT_VERSION.to_string()),
);
package.insert("type".to_string(), Value::String("none".to_string()));
package.insert("description".to_string(), Value::String("".to_string()));
package.insert("author".to_string(), Value::String("".to_string()));
package.insert(
"license".to_string(),
Value::String("GPL-3.0-or-later".to_string()),
);
let mut build = Map::new();
build.insert(
"language".to_string(),
Value::String(DEFAULT_LANGUAGE.to_string()),
);
build.insert(
"standard".to_string(),
Value::String(DEFAULT_STANDARD.to_string()),
);
build.insert(
"compiler".to_string(),
Value::String(DEFAULT_COMPILER.to_string()),
);
build.insert("kind".to_string(), Value::String(DEFAULT_KIND.to_string()));
let mut root = Map::new();
root.insert("package".to_string(), Value::Table(package));
root.insert("build".to_string(), Value::Table(build));
root.insert("dependencies".to_string(), Value::Table(Map::new()));
Ok(Value::Table(root))
format!(
"[package]\n\
name = \"{name}\"\n\
version = \"{DEFAULT_VERSION}\"\n\
type = \"none\"\n\
description = \"\"\n\
author = \"\"\n\
license = \"GPL-3.0-or-later\"\n\
\n\
[build]\n\
language = \"{DEFAULT_LANGUAGE}\"\n\
standard = \"{DEFAULT_STANDARD}\"\n\
compiler = \"{DEFAULT_COMPILER}\"\n\
kind = \"{DEFAULT_KIND}\"\n\
\n\
[dependencies]\n"
)
}
fn format_toml(value: &Value) -> Result<String, ConfigError> {
let root = value
.as_table()
.ok_or_else(|| ConfigError::Invalid("root is not a table".into()))?;
let package = root
.get("package")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
let build = root
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
let deps = root
.get("dependencies")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [dependencies]".into()))?;
let toolchain = root.get("toolchain").and_then(|v| v.as_table());
let name = package.get("name").and_then(|v| v.as_str()).unwrap_or("");
let version = package
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("");
let language_value = build.get("language");
let language = match language_value {
Some(Value::String(s)) => s.to_string(),
Some(Value::Array(arr)) => format_string_array(&Value::Array(arr.clone())),
_ => "".to_string(),
};
let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or("");
let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
let mut out = String::new();
out.push_str("[package]\n");
out.push_str(&format!("name = \"{name}\"\n"));
out.push_str(&format!("version = \"{version}\"\n"));
for key in [
"type",
"description",
"author",
"authors",
"homepage",
"license",
"repository",
"readme",
"keywords",
"categories",
] {
if let Some(val) = package.get(key) {
match val {
Value::String(s) => out.push_str(&format!("{key} = \"{s}\"\n")),
Value::Array(_) => out.push_str(&format!("{key} = {}\n", format_string_array(val))),
_ => {}
}
fn set_doc_path(doc: &mut DocumentMut, path: &[&str], value: &Value) -> Result<(), ConfigError> {
let mut current = doc.as_table_mut();
for &key in &path[..path.len().saturating_sub(1)] {
if !current.contains_key(key) {
current.insert(key, Item::Table(Table::new()));
}
current = current
.get_mut(key)
.and_then(|item| item.as_table_mut())
.ok_or_else(|| ConfigError::Invalid(format!("'{key}' is not a table")))?;
}
out.push('\n');
out.push_str("[build]\n");
if language.starts_with('[') {
out.push_str(&format!("language = {language}\n"));
if let Some(&last) = path.last() {
current.insert(last, value_to_item(value));
Ok(())
} else {
out.push_str(&format!("language = \"{language}\"\n"));
Err(ConfigError::Invalid("empty key".into()))
}
if !standard.is_empty() {
out.push_str(&format!("standard = \"{standard}\"\n"));
}
out.push_str(&format!("compiler = \"{compiler}\"\n"));
let kind = build
.get("kind")
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_KIND);
out.push_str(&format!("kind = \"{kind}\"\n"));
if let Some(target) = build.get("target").and_then(|v| v.as_str())
&& !target.trim().is_empty()
{
out.push_str(&format!("target = \"{target}\"\n"));
}
if let Some(out_dir) = build.get("out_dir").and_then(|v| v.as_str())
&& !out_dir.trim().is_empty()
{
out.push_str(&format!("out_dir = \"{out_dir}\"\n"));
}
if let Some(cflags) = build.get("cflags") {
out.push_str(&format!("cflags = {}\n", format_string_array(cflags)));
}
if let Some(ldflags) = build.get("ldflags") {
out.push_str(&format!("ldflags = {}\n", format_string_array(ldflags)));
}
out.push('\n');
if let Some(toolchain) = toolchain {
let mut lines = Vec::new();
for key in ["cc", "cxx", "as", "ar", "ld"] {
if let Some(value) = toolchain.get(key).and_then(|v| v.as_str())
&& !value.trim().is_empty()
{
lines.push(format!("{key} = \"{value}\""));
}
}
if !lines.is_empty() {
out.push_str("[toolchain]\n");
for line in lines {
out.push_str(&format!("{line}\n"));
}
out.push('\n');
}
}
out.push_str("[dependencies]\n");
if !deps.is_empty() {
let mut keys: Vec<&String> = deps.keys().collect();
keys.sort();
for key in keys {
if let Some(val) = deps.get(key) {
out.push_str(&format!("{key} = {}\n", format_dep_value(val)));
}
}
}
Ok(out)
}
fn format_dep_value(value: &Value) -> String {
fn value_to_item(value: &Value) -> Item {
Item::Value(value_to_edit_value(value))
}
fn value_to_edit_value(value: &Value) -> toml_edit::Value {
match value {
Value::String(s) => format!("\"{s}\""),
Value::Table(tbl) => {
let mut parts = Vec::new();
if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) {
parts.push(format!("version = \"{v}\""));
}
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
parts.push(format!("path = \"{v}\""));
}
if let Some(v) = tbl.get("git").and_then(|v| v.as_str()) {
parts.push(format!("git = \"{v}\""));
}
if let Some(v) = tbl.get("branch").and_then(|v| v.as_str()) {
parts.push(format!("branch = \"{v}\""));
}
if let Some(v) = tbl.get("tag").and_then(|v| v.as_str()) {
parts.push(format!("tag = \"{v}\""));
}
if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) {
parts.push(format!("rev = \"{v}\""));
}
if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) {
parts.push(format!(
"default-features = {}",
if v { "true" } else { "false" }
));
}
if let Some(v) = tbl.get("features") {
parts.push(format!("features = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) {
parts.push(format!("system = {}", if v { "true" } else { "false" }));
}
format!("{{ {} }}", parts.join(", "))
Value::String(s) => toml_edit::Value::from(s.clone()),
Value::Integer(i) => toml_edit::Value::from(*i),
Value::Float(f) => toml_edit::Value::from(*f),
Value::Boolean(b) => toml_edit::Value::from(*b),
Value::Datetime(dt) => {
let s = dt.to_string();
s.parse::<toml_edit::Datetime>()
.map(toml_edit::Value::from)
.unwrap_or_else(|_| toml_edit::Value::from(s))
}
Value::Array(arr) => {
let mut out = toml_edit::Array::new();
for item in arr {
out.push(value_to_edit_value(item));
}
toml_edit::Value::Array(out)
}
Value::Table(tbl) => {
let mut out = toml_edit::InlineTable::new();
for key in ordered_table_keys(tbl) {
if let Some(v) = tbl.get(&key) {
out.insert(&key, value_to_edit_value(v));
}
}
toml_edit::Value::InlineTable(out)
}
_ => "\"\"".to_string(),
}
}
fn format_string_array(value: &Value) -> String {
if let Some(arr) = value.as_array() {
let items: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(|s| format!("\"{s}\"")))
.collect();
return format!("[{}]", items.join(", "));
}
"[]".to_string()
const DEP_KEY_ORDER: &[&str] = &[
"version",
"path",
"git",
"branch",
"tag",
"rev",
"default-features",
"features",
"system",
];
fn ordered_table_keys(tbl: &Map<String, Value>) -> Vec<String> {
let mut keys: Vec<String> = DEP_KEY_ORDER
.iter()
.filter(|k| tbl.contains_key(**k))
.map(|k| (*k).to_string())
.collect();
let mut rest: Vec<String> = tbl
.keys()
.filter(|k| !DEP_KEY_ORDER.contains(&k.as_str()))
.cloned()
.collect();
rest.sort();
keys.extend(rest);
keys
}
fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
@@ -1133,6 +1070,84 @@ mod tests {
);
}
#[test]
fn save_preserves_untyped_sections() {
let dir = temp_dir("preserve_untyped");
let path = write_toml_file(
&dir,
"# top comment\n\
[package]\n\
name = \"test\"\n\
version = \"0.1.0\"\n\n\
[build]\n\
language = \"c\"\n\
standard = \"c11\"\n\
compiler = \"clang\"\n\
kind = \"elf\"\n\
filename = \"KERNEL\"\n\
extension = \"ELF\"\n\
inherit = false\n\
include = [\"src/include\"]\n\n\
[[build.post_steps]]\n\
name = \"iso\"\n\
cmd = \"grub-mkrescue\"\n\n\
[run]\n\
cmd = \"qemu-system-aarch64 -kernel KERNEL\"\n\n\
[dependencies]\n",
);
let mut config = Config::open(&path.to_string_lossy()).unwrap();
config
.set("dependencies.zlib", {
let mut tbl = Map::new();
tbl.insert("path".to_string(), Value::String("../zlib".to_string()));
Value::Table(tbl)
})
.unwrap();
// Reload from disk and confirm nothing modeled-but-not-whitelisted was dropped.
let reloaded = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(
reloaded.get("build.filename").and_then(|v| v.as_str()),
Some("KERNEL"),
"build.filename must survive save"
);
assert_eq!(
reloaded.get("build.extension").and_then(|v| v.as_str()),
Some("ELF")
);
assert_eq!(
reloaded.get("build.inherit").and_then(|v| v.as_bool()),
Some(false)
);
assert!(
reloaded
.get("build.include")
.and_then(|v| v.as_array())
.is_some(),
"build.include array must survive save"
);
assert_eq!(
reloaded.get("run.cmd").and_then(|v| v.as_str()),
Some("qemu-system-aarch64 -kernel KERNEL"),
"[run] section must survive save"
);
assert!(
reloaded.get("build.post_steps").is_some(),
"[[build.post_steps]] must survive save"
);
let saved = fs::read_to_string(&path).unwrap();
assert!(
saved.contains("zlib = { path = \"../zlib\" }"),
"dependency must be an inline table, got:\n{saved}"
);
assert!(
saved.contains("# top comment"),
"leading comment must be preserved, got:\n{saved}"
);
}
#[test]
fn new_creates_default_config() {
let dir = temp_dir("new_default");

View File

@@ -1,183 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err("GAS backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
"as"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// GAS handles only lowercase .s files
common::collect_sources(
ctx.source_roots,
&["s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -1,94 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod common;
pub mod gas;
pub mod msvc;
pub mod nasm;
pub mod unix_cc;
pub struct BuildContext<'a> {
pub profile: &'a str,
pub project_name: &'a str,
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
pub cxx_standard: &'a str,
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub package_type: Option<&'a str>,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String],
pub lib_dirs: &'a [String],
pub libs: &'a [String],
pub cflags: &'a [String],
pub ldflags: &'a [String],
pub output_filename: Option<&'a str>,
pub output_extension: Option<&'a str>,
pub verbose: bool,
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = ctx.compiler.to_lowercase();
if !check_compiler_exists(ctx.compiler) {
return Err(format!(
"Compiler not found: {}. Make sure it is installed and available in PATH.",
ctx.compiler
));
}
if compiler.contains("clang-cl") {
return msvc::build(ctx);
}
if compiler == "as" || compiler.contains("gas") {
return gas::build(ctx);
}
if compiler.contains("nasm") {
return nasm::build(ctx);
}
if compiler == "cl" || compiler.contains("msvc") {
return msvc::build(ctx);
}
unix_cc::build(ctx)
}
fn check_compiler_exists(compiler: &str) -> bool {
let name = if compiler.is_empty() { "cc" } else { compiler };
std::process::Command::new(name)
.arg("--version")
.output()
.is_ok()
}
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let compiler = ctx.compiler.to_lowercase();
if compiler.contains("clang-cl") || compiler == "cl" || compiler.contains("msvc") {
return msvc::collect_sources(ctx);
}
if compiler == "as" || compiler.contains("gas") {
return gas::collect_sources(ctx);
}
if compiler.contains("nasm") {
return nasm::collect_sources(ctx);
}
unix_cc::collect_sources(ctx)
}

View File

@@ -1,230 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err("NASM backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
"nasm"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed)
common::collect_sources(
ctx.source_roots,
&["asm", "s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
let format = nasm_format(ctx.platform);
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], format, ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
format: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {
let p = p.to_lowercase().replace('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
#[cfg(target_os = "macos")]
{
return "macho32";
}
#[cfg(target_os = "windows")]
{
return "win32";
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return "elf32";
}
}
}
#[cfg(target_os = "linux")]
{
"elf64"
}
#[cfg(target_os = "macos")]
{
"macho64"
}
#[cfg(target_os = "windows")]
{
"win64"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"elf64"
}
}

View File

@@ -1,285 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
cmd.arg("rcs").arg(&lib_path);
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or(compiler));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
if ctx.kind == "efi" {
cmd.arg("-shared");
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "efi" {
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => out.extend(["s", "S", "asm"]),
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
],
_ => &[],
}
}
fn is_bare_metal_target(target: Option<&str>) -> bool {
if let Some(t) = target {
let lower = t.to_lowercase();
lower.contains("none")
|| lower.contains("-elf")
|| lower.contains("eabi")
|| lower.contains("baremetal")
|| lower.contains("bare-metal")
} else {
false
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("-c");
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
cmd.arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp && !ctx.cxx_standard.is_empty() {
cmd.arg(format!("-std={}", ctx.cxx_standard));
} else if !is_cpp {
cmd.arg(format!("-std={}", ctx.standard));
}
}
let use_dcr_defaults = ctx.cflags.is_empty() && !is_bare_metal_target(ctx.target);
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}

View File

@@ -1,128 +1,6 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[allow(dead_code)]
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum DepSource {
Path(String),
Git {
url: String,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
},
Registry {
version: String,
features: Option<Vec<String>>,
},
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub source: DepSource,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
#[allow(dead_code)]
pub fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
#[allow(dead_code)]
pub fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
#[allow(dead_code)]
pub fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
#[allow(dead_code)]
pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
#[allow(dead_code)]
pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}

View File

@@ -1,71 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::Path;
#[allow(dead_code)]
pub fn fetch_git_dep(
url: &str,
target_dir: &Path,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<(), String> {
if target_dir.exists() {
return Ok(());
}
let mut builder = git2::build::RepoBuilder::new();
if let Some(b) = branch {
builder.branch(b);
}
let repo = builder
.clone(url, target_dir)
.map_err(|e| format!("failed to clone {}: {}", url, e))?;
if let Some(t) = tag {
checkout_ref(&repo, &format!("refs/tags/{}", t))?;
} else if let Some(r) = rev {
checkout_ref(&repo, r)?;
}
Ok(())
}
#[allow(dead_code)]
fn checkout_ref(repo: &git2::Repository, reference_str: &str) -> Result<(), String> {
let (object, reference) = repo
.revparse_ext(reference_str)
.map_err(|e| format!("failed to find ref {}: {}", reference_str, e))?;
repo.checkout_tree(&object, None).map_err(|e| {
let ref_name = reference.as_ref().and_then(|r| r.name()).unwrap_or("HEAD");
format!("failed to checkout {}: {}", ref_name, e)
})?;
match reference {
Some(gref) => {
let ref_name = gref.name().unwrap_or("HEAD");
repo.set_head(ref_name)
}
None => repo.set_head_detached(object.id()),
}
.map_err(|e| format!("failed to set HEAD: {}", e))?;
Ok(())
}

View File

@@ -15,10 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepLock {
@@ -73,65 +71,3 @@ fn quote_list(items: &[String]) -> String {
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
#[allow(dead_code)]
pub fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[allow(dead_code)]
pub fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
#[allow(dead_code)]
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
#[allow(dead_code)]
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

View File

@@ -16,11 +16,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod common;
pub mod git;
pub mod lock;
pub mod register;
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock};
use std::path::Path;
@@ -104,6 +103,10 @@ pub fn resolve_deps(
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists(
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
}
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
@@ -129,6 +132,10 @@ pub fn resolve_deps(
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists(
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
resolved.libs.push(name.clone());
}

View File

@@ -33,13 +33,7 @@ pub struct DcrConfig {
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
crate::utils::fs::home_dir()
}
fn dcr_config_dir() -> Option<PathBuf> {

View File

@@ -15,9 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod builder;
pub mod config;
pub mod build;
pub mod build_config;
pub mod deps;
pub mod registry;
pub mod runner;
pub mod vcs;
pub mod workspace;

View File

@@ -38,13 +38,7 @@ pub struct RegistryManager {
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
crate::utils::fs::home_dir()
}
impl RegistryManager {

32
src/core/vcs/git.rs Normal file
View File

@@ -0,0 +1,32 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
// creating git init and .gitignore
pub fn init(path: &Path) -> Result<(), String> {
crate::utils::git::git_init(path)?;
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
fs::write(&gitignore_path, "/target\n")
.map_err(|e| format!("failed to create .gitignore: {}", e))?;
}
Ok(())
}

61
src/core/vcs/mod.rs Normal file
View File

@@ -0,0 +1,61 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod git;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VcsKind {
Git,
None,
}
impl VcsKind {
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"git" => Ok(Self::Git),
"none" => Ok(Self::None),
_ => Err(format!("Unknown VCS: {}", s)),
}
}
}
pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
match kind {
VcsKind::Git => git::init(path),
VcsKind::None => Ok(()),
}
}
// look for git repo in parents to avoid nested repos
pub fn find_existing_vcs(path: &Path) -> Option<VcsKind> {
let mut current = if path.is_file() {
path.parent()
} else {
Some(path)
};
while let Some(dir) = current {
if dir.join(".git").exists() {
return Some(VcsKind::Git);
}
current = dir.parent();
}
None
}

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::build::normalize_target_os;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
@@ -50,7 +50,6 @@ pub fn parse_workspace(
root: &Path,
) -> Result<Option<Workspace>, String> {
let mut table = None;
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
let combinations = if let Some(t) = target {
let normalized_t = normalize_target_os(t);
vec![

View File

@@ -41,10 +41,16 @@ fn main() {
"tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest),
"fmt" => cli::fmt::fmt(rest),
"lint" => cli::lint::lint(rest),
"clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),
"--version" => {
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET"));
println!(
"dcr {}{} ({})",
env!("CARGO_PKG_VERSION"),
env!("DCR_GIT_INFO"),
env!("DCR_TARGET")
);
0
}
"--help" => cli::help::help(),

View File

@@ -26,6 +26,16 @@ pub mod bsd;
pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
pub mod posix;
#[cfg(target_os = "windows")]
pub mod windows;
@@ -61,10 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
return posix::bin_path(profile, name, target_dir);
}
}
@@ -100,10 +107,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
return posix::elf_path(profile, name, target_dir);
}
}
@@ -139,10 +143,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
}
return posix::efi_path(profile, name, target_dir);
}
}
@@ -178,10 +179,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
}
return posix::lib_path(profile, name, target_dir);
}
}
@@ -217,9 +215,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
}
return posix::shared_lib_path(profile, name, target_dir);
}
}

31
src/platform/posix.rs Normal file
View File

@@ -0,0 +1,31 @@
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
}
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
}
}
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
bin_path(profile, name, target_dir)
}
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
}
}
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
}
}

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::log::warn;
pub struct VersionInfo {
@@ -32,17 +32,16 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
if full.is_empty() {
full = "0.0.0".to_string();
}
let full_clone = full.clone();
let (base, suffix) = match full_clone.split_once('-') {
Some((head, tail)) => (head, tail),
None => (full_clone.as_str(), ""),
let (base, suffix) = match full.split_once('-') {
Some((head, tail)) => (head.to_string(), tail.to_string()),
None => (full.clone(), String::new()),
};
let mut parts = base.split('.');
let major = parts.next().unwrap_or("0").to_string();
let minor = parts.next().unwrap_or("0").to_string();
let patch = parts.next().unwrap_or("0").to_string();
let suffix_dash = if suffix.is_empty() {
"".to_string()
String::new()
} else {
format!("-{suffix}")
};
@@ -51,11 +50,51 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
major,
minor,
patch,
suffix: suffix.to_string(),
suffix,
suffix_dash,
}
}
pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String {
let s = template
.replace("{profile}", profile)
.replace("{name}", name);
substitute_version_vars(&s, info)
}
pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
template
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(target.trim());
if trimmed.is_empty() {
None
} else {
Some(format!("target/{trimmed}/{profile}"))
}
}
pub fn normalize_kind(kind: &str) -> &str {
let trimmed = kind.trim();
if trimmed.is_empty() { "bin" } else { trimmed }
}
pub fn normalize_platform(platform: &str) -> Option<&str> {
let trimmed = platform.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
pub fn normalize_target_os(target: &str) -> &str {
match target {
"linux" => "x86_64-unknown-linux-gnu",
@@ -72,6 +111,53 @@ pub fn normalize_target_os(target: &str) -> &str {
}
}
pub fn default_target_triple() -> String {
let arch = std::env::consts::ARCH;
if cfg!(target_os = "linux") {
let env = if cfg!(target_env = "musl") {
"musl"
} else {
"gnu"
};
format!("{arch}-unknown-linux-{env}")
} else if cfg!(target_os = "macos") {
format!("{arch}-apple-darwin")
} else if cfg!(target_os = "windows") {
let env = if cfg!(target_env = "gnu") {
"gnu"
} else {
"msvc"
};
format!("{arch}-pc-windows-{env}")
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
format!("{arch}-unknown-{}", std::env::consts::OS)
} else {
"unknown".to_string()
}
}
pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>, tool: &str) {
let Some(target) = target.map(str::trim).filter(|t| !t.is_empty()) else {
return;
};
if !tool.to_lowercase().contains("clang") {
return;
}
let target_flag = format!("--target={target}");
if !flags
.iter()
.any(|f| f == &target_flag || f.starts_with("--target="))
{
flags.insert(0, target_flag);
}
}
pub fn get_config_str(config: &Config, key: &str) -> String {
config
.get(key)
@@ -140,6 +226,43 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec
out
}
fn contains_ignore_case(text: &str, pattern: &str) -> bool {
if pattern.len() > text.len() {
return false;
}
text.as_bytes()
.windows(pattern.len())
.any(|w| w.eq_ignore_ascii_case(pattern.as_bytes()))
}
pub fn is_bare_metal_target(target: Option<&str>) -> bool {
match target {
Some(t) => {
contains_ignore_case(t, "none")
|| contains_ignore_case(t, "-elf")
|| contains_ignore_case(t, "eabi")
|| contains_ignore_case(t, "baremetal")
|| contains_ignore_case(t, "bare-metal")
}
None => false,
}
}
pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
],
_ => &[],
}
}
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
let base = config
.get(&format!("build.{field}"))
@@ -157,7 +280,7 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<Strin
}
match config.get("build.language") {
Some(v) => parse_language_value(v, "build.language"),
None => Err("build.language is missing".to_string()),
None => Ok("c".to_string()),
}
}
@@ -201,9 +324,14 @@ pub fn resolve_compiler(
tc_as: Option<&str>,
) -> String {
let lang = primary_language(language);
env_override_compiler(&lang)
let result = env_override_compiler(&lang)
.or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as))
.unwrap_or_else(|| compiler.to_string())
.unwrap_or_else(|| compiler.to_string());
if lang == "asm" {
map_asm_compiler(&result)
} else {
result
}
}
fn env_override_compiler(lang: &str) -> Option<String> {
@@ -246,7 +374,8 @@ fn toolchain_override_compiler(
tc_as: Option<&str>,
) -> Option<String> {
if lang == "asm" {
return tc_as.map(|v| v.to_string());
let as_compiler = tc_as.map(|v| v.to_string());
return as_compiler.map(|v| map_asm_compiler(&v));
}
if (lang == "c++" || lang == "cpp" || lang == "cxx")
&& let Some(v) = tc_cxx
@@ -256,23 +385,31 @@ fn toolchain_override_compiler(
tc_cc.map(|v| v.to_string())
}
fn map_asm_compiler(compiler: &str) -> String {
match compiler.to_lowercase().as_str() {
"gas" | "gnu-as" => "as".to_string(),
"nasm" => "nasm".to_string(),
"fasm" | "fasm64" => "fasm".to_string(),
"masm" | "ml" | "ml64" => "ml".to_string(),
_ => compiler.to_string(),
}
}
pub fn primary_language(language: &str) -> String {
let parts: Vec<String> = language
let tokens: Vec<&str> = language
.split(',')
.map(|p| p.trim().to_lowercase())
.filter(|p| !p.is_empty())
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for p in &parts {
if p == "c++" || p == "cpp" || p == "cxx" {
return p.clone();
// Priority: c++ > c > asm.
for id in ["cxx", "c", "asm"] {
let hit = tokens.iter().any(|t| {
crate::core::build::language::language_for_token(t).map(|l| l.id()) == Some(id)
});
if hit {
return id.to_string();
}
}
if parts.iter().any(|p| p == "c") {
return "c".to_string();
}
if parts.iter().any(|p| p == "asm") {
return "asm".to_string();
}
language.to_lowercase()
}
@@ -316,7 +453,7 @@ pub fn resolve_pkg_config_flags_lossy(
}
}
fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
let output = std::process::Command::new("pkg-config")
.arg(arg)
.arg(pkg)
@@ -380,6 +517,61 @@ mod tests {
assert_eq!(normalize_target_os("unknown"), "unknown");
}
#[test]
fn default_target_triple_uses_host_arch() {
let target = default_target_triple();
if cfg!(target_os = "linux") {
assert_eq!(
target,
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
);
} else if cfg!(target_os = "macos") {
assert_eq!(target, format!("{}-apple-darwin", std::env::consts::ARCH));
} else if cfg!(target_os = "windows") {
assert_eq!(
target,
format!("{}-pc-windows-msvc", std::env::consts::ARCH)
);
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
assert_eq!(
target,
format!(
"{}-unknown-{}",
std::env::consts::ARCH,
std::env::consts::OS
)
);
} else {
assert_eq!(target, "unknown");
}
}
#[test]
fn prepend_clang_target_flag_adds_once_for_clang() {
let mut flags = vec!["-O2".to_string()];
prepend_clang_target_flag(&mut flags, Some("aarch64-apple-darwin"), "clang");
prepend_clang_target_flag(&mut flags, Some("aarch64-apple-darwin"), "clang");
assert_eq!(
flags,
vec![
"--target=aarch64-apple-darwin".to_string(),
"-O2".to_string()
]
);
}
#[test]
fn prepend_clang_target_flag_ignores_non_clang_tools() {
let mut flags = vec!["-O2".to_string()];
prepend_clang_target_flag(&mut flags, Some("aarch64-apple-darwin"), "gcc");
assert_eq!(flags, vec!["-O2".to_string()]);
}
#[test]
fn parse_version_parts() {
let info = parse_version_info("1.2.3-beta");
@@ -390,4 +582,52 @@ mod tests {
assert_eq!(info.suffix, "beta");
assert_eq!(info.suffix_dash, "-beta");
}
#[test]
fn normalize_target_with_profile() {
assert_eq!(
normalize_target("linux", "debug"),
Some("target/x86_64-unknown-linux-gnu/debug".into())
);
assert_eq!(
normalize_target("x86_64-unknown-linux-gnu", "release"),
Some("target/x86_64-unknown-linux-gnu/release".into())
);
assert_eq!(normalize_target("", "debug"), None);
}
#[test]
fn normalize_kind_empty_defaults_to_bin() {
assert_eq!(normalize_kind(""), "bin");
assert_eq!(normalize_kind(" "), "bin");
assert_eq!(normalize_kind("staticlib"), "staticlib");
assert_eq!(normalize_kind(" elf "), "elf");
}
#[test]
fn normalize_platform_empty_is_none() {
assert_eq!(normalize_platform(""), None);
assert_eq!(normalize_platform(" "), None);
assert_eq!(normalize_platform("native"), Some("native"));
assert_eq!(normalize_platform(" efi "), Some("efi"));
}
#[test]
fn default_profile_flags_by_profile() {
assert!(default_profile_flags("debug").contains(&"-O0"));
assert!(default_profile_flags("debug").contains(&"-g"));
assert!(default_profile_flags("release").contains(&"-O3"));
assert!(default_profile_flags("release").contains(&"-DNDEBUG"));
assert!(default_profile_flags("unknown").is_empty());
}
#[test]
fn is_bare_metal_detects_embedded_targets() {
assert!(is_bare_metal_target(Some("aarch64-none-elf")));
assert!(is_bare_metal_target(Some("i686-elf")));
assert!(is_bare_metal_target(Some("armv7e-m-none-eabi")));
assert!(is_bare_metal_target(Some("riscv32-unknown-none-elf")));
assert!(!is_bare_metal_target(Some("x86_64-unknown-linux-gnu")));
assert!(!is_bare_metal_target(None));
}
}

View File

@@ -18,6 +18,24 @@
use std::io;
use std::path::{Path, PathBuf};
pub fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
}
pub fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}
#[allow(dead_code)]
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
let path: PathBuf = match dir {

28
src/utils/git.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::path::Path;
use std::process::Command;
pub fn is_git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn git_init(path: &Path) -> Result<(), String> {
if !is_git_available() {
return Err("git executable not found in PATH".to_string());
}
let status = Command::new("git")
.arg("init")
.current_dir(path)
.status()
.map_err(|e| format!("failed to execute git init: {}", e))?;
if !status.success() {
return Err("git init failed".to_string());
}
Ok(())
}

View File

@@ -17,5 +17,6 @@
pub mod build;
pub mod fs;
pub mod git;
pub mod log;
pub mod text;

View File

@@ -1,502 +0,0 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
run_dcr_env(args, cwd, &[])
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
fn available_compiler() -> Option<&'static str> {
for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if ok {
return Some(candidate);
}
}
None
}
#[test]
fn help_and_version_work() {
let dir = unique_sandbox_dir("help");
let out = run_dcr(&["--help"], &dir);
assert!(out.status.success(), "--help should succeed");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success(), "--version should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("dcr"), "version output should mention dcr");
}
#[test]
fn new_creates_project_layout() {
let dir = unique_sandbox_dir("new");
let out = run_dcr(&["new", "hello"], &dir);
assert!(out.status.success(), "dcr new should succeed");
let project_dir = dir.join("hello");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
assert!(
project_dir.join("src").join("main.c").is_file(),
"src/main.c missing"
);
}
#[test]
fn init_and_clean_remove_target() {
let dir = unique_sandbox_dir("init");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let target_debug = dir.join("target").join("debug");
std::fs::create_dir_all(&target_debug).expect("failed to create target/debug");
std::fs::write(target_debug.join("dummy.o"), "x").expect("failed to write dummy file");
let out = run_dcr(&["clean"], &dir);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn build_run_clean_flags_normal_project() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping build/run test");
return;
};
let dir = unique_sandbox_dir("normal");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "dcr build should succeed");
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!(
!dir.join(&target_dir).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join(&target_dir).join("debug").is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
);
let out = run_dcr_env(&["clean"], &dir, &envs);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn workspace_build_and_clean_all() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping workspace test");
return;
};
let root = unique_sandbox_dir("workspace");
let out = run_dcr(&["init"], &root);
assert!(out.status.success(), "root init should succeed");
let members = [
("userspace", &[][..]),
("core", &["userspace"][..]),
("kernel", &["core"][..]),
];
for (name, _) in &members {
let member_dir = root.join("src").join(name);
std::fs::create_dir_all(&member_dir).expect("failed to create member dir");
let out = run_dcr(&["init"], &member_dir);
assert!(out.status.success(), "member init should succeed");
}
let workspace_toml = "[package]\nname = \"ws-root\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[workspace]\nuserspace = { path = \"src/userspace\", deps = [] }\ncore = { path = \"src/core\", deps = [\"userspace\"] }\nkernel = { path = \"src/kernel\", deps = [\"core\"] }\n\n[dependencies]\n";
std::fs::write(root.join("dcr.toml"), workspace_toml).expect("failed to write root dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build", "--release"], &root, &envs);
assert!(
out.status.success(),
"workspace build --release should succeed"
);
let out = run_dcr_env(&["clean", "--release", "--all"], &root, &envs);
assert!(
out.status.success(),
"workspace clean --all --release should succeed"
);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!root.join(target_dir).join("release").exists(),
"root target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
root.join(target_dir).join("debug").exists(),
"root target/x86_64-unknown-linux-gnu/debug should remain"
);
}
#[test]
fn dcr_add_dependencies() {
let dir = unique_sandbox_dir("add_dep");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
// Test path: prefix
let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir);
assert!(out.status.success(), "dcr add path should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("mylib = { path = \"./libs/mylib\" }"),
"path dep not found in toml"
);
// Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"),
"github dep not found in toml"
);
// Test git: prefix (generic)
let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir);
assert!(out.status.success(), "dcr add custom git should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"),
"custom git dep not found in toml"
);
// Test git: prefix (github default)
let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir);
assert!(out.status.success(), "dcr add git short should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("git_short = { git = \"https://github.com/user/repo\" }"),
"git short dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
&dir,
);
assert!(out.status.success(), "dcr add with branch should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"),
"branch lib not found in toml"
);
// Test failure on no prefix
let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir);
assert!(!out.status.success(), "dcr add without prefix should fail");
}
#[test]
fn dcr_builds_lib_package() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping lib package test");
return;
};
let dir = unique_sandbox_dir("lib_package");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
let updated_toml = toml
.replace("kind = \"bin\"", "kind = \"staticlib\"")
.replace("type = \"none\"", "type = \"lib\"");
std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml");
std::fs::write(dir.join("src").join("my_lib.h"), "void hello();")
.expect("failed to write header");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(out.status.success(), "dcr build should succeed");
let target_dir = dir.join("target");
assert!(
target_dir.join("include").join("my_lib.h").is_file(),
"include/my_lib.h missing"
);
assert!(target_dir.join("lib").exists(), "lib directory missing");
}
#[test]
fn registry_dependency_is_built_from_cache() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping registry dependency build test");
return;
};
let root = unique_sandbox_dir("registry_dep");
let home = root.join("home");
let dcr_home = home.join(".dcr");
let dep = root.join("cache").join("mylib");
let app = root.join("app");
std::fs::create_dir_all(dcr_home.as_path()).expect("failed to create dcr home");
std::fs::create_dir_all(dep.join("src")).expect("failed to create dep src");
std::fs::create_dir_all(app.join("src")).expect("failed to create app src");
std::fs::write(
dcr_home.join("config.toml"),
"[registry.local]\nurl = \"file://local\"\npriority = 1\n",
)
.expect("failed to write registry config");
std::fs::write(
dcr_home.join("index.json"),
format!(
"{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}",
dep.display()
),
)
.expect("failed to write registry index");
std::fs::write(
dep.join("dcr.toml"),
"[package]\nname = \"mylib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\n",
)
.expect("failed to write dep dcr.toml");
std::fs::write(dep.join("src").join("mylib.h"), "int answer(void);\n")
.expect("failed to write header");
std::fs::write(
dep.join("src").join("mylib.c"),
"int answer(void) { return 42; }\n",
)
.expect("failed to write dep source");
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\ntype = \"none\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmylib = \"0.1.0\"\n",
)
.expect("failed to write app dcr.toml");
std::fs::write(
app.join("src").join("main.c"),
"#include \"mylib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
)
.expect("failed to write app source");
let index_path = dcr_home.join("index.json");
let envs = [
("DCR_COMPILER", compiler),
("HOME", home.to_str().unwrap()),
("DCR_INDEX_PATH", index_path.to_str().unwrap()),
];
let out = run_dcr_env(&["build"], &app, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"registry dependency build should succeed"
);
assert!(
dep.join("target").join("include").join("mylib.h").is_file(),
"registry dependency headers were not packaged"
);
assert!(
dep.join("target").join("lib").exists(),
"registry dependency library directory missing"
);
}
fn parse_project_name(toml: &str) -> String {
toml.lines()
.find(|l| l.trim().starts_with("name ="))
.and_then(|l| l.split('=').nth(1))
.map(|s| s.trim().trim_matches('"').to_string())
.expect("could not parse project name from dcr.toml")
}
fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
project_root
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug")
.join(project_name)
}
#[test]
fn build_with_target_config() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping target config test");
return;
};
let dir = unique_sandbox_dir("target_cfg");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\ntarget = \"linux\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(
out.status.success(),
"dcr build with target = \"linux\" should succeed"
);
let artifact = default_artifact_path(&dir, &project_name);
assert!(
artifact.is_file(),
"artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}",
project_name
);
}
#[test]
fn build_with_out_dir() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping out_dir test");
return;
};
let dir = unique_sandbox_dir("out_dir");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\nout_dir = \"./_BUILD\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"dcr build with out_dir should succeed"
);
let artifact = dir.join("_BUILD").join(&project_name);
assert!(
artifact.is_file(),
"artifact should be at _BUILD/{} (custom out_dir)",
project_name
);
let default_path = default_artifact_path(&dir, &project_name);
assert!(
!default_path.exists(),
"artifact should NOT be at default path when out_dir is set"
);
}
#[test]
fn dcr_test_runs_without_sandbox_dependency() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping dcr test integration");
return;
};
let dir = unique_sandbox_dir("dcr_test_independent");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_CC", compiler)];
let out_init = run_dcr_env(&["test", "--init"], &dir, &envs);
assert!(out_init.status.success(), "dcr test --init should succeed");
let out = run_dcr_env(&["test"], &dir, &envs);
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{}",
stdout,
stderr
);
}

172
tests/cli_build.rs Normal file
View File

@@ -0,0 +1,172 @@
mod common;
use common::*;
#[test]
fn init_and_clean_remove_target() {
let dir = unique_sandbox_dir("init");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let target_debug = dir.join("target").join("debug");
std::fs::create_dir_all(&target_debug).expect("failed to create target/debug");
std::fs::write(target_debug.join("dummy.o"), "x").expect("failed to write dummy file");
let out = run_dcr(&["clean"], &dir);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn add_preserves_run_and_untyped_keys() {
// Regression: `dcr add` used to drop the [run] section and non-whitelisted
// [build] keys (filename/extension/include) when re-serializing dcr.toml.
let dir = unique_sandbox_dir("preserve_add");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let updated = toml.replace(
"[build]",
"[build]\nfilename = \"KERNEL\"\nextension = \"ELF\"\ninclude = [\"src/include\"]",
) + "\n[run]\ncmd = \"qemu-system-aarch64 -kernel KERNEL\"\n";
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let out = run_dcr(&["add", "zlib", "path:../zlib"], &dir);
assert!(out.status.success(), "dcr add should succeed");
let saved = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
assert!(
saved.contains("filename = \"KERNEL\""),
"build.filename lost:\n{saved}"
);
assert!(
saved.contains("extension = \"ELF\""),
"build.extension lost:\n{saved}"
);
assert!(
saved.contains("include = [\"src/include\"]"),
"build.include lost:\n{saved}"
);
assert!(
saved.contains("[run]") && saved.contains("qemu-system-aarch64"),
"[run] section lost:\n{saved}"
);
assert!(
saved.contains("zlib = { path = \"../zlib\" }"),
"dependency not added as inline table:\n{saved}"
);
}
#[test]
fn build_run_clean_flags_normal_project() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping build/run test");
return;
};
let dir = unique_sandbox_dir("normal");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "dcr build should succeed");
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!(
!dir.join(&target_dir).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join(&target_dir).join("debug").is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
);
let out = run_dcr_env(&["clean"], &dir, &envs);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn build_with_target_config() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping target config test");
return;
};
let dir = unique_sandbox_dir("target_cfg");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\ntarget = \"linux\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(
out.status.success(),
"dcr build with target = \"linux\" should succeed"
);
let artifact = default_artifact_path(&dir, &project_name);
assert!(
artifact.is_file(),
"artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}",
project_name
);
}
#[test]
fn build_with_out_dir() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping out_dir test");
return;
};
let dir = unique_sandbox_dir("out_dir");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\nout_dir = \"./_BUILD\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"dcr build with out_dir should succeed"
);
let artifact = dir.join("_BUILD").join(&project_name);
assert!(
artifact.is_file(),
"artifact should be at _BUILD/{} (custom out_dir)",
project_name
);
let default_path = default_artifact_path(&dir, &project_name);
assert!(
!default_path.exists(),
"artifact should NOT be at default path when out_dir is set"
);
}

176
tests/cli_deps.rs Normal file
View File

@@ -0,0 +1,176 @@
mod common;
use common::*;
#[test]
fn dcr_add_dependencies() {
let dir = unique_sandbox_dir("add_dep");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
// Test path: prefix
let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir);
assert!(out.status.success(), "dcr add path should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("mylib = { path = \"./libs/mylib\" }"),
"path dep not found in toml"
);
// Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"),
"github dep not found in toml"
);
// Test git: prefix (generic)
let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir);
assert!(out.status.success(), "dcr add custom git should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"),
"custom git dep not found in toml"
);
// Test git: prefix (github default)
let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir);
assert!(out.status.success(), "dcr add git short should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("git_short = { git = \"https://github.com/user/repo\" }"),
"git short dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
&dir,
);
assert!(out.status.success(), "dcr add with branch should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"),
"branch lib not found in toml"
);
// Test failure on no prefix
let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir);
assert!(!out.status.success(), "dcr add without prefix should fail");
}
#[test]
fn dcr_builds_lib_package() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping lib package test");
return;
};
let dir = unique_sandbox_dir("lib_package");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
let updated_toml = toml
.replace("kind = \"bin\"", "kind = \"staticlib\"")
.replace("type = \"none\"", "type = \"lib\"");
std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml");
std::fs::write(dir.join("src").join("my_lib.h"), "void hello();")
.expect("failed to write header");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(out.status.success(), "dcr build should succeed");
let target_dir = dir.join("target");
assert!(
target_dir.join("include").join("my_lib.h").is_file(),
"include/my_lib.h missing"
);
assert!(target_dir.join("lib").exists(), "lib directory missing");
}
#[test]
fn registry_dependency_is_built_from_cache() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping registry dependency build test");
return;
};
let root = unique_sandbox_dir("registry_dep");
let home = root.join("home");
let dcr_home = home.join(".dcr");
let dep = root.join("cache").join("mylib");
let app = root.join("app");
std::fs::create_dir_all(dcr_home.as_path()).expect("failed to create dcr home");
std::fs::create_dir_all(dep.join("src")).expect("failed to create dep src");
std::fs::create_dir_all(app.join("src")).expect("failed to create app src");
std::fs::write(
dcr_home.join("config.toml"),
"[registry.local]\nurl = \"file://local\"\npriority = 1\n",
)
.expect("failed to write registry config");
std::fs::write(
dcr_home.join("index.json"),
format!(
"{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}",
dep.display()
),
)
.expect("failed to write registry index");
std::fs::write(
dep.join("dcr.toml"),
"[package]\nname = \"mylib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\n",
)
.expect("failed to write dep dcr.toml");
std::fs::write(dep.join("src").join("mylib.h"), "int answer(void);\n")
.expect("failed to write header");
std::fs::write(
dep.join("src").join("mylib.c"),
"int answer(void) { return 42; }\n",
)
.expect("failed to write dep source");
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\ntype = \"none\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmylib = \"0.1.0\"\n",
)
.expect("failed to write app dcr.toml");
std::fs::write(
app.join("src").join("main.c"),
"#include \"mylib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
)
.expect("failed to write app source");
let index_path = dcr_home.join("index.json");
let envs = [
("DCR_COMPILER", compiler),
("HOME", home.to_str().unwrap()),
("DCR_INDEX_PATH", index_path.to_str().unwrap()),
];
let out = run_dcr_env(&["build"], &app, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"registry dependency build should succeed"
);
assert!(
dep.join("target").join("include").join("mylib.h").is_file(),
"registry dependency headers were not packaged"
);
assert!(
dep.join("target").join("lib").exists(),
"registry dependency library directory missing"
);
}

66
tests/cli_lint.rs Normal file
View File

@@ -0,0 +1,66 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
#[test]
fn lint_command_runs() {
let dir = unique_sandbox_dir("lint");
let out = run_dcr_env(&["init"], &dir, &[]);
assert!(out.status.success(), "dcr init should succeed");
// Linting a clean project should succeed or at least run without crashing
let out = run_dcr_env(&["lint"], &dir, &[]);
assert!(out.status.success(), "dcr lint should run on clean project");
}

75
tests/cli_new.rs Normal file
View File

@@ -0,0 +1,75 @@
mod common;
use common::*;
#[test]
fn help_and_version_work() {
let dir = unique_sandbox_dir("help");
let out = run_dcr(&["--help"], &dir);
assert!(out.status.success(), "--help should succeed");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success(), "--version should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("dcr"), "version output should mention dcr");
}
#[test]
fn new_creates_project_layout() {
let dir = unique_sandbox_dir("new");
let out = run_dcr(&["new", "hello"], &dir);
assert!(out.status.success(), "dcr new should succeed");
let project_dir = dir.join("hello");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
assert!(
project_dir.join("src").join("main.c").is_file(),
"src/main.c missing"
);
}
#[test]
fn new_vcs_options_work() {
// 1. Test --vcs none
let dir = unique_sandbox_dir("new_vcs_none");
let out = run_dcr(&["new", "hello_none", "--vcs", "none"], &dir);
assert!(out.status.success(), "dcr new --vcs none should succeed");
let project_dir = dir.join("hello_none");
assert!(project_dir.is_dir());
assert!(!project_dir.join(".git").exists());
assert!(!project_dir.join(".gitignore").exists());
// 2. Test --vcs git
if is_git_in_path() {
let dir = unique_sandbox_dir("new_vcs_git");
let out = run_dcr(&["new", "hello_git", "--vcs", "git"], &dir);
assert!(out.status.success(), "dcr new --vcs git should succeed");
let project_dir = dir.join("hello_git");
assert!(project_dir.is_dir());
assert!(project_dir.join(".git").is_dir());
assert!(project_dir.join(".gitignore").is_file());
let gitignore = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/target"));
}
}
#[test]
fn init_vcs_options_work() {
// 1. Test --vcs none
let dir = unique_sandbox_dir("init_vcs_none");
let out = run_dcr(&["init", "--vcs", "none"], &dir);
assert!(out.status.success(), "dcr init --vcs none should succeed");
assert!(!dir.join(".git").exists());
assert!(!dir.join(".gitignore").exists());
// 2. Test --vcs git
if is_git_in_path() {
let dir = unique_sandbox_dir("init_vcs_git");
let out = run_dcr(&["init", "--vcs", "git"], &dir);
assert!(out.status.success(), "dcr init --vcs git should succeed");
assert!(dir.join(".git").is_dir());
assert!(dir.join(".gitignore").is_file());
let gitignore = std::fs::read_to_string(dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/target"));
}
}

79
tests/cli_qt.rs Normal file
View File

@@ -0,0 +1,79 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
#[test]
fn qt_support_config_detected() {
let dir = unique_sandbox_dir("qt");
let out = run_dcr_env(&["init"], &dir, &[]);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let updated = toml.replace("[build]", "[build]\nqt = true\nlanguage = \"cpp\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
// This might fail if Qt is not installed, but it should at least try to run Qt processing
// We just check if it doesn't crash immediately due to bad config
let out = run_dcr_env(&["build"], &dir, &[]);
// Depending on environment, this might be success or failure, but it should not panic.
// If it fails with "qt not found", it means it parsed the config and attempted to use it.
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("panicked"),
"dcr should not panic on Qt config"
);
}

30
tests/cli_test.rs Normal file
View File

@@ -0,0 +1,30 @@
mod common;
use common::*;
#[test]
fn dcr_test_runs_without_sandbox_dependency() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping dcr test integration");
return;
};
let dir = unique_sandbox_dir("dcr_test_independent");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_CC", compiler)];
let out_init = run_dcr_env(&["test", "--init"], &dir, &envs);
assert!(out_init.status.success(), "dcr test --init should succeed");
let out = run_dcr_env(&["test"], &dir, &envs);
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{}",
stdout,
stderr
);
}

58
tests/cli_workspace.rs Normal file
View File

@@ -0,0 +1,58 @@
mod common;
use common::*;
#[test]
fn workspace_build_and_clean_all() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping workspace test");
return;
};
let root = unique_sandbox_dir("workspace");
let out = run_dcr(&["init"], &root);
assert!(out.status.success(), "root init should succeed");
let members = [
("userspace", &[][..]),
("core", &["userspace"][..]),
("kernel", &["core"][..]),
];
for (name, _) in &members {
let member_dir = root.join("src").join(name);
std::fs::create_dir_all(&member_dir).expect("failed to create member dir");
let out = run_dcr(&["init"], &member_dir);
assert!(out.status.success(), "member init should succeed");
}
let workspace_toml = "[package]\nname = \"ws-root\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[workspace]\nuserspace = { path = \"src/userspace\", deps = [] }\ncore = { path = \"src/core\", deps = [\"userspace\"] }\nkernel = { path = \"src/kernel\", deps = [\"core\"] }\n\n[dependencies]\n";
std::fs::write(root.join("dcr.toml"), workspace_toml).expect("failed to write root dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build", "--release"], &root, &envs);
assert!(
out.status.success(),
"workspace build --release should succeed"
);
let out = run_dcr_env(&["clean", "--release", "--all"], &root, &envs);
assert!(
out.status.success(),
"workspace clean --all --release should succeed"
);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!root.join(target_dir).join("release").exists(),
"root target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
root.join(target_dir).join("debug").exists(),
"root target/x86_64-unknown-linux-gnu/debug should remain"
);
}

101
tests/common/mod.rs Normal file
View File

@@ -0,0 +1,101 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
pub fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
pub fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
pub fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
run_dcr_env(args, cwd, &[])
}
pub fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
pub fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
#[allow(dead_code)]
pub fn available_compiler() -> Option<&'static str> {
for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if ok {
return Some(candidate);
}
}
None
}
#[allow(dead_code)]
pub fn is_git_in_path() -> bool {
std::process::Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[allow(dead_code)]
pub fn parse_project_name(toml: &str) -> String {
toml.lines()
.find(|l| l.trim().starts_with("name ="))
.and_then(|l| l.split('=').nth(1))
.map(|s| s.trim().trim_matches('"').to_string())
.expect("could not parse project name from dcr.toml")
}
#[allow(dead_code)]
pub fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
project_root
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug")
.join(project_name)
}