diff --git a/.gitignore b/.gitignore index 8f346c0..d0a4f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ sandbox/** .cargo/ .env -personal/ \ No newline at end of file +personal/ +.* +!.gitignore \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 494cee2..fa67b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## [0.8.0] - 2026-07-18 "Модульный Движок Сборки, Новая Архитектура Ядра и Улучшение Workspaces / Modular Build Engine, New Core Architecture, & Workspace Improvements" + +### RU + +**Добавлено:** + +- **Выделенное ядро сборки (Build Engine extraction)** — вся логика оркестрации компиляции и сборки полностью вынесена из интерфейсного модуля `src/cli/build.rs` в изолированный, переиспользуемый модуль ядра `src/core/build/engine.rs`. +- **Полиморфная модель языков (Трейт `Language`)** — введение абстрактного трейта `Language` (`src/core/build/language/mod.rs`). Логика поиска файлов, сопоставления расширений и компиляторов распределена по выделенным языковым бэкендам для C, C++ (включая шаги генерации Qt), LLVM IR и ассемблера (GAS, NASM, FASM, MASM). +- **Абстракция сборщиков (Трейт `Builder`)** — введение трейта `Builder` для полиморфного управления процессами компиляции (`src/core/build/builder/mod.rs`). Логика вызова компиляторов семейства GCC/Clang вынесена в общий модуль `builder/cc_common.rs`, а MSVC — в `builder/msvc/`. +- **Поязыковая конфигурация (`[build.]`)** — добавлена поддержка индивидуальных настроек для каждого языка в `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`), позволяющий безопасно прервать выполнение компиляции на любом этапе. +- **Сохранение кастомных ключей 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.]`)** — 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`) 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 diff --git a/Cargo.lock b/Cargo.lock index 3a41b9f..4f5a03f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,7 +98,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.7.4" +version = "0.8.0" dependencies = [ "ctrlc", "glob", @@ -107,6 +107,7 @@ dependencies = [ "serde_json", "sha2", "toml", + "toml_edit", "ureq", ] @@ -703,12 +704,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 +725,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" @@ -930,6 +955,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" diff --git a/Cargo.toml b/Cargo.toml index 50c7666..ae168cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.7.4" +version = "0.8.0" edition = "2024" build = "build.rs" description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." @@ -66,6 +66,7 @@ serde = { version = "1", features = ["derive"] } self-replace = "1" sha2 = "0.10" toml = "1.0.3" +toml_edit = "0.22" serde_json = "1" ctrlc = "3" diff --git a/TODO.md b/TODO.md index 964cb6c..c660942 100644 --- a/TODO.md +++ b/TODO.md @@ -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* diff --git a/docs/commands/project-commands.mdx b/docs/commands/project-commands.mdx index 0adacb8..04fe84e 100644 --- a/docs/commands/project-commands.mdx +++ b/docs/commands/project-commands.mdx @@ -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 ` — 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 ` — 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`. diff --git a/docs/commands/system-commands.mdx b/docs/commands/system-commands.mdx index df85226..1805a05 100644 --- a/docs/commands/system-commands.mdx +++ b/docs/commands/system-commands.mdx @@ -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` diff --git a/docs/faq.mdx b/docs/faq.mdx index 75e1036..085dbe2 100644 --- a/docs/faq.mdx +++ b/docs/faq.mdx @@ -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`: diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index cdc55fd..b63f429 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -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 ``` diff --git a/docs/license.mdx b/docs/license.mdx index f424731..ae1d607 100644 --- a/docs/license.mdx +++ b/docs/license.mdx @@ -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 diff --git a/docs/recipes/multi-package-workspace.mdx b/docs/recipes/multi-package-workspace.mdx index cd4b5cd..fca49cc 100644 --- a/docs/recipes/multi-package-workspace.mdx +++ b/docs/recipes/multi-package-workspace.mdx @@ -34,6 +34,7 @@ type = "none" inherit = true language = "c" standard = "c11" +workspace_only = true [workspace.lib-core] path = "lib-core" diff --git a/docs/reference/dcr-toml.mdx b/docs/reference/dcr-toml.mdx index 829b51c..63b4c68 100644 --- a/docs/reference/dcr-toml.mdx +++ b/docs/reference/dcr-toml.mdx @@ -41,7 +41,7 @@ Main project configuration file. Located at the project root. |-------|----------|-------------| | `name` | yes | Project name | | `version` | yes | Semantic version | -| `type` | yes | `app`, `lib`, `none` | +| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) | | `license` | no | SPDX license identifier | | `author` | no | Author | @@ -62,10 +62,10 @@ author = "John Doe" | Field | Default | Description | |-------|---------|-------------| -| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` | +| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` (optional, defaults to `"c"`) | | `standard` | `"c11"` | C standard (`c11`, `c17`, `c23`) | -| `cxx_standard` | `"c++20"` | C++ standard (`c++17`, `c++20`, `c++23`) | -| `compiler` | `"clang"` | Preferred compiler | +| `cxx_standard` | — | C++ standard (`c++17`, `c++20`, `c++23`) | +| `compiler` | `"clang"` | Preferred compiler (optional, defaults to `"clang"`) | | `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` | | `target` | host | Target triple for cross-compilation | | `platform` | `"native"` | `native`, `efi` | @@ -80,13 +80,13 @@ author = "John Doe" | `inherit` | `false` | Inherit build from workspace root | | `clean` | `[]` | Glob patterns for custom clean paths | | `out_dir` | `""` | Custom output directory | -| `workspace_only` | `false` | Workspace-only, not built standalone | +| `workspace_only` | `false` | Workspace-only, not built standalone (no `language`/`compiler` required) | | `freestanding` | `false` | Compile in freestanding mode (`-ffreestanding` + `-nostdlib -static`) | -| `opt_level` | `"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 +96,30 @@ 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]` + +Each language can have its own table that overrides the flat `[build]` settings: + +```toml +[build] +compiler = "clang" +standard = "c11" + +[build.c] +standard = "c23" +compiler = "gcc" + +[build.cxx] +standard = "c++23" +compiler = "g++" + +[build.asm] +compiler = "nasm" +flags = ["-felf64"] +``` + +The flat `[build]` acts as fallback; per-language tables take precedence for their language. + Example: ```toml diff --git a/docs/reference/dependencies.mdx b/docs/reference/dependencies.mdx index d96f707..1cb2be4 100644 --- a/docs/reference/dependencies.mdx +++ b/docs/reference/dependencies.mdx @@ -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//`. - ## Path dependencies Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`. diff --git a/docs/reference/workspaces.mdx b/docs/reference/workspaces.mdx index 5730d33..b9e4331 100644 --- a/docs/reference/workspaces.mdx +++ b/docs/reference/workspaces.mdx @@ -65,3 +65,21 @@ standard = "c17" # member inherits language and standard ``` + +## Workspace-only root (no build of its own) + +A workspace root can set `workspace_only = true` — it won't be built itself, and doesn't need `language` or `compiler`: + +```toml +[package] +name = "my-workspace" +version = "0.1.0" + +[build] +workspace_only = true +kind = "bin" + +[workspace] +lib-core = { path = "lib-core" } +app = { path = "app", deps = ["lib-core"] } +``` diff --git a/src/cli/add.rs b/src/cli/add.rs index e4a5339..e9b8e6c 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -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 { if args.is_empty() { error("Usage: dcr add [--branch | --tag | --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"); diff --git a/src/cli/build.rs b/src/cli/build.rs index c30f2d4..0c7881c 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -17,30 +17,98 @@ use crate::cli::clean::clean; use crate::cli::flags::parse_build_run_flags; +use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build}; use crate::core::build_config::Config; -use crate::core::builder::common; -use crate::core::builder::{BuildContext, build as build_project, collect_sources}; -use crate::core::deps::{register, resolve_deps}; -use crate::core::workspace::parse_workspace; -use crate::utils::build::{ - default_target_triple, get_bool_with_profile, get_config_opt, get_config_str, - get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform, - normalize_target, normalize_target_os, parse_version_info, prepend_clang_target_flag, - profile_table, resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars, -}; -use crate::utils::fs::{check_dir, find_project_root, with_dir}; +use crate::utils::build::default_target_triple; +use crate::utils::fs::find_project_root; use crate::utils::log::error; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; -use glob::glob; -use sha2::{Digest, Sha256}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::Once; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Instant; +use std::sync::{Arc, atomic::AtomicBool}; -static BUILD_INTERRUPTED: AtomicBool = AtomicBool::new(false); -static SIGNAL_HANDLER: Once = Once::new(); +pub use crate::core::build::get_build_string_with_profile; + +struct CliReporter; + +impl BuildReporter for CliReporter { + fn on_event(&mut self, event: BuildEvent<'_>) { + match event { + BuildEvent::TargetStart { + index, + total, + target, + } => { + println!(" Building target {} of {}: {}", index, total, target); + } + BuildEvent::ProjectStart { + name, + profile, + target, + } => { + println!( + " Building project `{}`\n Profile: {}\n Target: {}", + colored(name, BOLD_GREEN), + colored(profile, BOLD_GREEN), + colored(target, BOLD_GREEN) + ); + } + BuildEvent::DepBuilding { name, version } => { + print!( + "\r{:100}\r {} {} v{}", + "", + colored("Building", BOLD_GREEN), + name, + version + ); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + } + BuildEvent::DepReady { + name, + version, + rebuilt, + } => { + if rebuilt { + print!( + "\r{:100}\r {} {} v{}", + "", + colored("Ready", BOLD_GREEN), + name, + version + ); + println!(); + } else { + println!( + " {} {} v{}", + colored("Ready", BOLD_GREEN), + name, + version + ); + } + } + BuildEvent::Compiling { name, version } => { + println!( + " {} {} v{}", + colored("Compiling", BOLD_GREEN), + name, + version + ); + } + BuildEvent::Finished { secs } => { + println!( + " {} Build completed successfully in {} seconds", + colored("✔", BOLD_GREEN), + colored(&secs.to_string(), BOLD_GREEN) + ); + } + BuildEvent::CompilerOutput { stream, text } => { + if stream == "stderr" { + eprintln!("{}", text); + } else { + println!("{}", text); + } + } + } + } +} pub fn build(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { @@ -64,8 +132,12 @@ pub fn build(args: &[String]) -> i32 { return 0; } - install_signal_handler(); - BUILD_INTERRUPTED.store(false, Ordering::SeqCst); + let cancel = Arc::new(AtomicBool::new(false)); + let cancel_clone = cancel.clone(); + ctrlc::set_handler(move || { + cancel_clone.store(true, std::sync::atomic::Ordering::SeqCst); + }) + .ok(); let start_dir = match std::env::current_dir() { Ok(dir) => dir, Err(_) => { @@ -108,1578 +180,21 @@ pub fn build(args: &[String]) -> i32 { clean_args.push(format!("--{}", flags.profile)); let _ = clean(&clean_args); } - match with_dir(&root, || { - build_from_root( - &root, - &flags.profile, - flags.target.as_deref(), - flags.force, - flags.verbose, - flags.workspace.as_deref(), - ) - }) { - Ok(()) => 0, - Err(msg) => { - error(&msg); + + let req = BuildRequest { + profile: flags.profile, + target: flags.target, + force: flags.force, + verbose: flags.verbose, + workspace: flags.workspace, + cancel, + }; + let mut reporter = CliReporter; + match run_build(&root, &req, &mut reporter) { + Ok(_) => 0, + Err(err) => { + error(&err.message); 1 } } } - -fn install_signal_handler() { - SIGNAL_HANDLER.call_once(|| { - let _ = ctrlc::set_handler(|| { - BUILD_INTERRUPTED.store(true, Ordering::SeqCst); - }); - }); -} - -fn check_interrupted() -> Result<(), String> { - if BUILD_INTERRUPTED.load(Ordering::SeqCst) { - Err("Build interrupted".to_string()) - } else { - Ok(()) - } -} - -fn get_config_value_raw( - config: &Config, - section: &str, - field: &str, - profile: &str, - target: Option<&str>, -) -> Option { - // 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 -} - -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) -} - -fn get_config_value( - config: &Config, - section: &str, - field: &str, - profile: &str, - target: Option<&str>, -) -> Option { - 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()) -} - -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) -} - -fn parse_string_array(value: &toml::Value, key: &str) -> Result, 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) -} - -fn get_list_with_profile_and_target( - config: &Config, - field: &str, - profile: &str, - target: Option<&str>, -) -> Result, String> { - let inherit = get_inherit(config, "build", profile, target); - let mut out = if inherit { - get_config_list(config, &format!("build.{field}"))? - } else { - Vec::new() - }; - // Custom from target/profile - 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 { - // Legacy append - 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) -} - -#[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"); - } -} - -fn get_list_with_profile( - config: &Config, - field: &str, - profile: &str, -) -> Result, String> { - get_list_with_profile_and_target(config, field, profile, None) -} - -fn get_targets(config: &Config, profile: &str) -> Result, String> { - get_list_with_profile(config, "targets", profile) -} - -fn get_config_list(config: &Config, key: &str) -> Result, 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) -} - -fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result, 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) -} - -fn get_build_steps_with_profile( - config: &Config, - field: &str, - profile: &str, -) -> Result, 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 ensure_target_dirs( - items: &[String], - profile: &str, - target_dir: Option, - skip_local_target: bool, -) { - if !skip_local_target && !items.contains(&"target".to_string()) { - let _ = fs::create_dir("./target"); - } - if let Some(dir) = &target_dir { - let _ = fs::create_dir_all(dir); - } else { - let default_dir = if cfg!(target_os = "linux") { - let arch = std::env::consts::ARCH; - format!("{arch}-unknown-linux-gnu/{profile}") - } else if cfg!(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "dragonfly" - )) { - let arch = std::env::consts::ARCH; - let os = std::env::consts::OS; - format!("{arch}-unknown-{os}/{profile}") - } else { - profile.to_string() - }; - let target_items = check_dir(Some("target")).unwrap_or_default(); - if !target_items.contains(&default_dir) { - let _ = fs::create_dir_all(format!("./target/{default_dir}")); - } - } -} - -fn run_build(ctx: &BuildContext) -> Result { - let start_time = Instant::now(); - match build_project(ctx) { - Ok(times) => { - let times = if times == 0.0 { - ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0 - } else { - times - }; - Ok(times) - } - Err(_) => Err("Build failed".to_string()), - } -} - -#[allow(unused_variables)] -fn build_from_root( - root: &Path, - profile: &str, - target: Option<&str>, - force: bool, - verbose: bool, - workspace: Option<&str>, -) -> Result<(), String> { - let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; - - if config.is_workspace_only() { - let ws = parse_workspace(&config, profile, target, root)? - .ok_or_else(|| "Workspace root has no members defined".to_string())?; - let members: Vec<_> = if let Some(filter_name) = workspace { - ws.members - .into_iter() - .filter(|m| m.name == filter_name) - .collect() - } else { - ws.members - }; - if members.is_empty() { - return Err("No matching workspace members to build".to_string()); - } - for member in &members { - build_project_at( - &member.path, - profile, - target, - &[], - force, - verbose, - Some(root), - )?; - } - return Ok(()); - } - - let project_name = get_config_str(&config, "package.name"); - let project_version = get_config_str(&config, "package.version"); - - let targets_to_build: Vec> = if let Some(t) = target { - vec![Some(normalize_target_os(t).to_string())] - } else { - let config_targets = get_targets(&config, profile)?; - if config_targets.is_empty() { - vec![None] - } else { - config_targets - .into_iter() - .map(|t| Some(normalize_target_os(&t).to_string())) - .collect() - } - }; - - let start_time = Instant::now(); - for (i, build_target) in targets_to_build.iter().enumerate() { - check_interrupted()?; - if targets_to_build.len() > 1 { - println!( - " Building target {} of {}: {}", - i + 1, - targets_to_build.len(), - build_target.as_ref().map_or("native", |t| t.as_str()) - ); - } else { - println!( - " Building project `{}`\n Profile: {}\n Target: {}", - colored(&project_name, BOLD_GREEN), - colored(profile, BOLD_GREEN), - colored( - build_target.as_ref().map_or("native", |t| t.as_str()), - BOLD_GREEN - ) - ); - } - if let Some(ws) = parse_workspace(&config, profile, build_target.as_deref(), root)? { - if let Some(filter_name) = workspace { - for member in ws.members.iter().filter(|m| m.name == filter_name) { - build_project_at( - &member.path, - profile, - build_target.as_deref(), - &[], - force, - verbose, - Some(root), - )?; - } - } else { - build_workspace( - &ws, - profile, - build_target.as_deref(), - force, - verbose, - Some(root), - )?; - let excludes: Vec = - ws.members.iter().map(|m| m.path.clone()).collect(); - build_project_at( - root, - profile, - build_target.as_deref(), - &excludes, - force, - verbose, - None, - )?; - } - } else { - build_project_at( - root, - profile, - build_target.as_deref(), - &[], - force, - verbose, - None, - )?; - } - } - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; - println!( - " {} Build completed successfully in {} seconds", - colored("✔", BOLD_GREEN), - colored(&elapsed.to_string(), BOLD_GREEN) - ); - Ok(()) -} - -fn build_workspace( - workspace: &crate::core::workspace::Workspace, - profile: &str, - target: Option<&str>, - force: bool, - verbose: bool, - workspace_root: Option<&Path>, -) -> Result<(), String> { - for member in &workspace.members { - build_project_at( - &member.path, - profile, - target, - &[], - force, - verbose, - workspace_root, - )?; - } - Ok(()) -} - -fn build_project_at( - project_root: &Path, - profile: &str, - target: Option<&str>, - exclude_dirs: &[std::path::PathBuf], - force: bool, - verbose: bool, - workspace_root: Option<&Path>, -) -> Result<(), String> { - with_dir(project_root, || { - check_interrupted()?; - let items = check_dir(None).map_err(|_| "Failed to read project directory".to_string())?; - if !items.contains(&"dcr.toml".to_string()) { - return Err("dcr.toml file not found".to_string()); - } - let mut config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; - // Inherit build fields from workspace root if configured - if let Some(root) = workspace_root { - let has_inherit = config - .get("build.inherit") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - if has_inherit { - let root_path = root.join("dcr.toml"); - if root_path.is_file() { - let parent_config = - Config::open(&root_path.to_string_lossy()).map_err(|e| e.to_string())?; - config.merge_parent(&parent_config); - } - } - } - let project_name = get_config_str(&config, "package.name"); - let project_version = get_config_str(&config, "package.version"); - let build_target_config = get_build_string_with_profile(&config, "target", profile); - let build_target = target.or(if build_target_config.is_empty() { - None - } else { - Some(build_target_config.as_str()) - }); - let project_compiler = - get_string_with_profile_and_target(&config, "compiler", profile, build_target); - let build_language = get_language_with_profile(&config, profile)?; - let build_qt = get_bool_with_profile(&config, "qt", profile, false); - let build_standard = - get_string_with_profile_and_target(&config, "standard", profile, build_target); - let build_cxx_standard = - get_string_with_profile_and_target(&config, "cxx_standard", profile, build_target); - let build_kind = get_string_with_profile_and_target(&config, "kind", profile, build_target); - let build_platform = - get_string_with_profile_and_target(&config, "platform", profile, build_target); - let mut build_type = - get_string_with_profile_and_target(&config, "type", profile, build_target); - if build_type.is_empty() { - build_type = get_config_str(&config, "package.type"); - } - let tc_cc = get_config_value(&config, "toolchain", "cc", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.cc")); - let tc_cxx = get_config_value(&config, "toolchain", "cxx", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.cxx")); - let tc_as = get_config_value(&config, "toolchain", "as", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.as")); - let tc_ar = get_config_value(&config, "toolchain", "ar", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.ar")); - let tc_ld = get_config_value(&config, "toolchain", "ld", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.ld")); - let tc_uic = get_config_value(&config, "toolchain", "uic", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.uic")); - let tc_moc = get_config_value(&config, "toolchain", "moc", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.moc")); - let tc_rcc = get_config_value(&config, "toolchain", "rcc", profile, build_target) - .or_else(|| get_config_opt(&config, "toolchain.rcc")); - let mut build_cflags = - get_list_with_profile_and_target(&config, "cflags", profile, build_target)?; - let mut build_ldflags = - get_list_with_profile_and_target(&config, "ldflags", profile, build_target)?; - let build_ldscript = - get_string_with_profile_and_target(&config, "ldscript", profile, build_target); - if !build_ldscript.is_empty() { - build_ldflags.push(format!("-T{}", build_ldscript)); - } - - // Новые поля: filename и extension - let output_filename = - get_string_with_profile_and_target(&config, "filename", profile, build_target); - let output_extension = - get_string_with_profile_and_target(&config, "extension", profile, build_target); - let build_excludes = - get_list_with_profile_and_target(&config, "exclude", profile, build_target)?; - let build_includes = - get_list_with_profile_and_target(&config, "include", profile, build_target)?; - let build_roots = - get_list_with_profile_and_target(&config, "roots", profile, build_target)?; - let src_disable = get_bool_with_profile(&config, "src_disable", profile, false); - let freestanding = get_bool_with_profile(&config, "freestanding", profile, false); - let lto = get_bool_with_profile(&config, "lto", profile, false); - let strip = get_bool_with_profile(&config, "strip", profile, false); - let opt_level = - get_string_with_profile_and_target(&config, "opt_level", profile, build_target); - let debug = get_bool_with_profile(&config, "debug", profile, profile != "release"); - let warnings = - get_list_with_profile_and_target(&config, "warnings", profile, build_target)?; - let panic_val = get_string_with_profile_and_target(&config, "panic", profile, build_target); - let panic_abort = panic_val == "abort"; - let codegen_units: usize = - get_string_with_profile_and_target(&config, "codegen-units", profile, build_target) - .parse() - .unwrap_or(0); - - if build_cflags.is_empty() && !freestanding && !is_bare_metal_target(build_target) { - if !opt_level.is_empty() { - build_cflags.push(format!("-O{opt_level}")); - } else { - build_cflags.push(match profile { - "release" => "-O3".to_string(), - _ => "-O0".to_string(), - }); - } - if debug { - build_cflags.push("-g".to_string()); - } - if warnings.is_empty() { - if profile == "debug" { - build_cflags.push("-Wall".to_string()); - build_cflags.push("-Wextra".to_string()); - } - } else { - for w in &warnings { - build_cflags.push(format!("-W{w}")); - } - } - match profile { - "debug" => { - build_cflags.push("-fno-omit-frame-pointer".to_string()); - build_cflags.push("-DDCR_DEBUG".to_string()); - } - "release" => { - build_cflags.push("-DNDEBUG".to_string()); - } - _ => {} - } - } - - if lto { - build_cflags.push("-flto".to_string()); - build_ldflags.push("-flto".to_string()); - } - if strip { - build_ldflags.push("-s".to_string()); - } - - let build_expects = - get_list_with_profile_and_target(&config, "expect", profile, build_target)?; - let pkg_configs = - get_list_with_profile_and_target(&config, "pkg_config", profile, build_target)?; - let build_generated = - get_list_with_profile_and_target(&config, "generated", profile, build_target)?; - let build_steps = get_build_steps_with_profile(&config, "steps", profile)?; - let build_post_steps = get_build_steps_with_profile(&config, "post_steps", profile)?; - - let resolved_compiler = resolve_compiler( - &build_language, - &project_compiler, - tc_cc.as_deref(), - tc_cxx.as_deref(), - tc_as.as_deref(), - ); - let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); - prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler); - prepend_clang_target_flag( - &mut build_ldflags, - build_target, - resolved_linker.as_deref().unwrap_or(&resolved_compiler), - ); - let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); - - let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile); - let target_dir_binding = if !out_dir_config.is_empty() { - out_dir_config - } else { - match workspace_root { - Some(root) => { - let target_str = build_target - .map(normalize_target_os) - .filter(|t| !t.is_empty()); - let rel: PathBuf = match target_str { - Some(ref t) => Path::new("target").join(t).join(profile), - None => { - let default_dir = if cfg!(target_os = "linux") { - let arch = std::env::consts::ARCH; - format!("{arch}-unknown-linux-gnu/{profile}") - } else { - profile.to_string() - }; - Path::new("target").join(&default_dir) - } - }; - root.join(&rel).to_string_lossy().to_string() - } - None => normalize_target(build_target.unwrap_or(""), profile).unwrap_or_default(), - } - }; - let target_dir = if target_dir_binding.is_empty() { - None - } else { - Some(target_dir_binding.clone()) - }; - ensure_target_dirs(&items, profile, target_dir, workspace_root.is_some()); - - let deps_table = config.get("dependencies").and_then(|v| v.as_table()); - let resolved = resolve_deps(&config, profile, build_target, project_root)?; - - // Registry dependencies are cached under the DCR registry root. Build - // them as normal DCR projects before the current project is linked. - if let Some(deps) = deps_table { - for (name, value) in deps { - if register::is_registry_dep(value) { - let pkg_info = register::resolve_package_from_registry(name)?; - let version = pkg_info - .get("latest_version") - .or_else(|| pkg_info.get("version")) - .and_then(|v| v.as_str()) - .unwrap_or("unknown"); - let dep_root = register::package_root_from_registry_info(&pkg_info)?; - let include_dir = dep_root.join("target").join("include"); - let lib_dir = dep_root.join("target").join("lib"); - - if !include_dir.exists() || !lib_dir.exists() { - print!( - "\r{:100}\r {} {} v{}", - "", - colored("Building", BOLD_GREEN), - name, - version - ); - std::io::Write::flush(&mut std::io::stdout()).unwrap(); - if !dep_root.join("dcr.toml").is_file() { - return Err(format!( - "Registry dependency `{}` is missing dcr.toml at {}", - name, - dep_root.display() - )); - } - build_project_at( - &dep_root, - profile, - build_target, - &[], - force, - verbose, - None, - )?; - print!( - "\r{:100}\r {} {} v{}", - "", - colored("Ready", BOLD_GREEN), - name, - version - ); - println!(); - } else { - println!( - " {} {} v{}", - colored("Ready", BOLD_GREEN), - name, - version - ); - } - } - } - } - - let (resolved_cflags, resolved_ldflags) = - resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?; - let mut combined_excludes = Vec::new(); - for dir in exclude_dirs { - combined_excludes.push(dir.clone()); - } - let mut exclude_patterns = Vec::new(); - for raw in build_excludes { - let trimmed = raw.trim(); - if trimmed.is_empty() { - continue; - } - let normalized = trimmed.replace('\\', "/"); - if common::has_glob_magic(&normalized) { - exclude_patterns.push(normalized); - continue; - } - let p = Path::new(trimmed); - if p.is_absolute() { - combined_excludes.push(p.to_path_buf()); - exclude_patterns.push(normalized); - } else { - combined_excludes.push(project_root.join(p)); - exclude_patterns.push(normalized); - } - } - let mut combined_includes: Vec = Vec::new(); - combined_includes.extend(exclude_patterns.iter().map(|v| format!("!{v}"))); - combined_includes.extend(build_includes.iter().map(|v| v.replace('\\', "/"))); - - let mut source_roots: Vec = Vec::new(); - for raw in &build_roots { - let trimmed = raw.trim(); - if trimmed.is_empty() { - continue; - } - let p = Path::new(trimmed); - if p.is_absolute() { - source_roots.push(p.to_path_buf()); - } else { - source_roots.push(project_root.join(p)); - } - } - if !src_disable && source_roots.is_empty() { - source_roots.push(project_root.join("src")); - } - - let mut merged_include_dirs = resolved.include_dirs.clone(); - for raw in &build_includes { - let trimmed = raw.trim(); - if trimmed.is_empty() { - continue; - } - let normalized = trimmed.replace('\\', "/"); - if common::has_glob_magic(&normalized) { - continue; - } - let p = Path::new(trimmed); - let dir = if p.is_absolute() { - p.to_path_buf() - } else { - project_root.join(p) - }; - if dir.is_dir() { - merged_include_dirs.push(dir.to_string_lossy().to_string()); - } - } - - let version_info = parse_version_info(&project_version); - let resolved_cflags: Vec = resolved_cflags - .iter() - .map(|f| substitute_vars(f, &version_info, profile, &project_name)) - .collect(); - let modified_ldflags: Vec = resolved_ldflags - .iter() - .map(|f| substitute_vars(f, &version_info, profile, &project_name)) - .collect(); - let merged_include_dirs: Vec = merged_include_dirs - .iter() - .map(|f| substitute_vars(f, &version_info, profile, &project_name)) - .collect(); - let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs); - let ctx = BuildContext { - profile, - project_name: &project_name, - compiler: &resolved_compiler, - language: &build_language, - standard: &build_standard, - cxx_standard: &build_cxx_standard, - target: build_target, - target_dir: if target_dir_binding.is_empty() { - None - } else { - Some(target_dir_binding.as_str()) - }, - kind: normalize_kind(&build_kind), - platform: normalize_platform(&build_platform), - linker: resolved_linker.as_deref(), - archiver: resolved_archiver.as_deref(), - moc: Some(&tool_execs.moc), - uic: Some(&tool_execs.uic), - rcc: Some(&tool_execs.rcc), - package_type: if build_type.is_empty() { - None - } else { - Some(build_type.as_str()) - }, - freestanding, - panic_abort, - codegen_units, - source_roots: &source_roots, - exclude_dirs: &combined_excludes, - include_paths: &combined_includes, - include_dirs: &merged_include_dirs, - lib_dirs: &resolved.lib_dirs, - libs: &resolved.libs, - cflags: &resolved_cflags, - ldflags: &modified_ldflags, - output_filename: if output_filename.is_empty() { - None - } else { - Some(output_filename.as_str()) - }, - output_extension: if output_extension.is_empty() { - None - } else { - Some(output_extension.as_str()) - }, - verbose, - qt: build_qt, - }; - let step_flags = - build_step_flags(&resolved_cflags, &resolved.include_dirs, &resolved_compiler); - let step_vars = StepVars { - profile, - version: &version_info.full, - version_major: &version_info.major, - version_minor: &version_info.minor, - version_patch: &version_info.patch, - version_suffix: &version_info.suffix, - version_suffix_dash: &version_info.suffix_dash, - }; - let steps_dirty = build_steps_need_run(&build_steps, &step_vars)?; - if steps_dirty { - clean_generated_files(&build_generated)?; - run_build_steps(&build_steps, &tool_execs, &step_flags, &step_vars)?; - } - let sources = collect_sources(&ctx)?; - let headers = collect_header_files(&ctx, project_root)?; - let lib_files = collect_lib_files(&ctx); - let fingerprint = compute_build_fingerprint(&ctx, &sources, &headers, &lib_files)?; - let mut skip = should_skip_build(&ctx, &fingerprint); - if steps_dirty { - skip = false; - } - let debug_enabled = std::env::var("DCR_DEBUG").is_ok(); - if force { - skip = false; - } - if skip && !debug_enabled { - return Ok(()); - } - println!( - " {} {} v{}", - colored("Compiling", BOLD_GREEN), - project_name, - project_version - ); - if !skip { - run_build(&ctx)?; - check_interrupted()?; - write_build_fingerprint(&ctx, &fingerprint)?; - } - if ctx.package_type == Some("lib") { - package_library(&ctx, &headers)?; - } - let post_steps_dirty = build_steps_need_run(&build_post_steps, &step_vars)?; - if post_steps_dirty { - run_build_steps(&build_post_steps, &tool_execs, &step_flags, &step_vars)?; - } - verify_expectations(&build_expects, &step_vars)?; - Ok(()) - }) -} - -fn package_library(ctx: &BuildContext, headers: &[PathBuf]) -> Result<(), String> { - let target_root = if let Some(dir) = ctx.target_dir { - Path::new(dir) - .parent() - .and_then(|p| p.parent()) - .unwrap_or_else(|| Path::new("target")) - } else { - Path::new("target") - }; - - let include_dir = target_root.join("include"); - let lib_dir = target_root.join("lib"); - - fs::create_dir_all(&include_dir).map_err(|e| e.to_string())?; - fs::create_dir_all(&lib_dir).map_err(|e| e.to_string())?; - - for header in headers { - if let Some(name) = header.file_name() { - let dest = include_dir.join(name); - fs::copy(header, dest) - .map_err(|e| format!("Failed to copy header {:?}: {}", header, e))?; - } - } - - let mut outputs = Vec::new(); - if ctx.kind == "staticlib" || ctx.kind == "any" { - outputs.push(crate::platform::lib_path( - ctx.profile, - ctx.project_name, - ctx.target_dir, - )); - } - if ctx.kind == "sharedlib" || ctx.kind == "any" { - outputs.push(crate::platform::shared_lib_path( - ctx.profile, - ctx.project_name, - ctx.target_dir, - )); - } - - for output in outputs { - let path = Path::new(&output); - if path.exists() - && let Some(name) = path.file_name() - { - let dest = lib_dir.join(name); - fs::copy(path, dest).map_err(|e| format!("Failed to copy lib {:?}: {}", path, e))?; - } - } - - Ok(()) -} - -struct ToolchainExecs { - uic: String, - moc: String, - rcc: String, -} - -fn resolve_toolchain_execs( - uic: &Option, - moc: &Option, - rcc: &Option, - pkg_configs: &[String], -) -> ToolchainExecs { - let qt_bins = resolve_qt_host_bins(pkg_configs); - ToolchainExecs { - uic: resolve_qt_tool(uic, qt_bins.as_deref(), "uic"), - moc: resolve_qt_tool(moc, qt_bins.as_deref(), "moc"), - rcc: resolve_qt_tool(rcc, qt_bins.as_deref(), "rcc"), - } -} - -fn resolve_qt_tool(configured: &Option, qt_bins: Option<&Path>, tool: &str) -> String { - if let Some(value) = configured { - return value.clone(); - } - if let Some(dir) = qt_bins { - let candidate = dir.join(tool); - if candidate.is_file() { - return candidate.to_string_lossy().to_string(); - } - if cfg!(target_os = "windows") { - let candidate = dir.join(format!("{tool}.exe")); - if candidate.is_file() { - return candidate.to_string_lossy().to_string(); - } - } - } - if let Some(candidate) = detect_qt6_tool_variant(tool) { - return candidate; - } - tool.to_string() -} - -fn resolve_qt_host_bins(pkgs: &[String]) -> Option { - let qt_pkgs: Vec<&String> = pkgs.iter().filter(|p| p.starts_with("Qt6")).collect(); - if qt_pkgs.is_empty() { - return None; - } - let preferred = ["Qt6Core", "Qt6Widgets", "Qt6Gui"]; - for name in preferred { - if let Some(dir) = query_pkg_config_var(name, "host_bins") { - return Some(dir); - } - if let Some(dir) = query_pkg_config_var(name, "libexecdir") - && let Some(bin) = qt_bins_from_libexec(&dir) - { - return Some(bin); - } - if let Some(dir) = query_pkg_config_var(name, "bindir") { - return Some(dir); - } - } - for pkg in qt_pkgs { - if let Some(dir) = query_pkg_config_var(pkg, "host_bins") { - return Some(dir); - } - if let Some(dir) = query_pkg_config_var(pkg, "libexecdir") - && let Some(bin) = qt_bins_from_libexec(&dir) - { - return Some(bin); - } - if let Some(dir) = query_pkg_config_var(pkg, "bindir") { - return Some(dir); - } - } - None -} - -fn query_pkg_config_var(pkg: &str, var: &str) -> Option { - let output = std::process::Command::new("pkg-config") - .arg(format!("--variable={var}")) - .arg(pkg) - .output() - .ok()?; - if !output.status.success() { - return None; - } - let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if value.is_empty() { - return None; - } - let path = PathBuf::from(value); - if path.is_dir() { Some(path) } else { None } -} - -fn qt_bins_from_libexec(libexec: &Path) -> Option { - for tool in ["moc", "uic", "rcc"] { - if libexec.join(tool).is_file() { - return Some(libexec.to_path_buf()); - } - if cfg!(target_os = "windows") && libexec.join(format!("{tool}.exe")).is_file() { - return Some(libexec.to_path_buf()); - } - } - let bin = libexec.join("bin"); - if bin.is_dir() { Some(bin) } else { None } -} - -fn detect_qt6_tool_variant(tool: &str) -> Option { - [format!("{tool}6"), format!("{tool}-qt6")] - .into_iter() - .find(|candidate| is_on_path(candidate)) -} - -fn is_on_path(cmd: &str) -> bool { - if Path::new(cmd).is_file() { - return true; - } - std::env::var_os("PATH") - .is_some_and(|paths| std::env::split_paths(&paths).any(|dir| dir.join(cmd).is_file())) -} - -#[derive(Clone)] -struct BuildStep { - name: String, - input: String, - output: String, - cmd: String, -} - -fn get_build_steps(config: &Config, key: &str) -> Result, String> { - let value = match config.get(key) { - Some(v) => v, - None => return Ok(Vec::new()), - }; - get_build_steps_from_value(value, key) -} - -fn run_build_steps( - steps: &[BuildStep], - tools: &ToolchainExecs, - step_flags: &str, - vars: &StepVars, -) -> Result<(), String> { - for step in steps { - run_build_step(step, tools, step_flags, vars)?; - } - Ok(()) -} - -fn run_build_step( - step: &BuildStep, - tools: &ToolchainExecs, - step_flags: &str, - vars: &StepVars, -) -> 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) - .map_err(|err| format!("Failed to run step '{}': {err}", step.name))?; - if !status.success() { - return Err(format!("Step '{}' failed", step.name)); - } - } - Ok(()) -} - -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(()) -} - -fn expand_glob(pattern: &str) -> Result, 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) -} - -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) -} - -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::>() - .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) -> Result { - if cfg!(target_os = "windows") { - std::process::Command::new("cmd") - .arg("/C") - .arg(cmd) - .status() - } else { - std::process::Command::new("sh").arg("-c").arg(cmd).status() - } -} - -fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result { - 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) -> crate::utils::build::VersionInfo { - crate::utils::build::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) -} - -struct StepVars<'a> { - profile: &'a str, - version: &'a str, - version_major: &'a str, - version_minor: &'a str, - version_patch: &'a str, - version_suffix: &'a str, - version_suffix_dash: &'a str, -} - -fn compute_build_fingerprint( - ctx: &BuildContext, - sources: &[String], - headers: &[std::path::PathBuf], - lib_files: &[std::path::PathBuf], -) -> Result { - 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())) -} - -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 -} - -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>) -> std::path::PathBuf { - match target_dir { - Some(dir) => Path::new(dir).join(".dcr-build.hash"), - None => Path::new("./target").join(profile).join(".dcr-build.hash"), - } -} - -fn build_output_path(ctx: &BuildContext) -> String { - let name = ctx.output_filename.unwrap_or(ctx.project_name); - let ext = ctx.output_extension.unwrap_or(""); - - let final_name = if ext.is_empty() { - name.to_string() - } else { - format!("{}.{}", name, ext) - }; - - if ctx.kind == "staticlib" { - return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir); - } - if ctx.kind == "sharedlib" { - return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir); - } - if ctx.kind == "efi" { - return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir); - } - if ctx.kind == "elf" { - return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir); - } - crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir) -} - -fn collect_header_files( - ctx: &BuildContext, - project_root: &Path, -) -> Result, 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()); - } - for dir in ctx.include_dirs { - roots.push(Path::new(dir).to_path_buf()); - } - for root in roots { - if !root.exists() { - continue; - } - collect_header_files_rec(&root, &mut out, ctx.exclude_dirs, ctx.include_paths)?; - } - out.sort(); - out.dedup(); - Ok(out) -} - -fn collect_header_files_rec( - dir: &Path, - out: &mut Vec, - exclude_dirs: &[std::path::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") -} - -fn collect_lib_files(ctx: &BuildContext) -> Vec { - 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 { - 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) -} diff --git a/src/cli/gen.rs b/src/cli/gen.rs index 8cb1e28..5812cd2 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -15,10 +15,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::core::build::builder::BuildContext; +use crate::core::build::builder::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, diff --git a/src/cli/lint.rs b/src/cli/lint.rs index 118a687..3556ccb 100644 --- a/src/cli/lint.rs +++ b/src/cli/lint.rs @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::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; diff --git a/src/cli/run.rs b/src/cli/run.rs index b8f1aac..5658681 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -218,7 +218,14 @@ fn run_project( return Err("Cannot run library build".to_string()); } - let build_status = build(&args_for_build(flags)); + let build_status = if let Some(wroot) = workspace_root { + with_dir(wroot, || { + Ok(build(&args_for_workspace_build(flags, project_name))) + }) + .unwrap_or(1) + } else { + build(&args_for_build(flags)) + }; let bin_path = crate::platform::bin_path( &flags.profile, project_name, @@ -266,6 +273,36 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec { 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 { + 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()); } diff --git a/src/core/build/builder/artifact.rs b/src/core/build/builder/artifact.rs new file mode 100644 index 0000000..f78bdf4 --- /dev/null +++ b/src/core/build/builder/artifact.rs @@ -0,0 +1,126 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +use crate::core::build::builder::BuildContext; +use crate::core::build::common; +use crate::platform; +use crate::utils::build::is_bare_metal_target; +use std::process::Command; +use std::time::Instant; + +pub(crate) fn archive_static( + ctx: &BuildContext, + objects: &[String], + start_time: Instant, +) -> Result { + 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 { + 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 { + 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}")) + } +} diff --git a/src/core/builder/unix_cc.rs b/src/core/build/builder/cc_common.rs similarity index 63% rename from src/core/builder/unix_cc.rs rename to src/core/build/builder/cc_common.rs index eb6d8a9..4ec52ef 100644 --- a/src/core/builder/unix_cc.rs +++ b/src/core/build/builder/cc_common.rs @@ -15,9 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::core::builder::BuildContext; -use crate::core::builder::common; -use crate::platform; +use crate::core::build::builder::BuildContext; +use crate::core::build::builder::artifact; +use crate::core::build::common; use crate::utils::build::is_bare_metal_target; use std::fs; use std::path::{Path, PathBuf}; @@ -36,7 +36,7 @@ pub fn build(ctx: &BuildContext) -> Result { 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 }; @@ -56,93 +56,10 @@ pub fn build(ctx: &BuildContext) -> Result { let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?; 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, String> { @@ -156,7 +73,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, 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] { @@ -288,5 +205,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) } diff --git a/src/core/build/builder/mod.rs b/src/core/build/builder/mod.rs new file mode 100644 index 0000000..e13fbbe --- /dev/null +++ b/src/core/build/builder/mod.rs @@ -0,0 +1,234 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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; + fn collect_sources(&self, ctx: &BuildContext) -> Result, 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 { + cc_common::build(ctx) + } + fn collect_sources(&self, ctx: &BuildContext) -> Result, String> { + cc_common::collect_sources(ctx) + } +} + +impl Builder for Msvc { + fn id(&self) -> &'static str { + "msvc" + } + fn build(&self, ctx: &BuildContext) -> Result { + msvc::build(ctx) + } + fn collect_sources(&self, ctx: &BuildContext) -> Result, String> { + msvc::collect_sources(ctx) + } +} + +impl Builder for Nasm { + fn id(&self) -> &'static str { + "nasm" + } + fn build(&self, ctx: &BuildContext) -> Result { + nasm::build(ctx) + } + fn collect_sources(&self, ctx: &BuildContext) -> Result, String> { + nasm::collect_sources(ctx) + } +} + +impl Builder for Gas { + fn id(&self) -> &'static str { + "gas" + } + fn build(&self, ctx: &BuildContext) -> Result { + gas::build(ctx) + } + fn collect_sources(&self, ctx: &BuildContext) -> Result, String> { + gas::collect_sources(ctx) + } +} + +impl Builder for Masm { + fn id(&self) -> &'static str { + "masm" + } + fn build(&self, ctx: &BuildContext) -> Result { + masm::build(ctx) + } + fn collect_sources(&self, ctx: &BuildContext) -> Result, String> { + masm::collect_sources(ctx) + } +} + +impl Builder for Fasm { + fn id(&self) -> &'static str { + "fasm" + } + fn build(&self, ctx: &BuildContext) -> Result { + fasm::build(ctx) + } + fn collect_sources(&self, ctx: &BuildContext) -> Result, String> { + fasm::collect_sources(ctx) + } +} + +impl Builder for Llc { + fn id(&self) -> &'static str { + "llvm_ir" + } + fn build(&self, ctx: &BuildContext) -> Result { + llvm_ir::build(ctx) + } + fn collect_sources(&self, ctx: &BuildContext) -> Result, 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 { + 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, 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"); + } +} diff --git a/src/core/builder/msvc.rs b/src/core/build/builder/msvc/mod.rs similarity index 98% rename from src/core/builder/msvc.rs rename to src/core/build/builder/msvc/mod.rs index b3893c6..fd91ed6 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/build/builder/msvc/mod.rs @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::core::builder::BuildContext; -use crate::core::builder::common; +use crate::core::build::builder::BuildContext; +use crate::core::build::common; use crate::platform; use std::fs; use std::path::Path; @@ -152,7 +152,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, 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 { diff --git a/src/core/build/cache.rs b/src/core/build/cache.rs new file mode 100644 index 0000000..b93d543 --- /dev/null +++ b/src/core/build/cache.rs @@ -0,0 +1,254 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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 { + let mut hasher = Sha256::new(); + hasher.update(ctx.profile.as_bytes()); + hasher.update(ctx.project_name.as_bytes()); + hasher.update(ctx.compiler.as_bytes()); + hasher.update(ctx.language.as_bytes()); + hasher.update(ctx.standard.as_bytes()); + hasher.update(ctx.kind.as_bytes()); + if let Some(v) = ctx.target_dir { + hasher.update(v.as_bytes()); + } + if let Some(v) = ctx.platform { + hasher.update(v.as_bytes()); + } + if let Some(v) = ctx.linker { + hasher.update(v.as_bytes()); + } + if let Some(v) = ctx.archiver { + hasher.update(v.as_bytes()); + } + for value in ctx.include_dirs { + hasher.update(value.as_bytes()); + } + for value in ctx.lib_dirs { + hasher.update(value.as_bytes()); + } + for value in ctx.libs { + hasher.update(value.as_bytes()); + } + for value in ctx.cflags { + hasher.update(value.as_bytes()); + } + for value in ctx.ldflags { + hasher.update(value.as_bytes()); + } + let toml = + fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?; + hasher.update(toml.as_bytes()); + if let Ok(lock) = fs::read_to_string("dcr.lock") { + hasher.update(lock.as_bytes()); + } + for source in sources { + let path = Path::new(source); + update_hasher_with_file(&mut hasher, path)?; + } + for header in headers { + update_hasher_with_file(&mut hasher, header)?; + } + for lib in lib_files { + update_hasher_with_file(&mut hasher, lib)?; + } + Ok(to_hex(&hasher.finalize())) +} + +pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool { + let output = build_output_path(ctx); + if !Path::new(&output).is_file() { + return false; + } + let cache_path = build_cache_path(ctx.profile, ctx.target_dir); + let cached = fs::read_to_string(cache_path).unwrap_or_default(); + cached.trim() == fingerprint +} + +pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> { + let cache_path = build_cache_path(ctx.profile, ctx.target_dir); + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent).map_err(|err| format!("Failed to create cache dir: {err}"))?; + } + fs::write(cache_path, format!("{fingerprint}\n")) + .map_err(|err| format!("Failed to write cache: {err}")) +} + +fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf { + match target_dir { + Some(dir) => Path::new(dir).join(".dcr-build.hash"), + None => Path::new("./target").join(profile).join(".dcr-build.hash"), + } +} + +fn build_output_path(ctx: &BuildContext) -> String { + let name = ctx.output_filename.unwrap_or(ctx.project_name); + let ext = ctx.output_extension.unwrap_or(""); + + let final_name = if ext.is_empty() { + name.to_string() + } else { + format!("{}.{}", name, ext) + }; + + if ctx.kind == "staticlib" { + return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir); + } + if ctx.kind == "sharedlib" { + return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir); + } + if ctx.kind == "efi" { + return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir); + } + if ctx.kind == "elf" { + return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir); + } + crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir) +} + +pub(crate) fn collect_header_files( + ctx: &BuildContext, + project_root: &Path, +) -> Result, String> { + let mut out = Vec::new(); + let mut roots = Vec::new(); + if ctx.source_roots.is_empty() { + roots.push(project_root.join("src")); + } else { + roots.extend(ctx.source_roots.iter().cloned()); + } + let abs_root = project_root + .canonicalize() + .unwrap_or_else(|_| project_root.to_path_buf()); + for dir in ctx.include_dirs { + let p = Path::new(dir).to_path_buf(); + if let Ok(abs_p) = p.canonicalize() { + if abs_p.starts_with(&abs_root) { + roots.push(abs_p); + } + } else if p.starts_with(&abs_root) { + roots.push(p); + } + } + for root in roots { + if !root.exists() { + continue; + } + collect_header_files_rec(&root, &mut out, ctx.exclude_dirs, ctx.include_paths)?; + } + out.sort(); + out.dedup(); + Ok(out) +} + +fn collect_header_files_rec( + dir: &Path, + out: &mut Vec, + 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 { + 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 { + 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) +} diff --git a/src/core/builder/common.rs b/src/core/build/common.rs similarity index 98% rename from src/core/builder/common.rs rename to src/core/build/common.rs index ab13fc9..14a4b51 100644 --- a/src/core/builder/common.rs +++ b/src/core/build/common.rs @@ -58,9 +58,10 @@ fn collect_sources_rec( if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() { return Ok(()); } - let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?; + let entries = fs::read_dir(&full_dir) + .map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?; for entry in entries { - let entry = entry.map_err(|err| format!("src dir error: {err}"))?; + let entry = entry.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?; let path = entry.path(); if path.is_dir() { if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() { @@ -177,12 +178,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 +328,7 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool { return true; } } else { - return true; // Missing dependency triggers rebuild + return true; } } } @@ -537,7 +534,6 @@ mod tests { let src = dir.join("test.c"); let obj = dir.join("test.o"); fs::write(&src, "int main() {}").unwrap(); - // Sleep briefly to ensure mtime difference std::thread::sleep(std::time::Duration::from_millis(50)); fs::write(&obj, "").unwrap(); assert!(!needs_rebuild( diff --git a/src/core/build/engine.rs b/src/core/build/engine.rs new file mode 100644 index 0000000..29ac7a6 --- /dev/null +++ b/src/core/build/engine.rs @@ -0,0 +1,1084 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +use crate::core::build::builder::{BuildContext, build as build_project, collect_sources}; +use crate::core::build::cache::{ + collect_header_files, collect_lib_files, compute_build_fingerprint, should_skip_build, + write_build_fingerprint, +}; +use crate::core::build::common; +use crate::core::build::report::{ + BuildError, BuildEvent, BuildOutcome, BuildReporter, BuildRequest, +}; +use crate::core::build::resolve::{ + get_build_string_with_profile, get_config_value, get_lang_list, get_lang_string, + get_list_with_profile_and_target, get_string_with_profile_and_target, get_targets, +}; +use crate::core::build::steps::{ + StepVars, build_step_flags, build_steps_need_run, clean_generated_files, + get_build_steps_with_profile, run_build_steps, verify_expectations, +}; +use crate::core::build_config::Config; +use crate::core::deps::{register, resolve_deps}; +use crate::core::workspace::parse_workspace; +use crate::utils::build::{ + get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile, + is_bare_metal_target, normalize_kind, normalize_platform, normalize_target, + normalize_target_os, parse_version_info, prepend_clang_target_flag, primary_language, + resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars, +}; +use crate::utils::fs::check_dir; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Instant; + +fn ensure_target_dirs( + project_root: &Path, + items: &[String], + profile: &str, + target_dir: Option, + skip_local_target: bool, +) { + if !skip_local_target && !items.contains(&"target".to_string()) { + let _ = fs::create_dir_all(project_root.join("target")); + } + if let Some(dir) = &target_dir { + let _ = fs::create_dir_all(dir); + } else { + let default_dir = if cfg!(target_os = "linux") { + let arch = std::env::consts::ARCH; + format!("{arch}-unknown-linux-gnu/{profile}") + } else if cfg!(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "dragonfly" + )) { + let arch = std::env::consts::ARCH; + let os = std::env::consts::OS; + format!("{arch}-unknown-{os}/{profile}") + } else { + profile.to_string() + }; + let target_path = project_root.join("target"); + let target_items = check_dir(target_path.to_str()).unwrap_or_default(); + if !target_items.contains(&default_dir) { + let _ = fs::create_dir_all(target_path.join(&default_dir)); + } + } +} + +fn run_compile( + ctx: &BuildContext, + _cancel: &Arc, + _rep: &mut dyn BuildReporter, +) -> Result { + let start_time = Instant::now(); + match build_project(ctx) { + Ok(times) => { + let times = if times == 0.0 { + ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0 + } else { + times + }; + Ok(times) + } + Err(e) => Err(e), + } +} + +// Library entry point: run a build described by `req`, streaming progress to +// `rep`. The CLI wraps this with a CliReporter; TUI/GUI/MCP plug in their own. +pub fn run_build( + root: &Path, + req: &BuildRequest, + rep: &mut dyn BuildReporter, +) -> Result { + let secs = build_all( + root, + &req.profile, + req.target.as_deref(), + req.force, + req.verbose, + req.workspace.as_deref(), + &req.cancel, + rep, + ) + .map_err(BuildError::from)?; + Ok(BuildOutcome { secs }) +} + +#[allow(unused_variables, clippy::too_many_arguments)] +fn build_all( + root: &Path, + profile: &str, + target: Option<&str>, + force: bool, + verbose: bool, + workspace: Option<&str>, + cancel: &Arc, + rep: &mut dyn BuildReporter, +) -> Result { + let config = + Config::open(root.join("dcr.toml").to_str().unwrap()).map_err(|err| err.to_string())?; + + if config.is_workspace_only() { + let ws = parse_workspace(&config, profile, target, root)? + .ok_or_else(|| "Workspace root has no members defined".to_string())?; + let members: Vec<_> = if let Some(filter_name) = workspace { + ws.members + .into_iter() + .filter(|m| m.name == filter_name) + .collect() + } else { + ws.members + }; + if members.is_empty() { + return Err("No matching workspace members to build".to_string()); + } + for member in &members { + build_project_at( + &member.path, + profile, + target, + &[], + force, + verbose, + Some(root), + cancel, + &mut *rep, + )?; + } + return Ok(0.0); + } + + let project_name = get_config_str(&config, "package.name"); + let project_version = get_config_str(&config, "package.version"); + + let targets_to_build: Vec> = if let Some(t) = target { + vec![Some(normalize_target_os(t).to_string())] + } else { + let config_targets = get_targets(&config, profile)?; + if config_targets.is_empty() { + vec![None] + } else { + config_targets + .into_iter() + .map(|t| Some(normalize_target_os(&t).to_string())) + .collect() + } + }; + + let start_time = Instant::now(); + for (i, build_target) in targets_to_build.iter().enumerate() { + if cancel.load(Ordering::SeqCst) { + return Err("Build interrupted".to_string()); + } + let target_label = build_target.as_ref().map_or("native", |t| t.as_str()); + if targets_to_build.len() > 1 { + rep.on_event(BuildEvent::TargetStart { + index: i + 1, + total: targets_to_build.len(), + target: target_label, + }); + } else { + rep.on_event(BuildEvent::ProjectStart { + name: &project_name, + profile, + target: target_label, + }); + } + if let Some(ws) = parse_workspace(&config, profile, build_target.as_deref(), root)? { + if let Some(filter_name) = workspace { + for member in ws.members.iter().filter(|m| m.name == filter_name) { + build_project_at( + &member.path, + profile, + build_target.as_deref(), + &[], + force, + verbose, + Some(root), + cancel, + &mut *rep, + )?; + } + } else { + build_workspace( + &ws, + profile, + build_target.as_deref(), + force, + verbose, + Some(root), + cancel, + &mut *rep, + )?; + let excludes: Vec = + ws.members.iter().map(|m| m.path.clone()).collect(); + build_project_at( + root, + profile, + build_target.as_deref(), + &excludes, + force, + verbose, + None, + cancel, + &mut *rep, + )?; + } + } else { + build_project_at( + root, + profile, + build_target.as_deref(), + &[], + force, + verbose, + None, + cancel, + &mut *rep, + )?; + } + } + let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + rep.on_event(BuildEvent::Finished { secs: elapsed }); + Ok(elapsed) +} + +#[allow(clippy::too_many_arguments)] +fn build_workspace( + workspace: &crate::core::workspace::Workspace, + profile: &str, + target: Option<&str>, + force: bool, + verbose: bool, + workspace_root: Option<&Path>, + cancel: &Arc, + rep: &mut dyn BuildReporter, +) -> Result<(), String> { + for member in &workspace.members { + build_project_at( + &member.path, + profile, + target, + &[], + force, + verbose, + workspace_root, + cancel, + &mut *rep, + )?; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn build_project_at( + project_root: &Path, + profile: &str, + target: Option<&str>, + exclude_dirs: &[std::path::PathBuf], + force: bool, + verbose: bool, + workspace_root: Option<&Path>, + cancel: &Arc, + rep: &mut dyn BuildReporter, +) -> Result<(), String> { + if cancel.load(Ordering::SeqCst) { + return Err("Build interrupted".to_string()); + } + let items = check_dir(project_root.to_str()) + .map_err(|_| "Failed to read project directory".to_string())?; + if !items.contains(&"dcr.toml".to_string()) { + return Err("dcr.toml file not found".to_string()); + } + let config_path = project_root.join("dcr.toml"); + let mut config = Config::open(&config_path.to_string_lossy()).map_err(|err| err.to_string())?; + // Inherit build fields from workspace root if configured + if let Some(root) = workspace_root { + let has_inherit = config + .get("build.inherit") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if has_inherit { + let root_path = root.join("dcr.toml"); + if root_path.is_file() { + let parent_config = + Config::open(&root_path.to_string_lossy()).map_err(|e| e.to_string())?; + config.merge_parent(&parent_config); + } + } + } + let project_name = get_config_str(&config, "package.name"); + let project_version = get_config_str(&config, "package.version"); + let build_target_config = get_build_string_with_profile(&config, "target", profile); + let build_target = target.or(if build_target_config.is_empty() { + None + } else { + Some(build_target_config.as_str()) + }); + let build_language = get_language_with_profile(&config, profile)?; + let build_qt = get_bool_with_profile(&config, "qt", profile, false); + let lang_key = primary_language(&build_language); + let project_compiler = get_lang_string(&config, &lang_key, "compiler", profile, build_target) + .unwrap_or_else(|| { + get_string_with_profile_and_target(&config, "compiler", profile, build_target) + }); + let build_standard = get_lang_string(&config, &lang_key, "standard", profile, build_target) + .unwrap_or_else(|| { + get_string_with_profile_and_target(&config, "standard", profile, build_target) + }); + let build_cxx_standard = + get_lang_string(&config, &lang_key, "cxx_standard", profile, build_target).unwrap_or_else( + || get_string_with_profile_and_target(&config, "cxx_standard", profile, build_target), + ); + let build_kind = get_string_with_profile_and_target(&config, "kind", profile, build_target); + let build_platform = + get_string_with_profile_and_target(&config, "platform", profile, build_target); + let mut build_type = get_string_with_profile_and_target(&config, "type", profile, build_target); + if build_type.is_empty() { + build_type = get_config_str(&config, "package.type"); + } + let tc_cc = get_config_value(&config, "toolchain", "cc", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.cc")); + let tc_cxx = get_config_value(&config, "toolchain", "cxx", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.cxx")); + let tc_as = get_config_value(&config, "toolchain", "as", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.as")); + let tc_ar = get_config_value(&config, "toolchain", "ar", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.ar")); + let tc_ld = get_config_value(&config, "toolchain", "ld", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.ld")); + let tc_uic = get_config_value(&config, "toolchain", "uic", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.uic")); + let tc_moc = get_config_value(&config, "toolchain", "moc", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.moc")); + let tc_rcc = get_config_value(&config, "toolchain", "rcc", profile, build_target) + .or_else(|| get_config_opt(&config, "toolchain.rcc")); + let mut build_cflags = + get_list_with_profile_and_target(&config, "cflags", profile, build_target)?; + let mut build_ldflags = + get_list_with_profile_and_target(&config, "ldflags", profile, build_target)?; + let build_ldscript = + get_string_with_profile_and_target(&config, "ldscript", profile, build_target); + if !build_ldscript.is_empty() { + build_ldflags.push(format!("-T{}", build_ldscript)); + } + + // Новые поля: filename и extension + let output_filename = + get_string_with_profile_and_target(&config, "filename", profile, build_target); + let output_extension = + get_string_with_profile_and_target(&config, "extension", profile, build_target); + let build_excludes = + get_list_with_profile_and_target(&config, "exclude", profile, build_target)?; + let build_includes = + get_list_with_profile_and_target(&config, "include", profile, build_target)?; + let build_roots = get_list_with_profile_and_target(&config, "roots", profile, build_target)?; + let src_disable = get_bool_with_profile(&config, "src_disable", profile, false); + let freestanding = get_bool_with_profile(&config, "freestanding", profile, false); + let lto = get_bool_with_profile(&config, "lto", profile, false); + let strip = get_bool_with_profile(&config, "strip", profile, false); + let opt_level = get_string_with_profile_and_target(&config, "opt_level", profile, build_target); + let debug = get_bool_with_profile(&config, "debug", profile, profile != "release"); + let warnings = get_list_with_profile_and_target(&config, "warnings", profile, build_target)?; + let panic_val = get_string_with_profile_and_target(&config, "panic", profile, build_target); + let panic_abort = panic_val == "abort"; + let codegen_units: usize = + get_string_with_profile_and_target(&config, "codegen-units", profile, build_target) + .parse() + .unwrap_or(0); + + if build_cflags.is_empty() && !freestanding && !is_bare_metal_target(build_target) { + if !opt_level.is_empty() { + build_cflags.push(format!("-O{opt_level}")); + } else { + build_cflags.push(match profile { + "release" => "-O3".to_string(), + _ => "-O0".to_string(), + }); + } + if debug { + build_cflags.push("-g".to_string()); + } + if warnings.is_empty() { + if profile == "debug" { + build_cflags.push("-Wall".to_string()); + build_cflags.push("-Wextra".to_string()); + } + } else { + for w in &warnings { + build_cflags.push(format!("-W{w}")); + } + } + match profile { + "debug" => { + build_cflags.push("-fno-omit-frame-pointer".to_string()); + build_cflags.push("-DDCR_DEBUG".to_string()); + } + "release" => { + build_cflags.push("-DNDEBUG".to_string()); + } + _ => {} + } + } + + for flag in get_lang_list(&config, &lang_key, "flags", profile, build_target)? { + build_cflags.push(flag); + } + + if lto { + build_cflags.push("-flto".to_string()); + build_ldflags.push("-flto".to_string()); + } + if strip { + build_ldflags.push("-s".to_string()); + } + + let build_expects = get_list_with_profile_and_target(&config, "expect", profile, build_target)?; + let pkg_configs = + get_list_with_profile_and_target(&config, "pkg_config", profile, build_target)?; + let build_generated = + get_list_with_profile_and_target(&config, "generated", profile, build_target)?; + let build_steps = get_build_steps_with_profile(&config, "steps", profile)?; + let build_post_steps = get_build_steps_with_profile(&config, "post_steps", profile)?; + + let resolved_compiler = resolve_compiler( + &build_language, + &project_compiler, + tc_cc.as_deref(), + tc_cxx.as_deref(), + tc_as.as_deref(), + ); + let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); + prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler); + prepend_clang_target_flag( + &mut build_ldflags, + build_target, + resolved_linker.as_deref().unwrap_or(&resolved_compiler), + ); + let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); + + let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile); + let target_dir_binding = if !out_dir_config.is_empty() { + let p = Path::new(&out_dir_config); + if p.is_absolute() { + out_dir_config + } else { + project_root.join(p).to_string_lossy().to_string() + } + } else { + match workspace_root { + Some(root) => { + let target_str = build_target + .map(normalize_target_os) + .filter(|t| !t.is_empty()); + let rel: PathBuf = match target_str { + Some(ref t) => Path::new("target").join(t).join(profile), + None => { + let default_dir = if cfg!(target_os = "linux") { + let arch = std::env::consts::ARCH; + format!("{arch}-unknown-linux-gnu/{profile}") + } else if cfg!(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "dragonfly" + )) { + let arch = std::env::consts::ARCH; + let os = std::env::consts::OS; + format!("{arch}-unknown-{os}/{profile}") + } else { + profile.to_string() + }; + Path::new("target").join(&default_dir) + } + }; + root.join(&rel).to_string_lossy().to_string() + } + None => { + let base_dir = + if let Some(rel) = normalize_target(build_target.unwrap_or(""), profile) { + PathBuf::from(rel) + } else { + let default_dir = if cfg!(target_os = "linux") { + let arch = std::env::consts::ARCH; + format!("{arch}-unknown-linux-gnu/{profile}") + } else if cfg!(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "dragonfly" + )) { + let arch = std::env::consts::ARCH; + let os = std::env::consts::OS; + format!("{arch}-unknown-{os}/{profile}") + } else { + profile.to_string() + }; + Path::new("target").join(&default_dir) + }; + project_root.join(base_dir).to_string_lossy().to_string() + } + } + }; + let target_dir = if target_dir_binding.is_empty() { + None + } else { + Some(target_dir_binding.clone()) + }; + ensure_target_dirs( + project_root, + &items, + profile, + target_dir, + workspace_root.is_some(), + ); + + let mut ws_include_dirs = Vec::new(); + let mut ws_lib_dirs = Vec::new(); + let mut ws_libs = Vec::new(); + + if let Some(wroot) = workspace_root { + let wroot_toml = wroot.join("dcr.toml"); + if wroot_toml.is_file() + && let Ok(wroot_config) = Config::open(&wroot_toml.to_string_lossy()) + && let Ok(Some(ws)) = + crate::core::workspace::parse_workspace(&wroot_config, profile, build_target, wroot) + { + let current_abs = project_root + .canonicalize() + .unwrap_or_else(|_| project_root.to_path_buf()); + if let Some(current_member) = ws + .members + .iter() + .find(|m| m.path.canonicalize().unwrap_or_else(|_| m.path.clone()) == current_abs) + { + for dep_name in ¤t_member.deps { + if let Some(dep_member) = ws.members.iter().find(|m| &m.name == dep_name) { + let dep_inc = dep_member.path.join("include"); + let dep_src = dep_member.path.join("src"); + let dep_target_inc = dep_member.path.join("target").join("include"); + let ws_target_inc = wroot.join("target").join("include"); + if dep_inc.is_dir() { + ws_include_dirs.push(dep_inc.to_string_lossy().to_string()); + } + if dep_src.is_dir() { + ws_include_dirs.push(dep_src.to_string_lossy().to_string()); + } + if dep_target_inc.is_dir() { + ws_include_dirs.push(dep_target_inc.to_string_lossy().to_string()); + } + if ws_target_inc.is_dir() { + ws_include_dirs.push(ws_target_inc.to_string_lossy().to_string()); + } + let dep_target_lib = dep_member.path.join("target").join("lib"); + if dep_target_lib.is_dir() { + ws_lib_dirs.push(dep_target_lib.to_string_lossy().to_string()); + } + let target_str = build_target + .map(normalize_target_os) + .filter(|t| !t.is_empty()); + let default_dir = if cfg!(target_os = "linux") { + let arch = std::env::consts::ARCH; + format!("{arch}-unknown-linux-gnu/{profile}") + } else if cfg!(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "dragonfly" + )) { + let arch = std::env::consts::ARCH; + let os = std::env::consts::OS; + format!("{arch}-unknown-{os}/{profile}") + } else { + profile.to_string() + }; + let rel = match target_str { + Some(ref t) => Path::new("target").join(t).join(profile), + None => Path::new("target").join(&default_dir), + }; + let dep_build_lib1 = dep_member.path.join(&rel); + let dep_build_lib2 = wroot.join(&rel); + if dep_build_lib1.is_dir() { + ws_lib_dirs.push(dep_build_lib1.to_string_lossy().to_string()); + } + if dep_build_lib2.is_dir() { + ws_lib_dirs.push(dep_build_lib2.to_string_lossy().to_string()); + } + let dep_toml = dep_member.path.join("dcr.toml"); + let is_lib = if dep_toml.is_file() { + if let Ok(dep_config) = Config::open(&dep_toml.to_string_lossy()) { + let kind = + get_build_string_with_profile(&dep_config, "kind", profile); + kind == "staticlib" || kind == "sharedlib" + } else { + false + } + } else { + false + }; + if is_lib { + ws_libs.push(dep_member.name.clone()); + } + } + } + } + } + } + + let deps_table = config.get("dependencies").and_then(|v| v.as_table()); + let mut resolved = resolve_deps(&config, profile, build_target, project_root)?; + resolved.include_dirs.extend(ws_include_dirs); + resolved.lib_dirs.extend(ws_lib_dirs); + resolved.libs.extend(ws_libs); + + // Registry dependencies are cached under the DCR registry root. Build + // them as normal DCR projects before the current project is linked. + if let Some(deps) = deps_table { + for (name, value) in deps { + if register::is_registry_dep(value) { + let pkg_info = register::resolve_package_from_registry(name)?; + let version = pkg_info + .get("latest_version") + .or_else(|| pkg_info.get("version")) + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + let dep_root = register::package_root_from_registry_info(&pkg_info)?; + let include_dir = dep_root.join("target").join("include"); + let lib_dir = dep_root.join("target").join("lib"); + + if !include_dir.exists() || !lib_dir.exists() { + rep.on_event(BuildEvent::DepBuilding { name, version }); + if !dep_root.join("dcr.toml").is_file() { + return Err(format!( + "Registry dependency `{}` is missing dcr.toml at {}", + name, + dep_root.display() + )); + } + build_project_at( + &dep_root, + profile, + build_target, + &[], + force, + verbose, + None, + cancel, + &mut *rep, + )?; + rep.on_event(BuildEvent::DepReady { + name, + version, + rebuilt: true, + }); + } else { + rep.on_event(BuildEvent::DepReady { + name, + version, + rebuilt: false, + }); + } + } + } + } + + let (resolved_cflags, resolved_ldflags) = + resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?; + let mut combined_excludes = Vec::new(); + for dir in exclude_dirs { + combined_excludes.push(dir.clone()); + } + let mut exclude_patterns = Vec::new(); + for raw in build_excludes { + let trimmed = raw.trim(); + if trimmed.is_empty() { + continue; + } + let normalized = trimmed.replace('\\', "/"); + if common::has_glob_magic(&normalized) { + exclude_patterns.push(normalized); + continue; + } + let p = Path::new(trimmed); + if p.is_absolute() { + combined_excludes.push(p.to_path_buf()); + exclude_patterns.push(normalized); + } else { + combined_excludes.push(project_root.join(p)); + exclude_patterns.push(normalized); + } + } + let mut combined_includes: Vec = Vec::new(); + combined_includes.extend(exclude_patterns.iter().map(|v| format!("!{v}"))); + combined_includes.extend(build_includes.iter().map(|v| v.replace('\\', "/"))); + + let mut source_roots: Vec = Vec::new(); + for raw in &build_roots { + let trimmed = raw.trim(); + if trimmed.is_empty() { + continue; + } + let p = Path::new(trimmed); + if p.is_absolute() { + source_roots.push(p.to_path_buf()); + } else { + source_roots.push(project_root.join(p)); + } + } + if !src_disable && source_roots.is_empty() { + source_roots.push(project_root.join("src")); + } + + let mut merged_include_dirs = resolved.include_dirs.clone(); + for raw in &build_includes { + let trimmed = raw.trim(); + if trimmed.is_empty() { + continue; + } + let normalized = trimmed.replace('\\', "/"); + if common::has_glob_magic(&normalized) { + continue; + } + let p = Path::new(trimmed); + let dir = if p.is_absolute() { + p.to_path_buf() + } else { + project_root.join(p) + }; + if dir.is_dir() { + merged_include_dirs.push(dir.to_string_lossy().to_string()); + } + } + + let version_info = parse_version_info(&project_version); + let resolved_cflags: Vec = resolved_cflags + .iter() + .map(|f| substitute_vars(f, &version_info, profile, &project_name)) + .collect(); + let modified_ldflags: Vec = resolved_ldflags + .iter() + .map(|f| substitute_vars(f, &version_info, profile, &project_name)) + .collect(); + let merged_include_dirs: Vec = merged_include_dirs + .iter() + .map(|f| substitute_vars(f, &version_info, profile, &project_name)) + .collect(); + let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs); + let ctx = BuildContext { + profile, + project_name: &project_name, + compiler: &resolved_compiler, + language: &build_language, + standard: &build_standard, + cxx_standard: &build_cxx_standard, + target: build_target, + target_dir: if target_dir_binding.is_empty() { + None + } else { + Some(target_dir_binding.as_str()) + }, + kind: normalize_kind(&build_kind), + platform: normalize_platform(&build_platform), + linker: resolved_linker.as_deref(), + archiver: resolved_archiver.as_deref(), + moc: Some(&tool_execs.moc), + uic: Some(&tool_execs.uic), + rcc: Some(&tool_execs.rcc), + package_type: if build_type.is_empty() { + None + } else { + Some(build_type.as_str()) + }, + freestanding, + panic_abort, + codegen_units, + source_roots: &source_roots, + exclude_dirs: &combined_excludes, + include_paths: &combined_includes, + include_dirs: &merged_include_dirs, + lib_dirs: &resolved.lib_dirs, + libs: &resolved.libs, + cflags: &resolved_cflags, + ldflags: &modified_ldflags, + output_filename: if output_filename.is_empty() { + None + } else { + Some(output_filename.as_str()) + }, + output_extension: if output_extension.is_empty() { + None + } else { + Some(output_extension.as_str()) + }, + verbose, + qt: build_qt, + }; + let step_flags = build_step_flags(&resolved_cflags, &resolved.include_dirs, &resolved_compiler); + let step_vars = StepVars { + profile, + version: &version_info.full, + version_major: &version_info.major, + version_minor: &version_info.minor, + version_patch: &version_info.patch, + version_suffix: &version_info.suffix, + version_suffix_dash: &version_info.suffix_dash, + }; + let steps_dirty = build_steps_need_run(&build_steps, &step_vars)?; + if steps_dirty { + clean_generated_files(&build_generated)?; + run_build_steps( + &build_steps, + &tool_execs, + &step_flags, + &step_vars, + cancel, + rep, + )?; + } + let sources = collect_sources(&ctx)?; + let headers = collect_header_files(&ctx, project_root)?; + let lib_files = collect_lib_files(&ctx); + let fingerprint = compute_build_fingerprint(&ctx, &sources, &headers, &lib_files)?; + let mut skip = should_skip_build(&ctx, &fingerprint); + let debug_enabled = std::env::var("DCR_DEBUG").is_ok(); + if steps_dirty { + skip = false; + } + if force { + skip = false; + } + if skip && !debug_enabled { + return Ok(()); + } + rep.on_event(BuildEvent::Compiling { + name: &project_name, + version: &project_version, + }); + if !skip { + run_compile(&ctx, cancel, rep)?; + if cancel.load(Ordering::SeqCst) { + return Err("Build interrupted".to_string()); + } + write_build_fingerprint(&ctx, &fingerprint)?; + } + if ctx.package_type == Some("lib") || ctx.kind == "staticlib" || ctx.kind == "sharedlib" { + package_library(&ctx, &headers, project_root)?; + } + let post_steps_dirty = build_steps_need_run(&build_post_steps, &step_vars)?; + if post_steps_dirty { + run_build_steps( + &build_post_steps, + &tool_execs, + &step_flags, + &step_vars, + cancel, + rep, + )?; + } + verify_expectations(&build_expects, &step_vars)?; + Ok(()) +} +fn find_target_root(dir: &str, fallback: &Path) -> PathBuf { + let p = Path::new(dir); + for ancestor in p.ancestors() { + if ancestor.file_name().and_then(|n| n.to_str()) == Some("target") { + return ancestor.to_path_buf(); + } + } + fallback.to_path_buf() +} + +fn package_library( + ctx: &BuildContext, + headers: &[PathBuf], + project_root: &Path, +) -> Result<(), String> { + let target_root = if let Some(dir) = ctx.target_dir { + find_target_root(dir, &project_root.join("target")) + } else { + project_root.join("target") + }; + + let include_dir = target_root.join("include"); + let lib_dir = target_root.join("lib"); + + fs::create_dir_all(&include_dir).map_err(|e| e.to_string())?; + fs::create_dir_all(&lib_dir).map_err(|e| e.to_string())?; + + for header in headers { + if let Some(name) = header.file_name() { + let dest = include_dir.join(name); + fs::copy(header, dest) + .map_err(|e| format!("Failed to copy header {:?}: {}", header, e))?; + } + } + + let mut outputs = Vec::new(); + if ctx.kind == "staticlib" || ctx.kind == "any" { + outputs.push(crate::platform::lib_path( + ctx.profile, + ctx.project_name, + ctx.target_dir, + )); + } + if ctx.kind == "sharedlib" || ctx.kind == "any" { + outputs.push(crate::platform::shared_lib_path( + ctx.profile, + ctx.project_name, + ctx.target_dir, + )); + } + + for output in outputs { + let path = Path::new(&output); + if path.exists() + && let Some(name) = path.file_name() + { + let dest = lib_dir.join(name); + fs::copy(path, dest).map_err(|e| format!("Failed to copy lib {:?}: {}", path, e))?; + } + } + + Ok(()) +} + +pub(crate) struct ToolchainExecs { + pub(crate) uic: String, + pub(crate) moc: String, + pub(crate) rcc: String, +} + +fn resolve_toolchain_execs( + uic: &Option, + moc: &Option, + rcc: &Option, + pkg_configs: &[String], +) -> ToolchainExecs { + let qt_bins = resolve_qt_host_bins(pkg_configs); + ToolchainExecs { + uic: resolve_qt_tool(uic, qt_bins.as_deref(), "uic"), + moc: resolve_qt_tool(moc, qt_bins.as_deref(), "moc"), + rcc: resolve_qt_tool(rcc, qt_bins.as_deref(), "rcc"), + } +} + +fn resolve_qt_tool(configured: &Option, qt_bins: Option<&Path>, tool: &str) -> String { + if let Some(value) = configured { + return value.clone(); + } + if let Some(dir) = qt_bins { + let candidate = dir.join(tool); + if candidate.is_file() { + return candidate.to_string_lossy().to_string(); + } + if cfg!(target_os = "windows") { + let candidate = dir.join(format!("{tool}.exe")); + if candidate.is_file() { + return candidate.to_string_lossy().to_string(); + } + } + } + if let Some(candidate) = detect_qt6_tool_variant(tool) { + return candidate; + } + tool.to_string() +} + +fn resolve_qt_host_bins(pkgs: &[String]) -> Option { + let qt_pkgs: Vec<&String> = pkgs.iter().filter(|p| p.starts_with("Qt6")).collect(); + if qt_pkgs.is_empty() { + return None; + } + let preferred = ["Qt6Core", "Qt6Widgets", "Qt6Gui"]; + for name in preferred { + if let Some(dir) = query_pkg_config_var(name, "host_bins") { + return Some(dir); + } + if let Some(dir) = query_pkg_config_var(name, "libexecdir") + && let Some(bin) = qt_bins_from_libexec(&dir) + { + return Some(bin); + } + if let Some(dir) = query_pkg_config_var(name, "bindir") { + return Some(dir); + } + } + for pkg in qt_pkgs { + if let Some(dir) = query_pkg_config_var(pkg, "host_bins") { + return Some(dir); + } + if let Some(dir) = query_pkg_config_var(pkg, "libexecdir") + && let Some(bin) = qt_bins_from_libexec(&dir) + { + return Some(bin); + } + if let Some(dir) = query_pkg_config_var(pkg, "bindir") { + return Some(dir); + } + } + None +} + +fn query_pkg_config_var(pkg: &str, var: &str) -> Option { + let output = std::process::Command::new("pkg-config") + .arg(format!("--variable={var}")) + .arg(pkg) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if value.is_empty() { + return None; + } + let path = PathBuf::from(value); + if path.is_dir() { Some(path) } else { None } +} + +fn qt_bins_from_libexec(libexec: &Path) -> Option { + for tool in ["moc", "uic", "rcc"] { + if libexec.join(tool).is_file() { + return Some(libexec.to_path_buf()); + } + if cfg!(target_os = "windows") && libexec.join(format!("{tool}.exe")).is_file() { + return Some(libexec.to_path_buf()); + } + } + let bin = libexec.join("bin"); + if bin.is_dir() { Some(bin) } else { None } +} + +fn detect_qt6_tool_variant(tool: &str) -> Option { + [format!("{tool}6"), format!("{tool}-qt6")] + .into_iter() + .find(|candidate| is_on_path(candidate)) +} + +fn is_on_path(cmd: &str) -> bool { + if Path::new(cmd).is_file() { + return true; + } + std::env::var_os("PATH") + .is_some_and(|paths| std::env::split_paths(&paths).any(|dir| dir.join(cmd).is_file())) +} diff --git a/src/core/build/language/asm/common.rs b/src/core/build/language/asm/common.rs new file mode 100644 index 0000000..771fada --- /dev/null +++ b/src/core/build/language/asm/common.rs @@ -0,0 +1,83 @@ +use crate::core::build::builder::BuildContext; +use crate::core::build::builder::artifact; +use crate::core::build::common; +use std::path::Path; +use std::time::Instant; + +pub fn filter_asm_flags(flags: &[String]) -> Vec { + 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( + ctx: &BuildContext, + backend_name: &str, + default_assembler: &str, + extensions: &[&str], + build_object: F, +) -> Result +where + F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync, +{ + let lang = ctx.language.to_lowercase(); + if lang.split(',').any(|p| p.trim() != "asm") { + return Err(format!( + "{backend_name} backend requires build.language = \"asm\"" + )); + } + + let assembler = if ctx.compiler.is_empty() { + default_assembler + } else { + ctx.compiler + }; + + let start_time = Instant::now(); + let sources = common::collect_sources( + ctx.source_roots, + extensions, + ctx.exclude_dirs, + ctx.include_paths, + )?; + let obj_dir = match ctx.target_dir { + Some(dir) => Path::new(dir).join("obj"), + None => Path::new("./target").join(ctx.profile).join("obj"), + }; + let objects = build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?; + + if ctx.kind == "staticlib" { + return artifact::archive_static(ctx, &objects, start_time); + } + + artifact::link_binary(ctx, &objects, "cc", start_time) +} + +fn build_objects_common( + assembler: &str, + sources: &[String], + obj_dir: &Path, + ctx: &BuildContext, + build_object: &F, +) -> Result, String> +where + F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync, +{ + let objects: Vec = 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) +} diff --git a/src/core/builder/fasm.rs b/src/core/build/language/asm/fasm/mod.rs similarity index 82% rename from src/core/builder/fasm.rs rename to src/core/build/language/asm/fasm/mod.rs index 51daee3..3d05d48 100644 --- a/src/core/builder/fasm.rs +++ b/src/core/build/language/asm/fasm/mod.rs @@ -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); } diff --git a/src/core/builder/gas.rs b/src/core/build/language/asm/gas/mod.rs similarity index 69% rename from src/core/builder/gas.rs rename to src/core/build/language/asm/gas/mod.rs index 51522d1..85865e2 100644 --- a/src/core/builder/gas.rs +++ b/src/core/build/language/asm/gas/mod.rs @@ -15,15 +15,24 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 { - 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(ctx, "GAS", &as_path, &["s"], build_object) } pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, 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) } diff --git a/src/core/builder/masm.rs b/src/core/build/language/asm/masm/mod.rs similarity index 83% rename from src/core/builder/masm.rs rename to src/core/build/language/asm/masm/mod.rs index 63b8d18..14e7a42 100644 --- a/src/core/builder/masm.rs +++ b/src/core/build/language/asm/masm/mod.rs @@ -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; @@ -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); } diff --git a/src/core/build/language/asm/mod.rs b/src/core/build/language/asm/mod.rs new file mode 100644 index 0000000..04888fb --- /dev/null +++ b/src/core/build/language/asm/mod.rs @@ -0,0 +1,38 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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") + } +} diff --git a/src/core/builder/nasm.rs b/src/core/build/language/asm/nasm/mod.rs similarity index 87% rename from src/core/builder/nasm.rs rename to src/core/build/language/asm/nasm/mod.rs index 53ea53c..24fa3f9 100644 --- a/src/core/builder/nasm.rs +++ b/src/core/build/language/asm/nasm/mod.rs @@ -15,22 +15,22 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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 { - 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, 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, ) @@ -59,7 +59,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); } diff --git a/src/core/build/language/c/mod.rs b/src/core/build/language/c/mod.rs new file mode 100644 index 0000000..c3a3da0 --- /dev/null +++ b/src/core/build/language/c/mod.rs @@ -0,0 +1,32 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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") + } +} diff --git a/src/core/build/language/cxx/mod.rs b/src/core/build/language/cxx/mod.rs new file mode 100644 index 0000000..8e8220b --- /dev/null +++ b/src/core/build/language/cxx/mod.rs @@ -0,0 +1,34 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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") + } +} diff --git a/src/core/builder/qt.rs b/src/core/build/language/cxx/qt.rs similarity index 94% rename from src/core/builder/qt.rs rename to src/core/build/language/cxx/qt.rs index d583320..e932461 100644 --- a/src/core/builder/qt.rs +++ b/src/core/build/language/cxx/qt.rs @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::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 Result &'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 { asm::build_assembly(ctx, "LLVM-IR", "llc", &["ll"], build_object) } diff --git a/src/core/build/language/mod.rs b/src/core/build/language/mod.rs new file mode 100644 index 0000000..d1966b7 --- /dev/null +++ b/src/core/build/language/mod.rs @@ -0,0 +1,57 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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"]); + } +} diff --git a/src/core/build/mod.rs b/src/core/build/mod.rs new file mode 100644 index 0000000..8928a31 --- /dev/null +++ b/src/core/build/mod.rs @@ -0,0 +1,29 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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; diff --git a/src/core/build/report.rs b/src/core/build/report.rs new file mode 100644 index 0000000..d439a21 --- /dev/null +++ b/src/core/build/report.rs @@ -0,0 +1,83 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +// 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, + pub force: bool, + pub verbose: bool, + pub workspace: Option, + pub cancel: Arc, +} + +#[allow(dead_code)] +pub struct BuildOutcome { + pub secs: f64, +} + +pub struct BuildError { + pub message: String, +} + +impl From 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<'_>); +} diff --git a/src/core/build/resolve.rs b/src/core/build/resolve.rs new file mode 100644 index 0000000..3d68f2a --- /dev/null +++ b/src/core/build/resolve.rs @@ -0,0 +1,232 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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 { + // 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 { + 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 { + 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, 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, 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, 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, String> { + get_list_with_profile_and_target(config, field, profile, None) +} + +pub(crate) fn get_targets(config: &Config, profile: &str) -> Result, String> { + get_list_with_profile(config, "targets", profile) +} + +pub(crate) fn get_config_list(config: &Config, key: &str) -> Result, 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"); + } +} diff --git a/src/core/build/steps.rs b/src/core/build/steps.rs new file mode 100644 index 0000000..a3c0048 --- /dev/null +++ b/src/core/build/steps.rs @@ -0,0 +1,398 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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, 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, 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, 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, + 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, + 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, 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::>() + .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, + rep: &mut dyn BuildReporter, +) -> Result { + 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 { + 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) +} diff --git a/src/core/build_config.rs b/src/core/build_config.rs index 3f229ca..9fb9c81 100644 --- a/src/core/build_config.rs +++ b/src/core/build_config.rs @@ -21,6 +21,7 @@ use std::fs; use std::path::{Path, PathBuf}; use toml::Value; use toml::map::Map; +use toml_edit::{DocumentMut, Item, Table}; const DEFAULT_VERSION: &str = "0.1.0"; const DEFAULT_LANGUAGE: &str = "c"; @@ -42,6 +43,7 @@ pub enum ConfigError { Io(std::io::Error), TomlDe(toml::de::Error), TomlSer(toml::ser::Error), + TomlEdit(toml_edit::TomlError), Invalid(String), } @@ -51,6 +53,7 @@ impl std::fmt::Display for ConfigError { ConfigError::Io(err) => write!(f, "I/O error: {err}"), ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"), ConfigError::TomlSer(err) => write!(f, "TOML serialize error: {err}"), + ConfigError::TomlEdit(err) => write!(f, "TOML parse error: {err}"), ConfigError::Invalid(msg) => write!(f, "Invalid config: {msg}"), } } @@ -80,6 +83,10 @@ pub struct Config { path: PathBuf, data: Value, typed: DcrConfig, + /// Editable document mirror of `data`, used for serialization. It preserves + /// comments, formatting, and any keys not modeled by the typed/raw views, so + /// `save()` never drops unknown sections (e.g. `[run]`, `[[build.post_steps]]`). + doc: DocumentMut, } #[allow(dead_code)] @@ -105,14 +112,24 @@ pub struct PackageConfig { pub pkg_type: Option, } +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, #[serde(default)] pub cxx_standard: Option, + #[serde(default = "default_compiler")] pub compiler: String, #[serde(default)] pub kind: Option, @@ -182,20 +199,29 @@ pub struct WorkspaceMemberConfig { pub path: String, #[serde(default)] pub deps: Vec, + #[serde(default)] + pub main: Option, } impl Config { pub fn new(path: &str) -> Result { 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 +231,14 @@ impl Config { if !path.exists() { return Err(ConfigError::Invalid("dcr.toml not found".into())); } - let data = read_toml(&path)?; + let (data, doc) = load_parts(&path)?; let typed = parse_typed_config(&data)?; - let cfg = Self { path, data, typed }; + let cfg = Self { + path, + data, + typed, + doc, + }; cfg.validate()?; Ok(cfg) } @@ -347,6 +378,13 @@ impl Config { .as_ref() .ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?; + // For workspace_only projects, [build] section is optional + // and language/compiler are not required + if build.workspace_only { + self.validate_workspace()?; + return Ok(()); + } + if pkg.name.trim().is_empty() { return Err(ConfigError::Invalid("package.name is empty".into())); } @@ -421,22 +459,35 @@ impl Config { } pub fn save(&self) -> Result<(), ConfigError> { - write_toml(&self.path, &self.data) + fs::write(&self.path, self.doc.to_string())?; + Ok(()) } fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> { let parts: Vec<&str> = key.split('.').collect(); - let previous = self.data.clone(); - set_path(&mut self.data, &parts, value)?; + let previous_data = self.data.clone(); + let previous_doc = self.doc.clone(); + + if let Err(err) = set_doc_path(&mut self.doc, &parts, &value) { + self.doc = previous_doc; + return Err(err); + } + if let Err(err) = set_path(&mut self.data, &parts, value) { + self.data = previous_data; + self.doc = previous_doc; + return Err(err); + } self.typed = match parse_typed_config(&self.data) { Ok(typed) => typed, Err(err) => { - self.data = previous; + self.data = previous_data; + self.doc = previous_doc; return Err(err); } }; if let Err(err) = self.validate() { - self.data = previous; + self.data = previous_data; + self.doc = previous_doc; self.typed = parse_typed_config(&self.data)?; return Err(err); } @@ -658,235 +709,119 @@ impl Config { } } -fn read_toml(path: &Path) -> Result { +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 { +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 { - 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::() + .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 = 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) -> Vec { + let mut keys: Vec = DEP_KEY_ORDER + .iter() + .filter(|k| tbl.contains_key(**k)) + .map(|k| (*k).to_string()) + .collect(); + let mut rest: Vec = 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 +1070,84 @@ mod tests { ); } + #[test] + fn save_preserves_untyped_sections() { + let dir = temp_dir("preserve_untyped"); + let path = write_toml_file( + &dir, + "# top comment\n\ + [package]\n\ + name = \"test\"\n\ + version = \"0.1.0\"\n\n\ + [build]\n\ + language = \"c\"\n\ + standard = \"c11\"\n\ + compiler = \"clang\"\n\ + kind = \"elf\"\n\ + filename = \"KERNEL\"\n\ + extension = \"ELF\"\n\ + inherit = false\n\ + include = [\"src/include\"]\n\n\ + [[build.post_steps]]\n\ + name = \"iso\"\n\ + cmd = \"grub-mkrescue\"\n\n\ + [run]\n\ + cmd = \"qemu-system-aarch64 -kernel KERNEL\"\n\n\ + [dependencies]\n", + ); + + let mut config = Config::open(&path.to_string_lossy()).unwrap(); + config + .set("dependencies.zlib", { + let mut tbl = Map::new(); + tbl.insert("path".to_string(), Value::String("../zlib".to_string())); + Value::Table(tbl) + }) + .unwrap(); + + // Reload from disk and confirm nothing modeled-but-not-whitelisted was dropped. + let reloaded = Config::open(&path.to_string_lossy()).unwrap(); + assert_eq!( + reloaded.get("build.filename").and_then(|v| v.as_str()), + Some("KERNEL"), + "build.filename must survive save" + ); + assert_eq!( + reloaded.get("build.extension").and_then(|v| v.as_str()), + Some("ELF") + ); + assert_eq!( + reloaded.get("build.inherit").and_then(|v| v.as_bool()), + Some(false) + ); + assert!( + reloaded + .get("build.include") + .and_then(|v| v.as_array()) + .is_some(), + "build.include array must survive save" + ); + assert_eq!( + reloaded.get("run.cmd").and_then(|v| v.as_str()), + Some("qemu-system-aarch64 -kernel KERNEL"), + "[run] section must survive save" + ); + assert!( + reloaded.get("build.post_steps").is_some(), + "[[build.post_steps]] must survive save" + ); + + let saved = fs::read_to_string(&path).unwrap(); + assert!( + saved.contains("zlib = { path = \"../zlib\" }"), + "dependency must be an inline table, got:\n{saved}" + ); + assert!( + saved.contains("# top comment"), + "leading comment must be preserved, got:\n{saved}" + ); + } + #[test] fn new_creates_default_config() { let dir = temp_dir("new_default"); diff --git a/src/core/builder/asm.rs b/src/core/builder/asm.rs deleted file mode 100644 index 9e04c77..0000000 --- a/src/core/builder/asm.rs +++ /dev/null @@ -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( - ctx: &BuildContext, - backend_name: &str, - default_assembler: &str, - extensions: &[&str], - build_object: F, -) -> Result -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( - assembler: &str, - sources: &[String], - obj_dir: &Path, - ctx: &BuildContext, - build_object: &F, -) -> Result, String> -where - F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync, -{ - let objects: Vec = 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 { - 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 { - 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}")), - } -} diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs deleted file mode 100644 index 257498b..0000000 --- a/src/core/builder/mod.rs +++ /dev/null @@ -1,124 +0,0 @@ -// DCR — Cargo-like C/C++ project manager. -// -// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) -// -// 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 . - -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 { - 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, 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) -} diff --git a/src/core/deps/mod.rs b/src/core/deps/mod.rs index 12aa565..d1d6675 100644 --- a/src/core/deps/mod.rs +++ b/src/core/deps/mod.rs @@ -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()); } diff --git a/src/core/mod.rs b/src/core/mod.rs index b99bcd9..638cdb9 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +pub mod build; pub mod build_config; -pub mod builder; pub mod deps; pub mod registry; pub mod runner; diff --git a/src/core/workspace.rs b/src/core/workspace.rs index fdf1d60..5ba1c64 100644 --- a/src/core/workspace.rs +++ b/src/core/workspace.rs @@ -50,7 +50,6 @@ pub fn parse_workspace( root: &Path, ) -> Result, 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![ diff --git a/src/utils/build.rs b/src/utils/build.rs index a3f7b71..9d82c69 100644 --- a/src/utils/build.rs +++ b/src/utils/build.rs @@ -280,7 +280,7 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result parse_language_value(v, "build.language"), - None => Err("build.language is missing".to_string()), + None => Ok("c".to_string()), } } @@ -324,9 +324,14 @@ pub fn resolve_compiler( tc_as: Option<&str>, ) -> String { let lang = primary_language(language); - env_override_compiler(&lang) + let result = env_override_compiler(&lang) .or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as)) - .unwrap_or_else(|| compiler.to_string()) + .unwrap_or_else(|| compiler.to_string()); + if lang == "asm" { + map_asm_compiler(&result) + } else { + result + } } fn env_override_compiler(lang: &str) -> Option { @@ -369,7 +374,8 @@ fn toolchain_override_compiler( tc_as: Option<&str>, ) -> Option { 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 +385,31 @@ fn toolchain_override_compiler( tc_cc.map(|v| v.to_string()) } +fn map_asm_compiler(compiler: &str) -> String { + match compiler.to_lowercase().as_str() { + "gas" | "gnu-as" => "as".to_string(), + "nasm" => "nasm".to_string(), + "fasm" | "fasm64" => "fasm".to_string(), + "masm" | "ml" | "ml64" => "ml".to_string(), + _ => compiler.to_string(), + } +} + pub fn primary_language(language: &str) -> String { - let parts: Vec = 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() } diff --git a/tests/cli_build.rs b/tests/cli_build.rs index 530c66b..3b02f26 100644 --- a/tests/cli_build.rs +++ b/tests/cli_build.rs @@ -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 {