5 Commits

59 changed files with 4678 additions and 2373 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,5 +1,99 @@
# Changelog
## [0.8.1] - 2026-07-20 "OS-Dev: flat-bin, FAT-образы / OS-Dev: flat-bin & FAT Images"
### RU
**Добавлено:**
- **`build.kind = "flat-bin"`** — сырой бинарный артефакт (расширение по умолчанию `.bin`):
- **NASM** — `-f bin`, прямой вывод `<stem>.bin`
- **FASM** — прямой вывод (ожидается `format binary` в исходнике)
- **GAS / MASM / LLVM IR (`llc`)** — объект → `objcopy -O binary` → `<stem>.bin`
- **C/C++ (gcc/clang/MSVC)** — compile → link (nostdlib/static) → `objcopy -O binary` → `<name>.bin`
- Требуется `llvm-objcopy` / `objcopy` / `gobjcopy` в PATH (кроме NASM/FASM direct)
- **Секция `[archive]`** — после успешной сборки DCR собирает образ диска (FAT12/16/32 через `fatfs`): размер, offset, volume label, опциональный bootsector (512 байт в начало при `offset = 0`) и `layout` (копирование файлов/glob в FS). Плейсхолдер `{profile}` в путях.
- **Одиночные файлы в `build.roots`** — корень может быть файлом (`.c`, `.asm`, header), а не только каталогом; то же для сбора заголовков в cache.
**Изменено:**
- **`dcr build --force` / `dcr run --force`** — force также перезапускает `build.steps` и `build.post_steps`.
- **`dcr run` на workspace-only root** — если задан `[run].cmd`, сначала build + run по конфигу корня; иначе делегирование члену workspace (как в 0.8.0).
**Зависимости:**
- `fatfs = "0.3"` для упаковки FAT-образов.
### EN
**Added:**
- **`build.kind = "flat-bin"`** — raw binary artifact (default extension `.bin`):
- **NASM** — `-f bin`, direct `<stem>.bin`
- **FASM** — direct output (`format binary` in source)
- **GAS / MASM / LLVM IR (`llc`)** — object → `objcopy -O binary` → `<stem>.bin`
- **C/C++ (gcc/clang/MSVC)** — compile → link (`-nostdlib -static`) → `objcopy -O binary` → `<name>.bin`
- Needs `llvm-objcopy` / `objcopy` / `gobjcopy` in PATH (except NASM/FASM direct)
- **`[archive]` section** — after a successful build, pack a disk image (FAT12/16/32 via `fatfs`): size, offset, volume label, optional bootsector (512 bytes at start when `offset = 0`), and `layout` (file/glob copy into the FS). `{profile}` substitution in paths.
- **Single-file `build.roots`** — a root may be a file (`.c`, `.asm`, header), not only a directory; header collection in the cache follows the same rule.
**Changed:**
- **`dcr build --force` / `dcr run --force`** — force also re-runs `build.steps` and `build.post_steps`.
- **`dcr run` on a workspace-only root** — if `[run].cmd` is set, build + run that command first; otherwise fall back to workspace member delegation (0.8.0 behavior).
**Dependencies:**
- `fatfs = "0.3"` for FAT image packing.
## [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

319
Cargo.lock generated
View File

@@ -2,18 +2,39 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.1"
@@ -38,6 +59,28 @@ dependencies = [
"objc2",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -50,6 +93,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -98,15 +154,17 @@ dependencies = [
[[package]]
name = "dcr"
version = "0.7.4"
version = "0.8.1"
dependencies = [
"ctrlc",
"fatfs",
"glob",
"self-replace",
"serde",
"serde_json",
"sha2",
"toml",
"toml_edit",
"ureq",
]
@@ -126,7 +184,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"bitflags 2.11.1",
"block2",
"libc",
"objc2",
@@ -165,6 +223,24 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fatfs"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"chrono",
"log",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "foldhash"
version = "0.1.5"
@@ -180,6 +256,30 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-core"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-task"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -230,6 +330,30 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -357,6 +481,17 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -399,12 +534,21 @@ version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags",
"bitflags 2.11.1",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "objc2"
version = "0.6.4"
@@ -438,6 +582,12 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -487,7 +637,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"bitflags 2.11.1",
"errno",
"libc",
"linux-raw-sys",
@@ -525,6 +675,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "schannel"
version = "0.1.29"
@@ -540,7 +696,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"bitflags 2.11.1",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -637,6 +793,18 @@ dependencies = [
"digest",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -703,12 +871,18 @@ dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow",
"winnow 1.0.3",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
@@ -718,15 +892,33 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
]
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow",
"winnow 1.0.3",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
@@ -808,6 +1000,51 @@ dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
@@ -836,18 +1073,71 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"bitflags 2.11.1",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -930,6 +1220,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "1.0.3"
@@ -1000,7 +1299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"bitflags 2.11.1",
"indexmap",
"log",
"serde",

View File

@@ -1,6 +1,6 @@
[package]
name = "dcr"
version = "0.7.4"
version = "0.8.1"
edition = "2024"
build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -66,8 +66,10 @@ serde = { version = "1", features = ["derive"] }
self-replace = "1"
sha2 = "0.10"
toml = "1.0.3"
toml_edit = "0.22"
serde_json = "1"
ctrlc = "3"
fatfs = "0.3"
[profile.release]
opt-level = "z"

View File

@@ -23,8 +23,8 @@ Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers
- [ ] **Static analysis** (`dcr check`)
Integration with clang-tidy, cppcheck, include-what-you-use
- [-] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
*`dcr fmt` (clang-format) implemented; `dcr lint` does not exist yet*
- [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.)
- [-] **Workspaces improvements** Per-member include directories, selective builds, better dependency graph
*Basic workspaces with dep ordering + topological sort work; advanced features missing*

View File

@@ -51,8 +51,9 @@ dcr gen vscode
- **Incremental builds** — mtime + `.d` headers + SHA256 fingerprint
- **Parallel compilation** — thread::scope, atomic task queue
- **4 backends** — gcc/clang, MSVC/clang-cl, GAS (ARM), NASM (x86)
- **7 project kinds** — bin, staticlib, sharedlib, efi, elf, none, custom
- **7 backends** — gcc/clang (unix_cc), MSVC/clang-cl (msvc), GAS, NASM, MASM, FASM, LLVM IR
- **8 project kinds** — bin, staticlib, sharedlib, efi, elf, none, custom, flat-bin
- **Disk images** — optional `[archive]` packs FAT12/16/32 images after build
- **Cross-compilation** — short names and full triples
- **Profiles** — debug / release with field overrides
- **Workspaces** — multi-package projects with topological sort

View File

@@ -4,6 +4,108 @@ sidebar_label: Changelog
# Changelog
## 0.8.1 (2026-07-20)
### Added
- **`build.kind = "flat-bin"`** — raw binary (default `.bin`) for OS-dev payloads:
- **NASM** — `-f bin` → `<stem>.bin`
- **FASM** — direct output (`format binary` in source)
- **GAS / MASM / LLC** — object → `objcopy -O binary` → `<stem>.bin`
- **C/C++** — objects → freestanding link → `objcopy -O binary` → `<name>.bin`
- Requires `llvm-objcopy` / `objcopy` / `gobjcopy` except for NASM/FASM direct emit
- **`[archive]` section** — after a successful build, pack a FAT disk image (`fatfs`): `output`, `format` (`fat12`/`fat16`/`fat32`), optional `size` (default 1.44 MiB), `offset`, `label`, `bootsector`, and `layout` (`from` → `to`, supports globs). `{profile}` works in paths.
- **Single-file `build.roots`** — a root may be a single source or header file, not only a directory.
### Changed
- **`--force`** also re-runs `build.steps` and `build.post_steps`.
- **`dcr run` on workspace-only root** — if `[run].cmd` is set, run that after build; otherwise delegate to a workspace member.
### Dependencies
- `fatfs = "0.3"` for FAT image packing.
## 0.8.0 (2026-07-18)
### 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.cxx]`, `[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)
### 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)
### 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)
### 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)
### Added

View File

@@ -13,7 +13,7 @@ dcr build # debug profile
dcr build --release # release build
dcr build --debug # explicit debug
dcr build --target aarch64-unknown-linux-gnu
dcr build --force # full rebuild
dcr build --force # full rebuild (also re-runs build.steps / post_steps)
dcr build --clean # clean + build
dcr build --verbose # show compilation commands
dcr build --workspace pkg-a # build specific workspace package
@@ -21,6 +21,8 @@ dcr build --workspace pkg-a # build specific workspace package
Artifacts: `target/<triple>/<profile>/<binary-name>` (Linux with explicit target).
With `[archive]` in `dcr.toml`, a FAT disk image is packed after a successful build.
## `dcr run`
Builds (if sources are newer than artifacts) and runs the binary.
@@ -28,8 +30,11 @@ Builds (if sources are newer than artifacts) and runs the binary.
```bash
dcr run
dcr run --release
dcr run --force # force rebuild then run
```
On a **workspace-only** root: if `[run].cmd` is set, that command is used after build; otherwise DCR delegates to a workspace member.
## `dcr clean`
Removes `target/` directory (or `target/<target>/<profile>`).

View File

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

View File

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

View File

@@ -45,6 +45,6 @@ cargo test --all-targets
CI runs:
- `cargo fmt --check`
- `cargo clippy --all-targets -- -D warnings`
- `cargo test --all-targets`
- `cargo check --all-targets --all-features`
- Build on Linux, macOS, Windows
- Integration tests
- Integration tests (compilation check)

View File

@@ -13,9 +13,9 @@ error: registry not found
**Solution:** Make sure `~/.dcr/config.toml` exists:
```toml
[[registry]]
name = "default"
[registry.default]
url = "https://index.dcr.pm"
priority = 1
```
And check `DCR_INDEX_PATH`:
@@ -39,27 +39,6 @@ which clang
Or specify explicitly via `[toolchain]` in `dcr.toml`.
## openssl-sys build failed
When building DCR from source:
```
error: failed to run custom build command for openssl-sys
```
**Solution:** Install OpenSSL development headers:
```bash
# Ubuntu/Debian
sudo apt install libssl-dev
# Fedora
sudo dnf install openssl-devel
# macOS
brew install openssl
```
## Ctrl+C during build
DCR handles SIGINT: interrupts the current compilation and exits with non-zero code.
@@ -76,6 +55,12 @@ See [Cross-Compile Recipe](/docs/recipes/cross-to-windows).
See [Workspace Recipe](/docs/recipes/multi-package-workspace).
## Bootloader / pure NASM OS image
Use `kind = "flat-bin"` (NASM `-f bin`) and optional `[archive]` for a FAT disk image.
See [OS-dev recipe](/docs/recipes/os-flat-bin-archive).
## Lock file
`dcr.lock` is created during builds (`dcr build`) when registry dependencies are present. To force an update — delete `dcr.lock` and run `dcr build`.

View File

@@ -68,3 +68,11 @@ dcr test # run tests
```bash
dcr fmt # clang-format with .clang-format
```
## Linting
```bash
dcr lint # clang-tidy checks
dcr lint --fix # apply fixes automatically
```

View File

@@ -100,7 +100,7 @@ man dcr-build
To use the registry, create `~/.dcr/config.toml`:
```toml
[[registry]]
name = "default"
[registry.default]
url = "https://index.dcr.pm"
priority = 1
```

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
---
sidebar_label: OS-dev (flat-bin + archive)
---
# Pure ASM / boot image (flat-bin + archive)
Minimal OS-dev style pipeline: assemble raw binaries with NASM, then pack a FAT floppy/image with an optional boot sector.
## Project layout
```
myos/
dcr.toml
src/
boot.asm # boot sector (512 bytes)
kernel.asm # payload
```
For multiple flat artifacts, use a **workspace** (one member per binary) or separate packages; a single `flat-bin` package produces one binary per source stem.
## Boot package (`flat-bin`)
```toml
[package]
name = "boot"
version = "0.1.0"
[build]
language = "asm"
compiler = "nasm"
kind = "flat-bin"
extension = "bin"
roots = ["src/boot.asm"]
```
```bash
dcr build
# → target/<…>/debug/boot.bin (NASM -f bin, no link)
```
## Disk image after build
```toml
[archive]
output = "target/{profile}/disk.img"
format = "fat12"
size = "1440K"
label = "MYOS"
bootsector = "target/{profile}/boot.bin"
[[archive.layout]]
from = "target/{profile}/kernel.bin"
to = "KERNEL.BIN"
```
- `format`: `fat12`, `fat16`, or `fat32`
- `size`: bytes or `K`/`KB`/`M`/`MB`/`G`/`GB` (default ~1.44 MiB)
- `bootsector`: written only when `offset` is omitted or `0`
- `from` may be a glob; `{profile}` is substituted in paths
## C kernel → flat binary
```toml
[build]
language = "c"
compiler = "clang"
kind = "flat-bin"
freestanding = true
filename = "kernel"
extension = "bin"
ldflags = ["-T", "linker.ld"]
```
Pipeline: objects → temporary linked ELF → `objcopy -O binary` → `kernel.bin`. Requires `objcopy` / `llvm-objcopy` in PATH.
## Other assemblers
| Tool | Notes |
|------|--------|
| **FASM** | Write `format binary` in the source; DCR writes `<stem>.bin` directly |
| **GAS** | Assemble `.s` → obj → `objcopy -O binary` |
| **LLC** | `language = "llvm_ir"` → obj → objcopy |
| **MASM** | COFF obj → objcopy (needs binutils/LLVM objcopy) |
## Notes
- Single-file `roots` are supported: `roots = ["src/boot.asm"]`.
- `--force` re-runs `build.steps` / `build.post_steps` as well as recompilation.
- Prefer `kind = "elf"` if you need a relocatable ELF kernel without stripping to raw binary.

View File

@@ -56,7 +56,7 @@ moc = "moc {in} -o {out}"
### GAS / NASM / MASM / FASM / LLVM-IR
- 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; **`bin` when `kind = "flat-bin"`**)
- MASM: `/nologo /c /Fo<obj> <source>`
- FASM: `<source> <output>` (output path is the object file)
- LLVM IR: `-filetype=obj <source> -o <output>`
@@ -87,6 +87,11 @@ Three levels of incrementality:
| `elf` | ELF without stdlib | `target/<triple>/<profile>/<name>` |
| `none` | Compile only, no link | — |
| `custom` | Full filename+extension control | `target/<triple>/<profile>/<filename>.<extension>` |
| `flat-bin` | Raw binary | ASM: `<stem>.bin` (NASM `-f bin` / FASM / GAS·MASM·LLC via objcopy); C/C++: `<name>.bin` (link + objcopy) |
## Disk images (`[archive]`)
After a successful build, if `[archive]` is present in `dcr.toml`, DCR formats a FAT volume and copies files from `layout` into the image. See [dcr.toml → archive](/docs/reference/dcr-toml#archive).
## Build Steps

View File

@@ -57,14 +57,8 @@ dcr build --target aarch64-none-elf
## Target Directory
On Linux with explicit target:
By default, the compilation output directories are structured as follows:
```
target/<triple>/<profile>/
```
On macOS/Windows or without target:
```
target/<profile>/
```
- **Linux and BSD**: Always output to `target/<triple>/<profile>/` (using host triple if no target is specified).
- **macOS and Windows (without target)**: Output to `target/<profile>/`.
- **macOS and Windows (with explicit target)**: Output to `target/<triple>/<profile>/`.

View File

@@ -33,6 +33,9 @@ Main project configuration file. Located at the project root.
[run]
# run settings
[archive]
# optional: pack FAT disk image after build
```
## [package]
@@ -41,7 +44,7 @@ Main project configuration file. Located at the project root.
|-------|----------|-------------|
| `name` | yes | Project name |
| `version` | yes | Semantic version |
| `type` | yes | `app`, `lib`, `none` |
| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) |
| `license` | no | SPDX license identifier |
| `author` | no | Author |
@@ -62,31 +65,31 @@ author = "John Doe"
| Field | Default | Description |
|-------|---------|-------------|
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` |
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"`, `"llvm_ir"`, `"llvm-ir"`, `"ll"` (optional, defaults to `"c"`) |
| `standard` | `"c11"` | C standard (`c11`, `c17`, `c23`) |
| `cxx_standard` | `"c++20"` | C++ standard (`c++17`, `c++20`, `c++23`) |
| `compiler` | `"clang"` | Preferred compiler |
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` |
| `cxx_standard` | — | C++ standard (`c++17`, `c++20`, `c++23`) |
| `compiler` | `"clang"` | Preferred compiler (optional, defaults to `"clang"`) |
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom`, `flat-bin` |
| `target` | host | Target triple for cross-compilation |
| `platform` | `"native"` | `native`, `efi` |
| `cflags` | `[]` | Additional C/C++/ASM flags |
| `ldflags` | `[]` | Additional linker flags |
| `filename` | `""` | Custom output file name |
| `extension` | `""` | Custom file extension |
| `roots` | `["src"]` | Source root directories |
| `extension` | `""` | Custom file extension (for `flat-bin`, default is `bin`) |
| `roots` | `["src"]` | Source roots: directories and/or individual source/header files |
| `exclude` | `[]` | Exclude patterns |
| `include` | `[]` | Additional include directories |
| `src_disable` | `false` | Disable auto source discovery |
| `inherit` | `false` | Inherit build from workspace root |
| `clean` | `[]` | Glob patterns for custom clean paths |
| `out_dir` | `""` | Custom output directory |
| `workspace_only` | `false` | Workspace-only, not built standalone |
| `workspace_only` | `false` | Workspace-only, not built standalone (no `language`/`compiler` required) |
| `freestanding` | `false` | Compile in freestanding mode (`-ffreestanding` + `-nostdlib -static`) |
| `opt_level` | `"0"` (debug) / `"3"` (release) | Optimization level: `0`-`3`, `"s"`, `"z"` |
| `debug` | `true` (debug) / `false` (release) | Emit debug symbols (`-g`) |
| `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` | `["all", "extra"]` (debug) / `[]` (release) | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`) |
| `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) |
@@ -96,6 +99,33 @@ Settings from raw config (not in typed struct):
- `ldscript` — linker script path
- `build.steps` / `build.post_steps` — codegen steps
## Per-language overrides: `[build.c]`, `[build.cxx]`, `[build.asm]`, `[build.llvm_ir]`
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"]
[build.llvm_ir]
compiler = "llc"
```
The flat `[build]` acts as fallback; per-language tables take precedence for their language.
Example:
```toml
@@ -148,3 +178,82 @@ Default `cmd` = `./target/{profile}/{name}` (macOS/Windows) or `./target/<triple
## [workspace]
See [workspaces](/docs/reference/workspaces).
## [archive]
Optional post-build step: format a FAT volume and copy built artifacts into a disk image. Runs after a successful package build (and after workspace member builds that define `[archive]`).
| Field | Required | Description |
|-------|----------|-------------|
| `output` | yes | Image path relative to project root (`{profile}` allowed) |
| `format` | yes | `fat12`, `fat16`, or `fat32` |
| `size` | no | Image size: bytes or `K`/`KB`/`M`/`MB`/`G`/`GB` (default `1474560` ≈ 1.44 MiB) |
| `offset` | no | Byte offset of the FAT volume inside the image (default `0`) |
| `label` | no | Volume label (max 11 chars, default `VOLUME`) |
| `bootsector` | no | Path to a 512-byte boot sector written at offset 0 when `offset` is 0 (`{profile}` allowed) |
| `layout` | no | List of `{ from, to }` entries (files or globs → path inside the volume) |
```toml
[archive]
output = "target/{profile}/disk.img"
format = "fat12"
size = "1440K"
label = "MYOS"
bootsector = "target/{profile}/boot.bin"
[[archive.layout]]
from = "target/{profile}/kernel.bin"
to = "KERNEL.BIN"
[[archive.layout]]
from = "assets/*"
to = "/"
```
Typical pairing with `kind = "flat-bin"` (NASM `-f bin`) for bootloaders and pure-ASM OS images.
## `flat-bin` (kind)
Produces a raw binary (default extension `bin`) for boot sectors, kernels, and freestanding payloads.
### Assemblers
| Tool | Language / compiler | How flat-bin is produced |
|------|---------------------|--------------------------|
| NASM | `language = "asm"`, `compiler = "nasm"` | `-f bin` → `<stem>.bin` |
| FASM | `compiler = "fasm"` | direct write (use `format binary` in source) |
| GAS | `compiler = "as"` / `"gas"` | assemble → `objcopy -O binary` |
| MASM | `compiler = "ml"` / `"ml64"` | assemble → `objcopy -O binary` |
| LLC | `language = "llvm_ir"`, `compiler = "llc"` | `-filetype=obj` → `objcopy -O binary` |
```toml
[build]
language = "asm"
compiler = "nasm"
kind = "flat-bin"
extension = "bin"
roots = ["src/boot.asm"]
```
### C / C++
Compile all sources, link with `-nostdlib -static` (plus your `ldflags` / `ldscript`), then convert the intermediate ELF/PE with `objcopy -O binary` to `<filename|name>.bin`.
```toml
[build]
language = "c"
compiler = "clang"
kind = "flat-bin"
freestanding = true
filename = "kernel"
extension = "bin"
ldscript = "linker.ld"
ldflags = ["-T", "linker.ld"]
```
Notes:
- Multi-file **ASM** packages emit one `<stem>.bin` per source; **C/C++** emit a single project binary.
- `objcopy` tools tried in order: `llvm-objcopy`, `objcopy`, `gobjcopy`.
- Incompatible with `build.qt = true`.
- `dcr run` rejects `flat-bin` (not a host executable).

View File

@@ -41,24 +41,22 @@ DCR uses a package registry for dependency lookup by name.
```toml
# ~/.dcr/config.toml
[[registry]]
name = "default"
[registry.default]
url = "https://index.dcr.pm"
priority = 1
```
Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrides the path to `index.json`.
## Git dependencies
DCR clones git repositories via `git2`:
Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying branch/tag/rev for git sources:
- `branch` — switch to a branch
- `tag` — switch to a tag
- `rev` — switch to a specific commit
- `features` — feature flags (parsed, but does not affect build)
Cache path: `~/.dcr/git/<hash>/`.
## Path dependencies
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.
@@ -69,6 +67,8 @@ mylib = { path = "/abs/path/to/lib" }
mylib = { path = "../relative/path" }
```
*Note: Header resolution automatically checks the packaged `target/include` directory of path dependencies, ensuring consumer packages can find headers from compiled static or dynamic libraries.*
## dcr.lock
Dependency lock file. Contains package names and sources.

View File

@@ -11,17 +11,18 @@ DCR normalizes target triples. Short platform names are expanded to full triples
### Linux
```
target/<arch>-unknown-linux-gnu/<profile>/
target/<arch>-unknown-linux-<env>/<profile>/
```
Architectures: `x86_64`, `aarch64` (host-detected).
Architectures: `x86_64`, `aarch64`, `i686`, `armv7`, `riscv64` (host-detected).
Environments: `gnu` (default), `musl`.
Artifact type: ELF. Extensions: `.so` (sharedlib), `.a` (staticlib).
### macOS
```
target/<profile>/
target/<profile>/ (default) or target/<triple>/<profile>/ (with target)
```
Architectures: `x86_64`, `aarch64` (host-detected).
@@ -31,10 +32,11 @@ Extensions: `.dylib` (sharedlib), `.a` (staticlib).
### Windows
```
target/<profile>/
target/<profile>/ (default) or target/<triple>/<profile>/ (with target)
```
Architectures: `x86_64`, `aarch64`.
Environments: `msvc` (default), `gnu` (MinGW).
Extensions: `.exe` (bin), `.lib` (staticlib), `.dll` (sharedlib), `.efi` (UEFI).

View File

@@ -43,7 +43,9 @@ dcr build # build all packages in dependency order
dcr build --workspace app # build only app (dependencies built automatically)
```
When building a workspace, member outputs (libraries) are automatically added to include/lib paths of dependent packages.
When building a workspace, DCR automatically injects include and library paths of dependent workspace members:
- **Include Paths**: Automatically resolves and injects header directories of dependencies, including the member's `src/` directory, local `include/` directory, and the packaged `target/include` directory.
- **Library Paths**: Injects compiled library search paths (`target/lib` as well as target-specific build folders) to allow automatic linking with member libraries.
## Clean
@@ -65,3 +67,21 @@ standard = "c17"
# member inherits language and standard
```
## Workspace-only root (no build of its own)
A workspace root can set `workspace_only = true` — it won't be built itself, and doesn't need `language` or `compiler`:
```toml
[package]
name = "my-workspace"
version = "0.1.0"
[build]
workspace_only = true
kind = "bin"
[workspace]
lib-core = { path = "lib-core" }
app = { path = "app", deps = ["lib-core"] }
```

View File

@@ -45,7 +45,6 @@ pub fn add(args: &[String]) -> i32 {
println!(" Adds a dependency to the project.");
println!();
printc("SOURCES:", BOLD_GREEN);
println!(" user/repo GitHub shorthand");
println!(" github:user/repo GitHub repository");
println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository");
@@ -140,7 +139,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:");
error(" user/repo -> github.com/user/repo");
error(" github:user/repo -> github.com/user/repo");
error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo");

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::collect_sources;
use crate::core::build::common;
use crate::core::build_config::Config;
use crate::core::builder::BuildContext;
use crate::core::builder::collect_sources;
use crate::core::builder::common;
use crate::core::workspace::parse_workspace;
use crate::utils::build::{
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,

View File

@@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::common;
use crate::core::build_config::Config;
use crate::core::builder::common;
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;

View File

@@ -110,8 +110,17 @@ pub fn run(args: &[String]) -> i32 {
Err(_) => return 1,
};
// Handle workspace-only root: delegate to workspace member
if config.is_workspace_only() {
if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") {
let build_status = build(&args_for_build(&flags));
if build_status == 0 {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------");
return run_shell(&cmd);
}
return build_status;
}
let ws = match crate::core::workspace::parse_workspace(
&config,
&flags.profile,
@@ -213,12 +222,23 @@ fn run_project(
let kind = build_kind.trim();
if run_cmd.is_none()
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf")
&& (kind == "staticlib"
|| kind == "sharedlib"
|| kind == "efi"
|| kind == "elf"
|| kind == "flat-bin")
{
return Err("Cannot run library build".to_string());
}
let build_status = build(&args_for_build(flags));
let build_status = if let Some(wroot) = workspace_root {
with_dir(wroot, || {
Ok(build(&args_for_workspace_build(flags, project_name)))
})
.unwrap_or(1)
} else {
build(&args_for_build(flags))
};
let bin_path = crate::platform::bin_path(
&flags.profile,
project_name,
@@ -266,6 +286,36 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
args.push("--workspace".to_string());
args.push(name.clone());
}
if flags.force {
args.push("--force".to_string());
}
if flags.clean {
args.push("--clean".to_string());
}
if flags.verbose {
args.push("--verbose".to_string());
}
args
}
fn args_for_workspace_build(
flags: &crate::cli::flags::BuildRunFlags,
member_name: &str,
) -> Vec<String> {
let mut args = Vec::new();
args.push(format!("--{}", flags.profile));
if let Some(ref target) = flags.target {
args.push("--target".to_string());
args.push(target.clone());
}
args.push("--workspace".to_string());
args.push(member_name.to_string());
if flags.force {
args.push("--force".to_string());
}
if flags.clean {
args.push("--clean".to_string());
}
if flags.verbose {
args.push("--verbose".to_string());
}

272
src/core/build/archive.rs Normal file
View File

@@ -0,0 +1,272 @@
// 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::ArchiveConfig;
use fatfs::{FileSystem, FormatVolumeOptions, format_volume};
use glob::glob;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
struct OffsetFile<T> {
inner: T,
offset: u64,
}
impl<T: Read> Read for OffsetFile<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<T: Write> Write for OffsetFile<T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<T: Seek> Seek for OffsetFile<T> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match pos {
SeekFrom::Start(n) => {
let actual = self.inner.seek(SeekFrom::Start(n + self.offset))?;
if actual < self.offset {
Ok(0)
} else {
Ok(actual - self.offset)
}
}
SeekFrom::Current(n) => {
let actual = self.inner.seek(SeekFrom::Current(n))?;
if actual < self.offset {
Ok(0)
} else {
Ok(actual - self.offset)
}
}
SeekFrom::End(n) => {
let actual = self.inner.seek(SeekFrom::End(n))?;
if actual < self.offset {
Ok(0)
} else {
Ok(actual - self.offset)
}
}
}
}
}
fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Ok(0);
}
let mut num_str = String::new();
let mut unit = String::new();
for c in s.chars() {
if c.is_ascii_digit() || c == '.' {
num_str.push(c);
} else {
unit.push(c);
}
}
let val: f64 = num_str
.parse()
.map_err(|_| format!("invalid size number: {num_str}"))?;
let unit = unit.trim().to_lowercase();
let multiplier = match unit.as_str() {
"k" | "kb" => 1024.0,
"m" | "mb" => 1024.0 * 1024.0,
"g" | "gb" => 1024.0 * 1024.0 * 1024.0,
"" => 1.0,
_ => return Err(format!("unknown unit: {unit}")),
};
Ok((val * multiplier) as u64)
}
pub fn pack_archive(
project_root: &Path,
config: &ArchiveConfig,
profile: &str,
) -> Result<(), String> {
let output_substituted = config.output.replace("{profile}", profile);
let output_path = project_root.join(&output_substituted);
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("failed to create output parent dir: {e}"))?;
}
let size = if let Some(sz_str) = &config.size {
parse_size(sz_str)?
} else {
1474560
};
let offset = if let Some(off_str) = &config.offset {
parse_size(off_str)?
} else {
0
};
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&output_path)
.map_err(|e| format!("failed to open output file: {e}"))?;
let current_len = file.metadata().map(|m| m.len()).unwrap_or(0);
if current_len < size {
file.set_len(size)
.map_err(|e| format!("failed to set image file size: {e}"))?;
}
if offset == 0
&& let Some(boot_rel) = &config.bootsector
{
let boot_rel_substituted = boot_rel.replace("{profile}", profile);
let boot_path = project_root.join(&boot_rel_substituted);
if boot_path.is_file() {
let mut boot_file =
File::open(&boot_path).map_err(|e| format!("failed to open bootsector: {e}"))?;
let mut boot_bytes = vec![0; 512];
let _ = boot_file
.read(&mut boot_bytes)
.map_err(|e| format!("failed to read bootsector: {e}"))?;
file.seek(SeekFrom::Start(0))
.map_err(|e| format!("failed to seek to start: {e}"))?;
file.write_all(&boot_bytes)
.map_err(|e| format!("failed to write bootsector: {e}"))?;
}
}
file.seek(SeekFrom::Start(0))
.map_err(|e| format!("failed to seek to start: {e}"))?;
let mut wrapper = OffsetFile {
inner: file,
offset,
};
let label = config.label.as_deref().unwrap_or("VOLUME");
let mut label_bytes = [b' '; 11];
let bytes_to_copy = label.len().min(11);
label_bytes[..bytes_to_copy].copy_from_slice(&label.as_bytes()[..bytes_to_copy]);
let fat_type = match config.format.to_lowercase().as_str() {
"fat12" => Some(fatfs::FatType::Fat12),
"fat16" => Some(fatfs::FatType::Fat16),
"fat32" => Some(fatfs::FatType::Fat32),
_ => None,
};
let mut options = FormatVolumeOptions::new().volume_label(label_bytes);
if let Some(t) = fat_type {
options = options.fat_type(t);
}
format_volume(&mut wrapper, options)
.map_err(|e| format!("failed to format FAT volume: {e}"))?;
wrapper
.seek(SeekFrom::Start(0))
.map_err(|e| format!("failed to seek to start after format: {e}"))?;
let fs = FileSystem::new(wrapper, fatfs::FsOptions::new())
.map_err(|e| format!("failed to mount FAT fs: {e}"))?;
let root_dir = fs.root_dir();
for layout in &config.layout {
let from_substituted = layout.from.replace("{profile}", profile);
let from_pattern = project_root
.join(&from_substituted)
.to_string_lossy()
.to_string();
let paths = if from_pattern.contains('*') || from_pattern.contains('?') {
let mut matched = Vec::new();
if let Ok(entries) = glob(&from_pattern) {
for path in entries.flatten() {
matched.push(path);
}
}
matched
} else {
let p = project_root.join(&from_substituted);
if p.exists() { vec![p] } else { Vec::new() }
};
for path in paths {
if !path.is_file() {
continue;
}
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename.is_empty() {
continue;
}
let mut target_filename = filename.to_string();
let mut target_dir_str = layout.to.trim_matches('/');
let is_glob = from_pattern.contains('*') || from_pattern.contains('?');
if !is_glob
&& !layout.to.ends_with('/')
&& let Some(last_part) = Path::new(&layout.to).file_name().and_then(|n| n.to_str())
&& last_part.contains('.')
{
target_filename = last_part.to_string();
let parent_path = Path::new(&layout.to).parent().unwrap_or(Path::new(""));
target_dir_str = parent_path.to_str().unwrap_or("").trim_matches('/');
}
let mut current_dir = root_dir.clone();
if !target_dir_str.is_empty() {
let parts = target_dir_str.split('/');
for part in parts {
if part.is_empty() {
continue;
}
current_dir = match current_dir.open_dir(part) {
Ok(dir) => dir,
Err(_) => current_dir
.create_dir(part)
.map_err(|e| format!("failed to create dir {part}: {e}"))?,
};
}
}
let mut out_file = current_dir
.create_file(&target_filename)
.map_err(|e| format!("failed to create file {target_filename}: {e}"))?;
let mut in_file =
File::open(&path).map_err(|e| format!("failed to open input file: {e}"))?;
let mut buffer = Vec::new();
in_file
.read_to_end(&mut buffer)
.map_err(|e| format!("failed to read input file: {e}"))?;
out_file
.write_all(&buffer)
.map_err(|e| format!("failed to write to FAT file {target_filename}: {e}"))?;
}
}
Ok(())
}

View File

@@ -0,0 +1,251 @@
// 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::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
match ctx.target_dir {
Some(dir) => Path::new(dir).to_path_buf(),
None => Path::new("./target").join(ctx.profile),
}
}
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("bin");
let file = if ext.is_empty() {
name.to_string()
} else {
format!("{name}.{ext}")
};
artifact_dir(ctx).join(file).to_string_lossy().to_string()
}
pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> String {
let stem = Path::new(source)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("out");
let ext = ctx.output_extension.unwrap_or("bin");
let file = if ext.is_empty() {
stem.to_string()
} else {
format!("{stem}.{ext}")
};
artifact_dir(ctx).join(file).to_string_lossy().to_string()
}
fn resolve_objcopy() -> Result<String, String> {
for candidate in ["llvm-objcopy", "objcopy", "gobjcopy"] {
if Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Ok(candidate.to_string());
}
}
Err(
"flat-bin requires llvm-objcopy, objcopy, or gobjcopy in PATH \
(install binutils or LLVM tools)"
.to_string(),
)
}
pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> Result<(), String> {
if let Some(parent) = Path::new(output).parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("output dir error: {e}"))?;
}
if !common::needs_rebuild(input, output) {
return Ok(());
}
let tool = resolve_objcopy()?;
let mut cmd = Command::new(&tool);
cmd.arg("-O").arg("binary").arg(input).arg(output);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
pub(crate) fn link_flat_binary(
ctx: &BuildContext,
objects: &[String],
default_linker: &str,
start_time: Instant,
) -> Result<f64, String> {
let out_path = flat_output_path(ctx);
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
let intermediate = {
let dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
std::fs::create_dir_all(&dir).map_err(|e| format!("obj dir error: {e}"))?;
dir.join(format!("{}.flat.elf", ctx.project_name))
.to_string_lossy()
.to_string()
};
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
cmd.arg("-nostdlib").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);
}
cmd.arg("-o").arg(&intermediate);
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() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
return Err(format!(
"flat-bin link failed:\nstdout: {stdout}\nstderr: {stderr}"
));
}
objcopy_binary(ctx, &intermediate, &out_path)?;
Ok(common::elapsed_secs(start_time))
}
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> {
if crate::utils::build::is_flat_bin(ctx.kind) {
return link_flat_binary(ctx, objects, default_linker, start_time);
}
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
if ctx.kind == "efi" {
cmd.arg("-shared");
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
} else if ctx.freestanding || is_bare_metal_target(ctx.target) {
cmd.arg("-nostdlib");
cmd.arg("-static");
}
for obj in objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "efi" {
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
cmd.arg("-o").arg(out_path);
run(ctx, cmd, start_time)
}
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
if output.status.success() {
Ok(common::elapsed_secs(start_time))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
}
}

View File

@@ -15,10 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use crate::utils::build::is_bare_metal_target;
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, is_compile_only, is_flat_bin};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -30,13 +30,16 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else {
ctx.compiler
};
if is_flat_bin(ctx.kind) && ctx.qt {
return Err("flat-bin is not supported with build.qt = true".to_string());
}
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::builder::qt::process_qt(ctx, &obj_dir)?
crate::core::build::language::cxx::qt::process_qt(ctx, &obj_dir)?
} else {
None
};
@@ -55,94 +58,15 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
)?;
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?;
if is_compile_only(ctx.kind) && !is_flat_bin(ctx.kind) {
return Ok(common::elapsed_secs(start_time));
}
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = common::elapsed_secs(start_time);
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 = common::elapsed_secs(start_time);
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
return artifact::archive_static(ctx, &objects, start_time);
}
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");
} 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) {
let elapsed = common::elapsed_secs(start_time);
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 = common::elapsed_secs(start_time);
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
artifact::link_binary(ctx, &objects, compiler, start_time)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
@@ -156,7 +80,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
}
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::builder::common::source_extensions(language)
crate::core::build::common::source_extensions(language)
}
fn default_flags(profile: &str) -> &'static [&'static str] {
@@ -221,7 +145,7 @@ fn build_object(
cmd.arg("-fPIC");
}
if (ctx.freestanding || is_bare_metal_target(ctx.target))
if (ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind))
&& ctx.language.to_lowercase() != "asm"
{
cmd.arg("-ffreestanding");
@@ -257,8 +181,8 @@ fn build_object(
}
}
let use_dcr_defaults =
ctx.cflags.is_empty() && !(ctx.freestanding || is_bare_metal_target(ctx.target));
let use_dcr_defaults = ctx.cflags.is_empty()
&& !(ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind));
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
@@ -288,5 +212,5 @@ fn build_object(
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
crate::core::builder::common::asm_lang_flag(source)
crate::core::build::common::asm_lang_flag(source)
}

View File

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

View File

@@ -15,9 +15,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::platform;
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::fs;
use std::path::Path;
use std::process::Command;
@@ -33,6 +35,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if lang.contains("asm") {
return Err("MSVC backend does not support build.language with asm".to_string());
}
if is_flat_bin(ctx.kind) && ctx.qt {
return Err("flat-bin is not supported with build.qt = true".to_string());
}
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
@@ -41,6 +46,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
if is_compile_only(ctx.kind) && !is_flat_bin(ctx.kind) {
return Ok(common::elapsed_secs(start_time));
}
if is_flat_bin(ctx.kind) {
return link_flat_msvc(ctx, compiler, &objects, &obj_dir, start_time);
}
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
@@ -141,6 +154,61 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
}
fn link_flat_msvc(
ctx: &BuildContext,
compiler: &str,
objects: &[String],
obj_dir: &Path,
start_time: Instant,
) -> Result<f64, String> {
let out_path = artifact::flat_output_path(ctx);
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
fs::create_dir_all(obj_dir).map_err(|e| format!("obj dir error: {e}"))?;
let intermediate = obj_dir
.join(format!("{}.flat.exe", ctx.project_name))
.to_string_lossy()
.to_string();
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
for obj in objects {
cmd.arg(obj);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("/LIBPATH:{dir}"));
}
for lib in ctx.libs {
if lib.to_lowercase().ends_with(".lib") {
cmd.arg(lib);
} else {
cmd.arg(format!("{lib}.lib"));
}
}
cmd.arg(format!("/Fe:{intermediate}"));
if !ctx.ldflags.is_empty() {
cmd.arg("/link");
for flag in ctx.ldflags {
cmd.arg(flag);
}
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let status = cmd
.status()
.map_err(|e| format!("flat-bin link failed: {e}"))?;
if !status.success() {
return Err("flat-bin link failed".to_string());
}
artifact::objcopy_binary(ctx, &intermediate, &out_path)?;
Ok(common::elapsed_secs(start_time))
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
@@ -152,7 +220,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
}
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::builder::common::source_extensions(language)
crate::core::build::common::source_extensions(language)
}
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {

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

@@ -0,0 +1,264 @@
// 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 {
if crate::utils::build::is_flat_bin(ctx.kind) {
return crate::core::build::builder::artifact::flat_output_path(ctx);
}
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;
}
if root.is_file() {
if is_header_file(&root) {
out.push(root);
}
} else {
collect_header_files_rec(&root, &mut out, ctx.exclude_dirs, ctx.include_paths)?;
}
}
out.sort();
out.dedup();
Ok(out)
}
fn collect_header_files_rec(
dir: &Path,
out: &mut Vec<PathBuf>,
exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<(), String> {
if common::is_excluded(dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(());
}
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir error: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if common::is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
continue;
}
collect_header_files_rec(&path, out, exclude_dirs, include_paths)?;
continue;
}
if !path.is_file() {
continue;
}
if common::is_excluded(&path, exclude_dirs, include_paths) {
continue;
}
if is_header_file(&path) {
out.push(path);
}
}
Ok(())
}
fn is_header_file(path: &Path) -> bool {
let ext = path.extension().and_then(|v| v.to_str()).unwrap_or("");
matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc")
}
pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
let mut out = Vec::new();
for dir in ctx.lib_dirs {
let dir_path = Path::new(dir);
for lib in ctx.libs {
for candidate in lib_candidates(lib) {
let path = dir_path.join(candidate);
if path.is_file() {
out.push(path);
}
}
}
}
out
}
fn lib_candidates(name: &str) -> Vec<String> {
if cfg!(target_os = "windows") {
return vec![format!("{name}.lib")];
}
if cfg!(target_os = "macos") {
return vec![
format!("lib{name}.a"),
format!("lib{name}.dylib"),
format!("lib{name}.so"),
];
}
vec![
format!("lib{name}.a"),
format!("lib{name}.so"),
format!("lib{name}.so.0"),
]
}
fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> {
hasher.update(path.to_string_lossy().as_bytes());
let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?;
hasher.update(meta.len().to_le_bytes());
if let Ok(modified) = meta.modified()
&& let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH)
{
hasher.update(duration.as_nanos().to_le_bytes());
}
Ok(())
}
fn to_hex(bytes: &[u8]) -> String {
crate::utils::fs::to_hex(bytes)
}

View File

@@ -35,7 +35,15 @@ pub fn collect_sources(
) -> Result<Vec<String>, String> {
let mut sources = Vec::new();
for root in roots {
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
if root.is_file() {
if let Some(ext) = root.extension().and_then(|e| e.to_str())
&& extensions.contains(&ext.to_lowercase().as_str())
{
sources.push(root.to_string_lossy().to_string());
}
} else {
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
}
}
sources.sort();
Ok(sources)
@@ -58,9 +66,10 @@ fn collect_sources_rec(
if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
let entries = fs::read_dir(&full_dir)
.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let entry = entry.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
@@ -177,12 +186,8 @@ pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
pub fn source_extensions(language: &str) -> Vec<&'static 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 let Some(lang) = crate::core::build::language::language_for_token(part) {
out.extend(lang.extensions().iter().copied());
}
}
if out.is_empty() {
@@ -331,7 +336,7 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
return true;
}
} else {
return true; // Missing dependency triggers rebuild
return true;
}
}
}
@@ -537,7 +542,6 @@ mod tests {
let src = dir.join("test.c");
let obj = dir.join("test.o");
fs::write(&src, "int main() {}").unwrap();
// Sleep briefly to ensure mtime difference
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap();
assert!(!needs_rebuild(

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,173 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::utils::build::{is_compile_only, is_flat_bin};
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,
{
build_assembly_with_mode(
ctx,
backend_name,
default_assembler,
extensions,
FlatEmit::Direct,
build_object,
)
}
pub fn build_assembly_via_objcopy<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,
{
build_assembly_with_mode(
ctx,
backend_name,
default_assembler,
extensions,
FlatEmit::ViaObjcopy,
build_object,
)
}
#[derive(Clone, Copy)]
enum FlatEmit {
Direct,
ViaObjcopy,
}
fn build_assembly_with_mode<F>(
ctx: &BuildContext,
backend_name: &str,
default_assembler: &str,
extensions: &[&str],
flat_mode: FlatEmit,
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| {
let t = p.trim();
t != "asm" && t != "llvm_ir" && t != "llvm-ir" && t != "ll"
}) {
return Err(format!(
"{backend_name} backend requires build.language = \"asm\" (or llvm_ir for LLC)"
));
}
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 flat = is_flat_bin(ctx.kind);
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 = match (flat, flat_mode) {
(true, FlatEmit::Direct) => {
let outs: Vec<String> = sources
.iter()
.map(|s| artifact::flat_source_output_path(ctx, s))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(assembler, &sources[i], &outs[i], ctx),
ctx.codegen_units,
)?;
outs
}
(true, FlatEmit::ViaObjcopy) => {
let intermediates: 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], &intermediates[i], ctx),
ctx.codegen_units,
)?;
let mut outs = Vec::with_capacity(sources.len());
for (i, src) in sources.iter().enumerate() {
let out = artifact::flat_source_output_path(ctx, src);
artifact::objcopy_binary(ctx, &intermediates[i], &out)?;
outs.push(out);
}
outs
}
(false, _) => build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?,
};
if is_compile_only(ctx.kind) {
return Ok(common::elapsed_secs(start_time));
}
if ctx.kind == "staticlib" {
return artifact::archive_static(ctx, &objects, start_time);
}
artifact::link_binary(ctx, &objects, "cc", start_time)
}
fn build_objects_common<F>(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
build_object: &F,
) -> Result<Vec<String>, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, "o"))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(assembler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects)
}

View File

@@ -1,6 +1,6 @@
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
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;
@@ -35,7 +35,7 @@ fn build_object(
let mut cmd = Command::new(assembler);
cmd.arg(source).arg(obj_path);
for flag in ctx.cflags {
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}

View File

@@ -15,15 +15,24 @@
// 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::asm;
use crate::core::builder::common;
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, "GAS", "as", &["s"], build_object)
// 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_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
@@ -52,9 +61,13 @@ fn build_object(
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -1,12 +1,12 @@
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
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)
asm::build_assembly_via_objcopy(ctx, "MASM", "ml", &["asm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
@@ -39,7 +39,7 @@ fn build_object(
.arg(obj_path)
.arg(source);
for flag in ctx.cflags {
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}

View File

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

View File

@@ -15,22 +15,22 @@
// 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::asm;
use crate::core::builder::common;
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", "s"], build_object)
asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed)
// NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed)
common::collect_sources(
ctx.source_roots,
&["asm", "s"],
&["asm"],
ctx.exclude_dirs,
ctx.include_paths,
)
@@ -50,7 +50,11 @@ fn build_object(
return Ok(());
}
let format = nasm_format(ctx.platform);
let format = if crate::utils::build::is_flat_bin(ctx.kind) {
"bin"
} else {
nasm_format(ctx.platform)
};
let mut cmd = Command::new(assembler);
cmd.arg("-f")
@@ -59,7 +63,7 @@ fn build_object(
.arg("-o")
.arg(obj_path);
for flag in ctx.cflags {
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
cmd.arg(flag);
}

View File

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

View File

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

View File

@@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::utils::build::run_pkg_config;
use std::fs;
use std::path::{Path, PathBuf};
@@ -34,14 +34,12 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
ctx.include_paths,
)?;
// Fetch Qt system include paths and defines for MOC
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() {
// Pass both -I and -D flags to moc
if flag.starts_with("-I") || flag.starts_with("-D") {
qt_moc_args.push(flag.to_string());
}
@@ -96,12 +94,10 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
let mut moc_args = Vec::new();
// Add Qt system cflags (includes + defines)
for arg in &qt_moc_args {
moc_args.push(arg.as_str());
}
// Add project include paths
for inc in ctx.include_paths.iter().chain(ctx.include_dirs.iter()) {
moc_args.push("-I");
moc_args.push(inc);

View File

@@ -1,12 +1,27 @@
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
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)
asm::build_assembly_via_objcopy(ctx, "LLVM-IR", "llc", &["ll"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {

View File

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

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

@@ -0,0 +1,30 @@
// 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 archive;
pub mod builder;
pub mod cache;
pub mod common;
pub mod engine;
pub mod language;
pub mod report;
pub mod resolve;
pub mod steps;
pub use engine::run_build;
pub use report::{BuildEvent, BuildReporter, BuildRequest};
pub use resolve::get_build_string_with_profile;

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

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

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

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

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

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

View File

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

View File

@@ -1,170 +0,0 @@
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
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 archive_staticlib(ctx, &objects, start_time);
}
link_objects(ctx, &objects, 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)
}
fn archive_staticlib(
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);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => Ok(common::elapsed_secs(start_time)),
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
fn link_objects(
ctx: &BuildContext,
objects: &[String],
start_time: Instant,
) -> Result<f64, String> {
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) {
return Ok(common::elapsed_secs(start_time));
}
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() => Ok(common::elapsed_secs(start_time)),
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}

View File

@@ -1,124 +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 asm;
pub mod common;
pub mod fasm;
pub mod gas;
pub mod llvm_ir;
pub mod masm;
pub mod msvc;
pub mod nasm;
pub mod qt;
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 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 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("llc") {
return llvm_ir::build(ctx);
}
if compiler.contains("fasm") {
return fasm::build(ctx);
}
if compiler.contains("nasm") {
return nasm::build(ctx);
}
if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") {
return masm::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("llc") {
return llvm_ir::collect_sources(ctx);
}
if compiler.contains("fasm") {
return fasm::collect_sources(ctx);
}
if compiler.contains("nasm") {
return nasm::collect_sources(ctx);
}
if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") {
return masm::collect_sources(ctx);
}
unix_cc::collect_sources(ctx)
}

View File

@@ -103,6 +103,10 @@ pub fn resolve_deps(
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists(
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
}
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
@@ -128,6 +132,10 @@ pub fn resolve_deps(
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists(
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
resolved.libs.push(name.clone());
}

View File

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

View File

@@ -50,7 +50,6 @@ pub fn parse_workspace(
root: &Path,
) -> Result<Option<Workspace>, String> {
let mut table = None;
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
let combinations = if let Some(t) = target {
let normalized_t = normalize_target_os(t);
vec![

View File

@@ -86,6 +86,14 @@ pub fn normalize_kind(kind: &str) -> &str {
if trimmed.is_empty() { "bin" } else { trimmed }
}
pub fn is_flat_bin(kind: &str) -> bool {
kind == "flat-bin"
}
pub fn is_compile_only(kind: &str) -> bool {
matches!(kind, "none" | "custom" | "flat-bin")
}
pub fn normalize_platform(platform: &str) -> Option<&str> {
let trimmed = platform.trim();
if trimmed.is_empty() {
@@ -280,7 +288,7 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<Strin
}
match config.get("build.language") {
Some(v) => parse_language_value(v, "build.language"),
None => Err("build.language is missing".to_string()),
None => Ok("c".to_string()),
}
}
@@ -324,9 +332,14 @@ pub fn resolve_compiler(
tc_as: Option<&str>,
) -> String {
let lang = primary_language(language);
env_override_compiler(&lang)
let result = env_override_compiler(&lang)
.or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as))
.unwrap_or_else(|| compiler.to_string())
.unwrap_or_else(|| compiler.to_string());
if lang == "asm" {
map_asm_compiler(&result)
} else {
result
}
}
fn env_override_compiler(lang: &str) -> Option<String> {
@@ -369,7 +382,8 @@ fn toolchain_override_compiler(
tc_as: Option<&str>,
) -> Option<String> {
if lang == "asm" {
return tc_as.map(|v| v.to_string());
let as_compiler = tc_as.map(|v| v.to_string());
return as_compiler.map(|v| map_asm_compiler(&v));
}
if (lang == "c++" || lang == "cpp" || lang == "cxx")
&& let Some(v) = tc_cxx
@@ -379,23 +393,31 @@ fn toolchain_override_compiler(
tc_cc.map(|v| v.to_string())
}
fn map_asm_compiler(compiler: &str) -> String {
match compiler.to_lowercase().as_str() {
"gas" | "gnu-as" => "as".to_string(),
"nasm" => "nasm".to_string(),
"fasm" | "fasm64" => "fasm".to_string(),
"masm" | "ml" | "ml64" => "ml".to_string(),
_ => compiler.to_string(),
}
}
pub fn primary_language(language: &str) -> String {
let parts: Vec<String> = language
let tokens: Vec<&str> = language
.split(',')
.map(|p| p.trim().to_lowercase())
.filter(|p| !p.is_empty())
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for p in &parts {
if p == "c++" || p == "cpp" || p == "cxx" {
return p.clone();
// Priority: c++ > c > asm.
for id in ["cxx", "c", "asm"] {
let hit = tokens.iter().any(|t| {
crate::core::build::language::language_for_token(t).map(|l| l.id()) == Some(id)
});
if hit {
return id.to_string();
}
}
if parts.iter().any(|p| p == "c") {
return "c".to_string();
}
if parts.iter().any(|p| p == "asm") {
return "asm".to_string();
}
language.to_lowercase()
}

View File

@@ -16,6 +16,48 @@ fn init_and_clean_remove_target() {
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 {