mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 15:49:40 +00:00
Compare commits
30 Commits
v0.7.1-dev
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dcc8e614a | |||
| 9eaa4635d5 | |||
| a3f8444c20 | |||
| 1f3fc62cb6 | |||
| f8d8c95912 | |||
| 9e07f70c78 | |||
| 0376eded3f | |||
| 803d5891ce | |||
| 9326e23a53 | |||
| 21932ac94a | |||
| 97db82e4f8 | |||
| 3d596d7280 | |||
| 5cb4473bd1 | |||
| 33f955d4e3 | |||
| 838be72dd2 | |||
| 39fad31c4f | |||
| c8ef38a99b | |||
| 8e0c535cfd | |||
| de88c160ad | |||
| 4daca925a0 | |||
| 595b7ac105 | |||
|
|
11ae3762d0 | ||
|
|
4c439317e3 | ||
| 0aa3f87f99 | |||
| a6ce0a3332 | |||
| cea43e99c8 | |||
| de4fb0bc29 | |||
| 59e27b6f5d | |||
| c8cb155e5a | |||
| 6424aab69d |
68
.github/workflows/release.yml
vendored
68
.github/workflows/release.yml
vendored
@@ -41,10 +41,6 @@ on:
|
|||||||
description: "Publish to crates.io"
|
description: "Publish to crates.io"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
run_snap:
|
|
||||||
description: "Publish to Snap Store (dcrup)"
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
run_brew:
|
run_brew:
|
||||||
description: "Update Homebrew tap"
|
description: "Update Homebrew tap"
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -83,24 +79,42 @@ jobs:
|
|||||||
notes:
|
notes:
|
||||||
needs: setup
|
needs: setup
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release_name: ${{ steps.extract.outputs.release_name }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.setup.outputs.tag }}
|
ref: ${{ needs.setup.outputs.tag }}
|
||||||
|
|
||||||
- name: Prepare Release Notes
|
- name: Prepare Release Notes
|
||||||
|
id: extract
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
tag="${{ needs.setup.outputs.version }}"
|
tag="${{ needs.setup.outputs.version }}"
|
||||||
notes_file=".github/release-notes.md"
|
notes_file=".github/release-notes.md"
|
||||||
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then
|
# Strip -dev suffix to match base version in CHANGELOG
|
||||||
awk -v ver="$tag" '
|
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 search_ver="$changelog_tag" -v display_ver="$tag" '
|
||||||
|
$0 ~ "^## \\[" search_ver "\\]" {
|
||||||
|
match($0, /"([^"]+)"/, arr)
|
||||||
|
if (arr[1] != "") {
|
||||||
|
print "v" display_ver " - " arr[1]
|
||||||
|
} else {
|
||||||
|
print "v" display_ver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' CHANGELOG.md)
|
||||||
|
echo "release_name=${release_name}" >> "$GITHUB_OUTPUT"
|
||||||
|
awk -v ver="$changelog_tag" '
|
||||||
$0 ~ "^## \\[" ver "\\]" {found=1; next}
|
$0 ~ "^## \\[" ver "\\]" {found=1; next}
|
||||||
found && $0 ~ "^## \\[" {exit}
|
found && $0 ~ "^## \\[" {exit}
|
||||||
found {print}
|
found {print}
|
||||||
' CHANGELOG.md | sed -e '${/^$/d;}' > "$notes_file"
|
' CHANGELOG.md | sed -e '${/^$/d;}' > "$notes_file"
|
||||||
else
|
else
|
||||||
|
echo "release_name=v${tag}" >> "$GITHUB_OUTPUT"
|
||||||
echo "No CHANGELOG section for $tag." > "$notes_file"
|
echo "No CHANGELOG section for $tag." > "$notes_file"
|
||||||
git log -1 --pretty=format:"%s%n%n%b" >> "$notes_file"
|
git log -1 --pretty=format:"%s%n%n%b" >> "$notes_file"
|
||||||
fi
|
fi
|
||||||
@@ -126,6 +140,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.setup.outputs.tag }}
|
tag_name: ${{ needs.setup.outputs.tag }}
|
||||||
|
name: ${{ needs.notes.outputs.release_name }}
|
||||||
body_path: .github/release-notes.md
|
body_path: .github/release-notes.md
|
||||||
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
|
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
|
||||||
|
|
||||||
@@ -651,6 +666,8 @@ jobs:
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
PKG
|
PKG
|
||||||
|
|
||||||
|
cat > packages/aur/dcr/.SRCINFO <<SRC
|
||||||
pkgbase = dcr
|
pkgbase = dcr
|
||||||
pkgdesc = Cargo-like utility to manage C/C++ projects
|
pkgdesc = Cargo-like utility to manage C/C++ projects
|
||||||
pkgver = ${PKGVER_CLEAN}
|
pkgver = ${PKGVER_CLEAN}
|
||||||
@@ -1229,34 +1246,11 @@ jobs:
|
|||||||
git commit -m "dcr ${{ needs.setup.outputs.version }}"
|
git commit -m "dcr ${{ needs.setup.outputs.version }}"
|
||||||
git push
|
git push
|
||||||
|
|
||||||
# ── Snap Store (dcrup) ─────────────────────────────────────────────────────
|
# ── Snap Store (dcrup) — temporarily disabled ────────────────────────────
|
||||||
publish-snap:
|
# Classic confinement was denied for this project (too new).
|
||||||
if: |
|
# Snap artifact is still built and attached to the release via
|
||||||
(github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') ||
|
# publish-dexoron-packages. Users can install manually:
|
||||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_snap == 'true')
|
# sudo snap install --classic --dangerous dcrup_*.snap
|
||||||
needs: [setup, build-linux-packages]
|
#
|
||||||
runs-on: ubuntu-latest
|
# publish-snap:
|
||||||
steps:
|
# if: ...
|
||||||
- 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
|
|
||||||
14
.github/workflows/trigger-site-build.yml
vendored
Normal file
14
.github/workflows/trigger-site-build.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: Trigger site rebuild
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'docs/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Trigger Cloudflare Pages build
|
||||||
|
run: curl -sS -X POST "${{ secrets.CF_PAGES_DEPLOY_HOOK }}"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,9 @@ target/
|
|||||||
sandbox/**
|
sandbox/**
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.cargo/
|
||||||
.env
|
.env
|
||||||
|
|
||||||
personal/
|
personal/
|
||||||
|
.*
|
||||||
|
!.gitignore
|
||||||
177
CHANGELOG.md
177
CHANGELOG.md
@@ -1,5 +1,169 @@
|
|||||||
# Changelog
|
# 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"
|
## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
|
||||||
|
|
||||||
### RU
|
### RU
|
||||||
@@ -12,7 +176,7 @@
|
|||||||
- Оптимизация сборки: `opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, `strip = true`.
|
- Оптимизация сборки: `opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, `strip = true`.
|
||||||
- Поддержка новых архитектур (i686, armv7, riscv64gc, aarch64) в рабочих процессах и установочных скриптах.
|
- Поддержка новых архитектур (i686, armv7, riscv64gc, aarch64) в рабочих процессах и установочных скриптах.
|
||||||
- Сборка пакетов `deb` и `rpm` для всех 5 архитектур Linux через `cargo-zigbuild`.
|
- Сборка пакетов `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` стала публичной.
|
- Функция `validate_package_name` стала публичной.
|
||||||
@@ -20,6 +184,15 @@
|
|||||||
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
|
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
|
||||||
- Отключена архитектура Windows 32-bit (GNU) из-за проблем со сборкой.
|
- Отключена архитектура Windows 32-bit (GNU) из-за проблем со сборкой.
|
||||||
|
|
||||||
|
**Исправлено:**
|
||||||
|
|
||||||
|
- **`dcr new <invalid-name>` создаёт директорию, затем падает** — валидация теперь происходит до операций с файлами.
|
||||||
|
- **`dcr init` создаёт dcr.toml, затем падает при невалидном имени директории** — то же исправление.
|
||||||
|
- **Man-страницы отсутствовали после установки** — теперь устанавливаются через скрипты установки, пакетные менеджеры и Homebrew.
|
||||||
|
- **Ложное срабатывание на букву диска в Windows** — в парсинге файлов зависимостей.
|
||||||
|
- **CRLF `\r\n` ломает парсер зависимостей** — теперь обрабатываются смешанные окончания строк.
|
||||||
|
- **Новая строка после обратного слеша не поглощалась** — в обработке escape-последовательностей `parse_d_file`.
|
||||||
|
|
||||||
### EN
|
### EN
|
||||||
|
|
||||||
**Added:**
|
**Added:**
|
||||||
@@ -30,7 +203,7 @@
|
|||||||
- Release profile optimizations (`opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, `strip = true`).
|
- 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.
|
- 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`.
|
- 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:**
|
**Changed:**
|
||||||
- `validate_package_name` made public and callable from CLI commands directly.
|
- `validate_package_name` made public and callable from CLI commands directly.
|
||||||
|
|||||||
1071
Cargo.lock
generated
1071
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.7.1"
|
version = "0.8.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
repository = "https://github.com/dexoron/dcr"
|
repository = "https://github.com/dexoron/dcr"
|
||||||
documentation = "https://dcr.dexoron.su/docs"
|
documentation = "https://dcr-tool.ru/docs"
|
||||||
homepage = "https://dcr.dexoron.su"
|
homepage = "https://dcr-tool.ru"
|
||||||
|
|
||||||
[package.metadata.binstall]
|
[package.metadata.binstall]
|
||||||
pkg-fmt = "bin"
|
pkg-fmt = "bin"
|
||||||
@@ -61,24 +61,26 @@ assets = [
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
glob = "0.3"
|
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"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
self-replace = "1"
|
self-replace = "1"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
toml = "1.0.3"
|
toml = "1.0.3"
|
||||||
|
toml_edit = "0.22"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
ctrlc = "3"
|
ctrlc = "3"
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
|
||||||
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
|
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
|
||||||
git2 = { version = "0.20.4", features = ["vendored-libgit2"] }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
lto = true
|
lto = "thin"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
strip = true
|
strip = true
|
||||||
|
|
||||||
|
[profile.fast]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = 1
|
||||||
|
lto = false
|
||||||
|
codegen-units = 16
|
||||||
|
panic = "abort"
|
||||||
|
strip = true
|
||||||
41
README.md
41
README.md
@@ -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>
|
||||||
|
|
||||||
[](https://github.com/dexoron/dcr/actions/workflows/ci.yml)
|
<!-- Snap badge removed: classic confinement was denied for this project. -->
|
||||||
[](https://github.com/dexoron/dcr/releases/latest)
|
|
||||||
[](https://github.com/dexoron/dcr/releases)
|
|
||||||
<br/>
|
|
||||||
[](https://aur.archlinux.org/packages/dcr)
|
|
||||||
[](https://crates.io/crates/dcr)
|
|
||||||
[](https://github.com/dexoron/homebrew-dexoron)
|
|
||||||
[](https://snapcraft.io/dcrup)
|
|
||||||
<br/>
|
|
||||||
[](https://github.com/dexoron/dcr/stargazers)
|
|
||||||
[](LICENSE)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
<td><b>Linux (Dexoron Packages)</b></td>
|
<td><b>Linux (Dexoron Packages)</b></td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
See https://dcr.dexoron.su for settings youre packages manager
|
See https://pkg.dexoron.su for settings youre packages manager
|
||||||
|
|
||||||
**Arch Linux:**
|
**Arch Linux:**
|
||||||
|
|
||||||
@@ -127,8 +126,11 @@ curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
|
|||||||
<td><b>Snap (Linux)</b></td>
|
<td><b>Snap (Linux)</b></td>
|
||||||
<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
|
```sh
|
||||||
sudo snap install dcrup
|
sudo snap install --classic --dangerous dcrup_*.snap
|
||||||
```
|
```
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
@@ -226,7 +228,7 @@ other in [docs/](docs/), `dcr --help` and `dcr <command> --help`
|
|||||||
| Linux (Musl) | ✅ | ✅ | ✅ | ✅ | 🚧 |
|
| Linux (Musl) | ✅ | ✅ | ✅ | ✅ | 🚧 |
|
||||||
| macOS | ✅ | ✅ | ❌ | ❌ | ❌ |
|
| macOS | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| Windows (MSVC) | ✅ | ✅ | ❌ | ✅ | ❌ |
|
| Windows (MSVC) | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||||
| Windows (MinGW) | ✅ | 🚧 | ❌ | ✅ | ❌ |
|
| Windows (MinGW) | ✅ | 🚧 | ❌ | ❌ | ❌ |
|
||||||
| FreeBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
|
| FreeBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
|
||||||
| OpenBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
|
| OpenBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
|
||||||
| NetBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
|
| NetBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
|
||||||
@@ -271,6 +273,7 @@ cargo test
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
GPL-3.0 — see [LICENSE](LICENSE)<br/>
|
GPL-3.0 — see [LICENSE](LICENSE)<br/>
|
||||||
|
DCR is a build tool, not a library. The GPL applies only to DCR's own source code, not to projects built with it.<br/>
|
||||||
Made with ❤️ by [Dexoron](https://github.com/dexoron) and contributors.
|
Made with ❤️ by [Dexoron](https://github.com/dexoron) and contributors.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
60
TODO.md
60
TODO.md
@@ -2,38 +2,35 @@
|
|||||||
|
|
||||||
Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers.
|
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**
|
## 🔥 High Priority
|
||||||
`git = "https://github.com/user/repo.git#branch"` and `git = "..."@tag`
|
|
||||||
- [ ] **Conan and vcpkg integration**
|
- [ ] **CLI Output Refactoring & Cleanup**
|
||||||
Automatic download, installation and linking of packages
|
- [ ] Move all terminal strings to a dedicated module (`src/ui/messages.rs`)
|
||||||
- [ ] **Full freestanding / embedded support**
|
- [ ] Standardize logging formats (`error:`, `warning:`, `status:`)
|
||||||
`-nostdlib`, linker scripts, bare-metal targets (i686-elf, aarch64-none-elf, etc.)
|
- [x] Add build execution time statistics to the final output (e.g., `Finished in 4.23s`)
|
||||||
- [ ] **EFI support**
|
- [ ] **Internationalization (i18n) / Localization**
|
||||||
Dedicated platform configuration for EFI targets
|
- [ ] Integrate a lightweight localization engine (`fluent` or `gettext` pure-Rust port)
|
||||||
- [ ] **Custom project name**
|
- [ ] Create initial translation catalogs (`en.ftl`, `ru.ftl`)
|
||||||
Override binary name in `dcr.toml`
|
- [x] **Self-Update Stability Validation**
|
||||||
- [ ] **Custom build steps / targets**
|
- [x] Self-update implemented via `self-replace` + `ureq` in `flag_update.rs`
|
||||||
Ability to define custom pre-build, post-build, codegen steps in `dcr.toml`
|
- [-] **Freestanding / Embedded Automation**
|
||||||
- [ ] **Improved mixed C/C++/ASM projects**
|
- *`-ffreestanding` injected at compile time, `-nostdlib -static` at link time for bare-metal targets or `build.freestanding = true`. Config option `build.freestanding` (bool) added.*
|
||||||
Better handling of `.S` and `.asm` files with correct flags
|
- [ ] **Conan and vcpkg integration** Automatic download, installation and linking of packages
|
||||||
- [ ] **dcr publish + official package registry**
|
|
||||||
Publish and fetch your own libraries easily
|
|
||||||
|
|
||||||
## ✅ Medium Priority
|
## ✅ Medium Priority
|
||||||
|
|
||||||
- [ ] **Static analysis** (`dcr check`)
|
- [ ] **Static analysis** (`dcr check`)
|
||||||
Integration with clang-tidy, cppcheck, include-what-you-use
|
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.)
|
- [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.)
|
||||||
- [ ] **Better cross-compilation experience**
|
- [-] **Workspaces improvements** Per-member include directories, selective builds, better dependency graph
|
||||||
Ready-to-use toolchains + automatic download
|
*Basic workspaces with dep ordering + topological sort work; advanced features missing*
|
||||||
- [ ] **Linker script support** + direct linker control
|
- [-] **Build-time code generation** Support for protobuf, flatbuffers, Qt moc, custom generators
|
||||||
- [ ] **Workspaces improvements**
|
*Generic `build.steps` machinery + Qt toolchain (uic/moc/rcc) work; no dedicated protobuf/flatbuffers generators*
|
||||||
Per-member include directories, selective builds, better dependency graph
|
- [-] **dcr publish + official package registry** *Registry consumption (fetch, resolve, link) works; `dcr publish` command / local packing missing*
|
||||||
- [ ] **Build-time code generation**
|
|
||||||
Support for protobuf, flatbuffers, Qt moc, custom generators
|
|
||||||
|
|
||||||
## 📌 Low Priority / Nice to Have
|
## 📌 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:**
|
**Currently in progress:**
|
||||||
|
- UI/UX terminal output unification and benchmarking metrics.
|
||||||
- ...
|
|
||||||
|
|
||||||
**Completed in recent releases:**
|
**Completed in recent releases:**
|
||||||
|
- Extreme binary size optimization (-50% weight reduction via `ureq` + `native-certs`)
|
||||||
- Workspaces
|
- Complete removal of `git2` and `openssl` in favor of system Git execution
|
||||||
- Cross-compilation
|
- Workspaces & Cross-compilation (`--target <triple>`)
|
||||||
- IDE config generation (VS Code, CLion, compile_commands.json)
|
- IDE config generation (VS Code, CLion, compile_commands.json)
|
||||||
- Self-update (`dcr --update`)
|
- EFI support & Linker script support (`build.ldscript`)
|
||||||
- Header tracking
|
- Mixed C/C++/ASM compilation with custom `std::thread::scope` work-stealing engine
|
||||||
33
build.rs
33
build.rs
@@ -18,4 +18,37 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
|
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
|
||||||
println!("cargo:rustc-env=DCR_TARGET={target}");
|
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=");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
crowdin.yml
Normal file
3
crowdin.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
files:
|
||||||
|
- source: /docs/**/*.mdx
|
||||||
|
translation: /docs-crowdin-export/%locale%/%original_path%/%original_file_name%.%file_extension%
|
||||||
@@ -44,6 +44,7 @@ dcr gen vscode
|
|||||||
| [Self Update](/docs/self-update) | Auto-update |
|
| [Self Update](/docs/self-update) | Auto-update |
|
||||||
| [FAQ](/docs/faq) | Frequently asked questions |
|
| [FAQ](/docs/faq) | Frequently asked questions |
|
||||||
| [Changelog](/docs/changelog) | Version history |
|
| [Changelog](/docs/changelog) | Version history |
|
||||||
|
| [License](/docs/license) | DCR and vendored library licenses |
|
||||||
| [Contributing](/docs/contributing) | DCR development |
|
| [Contributing](/docs/contributing) | DCR development |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ Only ASCII letters, digits, underscores `_` and hyphens `-` are allowed in the p
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
dcr new my-app
|
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:
|
Generates:
|
||||||
- `dcr.toml` with basic fields
|
- `dcr.toml` with basic fields
|
||||||
- `src/main.c` with a `main` template
|
- `src/main.c` with a `main` template
|
||||||
@@ -23,8 +27,11 @@ Generates:
|
|||||||
Initializes a DCR project in the current (empty) directory.
|
Initializes a DCR project in the current (empty) directory.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir app && cd app
|
|
||||||
dcr init
|
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`.
|
The project name is taken from the current directory name. The directory name must follow the same naming rules as `dcr new`.
|
||||||
|
|||||||
@@ -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}`.
|
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.
|
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.
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ dcr setup
|
|||||||
If `~/.dcr/config.toml` is not found, DCR returns an error. Create it manually:
|
If `~/.dcr/config.toml` is not found, DCR returns an error. Create it manually:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[registry]]
|
[registry.default]
|
||||||
name = "default"
|
|
||||||
url = "https://index.dcr.pm"
|
url = "https://index.dcr.pm"
|
||||||
|
priority = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
## `dcr --help`
|
## `dcr --help`
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ error: registry not found
|
|||||||
**Solution:** Make sure `~/.dcr/config.toml` exists:
|
**Solution:** Make sure `~/.dcr/config.toml` exists:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[registry]]
|
[registry.default]
|
||||||
name = "default"
|
|
||||||
url = "https://index.dcr.pm"
|
url = "https://index.dcr.pm"
|
||||||
|
priority = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
And check `DCR_INDEX_PATH`:
|
And check `DCR_INDEX_PATH`:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ sidebar_label: Installation
|
|||||||
|
|
||||||
## Linux (Dexoron Packages)
|
## 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**
|
**Arch Linux**
|
||||||
```sh
|
```sh
|
||||||
@@ -100,7 +100,7 @@ man dcr-build
|
|||||||
To use the registry, create `~/.dcr/config.toml`:
|
To use the registry, create `~/.dcr/config.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[registry]]
|
[registry.default]
|
||||||
name = "default"
|
|
||||||
url = "https://index.dcr.pm"
|
url = "https://index.dcr.pm"
|
||||||
|
priority = 1
|
||||||
```
|
```
|
||||||
|
|||||||
33
docs/license.mdx
Normal file
33
docs/license.mdx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
sidebar_label: License
|
||||||
|
---
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
## DCR
|
||||||
|
|
||||||
|
DCR itself is [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) licensed.
|
||||||
|
|
||||||
|
> **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.
|
||||||
|
|
||||||
|
## Rust dependencies
|
||||||
|
|
||||||
|
DCR is written in Rust. The following notable Rust crates are statically linked:
|
||||||
|
|
||||||
|
| Crate | License |
|
||||||
|
|---|---|
|
||||||
|
| [`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 |
|
||||||
|
| [`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 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
|
||||||
|
|
||||||
|
- DCR's CLI design and project model are inspired by [Cargo](https://doc.rust-lang.org/cargo/).
|
||||||
@@ -34,6 +34,7 @@ type = "none"
|
|||||||
inherit = true
|
inherit = true
|
||||||
language = "c"
|
language = "c"
|
||||||
standard = "c11"
|
standard = "c11"
|
||||||
|
workspace_only = true
|
||||||
|
|
||||||
[workspace.lib-core]
|
[workspace.lib-core]
|
||||||
path = "lib-core"
|
path = "lib-core"
|
||||||
|
|||||||
@@ -32,11 +32,37 @@ Set `inherit = false` to disable array inheritance (only profile's own arrays ar
|
|||||||
|
|
||||||
## Built-in profiles
|
## 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` |
|
| `debug` | `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` |
|
||||||
| `release` | `-O3 -DNDEBUG` |
|
| `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
|
## Target-specific profiles
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
|||||||
@@ -6,30 +6,60 @@ sidebar_label: Build system
|
|||||||
|
|
||||||
## Compiler Backends
|
## 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 |
|
| Backend | Files | When used |
|
||||||
|---------|-------|-----------|
|
|---------|-------|-----------|
|
||||||
| `unix_cc` | `.c`, `.cpp`, `.cxx`, `.cc`, `.S` | gcc/clang on Linux/macOS/BSD |
|
| `unix_cc` | `.c`, `.cpp`, `.cxx`, `.cc`, `.S` | gcc/clang on Linux/macOS/BSD |
|
||||||
| `msvc` | `.c`, `.cpp`, `.cxx`, `.cc` | Windows (cl, clang-cl) |
|
| `msvc` | `.c`, `.cpp`, `.cxx`, `.cc` | Windows (cl, clang-cl) |
|
||||||
| `gas` | `.s` | ARM/ARM64 assembler (no preprocessor) |
|
| `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
|
### Unix CC
|
||||||
|
|
||||||
- Compiler resolved via `resolve_compiler()`: `DCR_COMPILER` > `DCR_CC` > `[toolchain]` > `build.compiler` > `PATH`
|
- Compiler resolved via `resolve_compiler()`: `DCR_COMPILER` > `DCR_CC` > `[toolchain]` > `build.compiler` > `PATH`
|
||||||
- Supports `.d` files for header dependency tracking
|
- Supports `.d` files for header dependency tracking
|
||||||
- Flags: `-std=`, `-MMD -MF`, `-c -o`, `-I`, `-L`, `-l`
|
- 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
|
### MSVC
|
||||||
|
|
||||||
- Supports cl.exe and clang-cl.exe
|
- Supports cl.exe and clang-cl.exe
|
||||||
- Flags: `/std:`, `/Fo:`, `/Fe:`, `/I`, `/link`
|
- Flags: `/std:`, `/Fo:`, `/Fe:`, `/I`, `/link`
|
||||||
|
|
||||||
### GAS / NASM
|
### GAS / NASM / MASM / FASM / LLVM-IR
|
||||||
|
|
||||||
- GAS: `-I`, `-c -o`, `--defsym`
|
- GAS: `-I`, `-c -o`, `--defsym`
|
||||||
- NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32)
|
- 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
|
## Incremental Builds
|
||||||
|
|
||||||
@@ -44,7 +74,7 @@ Three levels of incrementality:
|
|||||||
- `thread::scope` for thread pool
|
- `thread::scope` for thread pool
|
||||||
- Atomic task queue (`AtomicU64`)
|
- Atomic task queue (`AtomicU64`)
|
||||||
- Mutex on stdout (`OUTPUT_MUTEX`)
|
- Mutex on stdout (`OUTPUT_MUTEX`)
|
||||||
- Thread count = `available_parallelism()`
|
- Thread count = `available_parallelism()`, capped by `build.codegen-units` if set
|
||||||
|
|
||||||
## Build Kinds
|
## Build Kinds
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,19 @@ target = "aarch64-unknown-linux-gnu"
|
|||||||
# Auto: cflags += ["--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
|
```bash
|
||||||
dcr build --target aarch64-none-elf
|
dcr build --target aarch64-none-elf
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Main project configuration file. Located at the project root.
|
|||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
| `name` | yes | Project name |
|
| `name` | yes | Project name |
|
||||||
| `version` | yes | Semantic version |
|
| `version` | yes | Semantic version |
|
||||||
| `type` | yes | `app`, `lib`, `none` |
|
| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) |
|
||||||
| `license` | no | SPDX license identifier |
|
| `license` | no | SPDX license identifier |
|
||||||
| `author` | no | Author |
|
| `author` | no | Author |
|
||||||
|
|
||||||
@@ -56,15 +56,19 @@ author = "John Doe"
|
|||||||
|
|
||||||
## [build]
|
## [build]
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
- `build.qt` — (bool) Enable automatic Qt meta-object handling (MOC, UIC, RCC). Requires `qt6` modules installed via `pkg-config`.
|
||||||
|
|
||||||
| Field | Default | Description |
|
| 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`) |
|
| `standard` | `"c11"` | C standard (`c11`, `c17`, `c23`) |
|
||||||
| `cxx_standard` | `"c++20"` | C++ standard (`c++17`, `c++20`, `c++23`) |
|
| `cxx_standard` | — | C++ standard (`c++17`, `c++20`, `c++23`) |
|
||||||
| `compiler` | `"clang"` | Preferred compiler |
|
| `compiler` | `"clang"` | Preferred compiler (optional, defaults to `"clang"`) |
|
||||||
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` |
|
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` |
|
||||||
| `target` | host | Target triple for cross-compilation |
|
| `target` | host | Target triple for cross-compilation |
|
||||||
| `platform` | `"native"` | `native`, `efi`, `freestanding` |
|
| `platform` | `"native"` | `native`, `efi` |
|
||||||
| `cflags` | `[]` | Additional C/C++/ASM flags |
|
| `cflags` | `[]` | Additional C/C++/ASM flags |
|
||||||
| `ldflags` | `[]` | Additional linker flags |
|
| `ldflags` | `[]` | Additional linker flags |
|
||||||
| `filename` | `""` | Custom output file name |
|
| `filename` | `""` | Custom output file name |
|
||||||
@@ -76,13 +80,46 @@ author = "John Doe"
|
|||||||
| `inherit` | `false` | Inherit build from workspace root |
|
| `inherit` | `false` | Inherit build from workspace root |
|
||||||
| `clean` | `[]` | Glob patterns for custom clean paths |
|
| `clean` | `[]` | Glob patterns for custom clean paths |
|
||||||
| `out_dir` | `""` | Custom output directory |
|
| `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):
|
Settings from raw config (not in typed struct):
|
||||||
- `pkg_config` — list of pkg-config packages
|
- `pkg_config` — list of pkg-config packages
|
||||||
- `ldscript` — linker script path
|
- `ldscript` — linker script path
|
||||||
- `build.steps` / `build.post_steps` — codegen steps
|
- `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:
|
Example:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -93,6 +130,11 @@ cxx_standard = "c++23"
|
|||||||
compiler = "clang"
|
compiler = "clang"
|
||||||
kind = "sharedlib"
|
kind = "sharedlib"
|
||||||
cflags = ["-Wall", "-Wextra"]
|
cflags = ["-Wall", "-Wextra"]
|
||||||
|
opt_level = "z"
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
codegen-units = "2"
|
||||||
```
|
```
|
||||||
|
|
||||||
## [toolchain]
|
## [toolchain]
|
||||||
|
|||||||
@@ -41,24 +41,22 @@ DCR uses a package registry for dependency lookup by name.
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
# ~/.dcr/config.toml
|
# ~/.dcr/config.toml
|
||||||
[[registry]]
|
[registry.default]
|
||||||
name = "default"
|
|
||||||
url = "https://index.dcr.pm"
|
url = "https://index.dcr.pm"
|
||||||
|
priority = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrides the path to `index.json`.
|
Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrides the path to `index.json`.
|
||||||
|
|
||||||
## Git dependencies
|
## 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
|
- `branch` — switch to a branch
|
||||||
- `tag` — switch to a tag
|
- `tag` — switch to a tag
|
||||||
- `rev` — switch to a specific commit
|
- `rev` — switch to a specific commit
|
||||||
- `features` — feature flags (parsed, but does not affect build)
|
- `features` — feature flags (parsed, but does not affect build)
|
||||||
|
|
||||||
Cache path: `~/.dcr/git/<hash>/`.
|
|
||||||
|
|
||||||
## Path dependencies
|
## Path dependencies
|
||||||
|
|
||||||
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.
|
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.
|
||||||
|
|||||||
@@ -65,3 +65,21 @@ standard = "c17"
|
|||||||
|
|
||||||
# member inherits language and standard
|
# 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"] }
|
||||||
|
```
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::core::config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::core::deps::register;
|
use crate::core::deps::register;
|
||||||
use crate::utils::fs::find_project_root;
|
use crate::utils::fs::find_project_root;
|
||||||
use crate::utils::log::error;
|
use crate::utils::log::error;
|
||||||
@@ -34,15 +34,17 @@ pub struct AddArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(args: &[String]) -> i32 {
|
pub fn add(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]", BOLD_CYAN);
|
printc(
|
||||||
|
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
|
||||||
|
BOLD_CYAN,
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
printc("DESCRIPTION:", BOLD_GREEN);
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
println!(" Adds a dependency to the project.");
|
println!(" Adds a dependency to the project.");
|
||||||
println!();
|
println!();
|
||||||
printc("SOURCES:", BOLD_GREEN);
|
printc("SOURCES:", BOLD_GREEN);
|
||||||
println!(" user/repo GitHub shorthand");
|
|
||||||
println!(" github:user/repo GitHub repository");
|
println!(" github:user/repo GitHub repository");
|
||||||
println!(" gitlab:user/repo GitLab repository");
|
println!(" gitlab:user/repo GitLab repository");
|
||||||
println!(" git:host.com/user/repo Generic git repository");
|
println!(" git:host.com/user/repo Generic git repository");
|
||||||
@@ -137,7 +139,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
|||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
|
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
|
||||||
error("Sources:");
|
error("Sources:");
|
||||||
error(" user/repo -> github.com/user/repo");
|
|
||||||
error(" github:user/repo -> github.com/user/repo");
|
error(" github:user/repo -> github.com/user/repo");
|
||||||
error(" gitlab:user/repo -> gitlab.com/user/repo");
|
error(" gitlab:user/repo -> gitlab.com/user/repo");
|
||||||
error(" git:host.com/user/repo -> host.com/user/repo");
|
error(" git:host.com/user/repo -> host.com/user/repo");
|
||||||
|
|||||||
1716
src/cli/build.rs
1716
src/cli/build.rs
File diff suppressed because it is too large
Load Diff
@@ -15,10 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::config::flags;
|
use crate::core::build_config::Config;
|
||||||
use crate::core::config::Config;
|
|
||||||
use crate::core::workspace::parse_workspace;
|
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::fs::{check_dir, find_project_root, with_dir};
|
||||||
use crate::utils::log::{error, warn};
|
use crate::utils::log::{error, warn};
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
@@ -27,9 +26,12 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn clean(args: &[String]) -> i32 {
|
pub fn clean(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr clean [--debug | --release] [--target <triple>] [--all]", BOLD_CYAN);
|
printc(
|
||||||
|
" dcr clean [--debug | --release] [--target <triple>] [--all]",
|
||||||
|
BOLD_CYAN,
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
printc("DESCRIPTION:", BOLD_GREEN);
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
println!(" Removes build artifacts from the target directory.");
|
println!(" Removes build artifacts from the target directory.");
|
||||||
@@ -103,7 +105,7 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
|||||||
}
|
}
|
||||||
if arg.starts_with("--") {
|
if arg.starts_with("--") {
|
||||||
let candidate = arg.trim_start_matches("--").to_string();
|
let candidate = arg.trim_start_matches("--").to_string();
|
||||||
if flags(&candidate).is_some() {
|
if !default_profile_flags(&candidate).is_empty() {
|
||||||
if profile.is_some() {
|
if profile.is_some() {
|
||||||
return Err("Duplicate profile flag".to_string());
|
return Err("Duplicate profile flag".to_string());
|
||||||
}
|
}
|
||||||
@@ -133,28 +135,7 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
|||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| Some(default_target_triple()));
|
||||||
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()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if flags.all
|
if flags.all
|
||||||
&& let Some(workspace) = parse_workspace(
|
&& let Some(workspace) = parse_workspace(
|
||||||
@@ -245,7 +226,7 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
|||||||
.filter_map(|item| item.as_str())
|
.filter_map(|item| item.as_str())
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
let value = s.replace("{profile}", profile);
|
let value = s.replace("{profile}", profile);
|
||||||
substitute_version_vars(&value, config)
|
substitute_clean_vars(&value, config)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
@@ -266,17 +247,11 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn substitute_version_vars(value: &str, config: &Config) -> String {
|
fn substitute_clean_vars(value: &str, config: &Config) -> String {
|
||||||
let version = config
|
let version = config
|
||||||
.get("package.version")
|
.get("package.version")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
let info = parse_version_info(version);
|
let info = parse_version_info(version);
|
||||||
value
|
crate::utils::build::substitute_version_vars(value, &info)
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
|
|
||||||
use crate::utils::log::{error, warn};
|
use crate::utils::log::{error, warn};
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
use reqwest::blocking::Client;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -40,7 +40,7 @@ struct ReleaseAsset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn flag_update(args: &[String]) -> i32 {
|
pub fn flag_update(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr --update", BOLD_CYAN);
|
printc(" dcr --update", BOLD_CYAN);
|
||||||
println!();
|
println!();
|
||||||
@@ -72,15 +72,8 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
|
|
||||||
let current_version = env!("CARGO_PKG_VERSION");
|
let current_version = env!("CARGO_PKG_VERSION");
|
||||||
let target = env!("DCR_TARGET");
|
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,
|
Ok(release) => release,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error(&format!("Failed to check for updates: {err}"));
|
error(&format!("Failed to check for updates: {err}"));
|
||||||
@@ -104,7 +97,7 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
let bytes = match download_asset(&client, &asset.browser_download_url) {
|
let bytes = match download_asset(&asset.browser_download_url) {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error(&format!("Failed to download update: {err}"));
|
error(&format!("Failed to download update: {err}"));
|
||||||
@@ -131,35 +124,35 @@ pub fn flag_update(args: &[String]) -> i32 {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_latest_release(client: &Client) -> Result<Release, String> {
|
fn fetch_latest_release() -> Result<Release, String> {
|
||||||
let response = client
|
let response = ureq::get(LATEST_RELEASE_URL)
|
||||||
.get(LATEST_RELEASE_URL)
|
.set("User-Agent", "dcr-updater")
|
||||||
.send()
|
.call()
|
||||||
.map_err(|_| "GitHub API request failed".to_string())?;
|
.map_err(|e| match e {
|
||||||
|
ureq::Error::Status(code, _) => format!("GitHub API returned status {code}"),
|
||||||
if !response.status().is_success() {
|
ureq::Error::Transport(e) => format!("GitHub API request failed: {e}"),
|
||||||
return Err(format!("GitHub API returned status {}", response.status()));
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
response
|
response
|
||||||
.json::<Release>()
|
.into_json::<Release>()
|
||||||
.map_err(|_| "GitHub API response has an unexpected format".to_string())
|
.map_err(|_| "GitHub API response has an unexpected format".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_asset(client: &Client, url: &str) -> Result<Vec<u8>, String> {
|
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
|
||||||
let response = client
|
let response = ureq::get(url)
|
||||||
.get(url)
|
.set("User-Agent", "dcr-updater")
|
||||||
.send()
|
.call()
|
||||||
.map_err(|_| "Download request failed".to_string())?;
|
.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() {
|
let mut reader = response.into_reader();
|
||||||
return Err(format!("Download returned status {}", response.status()));
|
let mut data = Vec::new();
|
||||||
}
|
reader
|
||||||
|
.read_to_end(&mut data)
|
||||||
response
|
.map_err(|_| "Failed to read downloaded data".to_string())?;
|
||||||
.bytes()
|
Ok(data)
|
||||||
.map(|bytes| bytes.to_vec())
|
|
||||||
.map_err(|_| "Failed to read downloaded data".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn asset_candidates(target: &str) -> Vec<String> {
|
fn asset_candidates(target: &str) -> Vec<String> {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use glob::glob;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn fmt(args: &[String]) -> i32 {
|
pub fn fmt(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr fmt", BOLD_CYAN);
|
printc(" dcr fmt", BOLD_CYAN);
|
||||||
println!();
|
println!();
|
||||||
|
|||||||
@@ -15,15 +15,15 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::core::builder::BuildContext;
|
use crate::core::build::builder::BuildContext;
|
||||||
use crate::core::builder::collect_sources;
|
use crate::core::build::builder::collect_sources;
|
||||||
use crate::core::builder::common;
|
use crate::core::build::common;
|
||||||
use crate::core::config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::core::workspace::parse_workspace;
|
use crate::core::workspace::parse_workspace;
|
||||||
use crate::utils::build::{
|
use crate::utils::build::{
|
||||||
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,
|
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,
|
get_list_with_profile, get_string_with_profile, normalize_kind, normalize_platform,
|
||||||
resolve_pkg_config_flags_lossy,
|
normalize_target, resolve_compiler, resolve_pkg_config_flags_lossy,
|
||||||
};
|
};
|
||||||
use crate::utils::fs::find_project_root;
|
use crate::utils::fs::find_project_root;
|
||||||
use crate::utils::log::error;
|
use crate::utils::log::error;
|
||||||
@@ -366,7 +366,13 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
platform: normalize_platform(&platform),
|
platform: normalize_platform(&platform),
|
||||||
linker: resolved_linker.as_deref(),
|
linker: resolved_linker.as_deref(),
|
||||||
archiver: resolved_archiver.as_deref(),
|
archiver: resolved_archiver.as_deref(),
|
||||||
|
moc: None,
|
||||||
|
uic: None,
|
||||||
|
rcc: None,
|
||||||
package_type: None,
|
package_type: None,
|
||||||
|
freestanding: false,
|
||||||
|
panic_abort: false,
|
||||||
|
codegen_units: 0,
|
||||||
source_roots: &source_roots,
|
source_roots: &source_roots,
|
||||||
exclude_dirs: &combined_excludes,
|
exclude_dirs: &combined_excludes,
|
||||||
include_paths: &combined_includes,
|
include_paths: &combined_includes,
|
||||||
@@ -378,6 +384,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
output_filename: None,
|
output_filename: None,
|
||||||
output_extension: None,
|
output_extension: None,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
|
qt: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let sources = collect_sources(&ctx).map_err(|e| format!("Failed to collect sources: {e}"))?;
|
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());
|
cmd.push("-c".to_string());
|
||||||
|
|
||||||
// ASM x flag — must be before source file
|
// 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("-x".to_string());
|
||||||
cmd.push(flag.to_string());
|
cmd.push(flag.to_string());
|
||||||
}
|
}
|
||||||
@@ -681,15 +688,6 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
|
|||||||
cmd
|
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 {
|
fn strip_src_prefix(p: &Path) -> PathBuf {
|
||||||
// Try to strip leading ./src or src
|
// Try to strip leading ./src or src
|
||||||
let s = p.to_string_lossy();
|
let s = p.to_string_lossy();
|
||||||
@@ -1238,25 +1236,6 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
|
|||||||
Ok((root, profile))
|
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 {
|
fn json_str(s: &str) -> String {
|
||||||
let mut result = String::with_capacity(s.len() + 2);
|
let mut result = String::with_capacity(s.len() + 2);
|
||||||
result.push('"');
|
result.push('"');
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::config::FILE_MAIN_C;
|
use crate::config::FILE_MAIN_C;
|
||||||
use crate::core::config::{validate_package_name, Config};
|
use crate::core::build_config::{Config, validate_package_name};
|
||||||
|
use crate::core::vcs::VcsKind;
|
||||||
use crate::utils::fs::check_dir;
|
use crate::utils::fs::check_dir;
|
||||||
use crate::utils::log::{error, warn};
|
use crate::utils::log::{error, warn};
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
@@ -25,9 +26,9 @@ use std::io::Write;
|
|||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
pub fn init(args: &[String]) -> i32 {
|
pub fn init(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr init", BOLD_CYAN);
|
printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
|
||||||
println!();
|
println!();
|
||||||
printc("DESCRIPTION:", BOLD_GREEN);
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
println!(" Initializes the current directory as a DCR project.");
|
println!(" Initializes the current directory as a DCR project.");
|
||||||
@@ -35,7 +36,25 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
return 0;
|
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");
|
warn("Command does not support additional arguments");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -106,6 +125,40 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
colored("src/main.c", BOLD_CYAN)
|
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!(
|
println!(
|
||||||
"Project `{}` successfully created\n",
|
"Project `{}` successfully created\n",
|
||||||
colored(project_name.as_str(), BOLD_GREEN)
|
colored(project_name.as_str(), BOLD_GREEN)
|
||||||
|
|||||||
189
src/cli/lint.rs
Normal file
189
src/cli/lint.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ pub mod fmt;
|
|||||||
pub mod r#gen;
|
pub mod r#gen;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
|
pub mod lint;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::config::FILE_MAIN_C;
|
use crate::config::FILE_MAIN_C;
|
||||||
use crate::core::config::{validate_package_name, Config};
|
use crate::core::build_config::{Config, validate_package_name};
|
||||||
|
use crate::core::vcs::VcsKind;
|
||||||
use crate::utils::fs::check_dir;
|
use crate::utils::fs::check_dir;
|
||||||
use crate::utils::log::{error, warn};
|
use crate::utils::log::{error, warn};
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
|
||||||
@@ -25,9 +26,9 @@ use std::io::Write;
|
|||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
pub fn new(args: &[String]) -> i32 {
|
pub fn new(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr new <name>", BOLD_CYAN);
|
printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
|
||||||
println!();
|
println!();
|
||||||
printc("DESCRIPTION:", BOLD_GREEN);
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
println!(" Creates a new C/C++ project with the given name.");
|
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();
|
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");
|
error("Project name not specified");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if args.len() > 1 {
|
if clean_args.len() > 1 {
|
||||||
warn("Command does not support additional arguments");
|
warn("Command does not support additional arguments");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let project_name = &args[0];
|
let project_name = &clean_args[0];
|
||||||
println!(
|
println!(
|
||||||
"Creating a Project `{}`...",
|
"Creating a Project `{}`...",
|
||||||
colored(project_name, BOLD_CYAN)
|
colored(project_name, BOLD_CYAN)
|
||||||
@@ -127,6 +146,40 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
colored("src/main.c", BOLD_CYAN)
|
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!(
|
println!(
|
||||||
"Project `{}` successfully created\n",
|
"Project `{}` successfully created\n",
|
||||||
colored(project_name, BOLD_GREEN)
|
colored(project_name, BOLD_GREEN)
|
||||||
|
|||||||
@@ -17,9 +17,11 @@
|
|||||||
|
|
||||||
use crate::cli::build::build;
|
use crate::cli::build::build;
|
||||||
use crate::cli::flags::parse_build_run_flags;
|
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::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::find_project_root;
|
||||||
use crate::utils::fs::with_dir;
|
use crate::utils::fs::with_dir;
|
||||||
use crate::utils::log::error;
|
use crate::utils::log::error;
|
||||||
@@ -56,9 +58,12 @@ fn get_run_cmd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(args: &[String]) -> i32 {
|
pub fn run(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]", BOLD_CYAN);
|
printc(
|
||||||
|
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
|
||||||
|
BOLD_CYAN,
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
printc("DESCRIPTION:", BOLD_GREEN);
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
||||||
@@ -175,27 +180,7 @@ fn run_project(
|
|||||||
// If no target specified, use default host target for target-specific config
|
// If no target specified, use default host target for target-specific config
|
||||||
let mut target = flags.target.clone();
|
let mut target = flags.target.clone();
|
||||||
if target.is_none() {
|
if target.is_none() {
|
||||||
let default_target = if cfg!(target_os = "linux") {
|
target = Some(default_target_triple());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let build_kind = config
|
let build_kind = config
|
||||||
@@ -212,11 +197,11 @@ fn run_project(
|
|||||||
match workspace_root {
|
match workspace_root {
|
||||||
Some(wr) => target
|
Some(wr) => target
|
||||||
.as_ref()
|
.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()),
|
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
|
||||||
None => target
|
None => target
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)),
|
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,7 +218,14 @@ fn run_project(
|
|||||||
return Err("Cannot run library build".to_string());
|
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(
|
let bin_path = crate::platform::bin_path(
|
||||||
&flags.profile,
|
&flags.profile,
|
||||||
project_name,
|
project_name,
|
||||||
@@ -281,6 +273,36 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
|||||||
args.push("--workspace".to_string());
|
args.push("--workspace".to_string());
|
||||||
args.push(name.clone());
|
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 {
|
if flags.verbose {
|
||||||
args.push("--verbose".to_string());
|
args.push("--verbose".to_string());
|
||||||
}
|
}
|
||||||
@@ -302,11 +324,5 @@ fn run_shell(cmd: &str) -> i32 {
|
|||||||
|
|
||||||
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
||||||
let info = parse_version_info(version);
|
let info = parse_version_info(version);
|
||||||
cmd.replace("{profile}", profile)
|
substitute_vars(cmd, &info, 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
|
|
||||||
pub fn setup(args: &[String]) -> i32 {
|
pub fn setup(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr setup", BOLD_CYAN);
|
printc(" dcr setup", BOLD_CYAN);
|
||||||
println!();
|
println!();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
use crate::cli::build;
|
use crate::cli::build;
|
||||||
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
|
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::{
|
use crate::utils::build::{
|
||||||
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
|
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
|
||||||
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
|
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::core::config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::utils::log::error;
|
use crate::utils::log::error;
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -23,7 +23,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
pub fn tree(args: &[String]) -> i32 {
|
pub fn tree(args: &[String]) -> i32 {
|
||||||
if args.first().map_or(false, |a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(" dcr tree", BOLD_CYAN);
|
printc(" dcr tree", BOLD_CYAN);
|
||||||
println!();
|
println!();
|
||||||
|
|||||||
@@ -97,16 +97,6 @@ int main(int argc, char **argv) {
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
||||||
match profile {
|
let f = crate::utils::build::default_profile_flags(profile);
|
||||||
"release" => Some(&["-O3", "-DNDEBUG"]),
|
if f.is_empty() { None } else { Some(f) }
|
||||||
"debug" => Some(&[
|
|
||||||
"-O0",
|
|
||||||
"-g",
|
|
||||||
"-Wall",
|
|
||||||
"-Wextra",
|
|
||||||
"-fno-omit-frame-pointer",
|
|
||||||
"-DDCR_DEBUG",
|
|
||||||
]),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/core/build/builder/artifact.rs
Normal file
126
src/core/build/builder/artifact.rs
Normal 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}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
209
src/core/build/builder/cc_common.rs
Normal file
209
src/core/build/builder/cc_common.rs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
// 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 == "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)
|
||||||
|
}
|
||||||
234
src/core/build/builder/mod.rs
Normal file
234
src/core/build/builder/mod.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::core::builder::BuildContext;
|
use crate::core::build::builder::BuildContext;
|
||||||
use crate::core::builder::common;
|
use crate::core::build::common;
|
||||||
use crate::platform;
|
use crate::platform;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -44,7 +44,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
if ctx.kind == "staticlib" {
|
if ctx.kind == "staticlib" {
|
||||||
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
|
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
|
||||||
if !common::needs_link(&objects, &lib_path) {
|
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);
|
return Ok(elapsed);
|
||||||
}
|
}
|
||||||
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
|
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
|
||||||
@@ -57,7 +57,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
}
|
}
|
||||||
match cmd.status() {
|
match cmd.status() {
|
||||||
Ok(status) if status.success() => {
|
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);
|
return Ok(elapsed);
|
||||||
}
|
}
|
||||||
Ok(_) => return Err("Build failed".to_string()),
|
Ok(_) => return Err("Build failed".to_string()),
|
||||||
@@ -123,17 +123,17 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !common::needs_link(&objects, &out_path) {
|
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);
|
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() {
|
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||||
eprintln!("[dcr] {:?}", cmd);
|
eprintln!("[dcr] {:?}", cmd);
|
||||||
}
|
}
|
||||||
match cmd.status() {
|
match cmd.status() {
|
||||||
Ok(status) if status.success() => {
|
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(elapsed)
|
||||||
}
|
}
|
||||||
Ok(_) => Err("Build failed".to_string()),
|
Ok(_) => Err("Build failed".to_string()),
|
||||||
@@ -151,21 +151,8 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_extensions(language: &str) -> Vec<&str> {
|
fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||||
let mut out = Vec::new();
|
crate::core::build::common::source_extensions(language)
|
||||||
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 msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
|
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
|
||||||
@@ -228,9 +215,11 @@ fn build_objects(
|
|||||||
.map(|s| common::object_path(obj_dir, s, obj_ext))
|
.map(|s| common::object_path(obj_dir, s, obj_ext))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
common::parallel_build(sources.len(), |i| {
|
common::parallel_build(
|
||||||
build_object(compiler, &sources[i], &objects[i], ctx)
|
sources.len(),
|
||||||
})?;
|
|i| build_object(compiler, &sources[i], &objects[i], ctx),
|
||||||
|
ctx.codegen_units,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(objects)
|
Ok(objects)
|
||||||
}
|
}
|
||||||
254
src/core/build/cache.rs
Normal file
254
src/core/build/cache.rs
Normal 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)
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
|
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() {
|
if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() {
|
||||||
return Ok(());
|
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 {
|
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();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
|
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, '*' | '?' | '['))
|
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
|
where
|
||||||
F: Fn(usize) -> Result<(), String> + Sync + Send,
|
F: Fn(usize) -> Result<(), String> + Sync + Send,
|
||||||
{
|
{
|
||||||
let num_threads = std::thread::available_parallelism()
|
let available = std::thread::available_parallelism()
|
||||||
.map(|n| n.get())
|
.map(|n| n.get())
|
||||||
.unwrap_or(1);
|
.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 counter = std::sync::atomic::AtomicUsize::new(0);
|
||||||
let err_msg = std::sync::Mutex::new(None);
|
let err_msg = std::sync::Mutex::new(None);
|
||||||
@@ -295,7 +328,7 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return true; // Missing dependency triggers rebuild
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,7 +534,6 @@ mod tests {
|
|||||||
let src = dir.join("test.c");
|
let src = dir.join("test.c");
|
||||||
let obj = dir.join("test.o");
|
let obj = dir.join("test.o");
|
||||||
fs::write(&src, "int main() {}").unwrap();
|
fs::write(&src, "int main() {}").unwrap();
|
||||||
// Sleep briefly to ensure mtime difference
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
fs::write(&obj, "").unwrap();
|
fs::write(&obj, "").unwrap();
|
||||||
assert!(!needs_rebuild(
|
assert!(!needs_rebuild(
|
||||||
@@ -791,4 +823,44 @@ mod tests {
|
|||||||
"changed header should trigger rebuild"
|
"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
1084
src/core/build/engine.rs
Normal file
File diff suppressed because it is too large
Load Diff
83
src/core/build/language/asm/common.rs
Normal file
83
src/core/build/language/asm/common.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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 == "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)
|
||||||
|
}
|
||||||
47
src/core/build/language/asm/fasm/mod.rs
Normal file
47
src/core/build/language/asm/fasm/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
73
src/core/build/language/asm/gas/mod.rs
Normal file
73
src/core/build/language/asm/gas/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
51
src/core/build/language/asm/masm/mod.rs
Normal file
51
src/core/build/language/asm/masm/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
38
src/core/build/language/asm/mod.rs
Normal file
38
src/core/build/language/asm/mod.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/core/build/language/asm/nasm/mod.rs
Normal file
107
src/core/build/language/asm/nasm/mod.rs
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/core/build/language/c/mod.rs
Normal file
32
src/core/build/language/c/mod.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/core/build/language/cxx/mod.rs
Normal file
34
src/core/build/language/cxx/mod.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/core/build/language/cxx/qt.rs
Normal file
137
src/core/build/language/cxx/qt.rs
Normal 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/core/build/language/llvm_ir/mod.rs
Normal file
58
src/core/build/language/llvm_ir/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
57
src/core/build/language/mod.rs
Normal file
57
src/core/build/language/mod.rs
Normal 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
29
src/core/build/mod.rs
Normal 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
83
src/core/build/report.rs
Normal 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
232
src/core/build/resolve.rs
Normal 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
398
src/core/build/steps.rs
Normal 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)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
use toml::map::Map;
|
use toml::map::Map;
|
||||||
|
use toml_edit::{DocumentMut, Item, Table};
|
||||||
|
|
||||||
const DEFAULT_VERSION: &str = "0.1.0";
|
const DEFAULT_VERSION: &str = "0.1.0";
|
||||||
const DEFAULT_LANGUAGE: &str = "c";
|
const DEFAULT_LANGUAGE: &str = "c";
|
||||||
@@ -42,6 +43,7 @@ pub enum ConfigError {
|
|||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
TomlDe(toml::de::Error),
|
TomlDe(toml::de::Error),
|
||||||
TomlSer(toml::ser::Error),
|
TomlSer(toml::ser::Error),
|
||||||
|
TomlEdit(toml_edit::TomlError),
|
||||||
Invalid(String),
|
Invalid(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ impl std::fmt::Display for ConfigError {
|
|||||||
ConfigError::Io(err) => write!(f, "I/O error: {err}"),
|
ConfigError::Io(err) => write!(f, "I/O error: {err}"),
|
||||||
ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"),
|
ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"),
|
||||||
ConfigError::TomlSer(err) => write!(f, "TOML serialize 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}"),
|
ConfigError::Invalid(msg) => write!(f, "Invalid config: {msg}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +83,10 @@ pub struct Config {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
data: Value,
|
data: Value,
|
||||||
typed: DcrConfig,
|
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)]
|
#[allow(dead_code)]
|
||||||
@@ -105,14 +112,24 @@ pub struct PackageConfig {
|
|||||||
pub pkg_type: Option<String>,
|
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)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct BuildConfig {
|
pub struct BuildConfig {
|
||||||
|
#[serde(default = "default_language")]
|
||||||
pub language: LanguageConfig,
|
pub language: LanguageConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub standard: Option<String>,
|
pub standard: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cxx_standard: Option<String>,
|
pub cxx_standard: Option<String>,
|
||||||
|
#[serde(default = "default_compiler")]
|
||||||
pub compiler: String,
|
pub compiler: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub kind: Option<String>,
|
pub kind: Option<String>,
|
||||||
@@ -138,6 +155,8 @@ pub struct BuildConfig {
|
|||||||
pub src_disable: Option<bool>,
|
pub src_disable: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub workspace_only: bool,
|
pub workspace_only: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub qt: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -180,20 +199,29 @@ pub struct WorkspaceMemberConfig {
|
|||||||
pub path: String,
|
pub path: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub deps: Vec<String>,
|
pub deps: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub main: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new(path: &str) -> Result<Self, ConfigError> {
|
pub fn new(path: &str) -> Result<Self, ConfigError> {
|
||||||
let path = PathBuf::from(path);
|
let path = PathBuf::from(path);
|
||||||
let data = if path.exists() {
|
let (data, doc) = if path.exists() {
|
||||||
read_toml(&path)?
|
load_parts(&path)?
|
||||||
} else {
|
} else {
|
||||||
let default_value = default_toml()?;
|
let content = default_toml_text();
|
||||||
write_toml(&path, &default_value)?;
|
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
|
||||||
default_value
|
fs::write(&path, doc.to_string())?;
|
||||||
|
let data: Value = toml::from_str(&content)?;
|
||||||
|
(data, doc)
|
||||||
};
|
};
|
||||||
let typed = parse_typed_config(&data)?;
|
let typed = parse_typed_config(&data)?;
|
||||||
let cfg = Self { path, data, typed };
|
let cfg = Self {
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
typed,
|
||||||
|
doc,
|
||||||
|
};
|
||||||
cfg.validate()?;
|
cfg.validate()?;
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
@@ -203,9 +231,14 @@ impl Config {
|
|||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err(ConfigError::Invalid("dcr.toml not found".into()));
|
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 typed = parse_typed_config(&data)?;
|
||||||
let cfg = Self { path, data, typed };
|
let cfg = Self {
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
typed,
|
||||||
|
doc,
|
||||||
|
};
|
||||||
cfg.validate()?;
|
cfg.validate()?;
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
@@ -345,6 +378,13 @@ impl Config {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?;
|
.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() {
|
if pkg.name.trim().is_empty() {
|
||||||
return Err(ConfigError::Invalid("package.name is empty".into()));
|
return Err(ConfigError::Invalid("package.name is empty".into()));
|
||||||
}
|
}
|
||||||
@@ -419,22 +459,35 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<(), ConfigError> {
|
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> {
|
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
||||||
let parts: Vec<&str> = key.split('.').collect();
|
let parts: Vec<&str> = key.split('.').collect();
|
||||||
let previous = self.data.clone();
|
let previous_data = self.data.clone();
|
||||||
set_path(&mut self.data, &parts, value)?;
|
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) {
|
self.typed = match parse_typed_config(&self.data) {
|
||||||
Ok(typed) => typed,
|
Ok(typed) => typed,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.data = previous;
|
self.data = previous_data;
|
||||||
|
self.doc = previous_doc;
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(err) = self.validate() {
|
if let Err(err) = self.validate() {
|
||||||
self.data = previous;
|
self.data = previous_data;
|
||||||
|
self.doc = previous_doc;
|
||||||
self.typed = parse_typed_config(&self.data)?;
|
self.typed = parse_typed_config(&self.data)?;
|
||||||
return Err(err);
|
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)?;
|
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> {
|
fn default_toml_text() -> String {
|
||||||
let content = format_toml(value)?;
|
|
||||||
fs::write(path, content)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_toml() -> Result<Value, ConfigError> {
|
|
||||||
let name = std::env::current_dir()
|
let name = std::env::current_dir()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
|
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
|
||||||
.unwrap_or_else(|| "project".to_string());
|
.unwrap_or_else(|| "project".to_string());
|
||||||
|
|
||||||
let mut package = Map::new();
|
format!(
|
||||||
package.insert("name".to_string(), Value::String(name));
|
"[package]\n\
|
||||||
package.insert(
|
name = \"{name}\"\n\
|
||||||
"version".to_string(),
|
version = \"{DEFAULT_VERSION}\"\n\
|
||||||
Value::String(DEFAULT_VERSION.to_string()),
|
type = \"none\"\n\
|
||||||
);
|
description = \"\"\n\
|
||||||
package.insert("type".to_string(), Value::String("none".to_string()));
|
author = \"\"\n\
|
||||||
package.insert("description".to_string(), Value::String("".to_string()));
|
license = \"GPL-3.0-or-later\"\n\
|
||||||
package.insert("author".to_string(), Value::String("".to_string()));
|
\n\
|
||||||
package.insert(
|
[build]\n\
|
||||||
"license".to_string(),
|
language = \"{DEFAULT_LANGUAGE}\"\n\
|
||||||
Value::String("GPL-3.0-or-later".to_string()),
|
standard = \"{DEFAULT_STANDARD}\"\n\
|
||||||
);
|
compiler = \"{DEFAULT_COMPILER}\"\n\
|
||||||
|
kind = \"{DEFAULT_KIND}\"\n\
|
||||||
let mut build = Map::new();
|
\n\
|
||||||
build.insert(
|
[dependencies]\n"
|
||||||
"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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_toml(value: &Value) -> Result<String, ConfigError> {
|
fn set_doc_path(doc: &mut DocumentMut, path: &[&str], value: &Value) -> Result<(), ConfigError> {
|
||||||
let root = value
|
let mut current = doc.as_table_mut();
|
||||||
.as_table()
|
for &key in &path[..path.len().saturating_sub(1)] {
|
||||||
.ok_or_else(|| ConfigError::Invalid("root is not a table".into()))?;
|
if !current.contains_key(key) {
|
||||||
|
current.insert(key, Item::Table(Table::new()));
|
||||||
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))),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 let Some(&last) = path.last() {
|
||||||
if language.starts_with('[') {
|
current.insert(last, value_to_item(value));
|
||||||
out.push_str(&format!("language = {language}\n"));
|
Ok(())
|
||||||
} else {
|
} 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 {
|
match value {
|
||||||
Value::String(s) => format!("\"{s}\""),
|
Value::String(s) => toml_edit::Value::from(s.clone()),
|
||||||
Value::Table(tbl) => {
|
Value::Integer(i) => toml_edit::Value::from(*i),
|
||||||
let mut parts = Vec::new();
|
Value::Float(f) => toml_edit::Value::from(*f),
|
||||||
if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) {
|
Value::Boolean(b) => toml_edit::Value::from(*b),
|
||||||
parts.push(format!("version = \"{v}\""));
|
Value::Datetime(dt) => {
|
||||||
}
|
let s = dt.to_string();
|
||||||
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
|
s.parse::<toml_edit::Datetime>()
|
||||||
parts.push(format!("path = \"{v}\""));
|
.map(toml_edit::Value::from)
|
||||||
}
|
.unwrap_or_else(|_| toml_edit::Value::from(s))
|
||||||
if let Some(v) = tbl.get("git").and_then(|v| v.as_str()) {
|
}
|
||||||
parts.push(format!("git = \"{v}\""));
|
Value::Array(arr) => {
|
||||||
}
|
let mut out = toml_edit::Array::new();
|
||||||
if let Some(v) = tbl.get("branch").and_then(|v| v.as_str()) {
|
for item in arr {
|
||||||
parts.push(format!("branch = \"{v}\""));
|
out.push(value_to_edit_value(item));
|
||||||
}
|
}
|
||||||
if let Some(v) = tbl.get("tag").and_then(|v| v.as_str()) {
|
toml_edit::Value::Array(out)
|
||||||
parts.push(format!("tag = \"{v}\""));
|
}
|
||||||
}
|
Value::Table(tbl) => {
|
||||||
if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) {
|
let mut out = toml_edit::InlineTable::new();
|
||||||
parts.push(format!("rev = \"{v}\""));
|
for key in ordered_table_keys(tbl) {
|
||||||
}
|
if let Some(v) = tbl.get(&key) {
|
||||||
if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) {
|
out.insert(&key, value_to_edit_value(v));
|
||||||
parts.push(format!(
|
}
|
||||||
"default-features = {}",
|
}
|
||||||
if v { "true" } else { "false" }
|
toml_edit::Value::InlineTable(out)
|
||||||
));
|
|
||||||
}
|
|
||||||
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(", "))
|
|
||||||
}
|
}
|
||||||
_ => "\"\"".to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_string_array(value: &Value) -> String {
|
const DEP_KEY_ORDER: &[&str] = &[
|
||||||
if let Some(arr) = value.as_array() {
|
"version",
|
||||||
let items: Vec<String> = arr
|
"path",
|
||||||
.iter()
|
"git",
|
||||||
.filter_map(|v| v.as_str().map(|s| format!("\"{s}\"")))
|
"branch",
|
||||||
.collect();
|
"tag",
|
||||||
return format!("[{}]", items.join(", "));
|
"rev",
|
||||||
}
|
"default-features",
|
||||||
"[]".to_string()
|
"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> {
|
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]
|
#[test]
|
||||||
fn new_creates_default_config() {
|
fn new_creates_default_config() {
|
||||||
let dir = temp_dir("new_default");
|
let dir = temp_dir("new_default");
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ResolvedDeps {
|
pub struct ResolvedDeps {
|
||||||
pub include_dirs: Vec<String>,
|
pub include_dirs: Vec<String>,
|
||||||
pub lib_dirs: Vec<String>,
|
pub lib_dirs: Vec<String>,
|
||||||
pub libs: 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(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
@@ -15,10 +15,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use toml::Value;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DepLock {
|
pub struct DepLock {
|
||||||
@@ -73,65 +71,3 @@ fn quote_list(items: &[String]) -> String {
|
|||||||
fn escape_value(input: &str) -> String {
|
fn escape_value(input: &str) -> String {
|
||||||
input.replace('\\', "\\\\").replace('"', "\\\"")
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,11 +16,10 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod git;
|
|
||||||
pub mod lock;
|
pub mod lock;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
|
||||||
use crate::core::config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::core::deps::common::ResolvedDeps;
|
use crate::core::deps::common::ResolvedDeps;
|
||||||
use crate::core::deps::lock::{DepLock, write_lock};
|
use crate::core::deps::lock::{DepLock, write_lock};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -104,6 +103,10 @@ pub fn resolve_deps(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
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()) {
|
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||||
@@ -129,6 +132,10 @@ pub fn resolve_deps(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
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);
|
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||||
resolved.libs.push(name.clone());
|
resolved.libs.push(name.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,7 @@ pub struct DcrConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn home_dir() -> Option<PathBuf> {
|
fn home_dir() -> Option<PathBuf> {
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
crate::utils::fs::home_dir()
|
||||||
return Some(PathBuf::from(home));
|
|
||||||
}
|
|
||||||
if let Ok(profile) = std::env::var("USERPROFILE") {
|
|
||||||
return Some(PathBuf::from(profile));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dcr_config_dir() -> Option<PathBuf> {
|
fn dcr_config_dir() -> Option<PathBuf> {
|
||||||
|
|||||||
@@ -15,9 +15,10 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
pub mod builder;
|
pub mod build;
|
||||||
pub mod config;
|
pub mod build_config;
|
||||||
pub mod deps;
|
pub mod deps;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
|
pub mod vcs;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|||||||
@@ -38,13 +38,7 @@ pub struct RegistryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn home_dir() -> Option<PathBuf> {
|
fn home_dir() -> Option<PathBuf> {
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
crate::utils::fs::home_dir()
|
||||||
return Some(PathBuf::from(home));
|
|
||||||
}
|
|
||||||
if let Ok(profile) = std::env::var("USERPROFILE") {
|
|
||||||
return Some(PathBuf::from(profile));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegistryManager {
|
impl RegistryManager {
|
||||||
|
|||||||
32
src/core/vcs/git.rs
Normal file
32
src/core/vcs/git.rs
Normal 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
61
src/core/vcs/mod.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::core::config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::utils::build::normalize_target_os;
|
use crate::utils::build::normalize_target_os;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -50,7 +50,6 @@ pub fn parse_workspace(
|
|||||||
root: &Path,
|
root: &Path,
|
||||||
) -> Result<Option<Workspace>, String> {
|
) -> Result<Option<Workspace>, String> {
|
||||||
let mut table = None;
|
let mut table = None;
|
||||||
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
|
|
||||||
let combinations = if let Some(t) = target {
|
let combinations = if let Some(t) = target {
|
||||||
let normalized_t = normalize_target_os(t);
|
let normalized_t = normalize_target_os(t);
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@@ -41,10 +41,16 @@ fn main() {
|
|||||||
"tree" => cli::tree::tree(rest),
|
"tree" => cli::tree::tree(rest),
|
||||||
"test" | "tests" => cli::test::test(rest),
|
"test" | "tests" => cli::test::test(rest),
|
||||||
"fmt" => cli::fmt::fmt(rest),
|
"fmt" => cli::fmt::fmt(rest),
|
||||||
|
"lint" => cli::lint::lint(rest),
|
||||||
"clean" => cli::clean::clean(rest),
|
"clean" => cli::clean::clean(rest),
|
||||||
"gen" => cli::r#gen::r#gen(rest),
|
"gen" => cli::r#gen::r#gen(rest),
|
||||||
"--version" => {
|
"--version" => {
|
||||||
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET"));
|
println!(
|
||||||
|
"dcr {}{} ({})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
env!("DCR_GIT_INFO"),
|
||||||
|
env!("DCR_TARGET")
|
||||||
|
);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
"--help" => cli::help::help(),
|
"--help" => cli::help::help(),
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ pub mod bsd;
|
|||||||
pub mod linux;
|
pub mod linux;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub mod 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")]
|
#[cfg(target_os = "windows")]
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|
||||||
@@ -61,10 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)))]
|
)))]
|
||||||
{
|
{
|
||||||
match target_dir {
|
return posix::bin_path(profile, name, target_dir);
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
|
||||||
None => format!("./target/{profile}/{name}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +107,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)))]
|
)))]
|
||||||
{
|
{
|
||||||
match target_dir {
|
return posix::elf_path(profile, name, target_dir);
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
|
||||||
None => format!("./target/{profile}/{name}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,10 +143,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)))]
|
)))]
|
||||||
{
|
{
|
||||||
match target_dir {
|
return posix::efi_path(profile, name, target_dir);
|
||||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
|
||||||
None => format!("./target/{profile}/{name}.efi"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,10 +179,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)))]
|
)))]
|
||||||
{
|
{
|
||||||
match target_dir {
|
return posix::lib_path(profile, name, target_dir);
|
||||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
|
||||||
None => format!("./target/{profile}/lib{name}.a"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,9 +215,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
|
|||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)))]
|
)))]
|
||||||
{
|
{
|
||||||
match target_dir {
|
return posix::shared_lib_path(profile, name, target_dir);
|
||||||
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
|
|
||||||
None => format!("./target/{profile}/lib{name}.so"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/platform/posix.rs
Normal file
31
src/platform/posix.rs
Normal 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::core::config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::utils::log::warn;
|
use crate::utils::log::warn;
|
||||||
|
|
||||||
pub struct VersionInfo {
|
pub struct VersionInfo {
|
||||||
@@ -32,17 +32,16 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
|
|||||||
if full.is_empty() {
|
if full.is_empty() {
|
||||||
full = "0.0.0".to_string();
|
full = "0.0.0".to_string();
|
||||||
}
|
}
|
||||||
let full_clone = full.clone();
|
let (base, suffix) = match full.split_once('-') {
|
||||||
let (base, suffix) = match full_clone.split_once('-') {
|
Some((head, tail)) => (head.to_string(), tail.to_string()),
|
||||||
Some((head, tail)) => (head, tail),
|
None => (full.clone(), String::new()),
|
||||||
None => (full_clone.as_str(), ""),
|
|
||||||
};
|
};
|
||||||
let mut parts = base.split('.');
|
let mut parts = base.split('.');
|
||||||
let major = parts.next().unwrap_or("0").to_string();
|
let major = parts.next().unwrap_or("0").to_string();
|
||||||
let minor = 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 patch = parts.next().unwrap_or("0").to_string();
|
||||||
let suffix_dash = if suffix.is_empty() {
|
let suffix_dash = if suffix.is_empty() {
|
||||||
"".to_string()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
format!("-{suffix}")
|
format!("-{suffix}")
|
||||||
};
|
};
|
||||||
@@ -51,11 +50,51 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
|
|||||||
major,
|
major,
|
||||||
minor,
|
minor,
|
||||||
patch,
|
patch,
|
||||||
suffix: suffix.to_string(),
|
suffix,
|
||||||
suffix_dash,
|
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 {
|
pub fn normalize_target_os(target: &str) -> &str {
|
||||||
match target {
|
match target {
|
||||||
"linux" => "x86_64-unknown-linux-gnu",
|
"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 {
|
pub fn get_config_str(config: &Config, key: &str) -> String {
|
||||||
config
|
config
|
||||||
.get(key)
|
.get(key)
|
||||||
@@ -140,6 +226,43 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec
|
|||||||
out
|
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 {
|
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
||||||
let base = config
|
let base = config
|
||||||
.get(&format!("build.{field}"))
|
.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") {
|
match config.get("build.language") {
|
||||||
Some(v) => parse_language_value(v, "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>,
|
tc_as: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let lang = primary_language(language);
|
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))
|
.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> {
|
fn env_override_compiler(lang: &str) -> Option<String> {
|
||||||
@@ -246,7 +374,8 @@ fn toolchain_override_compiler(
|
|||||||
tc_as: Option<&str>,
|
tc_as: Option<&str>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
if lang == "asm" {
|
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")
|
if (lang == "c++" || lang == "cpp" || lang == "cxx")
|
||||||
&& let Some(v) = tc_cxx
|
&& let Some(v) = tc_cxx
|
||||||
@@ -256,23 +385,31 @@ fn toolchain_override_compiler(
|
|||||||
tc_cc.map(|v| v.to_string())
|
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 {
|
pub fn primary_language(language: &str) -> String {
|
||||||
let parts: Vec<String> = language
|
let tokens: Vec<&str> = language
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(|p| p.trim().to_lowercase())
|
.map(|s| s.trim())
|
||||||
.filter(|p| !p.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
for p in &parts {
|
// Priority: c++ > c > asm.
|
||||||
if p == "c++" || p == "cpp" || p == "cxx" {
|
for id in ["cxx", "c", "asm"] {
|
||||||
return p.clone();
|
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()
|
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")
|
let output = std::process::Command::new("pkg-config")
|
||||||
.arg(arg)
|
.arg(arg)
|
||||||
.arg(pkg)
|
.arg(pkg)
|
||||||
@@ -380,6 +517,61 @@ mod tests {
|
|||||||
assert_eq!(normalize_target_os("unknown"), "unknown");
|
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]
|
#[test]
|
||||||
fn parse_version_parts() {
|
fn parse_version_parts() {
|
||||||
let info = parse_version_info("1.2.3-beta");
|
let info = parse_version_info("1.2.3-beta");
|
||||||
@@ -390,4 +582,52 @@ mod tests {
|
|||||||
assert_eq!(info.suffix, "beta");
|
assert_eq!(info.suffix, "beta");
|
||||||
assert_eq!(info.suffix_dash, "-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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,24 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
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)]
|
#[allow(dead_code)]
|
||||||
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
|
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
|
||||||
let path: PathBuf = match dir {
|
let path: PathBuf = match dir {
|
||||||
|
|||||||
28
src/utils/git.rs
Normal file
28
src/utils/git.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -17,5 +17,6 @@
|
|||||||
|
|
||||||
pub mod build;
|
pub mod build;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
pub mod git;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|||||||
@@ -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
172
tests/cli_build.rs
Normal 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
176
tests/cli_deps.rs
Normal 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
66
tests/cli_lint.rs
Normal 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
75
tests/cli_new.rs
Normal 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
79
tests/cli_qt.rs
Normal 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
30
tests/cli_test.rs
Normal 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
58
tests/cli_workspace.rs
Normal 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
101
tests/common/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user