5 Commits

Author SHA1 Message Date
d08ceaf31d Added:
- `dcr gen` command for IDE integration and build tool support
    - `dcr gen project-info`: Output project metadata as JSON
    - `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
    - `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
    - `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
2026-03-19 00:03:39 +08:00
5f0dca4bb3 Added:
- `dcr gen` command for IDE integration and build tool support
    - `dcr gen project-info`: Output project metadata as JSON
    - `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
    - `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
    - `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
2026-03-19 00:02:04 +08:00
0e93a1aa0f Added:
- `build.steps` for pre-build generators (e.g., `moc`/`uic`/`rcc`)
- `build.pkg_config` for `pkg-config`-driven cflags/ldflags resolution
- `[toolchain]` support for `uic`, `moc`, `rcc`
- Step command variables (`{uic}`, `{moc}`, `{rcc}`, `{cflags}`, `{stem}`, `{in}`, `{out}`)
- `{profile}` placeholder support for dependency paths
2026-03-16 20:50:32 +08:00
dd996b92d1 fix code format 2026-03-14 17:56:12 +08:00
a0e4fdbacf feat: Refactor builder modules by consolidating Unix C/C++ compilers, extracting common logic, enhancing MSVC with dependency tracking, and adding new CLI error tests. 2026-03-14 16:43:00 +08:00
39 changed files with 4742 additions and 793 deletions

126
AGENT.md Normal file
View File

@@ -0,0 +1,126 @@
# AGENT.md
## Project Overview
**DCR (Dexoron Cargo Realization)** — CLI-утилита для управления C/C++/ASM проектами в стиле Cargo.
- **Язык реализации**: Rust (edition 2024)
- **Лицензия**: MIT
- **Репозиторий**: [github.com/dexoron/dcr](https://github.com/dexoron/dcr)
## Architecture
### Source Tree (`src/`)
```
src/
├── main.rs # Точка входа, маршрутизация CLI-команд
├── config.rs # Глобальные константы: профили сборки, шаблон main.c
├── cli/ # Реализация CLI-команд
│ ├── new.rs # dcr new <name>
│ ├── init.rs # dcr init
│ ├── build.rs # dcr build [--debug|--release]
│ ├── run.rs # dcr run [--debug|--release]
│ ├── clean.rs # dcr clean [--all]
│ ├── help.rs # dcr --help
│ └── flag_update.rs # dcr --update (self-update через GitHub Releases)
├── core/ # Бизнес-логика
│ ├── config.rs # Парсинг/валидация/форматирование dcr.toml (struct Config)
│ ├── deps.rs # Разрешение path-зависимостей, генерация dcr.lock
│ ├── runner.rs # Запуск скомпилированного бинарника
│ ├── workspace.rs # Поддержка [workspace] и упорядочивание сборки
│ └── builder/ # Бэкенды компиляторов/ассемблеров
│ ├── mod.rs # Общий интерфейс сборки
│ ├── gcc.rs # GCC backend
│ ├── clang.rs # Clang backend
│ ├── msvc.rs # MSVC backend
│ ├── nasm.rs # NASM backend (ASM)
│ └── gas.rs # GAS backend (ASM)
├── platform/ # Платформенно-зависимая логика
│ ├── mod.rs # Общий интерфейс платформ
│ ├── linux.rs
│ ├── macos.rs
│ └── windows.rs
└── utils/ # Вспомогательные утилиты
├── fs.rs # Работа с файловой системой
├── log.rs # Логирование (error, warn)
└── text.rs # Форматирование текста / цвета
```
### Key Concepts
- **`dcr.toml`** — конфигурационный файл проекта (аналог `Cargo.toml`). Секции: `[package]`, `[build]`, `[toolchain]`, `[dependencies]`, `[workspace]`.
- **Профили сборки**: `debug` (по умолчанию) и `release`. Встроенные флаги зависят от компилятора. Пользовательские флаги через `build.cflags` / `build.ldflags`.
- **Инкрементальная сборка**: объектные файлы кешируются по mtime. Отслеживание заголовков пока не реализовано (см. `TODO.md`).
- **Workspace**: корневой `dcr.toml` с секцией `[workspace]` описывает member-проекты и их зависимости; сборка происходит в топологическом порядке.
- **Path dependencies**: зависимости по путям автоматически добавляют include/lib-пути; генерируется `dcr.lock`.
- **Build kinds**: `bin` (бинарник), `staticlib` (`.a`), `sharedlib` (`.so`/`.dylib`/`.dll`).
## Supported Platforms
| Platform | Target triple |
|---|---|
| Linux | `x86_64-unknown-linux-gnu` |
| macOS Intel | `x86_64-apple-darwin` |
| macOS ARM | `aarch64-apple-darwin` |
| Windows | `x86_64-pc-windows-msvc` |
## Dependencies (Cargo)
| Crate | Purpose |
|---|---|
| `reqwest` | HTTP-запросы для `--update` (blocking, rustls-tls) |
| `serde` | Сериализация/десериализация |
| `toml` | Парсинг `dcr.toml` |
| `sha2` | Хеширование при обновлении |
| `self-replace` | Замена бинарника при `--update` |
## Development
### Build & Run
```bash
cargo build # debug build
cargo build --release # release build
cargo run -- new hello # создать проект "hello"
```
### Code Quality
```bash
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check
cargo test
```
### CI/CD
- **CI**: `.github/workflows/ci.yml` — проверки при PR/push
- **Release**: `.github/workflows/release.yml` — сборка бинарников при пуше тега `v*`
### Tests
- Интеграционные CLI-тесты: `tests/cli_basic.rs`
## Code Conventions
- Rust stable edition 2024
- Форматирование: `cargo fmt`
- Линтинг: `cargo clippy` без warnings
- Функции — маленькие и сфокусированные
- Имена — понятные и описательные
- Пользовательские сообщения CLI — на английском языке
- Вывод ошибок/предупреждений через `utils::log::{error, warn}`
## TODO
- В файле `TODO.md`
## Useful Links
- Документация проекта: `docs/`
- Changelog: `CHANGELOG.md`
- Contributing: `CONTRIBUTING.md`
- FAQ: `FAQ.md`
- Установка: `install.sh` (Linux/macOS), `install.ps1` (Windows)

View File

@@ -1,10 +1,46 @@
# Changelog # Changelog
## [0.2.8] - 2026-03-06 ## [0.3.0] - 2026-03-18
Added: Added:
- `dcr gen` command for IDE integration and build tool support
- `dcr gen project-info`: Output project metadata as JSON
- `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
- `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
- `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
## [0.2.10] - 2026-03-16
Added:
- `build.steps` for pre-build generators (e.g., `moc`/`uic`/`rcc`)
- `build.pkg_config` for `pkg-config`-driven cflags/ldflags resolution
- `[toolchain]` support for `uic`, `moc`, `rcc`
- Step command variables (`{uic}`, `{moc}`, `{rcc}`, `{cflags}`, `{stem}`, `{in}`, `{out}`)
- `{profile}` placeholder support for dependency paths
## [0.2.9] - 2026-03-14
Added:
- Header dependency tracking for fine-grained incremental builds (rebuilds when `#include` files change)
- Complete internal rewrite of the builder module: merged duplicate compilation backends
Changed:
- `cflags` and `default_flags` no longer incorrectly leak to the linker stage in GCC/Clang
- Reduced builder codebase size by over 20% while increasing reliability
- Greatly expanded test suite with 34 new tests covering configuration, CLI errors, and path operations
## [0.2.8] - 2026-03-06
Added:
- Workspace support (`[workspace]` with deps ordering) - Workspace support (`[workspace]` with deps ordering)
- `clean --all` for workspace members - `clean --all` for workspace members
Changed: Changed:
- Project root discovery for `build/run/clean` (searches parent dirs) - Project root discovery for `build/run/clean` (searches parent dirs)
- Workspace paths are excluded from root source scan - Workspace paths are excluded from root source scan
- Build output now uses a Cargo-style `Compiling <name> v<version>` line - Build output now uses a Cargo-style `Compiling <name> v<version>` line
@@ -13,12 +49,16 @@ Changed:
- Safer build cache: fingerprints include headers and resolved library files - Safer build cache: fingerprints include headers and resolved library files
## [0.2.7] - 2026-03-03 ## [0.2.7] - 2026-03-03
Added: Added:
- Toolchain overrides via `[toolchain]` and env (`DCR_*`) - Toolchain overrides via `[toolchain]` and env (`DCR_*`)
- ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`) - ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`)
## [0.2.6] - 2026-03-01 ## [0.2.6] - 2026-03-01
Added: Added:
- `sharedlib` build kind (shared library output) - `sharedlib` build kind (shared library output)
- ASM projects (`build.language = "asm"`) - ASM projects (`build.language = "asm"`)
- NASM and GAS backends - NASM and GAS backends
@@ -26,24 +66,31 @@ Added:
- Basic CLI integration tests - Basic CLI integration tests
## [0.2.5] - 2026-02-26 ## [0.2.5] - 2026-02-26
Added: Added:
- Incremental builds (object caching by mtime) - Incremental builds (object caching by mtime)
- `build.kind` with `staticlib` support - `build.kind` with `staticlib` support
- Custom output directory via `build.target` - Custom output directory via `build.target`
## [0.2.4] - 2026-02-22 ## [0.2.4] - 2026-02-22
Added: Added:
- Custom build flags: `build.cflags` and `build.ldflags` - Custom build flags: `build.cflags` and `build.ldflags`
- Recursive source discovery inside `src/` - Recursive source discovery inside `src/`
- Path dependencies with auto include/lib resolution and `dcr.lock` generation (experimental) - Path dependencies with auto include/lib resolution and `dcr.lock` generation (experimental)
- Add docs/index.md and docs/dcr-toml.md - Add docs/index.md and docs/dcr-toml.md
## [0.2.3] - 2026-02-21 ## [0.2.3] - 2026-02-21
Added: Added:
- Modular builders for `gcc`, `clang`, `msvc` - Modular builders for `gcc`, `clang`, `msvc`
- Platform-specific binary path generation (Linux/macOS/Windows) - Platform-specific binary path generation (Linux/macOS/Windows)
Changed: Changed:
- Build configuration moved to `[build]` (`language`, `standard`, `compiler`) - Build configuration moved to `[build]` (`language`, `standard`, `compiler`)
- `dcr.toml` formatting now includes the `[build]` section - `dcr.toml` formatting now includes the `[build]` section
- Build uses built-in `debug/release` flags per compiler - Build uses built-in `debug/release` flags per compiler
@@ -51,20 +98,26 @@ Changed:
- Updated `dcr.toml` examples in documentation - Updated `dcr.toml` examples in documentation
## [0.2.2] - 2026-02-20 ## [0.2.2] - 2026-02-20
Changed: Changed:
- Reworked `dcr.toml` handling: added read, validation, and edit through `core::config` - Reworked `dcr.toml` handling: added read, validation, and edit through `core::config`
- `new` and `init` use the new config creation logic - `new` and `init` use the new config creation logic
- `build` and `run` now require `dcr.toml` and read `compiler` and `name` from it - `build` and `run` now require `dcr.toml` and read `compiler` and `name` from it
## [0.2.1] - 2026-02-18 ## [0.2.1] - 2026-02-18
Changed: Changed:
- Translated user-facing CLI messages to English - Translated user-facing CLI messages to English
- Unified error and warning output via `utils::log::{error, warn}` - Unified error and warning output via `utils::log::{error, warn}`
- Updated `--help` output: translated headers and examples to English, used `printc`, and applied `BOLD_*` styles - Updated `--help` output: translated headers and examples to English, used `printc`, and applied `BOLD_*` styles
- Translated installer script messages in `install.sh` and `install.ps1` to English - Translated installer script messages in `install.sh` and `install.ps1` to English
## [0.2.0] - 2026-02-17 ## [0.2.0] - 2026-02-17
Changed: Changed:
- Project migrated from Python to Rust - Project migrated from Python to Rust
- CLI and commands (`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`) ported to Rust - CLI and commands (`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`) ported to Rust
- Updated `--update` flag: added support for GNU/Linux, Windows, macOS - Updated `--update` flag: added support for GNU/Linux, Windows, macOS
@@ -73,31 +126,40 @@ Changed:
- Updated `install.sh` for GNU/Linux and macOS - Updated `install.sh` for GNU/Linux and macOS
Added: Added:
- Added `install.ps1` for Windows - Added `install.ps1` for Windows
- Support for GNU/Linux, Windows, macOS (x86_64/arm) - Support for GNU/Linux, Windows, macOS (x86_64/arm)
Important: Important:
- Code was ported with neural networks; future versions will include bug fixes and logic changes - Code was ported with neural networks; future versions will include bug fixes and logic changes
## [0.1.2] - 2026-02-12 ## [0.1.2] - 2026-02-12
Added: Added:
- Update command - Update command
- `install.sh` install script and README instructions - `install.sh` install script and README instructions
- `--version` flag - `--version` flag
Changed: Changed:
- Improved CLI output, added colors, and updated `--help` - Improved CLI output, added colors, and updated `--help`
- Updated project run/build handling in `run.py`/`build.py` - Updated project run/build handling in `run.py`/`build.py`
## [0.1.1] - 2026-02-11 ## [0.1.1] - 2026-02-11
Changed: Changed:
- Updated `--help` - Updated `--help`
- `main.py` now runs correctly when executed directly - `main.py` now runs correctly when executed directly
## [0.1.0] - 2026-02-11 ## [0.1.0] - 2026-02-11
First public release. First public release.
Added: Added:
- Base commands `new`, `init`, `build`, `run`, `clean` - Base commands `new`, `init`, `build`, `run`, `clean`
- Build profiles `debug` and `release` - Build profiles `debug` and `release`
- `dcr.toml` and `src/main.c` templates - `dcr.toml` and `src/main.c` templates

9
Cargo.lock generated
View File

@@ -90,8 +90,9 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.2.8" version = "0.3.0"
dependencies = [ dependencies = [
"glob",
"reqwest", "reqwest",
"self-replace", "self-replace",
"serde", "serde",
@@ -262,6 +263,12 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.5" version = "0.15.5"

View File

@@ -1,10 +1,11 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.2.8" version = "0.3.0"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
[dependencies] [dependencies]
glob = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
self-replace = "1" self-replace = "1"

View File

@@ -14,9 +14,11 @@ The current implementation is written in Rust.
- Build a project with `debug` and `release` profiles - Build a project with `debug` and `release` profiles
- Run the compiled binary - Run the compiled binary
- Clean build artifacts - Clean build artifacts
- Generate IDE integration files (VS Code, CLion) and compilation databases
- Generate a minimal C project template - Generate a minimal C project template
- Build static and shared libraries - Build static and shared libraries
- ASM projects with NASM/GAS or via GCC/Clang - ASM projects with NASM/GAS or via GCC/Clang
- Mixed language projects (`language = ["c", "asm"]` and similar)
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs) - Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
## Supported Platforms ## Supported Platforms
@@ -104,9 +106,11 @@ Creates a project in the current directory. The project name is taken from the d
### `dcr build [profile]` ### `dcr build [profile]`
Builds the project. If no profile is specified, `--debug` is used. Builds the project. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
### `dcr run [profile]` ### `dcr run [profile]`
Builds the project and runs the binary. If no profile is specified, `--debug` is used. Builds the project and runs the binary. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
Run manually: Run manually:
`./target/<profile>/<name>` `./target/<profile>/<name>`
@@ -121,11 +125,16 @@ Two profiles are supported:
- `--release` - built-in flags per compiler - `--release` - built-in flags per compiler
Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`. Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`.
Profile-specific flags can be added via `[build.debug]` and `[build.release]`.
You can also set `build.target` to override the output directory (profile-independent). You can also set `build.target` to override the output directory (profile-independent).
You can set `build.platform` to pass `-march=<platform>` (GCC/Clang) or `/arch:*` (MSVC) where supported. You can set `build.platform` to pass `-march=<platform>` (GCC/Clang) or `/arch:*` (MSVC) where supported.
Use `build.kind = "staticlib"` to build a static library instead of a binary. Use `build.kind = "staticlib"` to build a static library instead of a binary.
Use `build.kind = "sharedlib"` to build a shared library (`.so`/`.dylib`/`.dll`). Use `build.kind = "sharedlib"` to build a shared library (`.so`/`.dylib`/`.dll`).
`dcr run` is only for `build.kind = "bin"` and will fail for libraries. `dcr run` is only for `build.kind = "bin"` and will fail for libraries.
Use `build.exclude`/`build.include` to control source/header collection.
Use `build.roots` and `build.src_disable` to replace the default `src/` root.
Use `build.steps` and `build.post_steps` to run custom commands.
Use `build.clean` for extra cleanup paths and `[run].cmd` to override run command.
## Configuration ## Configuration
The main project file is `dcr.toml`. The main project file is `dcr.toml`.
@@ -146,6 +155,10 @@ compiler = "clang"
# Optional custom flags # Optional custom flags
cflags = ["-Wall", "-Wextra"] cflags = ["-Wall", "-Wextra"]
ldflags = ["-lm"] ldflags = ["-lm"]
#
# Optional include/exclude:
# exclude = ["src/vendor", "src/legacy/**"]
# include = ["src/boot/arch/**"]
[toolchain] [toolchain]
# Optional tool overrides: # Optional tool overrides:
@@ -154,6 +167,9 @@ ldflags = ["-lm"]
# as = "as" # as = "as"
# ar = "ar" # ar = "ar"
# ld = "ld" # ld = "ld"
# uic = "uic"
# moc = "moc"
# rcc = "rcc"
[dependencies] [dependencies]
``` ```

View File

@@ -1,6 +1,9 @@
# TODO # TODO
- Add header dependency tracking for incremental builds (GCC/Clang: -MMD, MSVC: /showIncludes)
- Add external package managers (Conan, vcpkg) - Add external package managers (Conan, vcpkg)
- Linker script + direct linker control - Linker script + direct linker control
- Custom targets/steps - Custom targets/steps
- Cross-compilation and freestanding targets (-nostdinc, -nostdlib, custom target triple like i686-elf)
- Mixed C + ASM in one project (compile .c and .s/.S sources together)
- Per-member include directories in workspace (e.g. include paths for subprojects)
- Build-time code generation (auto-generated headers, pre-build scripts)

View File

@@ -18,12 +18,19 @@ Current DCR focuses on a simple, predictable workflow:
- C++: `*.cpp`, `*.cxx`, `*.cc` - C++: `*.cpp`, `*.cxx`, `*.cc`
- Compiler selection from `dcr.toml` (`clang`, `gcc`, `cl`, `clang-cl`, etc.). - Compiler selection from `dcr.toml` (`clang`, `gcc`, `cl`, `clang-cl`, etc.).
- ASM support via `language = "asm"` (GCC/Clang, NASM, GAS). - ASM support via `language = "asm"` (GCC/Clang, NASM, GAS).
- Mixed language builds via `language = ["c", "asm"]` and similar combinations.
- Three build kinds: - Three build kinds:
- `bin` (default) - `bin` (default)
- `staticlib` - `staticlib`
- `sharedlib` - `sharedlib`
- Optional custom output directory via `build.target`. - Optional custom output directory via `build.target`.
- Optional `build.platform` to pass architecture hints (`-march` / `/arch`). - Optional `build.platform` to pass architecture hints (`-march` / `/arch`).
- Optional `build.exclude`/`build.include` for source/header selection.
- Optional `build.roots`/`build.src_disable` to replace the default `src/` root.
- Optional build steps with `build.steps` and `build.post_steps`.
- Optional `build.pkg_config` integration.
- Optional `build.clean` and `build.expect`.
- Optional `[run].cmd` to override run command.
- Workspace builds via `[workspace]` with per-project `dcr.toml`. - Workspace builds via `[workspace]` with per-project `dcr.toml`.
- Path dependencies with include/lib resolution and `dcr.lock` generation. - Path dependencies with include/lib resolution and `dcr.lock` generation.
@@ -31,7 +38,7 @@ Current DCR focuses on a simple, predictable workflow:
- `dcr.toml` is required for `build`, `run`, and `clean`. - `dcr.toml` is required for `build`, `run`, and `clean`.
- `run` works only with `build.kind = "bin"`. - `run` works only with `build.kind = "bin"`.
- Incremental rebuild currently tracks source file timestamps (not header dependency graphs). - Incremental rebuild tracks source file timestamps and header dependencies (rebuilds when either source or included headers change).
- Object files are always created under `target/<profile>/obj/`. - Object files are always created under `target/<profile>/obj/`.
## Repository ## Repository

View File

@@ -24,6 +24,7 @@
* [dcr build](commands/dcr-build.md) * [dcr build](commands/dcr-build.md)
* [dcr run](commands/dcr-run.md) * [dcr run](commands/dcr-run.md)
* [dcr clean](commands/dcr-clean.md) * [dcr clean](commands/dcr-clean.md)
* [dcr gen](commands/dcr-gen.md)
## Dependencies & Build ## Dependencies & Build

View File

@@ -8,6 +8,8 @@ Builds the project in `debug` or `release` profile.
dcr build dcr build
dcr build --debug dcr build --debug
dcr build --release dcr build --release
dcr build --release --force
dcr build --debug --clean
``` ```
## What `build` does ## What `build` does
@@ -30,17 +32,33 @@ dcr build --release
- `build.platform` - `build.platform`
- `build.cflags` - `build.cflags`
- `build.ldflags` - `build.ldflags`
- `build.debug` / `build.release`
- `build.exclude`
- `build.include`
- `build.pkg_config`
- `build.steps`
- `build.post_steps`
- `build.generated`
- `build.expect`
- `build.clean`
## Source selection ## Source selection
- `language = "c"` -> `*.c` - `language = "c"` -> `*.c`
- `language = "c++" | "cpp" | "cxx"` -> `*.cpp`, `*.cxx`, `*.cc` - `language = "c++" | "cpp" | "cxx"` -> `*.cpp`, `*.cxx`, `*.cc`
- `language = "asm"` -> `*.s`, `*.S`, `*.asm` - `language = "asm"` -> `*.s`, `*.S`, `*.asm`
- Mixed languages are supported with arrays, for example `language = ["c", "asm"]`.
- By default sources are searched in `src/`; use `build.roots` and `build.src_disable` to override.
## Notes ## Notes
- The profile is selected from the first argument only. - The profile is selected from the first argument only.
- Unknown profile flags return an error. - Unknown profile flags return an error.
- Incremental rebuild is based on source/object mtime comparison. - Incremental rebuild is based on source/object mtime comparison.
- `--force` skips build cache checks and recompiles.
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
- For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`. - For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`.
- In workspace root, `dcr build` builds all members in dependency order. - In workspace root, `dcr build` builds all members in dependency order.
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
- `build.steps` run before compilation; `build.post_steps` run after linking.
- `build.generated` is cleaned when `build.steps` need to rerun; `build.expect` is verified after post-steps.

View File

@@ -17,6 +17,8 @@ dcr clean --release --all
- Without arguments: removes entire `target/`. - Without arguments: removes entire `target/`.
- With profile: removes only `target/<profile>/`. - With profile: removes only `target/<profile>/`.
- With `--all`: also cleans all workspace members. - With `--all`: also cleans all workspace members.
- Additionally removes paths from `build.clean` if configured.
- `build.clean` supports `{profile}` and version placeholders.
## Validation ## Validation

168
docs/commands/dcr-gen.md Normal file
View File

@@ -0,0 +1,168 @@
# dcr gen
Generates IDE and build tool integration files for C/C++ development environments.
## Usage
```sh
dcr gen project-info
dcr gen project-info --release
dcr gen compile-commands
dcr gen compile-commands --debug
dcr gen vscode
dcr gen vscode --release
dcr gen clion
dcr gen clion --debug
```
## Subcommands
### `dcr gen project-info`
Outputs project metadata as JSON array.
```sh
dcr gen project-info [--debug | --release]
```
Outputs structured information about the project (root project + all workspace members if present):
```json
[
{
"name": "my-project",
"version": "1.0.0",
"root": "/path/to/root",
"profile": "debug",
"language": "c++",
"standard": "c++17",
"compiler": "g++",
"kind": "bin",
"sources": ["/path/to/root/src/main.cpp", ...],
"include_dirs": ["/path/to/dep/include", ...],
"lib_dirs": ["/path/to/dep/lib", ...],
"libs": ["somelib", ...],
"cflags": ["-Wall", ...],
"ldflags": ["-lm", ...]
}
]
```
This is useful for integrating DCR projects with custom build tools or scripts.
### `dcr gen compile-commands`
Generates `compile_commands.json` for use by `clangd`, `clang-tidy`, and other tools.
```sh
dcr gen compile-commands [--debug | --release]
```
Writes `./compile_commands.json` containing compilation database entries for all source files. This file is recognized by:
- LSP servers (`clangd`, `clang-tools-extra`)
- Linters (`clang-tidy`)
- Format tools (`clang-format`)
The entries include:
- Full compiler command with flags
- Source file path
- Object file output path
- Project root as working directory
### `dcr gen vscode`
Generates Visual Studio Code integration files.
```sh
dcr gen vscode [--debug | --release]
```
Creates `.vscode/` directory with:
- **`launch.json`**: Debug configurations (one per binary target, debug + release variants)
- **`tasks.json`**: Build/run/clean tasks using `dcr` commands
- **`settings.json`**: Clangd configuration pointing to `compile_commands.json`
- **`extensions.json`**: Recommends `clangd`, disables `cpptools`
- **`compile_commands.json`**: Compilation database (same as `dcr gen compile-commands`)
Usage in VSCode:
- Open the Command Palette (`Ctrl+Shift+P`) and select **"Debug: Start Debugging"** to run debugger
- Press `Ctrl+Shift+B` to run the build task
- Clangd will provide IntelliSense and diagnostics based on `compile_commands.json`
### `dcr gen clion`
Generates JetBrains CLion integration files.
```sh
dcr gen clion [--debug | --release]
```
Creates `.idea/` directory with:
- **`externalTools.xml`**: Registers DCR build/clean/gen commands as external tools
- **`customTargets.xml`**: Defines build targets for debug and release
- **`misc.xml`**: Points CLion to `compile_commands.json` for compilation database
- **`.idea/.gitignore`**: Ignores auto-generated files
- **`runConfigurations/*.xml`**: Debug run configs (one per binary target)
- **`compile_commands.json`**: Compilation database (same as `dcr gen compile-commands`)
Usage in CLion:
- Build/clean via **Tools → DCR → Build Debug/Release/Clean**
- Run/debug via **Run → Run Configurations** (dropdown) → select generated config
- Code completion and diagnostics come from the compilation database
## Config values used
- `package.name`
- `build.compiler`
- `build.language`
- `build.standard`
- `build.kind`
- `build.target`
- `build.platform`
- `build.cflags`
- `build.ldflags`
- `build.debug` / `build.release`
- `build.exclude`
- `build.include`
- `build.pkg_config`
- `build.roots`
- `build.src_disable`
- Workspace members (if `[workspace]` is present)
## Workspace support
When running `dcr gen` in a workspace root, all commands process both the root project and all workspace members:
- `project-info` returns an array with entries for each member + root
- `compile-commands` includes entries from all members
- `vscode` and `clion` generate unified configurations that work across all members
## Troubleshooting
### Clangd not picking up includes
- Ensure `compile_commands.json` exists in the workspace root
- In VSCode settings, verify `clangd.arguments` includes `--compile-commands-dir=<workspace-root>`
- Rebuild via `dcr gen compile-commands` if sources or dependencies changed
### Debug configuration not working
- Verify binary paths in `launch.json` match actual build output (`target/<profile>/binary-name`)
- For workspace projects, each member's debug config uses its own `target/` directory
- Ensure GDB/LLDB is installed: `gdb --version` or `lldb --version`
### CLion not recognizing includes
- Regenerate via `dcr gen clion --debug`
- File → Invalidate Caches → Invalidate and Restart
- Verify `.idea/misc.xml` points to correct `compile_commands.json` path
## Notes
- Profiles default to `debug` if not specified
- Generation does not require the project to be built first
- Generated files are safe to commit; they are generated from `dcr.toml` and thus repeatable
- IDE configurations may be customized after generation, but changes will be overwritten on next `dcr gen` run
- For multi-binary projects, `vscode` and `clion` generate debug configs for all binaries (both debug + release variants)

View File

@@ -8,6 +8,8 @@ Builds the project and runs the resulting binary.
dcr run dcr run
dcr run --debug dcr run --debug
dcr run --release dcr run --release
dcr run --release --force
dcr run --debug --clean
``` ```
## Behavior ## Behavior
@@ -16,11 +18,20 @@ dcr run --release
2. Reads `package.name`, `build.kind`, and optional `build.target`. 2. Reads `package.name`, `build.kind`, and optional `build.target`.
3. Runs the same build flow as `dcr build`. 3. Runs the same build flow as `dcr build`.
4. Executes the built artifact path for the selected profile. 4. Executes the built artifact path for the selected profile.
5. If `[run].cmd` is set, runs that command instead of the built binary.
## Restrictions ## Restrictions
- If `build.kind = "staticlib"` or `build.kind = "sharedlib"`, `run` exits with error. - If `build.kind = "staticlib"` or `build.kind = "sharedlib"`, `run` exits with error.
- The profile is selected from the first argument. - The profile is selected from the first argument.
- `--force` skips build cache checks and recompiles.
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
## `run.cmd` variables
`run.cmd` supports the same variables as build steps: `{profile}`, `{version}`, `{version_major}`,
`{version_minor}`, `{version_patch}`, `{version_suffix}`, `{version_suffix_dash}`.
The command runs via `sh -c` on Unix and `cmd /C` on Windows.
## On build failure ## On build failure

View File

@@ -14,16 +14,18 @@ dcr run --release
dcr clean --debug dcr clean --debug
``` ```
You can also add `--force` or `--clean` to `build`/`run`.
## Default profile flags ## Default profile flags
### GCC/Clang-like toolchains ### GCC/Clang-like toolchains
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG` - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG -march=native` - `release`: `-O3 -DNDEBUG -march=native`
### MSVC toolchain ### MSVC toolchain
- `debug`: `/Od /Zi /W4 /DDEBUG /Oy-` - `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
- `release`: `/O2 /DNDEBUG` - `release`: `/O2 /DNDEBUG`
## Artifacts ## Artifacts
@@ -37,12 +39,12 @@ Default compiler flags:
GCC/Clang: GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG` - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG -march=native` - `release`: `-O3 -DNDEBUG -march=native`
MSVC: MSVC:
- `debug`: `/Od /Zi /W4 /DDEBUG /Oy-` - `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
- `release`: `/O2 /DNDEBUG` - `release`: `/O2 /DNDEBUG`
Static library mode (`kind = "staticlib"`): Static library mode (`kind = "staticlib"`):
@@ -55,3 +57,18 @@ Shared library mode (`kind = "sharedlib"`):
- Linux: `lib<name>.so` - Linux: `lib<name>.so`
- macOS: `lib<name>.dylib` - macOS: `lib<name>.dylib`
- Windows: `<name>.dll` - Windows: `<name>.dll`
## Profile overrides
Use `[build.debug]` and `[build.release]` to override build settings per profile.
All values inherit from `[build]`. If a field is present in the profile table, it replaces the base value.
For arrays (like `cflags`/`ldflags`), the profile value is appended to the base array.
```toml
[build.debug]
cflags = ["-g3"]
[build.release]
cflags = ["-O3"]
ldflags = ["-s"]
```

View File

@@ -13,11 +13,20 @@ kind = "bin"
# platform = "x86_64" # platform = "x86_64"
# cflags = ["-Wall"] # cflags = ["-Wall"]
# ldflags = ["-lm"] # ldflags = ["-lm"]
# exclude = ["src/vendor", "src/legacy/**"]
# include = ["src/legacy/boot/**"]
# roots = ["kernel", "drivers"]
# src_disable = true
# pkg_config = ["Qt6Core", "Qt6Widgets"]
# generated = ["src/ui/*.h"]
# expect = ["./target/{profile}/boot/*.bin"]
# clean = ["./target/{profile}/boot", "/tmp/image/*.img"]
``` ```
## Fields ## Fields
- `language` (string, required): `c`, `c++`, `cpp`, `cxx`, or `asm`. - `language` (string or array, required): `c`, `c++`, `cpp`, `cxx`, or `asm`.
Mixed languages can be specified as an array, for example `["c", "c++", "asm"]`.
- `standard` (string, required): language standard passed to compiler. - `standard` (string, required): language standard passed to compiler.
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`). - `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
- `kind` (string, required): `bin`, `staticlib`, or `sharedlib`. - `kind` (string, required): `bin`, `staticlib`, or `sharedlib`.
@@ -25,6 +34,62 @@ kind = "bin"
- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`). - `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`).
- `cflags` (string array, optional): extra compile flags. - `cflags` (string array, optional): extra compile flags.
- `ldflags` (string array, optional): extra link flags. - `ldflags` (string array, optional): extra link flags.
- `exclude` (string array, optional): paths or glob patterns to skip during source/header collection.
- `include` (string array, optional): allowlist paths or globs that override `exclude`.
- `include` directory entries (non-glob) are also passed to the compiler as `-I` include paths.
- `roots` (string array, optional): additional source roots to scan instead of `src/`.
- `src_disable` (bool, optional): disables default `src/` root when true.
- `pkg_config` (string array, optional): packages used to append `pkg-config --cflags/--libs`.
- `generated` (string array, optional): generated files to delete when build steps rerun.
- `expect` (string array, optional): artifacts that must exist after build/post-steps.
- `clean` (string array, optional): additional paths removed by `dcr clean` and `--clean`.
Notes for `exclude`/`include`:
- Supports `*` and `**` glob patterns.
- If `include` is set, it has priority over `exclude`.
- Use `include` to re-allow nested paths, for example exclude `src/boot` but allow `src/boot/arch/**`.
When using glob patterns in `exclude`, DCR converts them internally, so `exclude` globs still work as expected.
### Build steps
Build steps run shell commands before compilation. Post steps run after linking.
```toml
[[build.steps]]
name = "uic"
in = "src/ui/*.ui"
out = "src/ui/{stem}.h"
cmd = "{uic} {in} -o {out}"
[[build.post_steps]]
name = "image"
in = "target/{profile}/boot/*.bin"
out = "/tmp/image/os-{version}.img"
cmd = "dd if=/dev/zero of={out} bs=1M count=8"
```
Step notes:
- `in` supports glob patterns.
- If `in` expands to multiple files, `out` must include `{stem}`.
- Steps run only when inputs are newer than outputs.
- Variables: `{in}`, `{out}`, `{stem}`, `{cflags}`, `{uic}`, `{moc}`, `{rcc}`, `{profile}`,
`{version}`, `{version_major}`, `{version_minor}`, `{version_patch}`, `{version_suffix}`, `{version_suffix_dash}`.
`build.clean` supports `{profile}` and version placeholders (`{version}`, `{version_major}`, ...).
### Profile overrides
Profile tables inherit all values from `[build]`. Any field present in the profile table replaces the base value.
Array fields (like `cflags`/`ldflags`) are appended to the base array.
```toml
[build.debug]
cflags = ["-g3"]
[build.release]
cflags = ["-O3"]
ldflags = ["-s"]
```
## Toolchain section ## Toolchain section
@@ -35,6 +100,9 @@ cxx = "clang++"
as = "as" as = "as"
ar = "ar" ar = "ar"
ld = "ld" ld = "ld"
uic = "uic"
moc = "moc"
rcc = "rcc"
``` ```
## Behavior notes ## Behavior notes

View File

@@ -45,6 +45,19 @@ Workspace-only root:
- `build.target` - `build.target`
- `build.cflags` - `build.cflags`
- `build.ldflags` - `build.ldflags`
- `build.exclude`
- `build.include`
- `build.roots`
- `build.src_disable`
- `build.pkg_config`
- `build.steps`
- `build.post_steps`
- `build.generated`
- `build.expect`
- `build.clean`
- `build.debug`
- `build.release`
- dependency fields (`include`, `lib`, `libs`, `system`) - dependency fields (`include`, `lib`, `libs`, `system`)
- `[toolchain]` overrides (`cc`, `cxx`, `as`, `ar`, `ld`) - `[toolchain]` overrides (`cc`, `cxx`, `as`, `ar`, `ld`, `uic`, `moc`, `rcc`)
- `[workspace]` members - `[workspace]` members
- `[run]` overrides (`cmd`)

View File

@@ -15,8 +15,11 @@ DCR picks a backend from `build.compiler`:
## Compilation model ## Compilation model
- Source files are found recursively in `src/` based on `build.language`. - Source files are found recursively in `src/` based on `build.language`.
- `build.roots` and `build.src_disable` can replace the default `src/` root.
- Mixed language builds use arrays, for example `build.language = ["c", "asm"]`.
- Each source is compiled to `target/<profile>/obj/<relative-path>.o` (`.obj` on MSVC). - Each source is compiled to `target/<profile>/obj/<relative-path>.o` (`.obj` on MSVC).
- Recompile happens when source mtime is newer than object mtime. - Recompile happens when source mtime is newer than object mtime.
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
## Default build flags ## Default build flags
@@ -24,12 +27,12 @@ These flags are applied automatically based on profile and compiler backend.
GCC/Clang: GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG` - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG -march=native` - `release`: `-O3 -DNDEBUG -march=native`
MSVC: MSVC:
- `debug`: `/Od /Zi /W4 /DDEBUG /Oy-` - `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
- `release`: `/O2 /DNDEBUG` - `release`: `/O2 /DNDEBUG`
## Platform hint ## Platform hint
@@ -46,10 +49,12 @@ If `[toolchain]` is set, DCR uses its values to override compiler/linker tools:
- `cc`, `cxx`, `as` override compilers for C/C++/ASM - `cc`, `cxx`, `as` override compilers for C/C++/ASM
- `ar` overrides the archiver (used for `staticlib`) - `ar` overrides the archiver (used for `staticlib`)
- `ld` overrides the linker (used by GCC/Clang/NASM/GAS backends) - `ld` overrides the linker (used by GCC/Clang/NASM/GAS backends)
- `uic`, `moc`, `rcc` override Qt tools used in `build.steps`
Environment overrides (highest priority): Environment overrides (highest priority):
- `DCR_COMPILER` (all languages) - `DCR_COMPILER` (all languages)
- `DCR_CC`, `DCR_CXX`, `DCR_AS`, `DCR_AR`, `DCR_LD` - `DCR_CC`, `DCR_CXX`, `DCR_AS`, `DCR_AR`, `DCR_LD`
- `DCR_DEBUG` enables verbose build logging
## Linking ## Linking

View File

@@ -11,6 +11,11 @@ For each dependency in `[dependencies]`, DCR:
5. Synchronizes dependency files to `target/<profile>/deps/<dep-name>/`. 5. Synchronizes dependency files to `target/<profile>/deps/<dep-name>/`.
6. Adds include/lib/link options to compile/link commands. 6. Adds include/lib/link options to compile/link commands.
### Profile placeholders
`path`, `include`, and `lib` entries may use `{profile}` which expands to the active profile
(`debug` or `release`).
## Failure cases ## Failure cases
- Dependency path is missing or not a directory. - Dependency path is missing or not a directory.

View File

@@ -8,6 +8,7 @@ Debug (default):
dcr build dcr build
# or # or
dcr build --debug dcr build --debug
dcr build --debug --clean
``` ```
Release: Release:
@@ -22,6 +23,7 @@ dcr build --release
dcr run dcr run
# or # or
dcr run --release dcr run --release
dcr run --release --force
``` ```
`dcr run` first builds, then launches the produced binary. `dcr run` first builds, then launches the produced binary.
@@ -47,3 +49,4 @@ dcr clean --release --all
- Build profile flag is parsed from the first command argument. - Build profile flag is parsed from the first command argument.
- Unknown profile flags produce an error. - Unknown profile flags produce an error.
- In workspace root, `clean --all` cleans all member projects. - In workspace root, `clean --all` cleans all member projects.
- `--clean` for `build`/`run` removes `target/<profile>` and paths from `build.clean`.

View File

@@ -8,7 +8,7 @@ name = "string" # required
version = "string" # required version = "string" # required
[build] [build]
language = "c|c++|cpp|cxx|asm" # required language = "c|c++|cpp|cxx|asm" # required (string or array)
standard = "string" # required standard = "string" # required
compiler = "string" # required compiler = "string" # required
kind = "bin|staticlib|sharedlib" # required kind = "bin|staticlib|sharedlib" # required
@@ -16,6 +16,42 @@ target = "string" # optional
platform = "string" # optional platform = "string" # optional
cflags = ["string", "..."] # optional cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional ldflags = ["string", "..."] # optional
exclude = ["string", "..."] # optional
include = ["string", "..."] # optional
roots = ["string", "..."] # optional
src_disable = false # optional
pkg_config = ["string", "..."] # optional
generated = ["string", "..."] # optional
expect = ["string", "..."] # optional
clean = ["string", "..."] # optional
[[build.steps]]
name = "string"
in = "glob"
out = "path or glob"
cmd = "string"
[[build.post_steps]]
name = "string"
in = "glob"
out = "path or glob"
cmd = "string"
# Inline array form (equivalent to [[build.steps]] / [[build.post_steps]])
steps = [
{ name = "string", in = "glob", out = "path or glob", cmd = "string" },
]
post_steps = [
{ name = "string", in = "glob", out = "path or glob", cmd = "string" },
]
[build.debug]
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
[build.release]
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
[toolchain] [toolchain]
cc = "string" # optional cc = "string" # optional
@@ -23,6 +59,12 @@ cxx = "string" # optional
as = "string" # optional as = "string" # optional
ar = "string" # optional ar = "string" # optional
ld = "string" # optional ld = "string" # optional
uic = "string" # optional
moc = "string" # optional
rcc = "string" # optional
[run]
cmd = "string" # optional
[workspace] [workspace]
name = { path = "string", deps = ["string", "..."] } name = { path = "string", deps = ["string", "..."] }
@@ -42,6 +84,15 @@ name = {
- Required string fields must be non-empty. - Required string fields must be non-empty.
- `build.kind` must be `bin`, `staticlib`, or `sharedlib`. - `build.kind` must be `bin`, `staticlib`, or `sharedlib`.
- Dependency fields `include/lib/libs` must be string arrays when provided. - Dependency fields `include/lib/libs` must be string arrays when provided.
- `build.exclude`/`build.include` must be string arrays when provided.
- `build.roots` must be a string array when provided.
- `build.src_disable` must be boolean when provided.
- `build.clean`/`build.generated`/`build.pkg_config`/`build.expect` must be string arrays when provided.
- `build.debug`/`build.release` must be tables; their `cflags`/`ldflags` must be string arrays.
- `build.steps`/`build.post_steps` entries must include `name`, `in`, `out`, `cmd`.
- `build.language` may be a string or array (for example `["c", "c++", "asm"]`).
- `[build.<profile>]` tables inherit all base values and override any fields they specify.
Array fields (like `cflags`/`ldflags`) are appended to the base array.
## Generated defaults (`dcr new` / `dcr init`) ## Generated defaults (`dcr new` / `dcr init`)

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ use crate::core::workspace::parse_workspace;
use crate::utils::fs::{check_dir, find_project_root}; use crate::utils::fs::{check_dir, find_project_root};
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored}; use crate::utils::text::{BOLD_GREEN, colored};
use glob::glob;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
@@ -85,6 +86,7 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), String> { fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), String> {
with_dir(project_root, || { with_dir(project_root, || {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let project_name = std::env::current_dir() let project_name = std::env::current_dir()
.ok() .ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string())) .and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
@@ -97,31 +99,33 @@ fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), St
" Cleaning project `{}`", " Cleaning project `{}`",
colored(&project_name, BOLD_GREEN) colored(&project_name, BOLD_GREEN)
); );
if !items.contains(&"target".to_string()) {
warn("Directory target not found");
return Ok(());
}
if let Some(profile) = profile { if let Some(profile) = profile {
let target_items = check_dir(Some("target")).unwrap_or_default(); let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&profile.to_string()) { if !target_items.contains(&profile.to_string()) {
warn(&format!("Directory target/{profile} not found")); warn(&format!("Directory target/{profile} not found"));
return Ok(()); } else {
println!(" Profile: {}", colored(profile, BOLD_GREEN));
let _ = fs::remove_dir_all(format!("target/{profile}"));
println!(
"{} Removed directory target/{profile}",
colored("\n ✔", BOLD_GREEN)
);
} }
println!(" Profile: {}", colored(profile, BOLD_GREEN)); clean_custom_paths(&config, profile)?;
let _ = fs::remove_dir_all(format!("target/{profile}"));
println!(
"{} Removed directory target/{profile}",
colored("\n ✔", BOLD_GREEN)
);
return Ok(()); return Ok(());
} }
let _ = fs::remove_dir_all("target"); if items.contains(&"target".to_string()) {
println!( let _ = fs::remove_dir_all("target");
"{} Removed directory target", println!(
colored("\n ✔", BOLD_GREEN) "{} Removed directory target",
); colored("\n ✔", BOLD_GREEN)
);
} else {
warn("Directory target not found");
}
clean_custom_paths(&config, "debug")?;
clean_custom_paths(&config, "release")?;
Ok(()) Ok(())
}) })
} }
@@ -136,3 +140,86 @@ where
let _ = std::env::set_current_dir(prev); let _ = std::env::set_current_dir(prev);
result result
} }
fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
let patterns = match config.get("build.clean") {
Some(v) => v
.as_array()
.ok_or_else(|| "build.clean must be an array of strings".to_string())?
.iter()
.filter_map(|item| item.as_str())
.map(|s| {
let value = s.replace("{profile}", profile);
substitute_version_vars(&value, config)
})
.collect::<Vec<String>>(),
None => Vec::new(),
};
if patterns.is_empty() {
return Ok(());
}
for pattern in patterns {
for entry in glob(&pattern).map_err(|err| format!("glob error: {err}"))? {
let path = entry.map_err(|err| format!("glob error: {err}"))?;
if path.is_dir() {
let _ = fs::remove_dir_all(&path);
} else if path.is_file() {
let _ = fs::remove_file(&path);
}
}
}
Ok(())
}
fn substitute_version_vars(value: &str, config: &Config) -> String {
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("");
let info = parse_version_info(version);
value
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}
struct VersionInfo {
full: String,
major: String,
minor: String,
patch: String,
suffix: String,
suffix_dash: String,
}
fn parse_version_info(version: &str) -> VersionInfo {
let mut full = version.trim().to_string();
if full.is_empty() {
full = "0.0.0".to_string();
}
let full_clone = full.clone();
let (base, suffix) = match full_clone.split_once('-') {
Some((head, tail)) => (head, tail),
None => (full_clone.as_str(), ""),
};
let mut parts = base.split('.');
let major = parts.next().unwrap_or("0").to_string();
let minor = parts.next().unwrap_or("0").to_string();
let patch = parts.next().unwrap_or("0").to_string();
let suffix_dash = if suffix.is_empty() {
"".to_string()
} else {
format!("-{suffix}")
};
VersionInfo {
full,
major,
minor,
patch,
suffix: suffix.to_string(),
suffix_dash,
}
}

46
src/cli/flags.rs Normal file
View File

@@ -0,0 +1,46 @@
use crate::config::{PROFILE, flags};
use crate::utils::log::warn;
pub struct BuildRunFlags {
pub profile: String,
pub force: bool,
pub clean: bool,
}
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
let mut profile = PROFILE.to_string();
let mut force = false;
let mut clean = false;
for arg in args {
if !arg.starts_with("--") {
warn("Unknown argument");
return Err(1);
}
let candidate = arg.trim_start_matches("--");
if candidate == "force" {
force = true;
continue;
}
if candidate == "clean" {
clean = true;
continue;
}
if flags(candidate).is_some() {
if profile != PROFILE {
warn("Duplicate profile flag");
return Err(1);
}
profile = candidate.to_string();
continue;
}
warn("Unknown build flag");
return Err(1);
}
Ok(BuildRunFlags {
profile,
force,
clean,
})
}

1440
src/cli/gen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,14 +13,18 @@ pub fn help() -> i32 {
println!(" build [--profile] Build the project (default: --debug)"); println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run the project (default: --debug)"); println!(" run [--profile] Build and run the project (default: --debug)");
println!(" clean Remove the target directory"); println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN); printc("FLAGS:", BOLD_GREEN);
println!(" --help Show command help"); println!(" --help Show command help");
println!(" --update Update dcr to the latest version"); println!(" --update Update dcr to the latest version");
println!(" --version Show dcr version"); println!(" --version Show dcr version");
println!(); println!();
printc("OPTIONS:", BOLD_GREEN); printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Build with debug profile"); println!(" --debug Use debug profile");
println!(" --release Build with release profile"); println!(" --release Use release profile");
println!(" --force Force rebuild (build/run)");
println!(" --clean Clean before build (build/run)");
println!(" --all Clean all workspace members (clean)");
println!(); println!();
printc("EXAMPLES:", BOLD_GREEN); printc("EXAMPLES:", BOLD_GREEN);
printc(" dcr new hello", BOLD_CYAN); printc(" dcr new hello", BOLD_CYAN);

View File

@@ -1,6 +1,8 @@
pub mod build; pub mod build;
pub mod clean; pub mod clean;
pub mod flag_update; pub mod flag_update;
pub mod flags;
pub mod r#gen;
pub mod help; pub mod help;
pub mod init; pub mod init;
pub mod new; pub mod new;

View File

@@ -1,15 +1,14 @@
use crate::cli::build::build; use crate::cli::build::build;
use crate::config::{PROFILE, flags}; use crate::cli::flags::parse_build_run_flags;
use crate::core::config::Config; use crate::core::config::Config;
use crate::core::runner::run_binary; use crate::core::runner::run_binary;
use crate::utils::fs::find_project_root; use crate::utils::fs::find_project_root;
use crate::utils::log::{error, warn}; use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored}; use crate::utils::text::{BOLD_GREEN, colored};
use std::path::Path; use std::path::Path;
use std::process::Command;
pub fn run(args: &[String]) -> i32 { pub fn run(args: &[String]) -> i32 {
let mut active_profile = PROFILE.to_string();
let start_dir = match std::env::current_dir() { let start_dir = match std::env::current_dir() {
Ok(dir) => dir, Ok(dir) => dir,
Err(_) => { Err(_) => {
@@ -51,35 +50,45 @@ pub fn run(args: &[String]) -> i32 {
.map(|s| s.trim()) .map(|s| s.trim())
.filter(|s| !s.is_empty()); .filter(|s| !s.is_empty());
if let Some(first_arg) = args.first() { let flags = match parse_build_run_flags(args) {
if first_arg.starts_with("--") { Ok(v) => v,
let candidate = first_arg.trim_start_matches("--"); Err(_) => return 1,
if flags(candidate).is_some() { };
active_profile = candidate.to_string();
} else { let version = config
warn("Unknown build flag"); .get("package.version")
return 1; .and_then(|v| v.as_str())
} .unwrap_or("");
} else { let run_cmd = config
warn("Unknown argument"); .get("run.cmd")
return 1; .and_then(|v| v.as_str())
} .map(|s| s.trim().to_string())
} .filter(|s| !s.is_empty())
.map(|cmd| substitute_run_vars(&cmd, &flags.profile, version));
let kind = build_kind.trim(); let kind = build_kind.trim();
if kind == "staticlib" || kind == "sharedlib" { if run_cmd.is_none() && (kind == "staticlib" || kind == "sharedlib") {
error("Cannot run library build"); error("Cannot run library build");
return 1; return 1;
} }
let build_status = build(args); let build_status = build(args);
let bin_path = crate::platform::bin_path(&active_profile, project_name, target_dir); let bin_path = crate::platform::bin_path(&flags.profile, project_name, target_dir);
if build_status == 0 { if build_status == 0 {
if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------");
return run_shell(&cmd);
}
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------"); println!("--------------------------------");
return run_binary(project_name, &active_profile, target_dir); return run_binary(project_name, &flags.profile, target_dir);
} }
let fallback_code = run_binary(project_name, &active_profile, target_dir); let fallback_code = if let Some(cmd) = run_cmd {
run_shell(&cmd)
} else {
run_binary(project_name, &flags.profile, target_dir)
};
if fallback_code != 1 { if fallback_code != 1 {
return fallback_code; return fallback_code;
} }
@@ -98,3 +107,65 @@ where
let _ = std::env::set_current_dir(prev); let _ = std::env::set_current_dir(prev);
result result
} }
fn run_shell(cmd: &str) -> i32 {
let status = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/C").arg(cmd).status()
} else {
Command::new("sh").arg("-c").arg(cmd).status()
};
match status {
Ok(s) if s.success() => 0,
Ok(s) => s.code().unwrap_or(1),
Err(_) => 1,
}
}
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
let info = parse_version_info(version);
cmd.replace("{profile}", profile)
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}
struct VersionInfo {
full: String,
major: String,
minor: String,
patch: String,
suffix: String,
suffix_dash: String,
}
fn parse_version_info(version: &str) -> VersionInfo {
let mut full = version.trim().to_string();
if full.is_empty() {
full = "0.0.0".to_string();
}
let full_clone = full.clone();
let (base, suffix) = match full_clone.split_once('-') {
Some((head, tail)) => (head, tail),
None => (full_clone.as_str(), ""),
};
let mut parts = base.split('.');
let major = parts.next().unwrap_or("0").to_string();
let minor = parts.next().unwrap_or("0").to_string();
let patch = parts.next().unwrap_or("0").to_string();
let suffix_dash = if suffix.is_empty() {
"".to_string()
} else {
format!("-{suffix}")
};
VersionInfo {
full,
major,
minor,
patch,
suffix: suffix.to_string(),
suffix_dash,
}
}

View File

@@ -16,7 +16,7 @@ pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-fno-omit-frame-pointer", "-fno-omit-frame-pointer",
"-DDEBUG", "-DDCR_DEBUG",
]), ]),
_ => None, _ => None,
} }

623
src/core/builder/common.rs Normal file
View File

@@ -0,0 +1,623 @@
use std::fs;
use std::path::{Path, PathBuf};
pub fn collect_sources(
roots: &[PathBuf],
extensions: &[&str],
exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<Vec<String>, String> {
let mut sources = Vec::new();
for root in roots {
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
}
sources.sort();
if sources.is_empty() {
return Err(format!("No source files found in {}", format_roots(roots)));
}
Ok(sources)
}
fn collect_sources_rec(
dir: &Path,
extensions: &[&str],
out: &mut Vec<String>,
exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<(), String> {
let full_dir = if dir.is_absolute() {
dir.to_path_buf()
} else {
std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))?
.join(dir)
};
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}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
continue;
}
collect_sources_rec(&path, extensions, out, exclude_dirs, include_paths)?;
continue;
}
if !path.is_file() {
continue;
}
if is_excluded(&path, exclude_dirs, include_paths) {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext_lower = ext_raw.to_lowercase();
let matched = extensions
.iter()
.any(|allowed| *allowed == ext_raw || *allowed == ext_lower);
if matched {
out.push(normalize_source_path(&path));
}
}
Ok(())
}
fn format_roots(roots: &[PathBuf]) -> String {
if roots.is_empty() {
return "<none>".to_string();
}
let mut out = Vec::new();
for root in roots {
out.push(root.to_string_lossy().to_string());
}
out.join(", ")
}
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
if matches_patterns(path, include_paths, true) {
return false;
}
if matches_patterns(path, include_paths, false) {
return true;
}
exclude_dirs.iter().any(|dir| path.starts_with(dir))
}
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
if patterns.is_empty() {
return false;
}
let cwd = std::env::current_dir().ok();
let abs_path = if path.is_absolute() {
path.to_path_buf()
} else if let Some(base) = cwd.as_ref() {
base.join(path)
} else {
path.to_path_buf()
};
let abs = abs_path.to_string_lossy().replace('\\', "/");
let rel = cwd
.as_ref()
.and_then(|base| abs_path.strip_prefix(base).ok())
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default();
let rel_dot = if rel.is_empty() {
String::new()
} else {
format!("./{rel}")
};
for raw in patterns {
let trimmed = raw.trim();
if trimmed.is_empty() {
continue;
}
let is_neg = trimmed.starts_with('!');
if is_neg == positive {
continue;
}
let normalized = trimmed.trim_start_matches('!').replace('\\', "/");
if normalized.is_empty() {
continue;
}
if has_glob_magic(&normalized) {
let pat = match glob::Pattern::new(&normalized) {
Ok(p) => p,
Err(_) => continue,
};
if pat.matches(&abs)
|| (!rel.is_empty() && (pat.matches(&rel) || pat.matches(&rel_dot)))
{
return true;
}
continue;
}
let norm = normalized.trim_end_matches('/');
let candidate = Path::new(norm);
let full = if candidate.is_absolute() {
candidate.to_path_buf()
} else if let Some(base) = cwd.as_ref() {
base.join(candidate)
} else {
candidate.to_path_buf()
};
if abs_path.starts_with(&full) {
return true;
}
}
false
}
pub fn has_glob_magic(value: &str) -> bool {
value.chars().any(|c| matches!(c, '*' | '?' | '['))
}
pub fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
}
if let Ok(base) = std::env::current_dir()
&& let Ok(rel) = path.strip_prefix(&base)
{
return format!("./{}", rel.to_string_lossy());
}
path.to_string_lossy().to_string()
}
pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
pub fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
let o_time = match obj_time {
Ok(t) => t,
Err(_) => return true,
};
match src_time {
Ok(s) if s > o_time => return true,
Err(_) => return true,
_ => {}
}
let d_file = PathBuf::from(object).with_extension("d");
if let Ok(content) = fs::read_to_string(&d_file) {
let deps = parse_d_file(&content);
for dep in deps {
let dep_path = Path::new(&dep);
if dep_path == Path::new(object) || dep_path == Path::new(source) {
continue;
}
if let Ok(dep_meta) = fs::metadata(dep_path) {
if let Ok(dep_time) = dep_meta.modified()
&& dep_time > o_time
{
return true;
}
} else {
return true; // Missing dependency triggers rebuild
}
}
}
false
}
fn parse_d_file(content: &str) -> Vec<String> {
let mut deps = Vec::new();
let text = content.replace("\\\n", " ").replace("\\\r\n", " ");
let mut target_end = 0;
let chars: Vec<char> = text.chars().collect();
for i in 0..chars.len() {
if chars[i] == ':' {
if i == 1
&& chars[0].is_ascii_alphabetic()
&& i + 1 < chars.len()
&& (chars[i + 1] == '\\' || chars[i + 1] == '/')
{
continue; // Windows drive letter
}
target_end = i + 1;
break;
}
}
let deps_str = if target_end > 0 {
&text[target_end..]
} else {
&text
};
let mut current_path = String::new();
let mut in_escape = false;
for c in deps_str.chars() {
if in_escape {
if c != '\n' && c != '\r' {
current_path.push(c);
}
in_escape = false;
} else if c == '\\' {
in_escape = true;
} else if c.is_whitespace() {
if !current_path.is_empty() {
deps.push(current_path.clone());
current_path.clear();
}
} else {
current_path.push(c);
}
}
if !current_path.is_empty() {
deps.push(current_path);
}
deps
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
static CWD_LOCK: Mutex<()> = Mutex::new(());
fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("dcr_test_{prefix}_{n}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn default_roots() -> Vec<PathBuf> {
vec![PathBuf::from("src")]
}
#[test]
fn object_path_basic() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "./src/main.c", "o");
assert_eq!(result, "target/debug/obj/main.o");
}
#[test]
fn object_path_nested() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "./src/core/utils.c", "o");
assert_eq!(result, "target/debug/obj/core/utils.o");
}
#[test]
fn object_path_no_prefix() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "src/main.c", "o");
assert_eq!(result, "target/debug/obj/main.o");
}
#[test]
fn object_path_msvc_ext() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "./src/main.c", "obj");
assert_eq!(result, "target/debug/obj/main.obj");
}
#[test]
fn needs_rebuild_no_object() {
let dir = temp_dir("rebuild_no_obj");
let src = dir.join("test.c");
fs::write(&src, "int main() {}").unwrap();
let obj = dir.join("test.o");
assert!(needs_rebuild(
&src.to_string_lossy(),
&obj.to_string_lossy()
));
}
#[test]
fn needs_rebuild_fresh() {
let dir = temp_dir("rebuild_fresh");
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(
&src.to_string_lossy(),
&obj.to_string_lossy()
));
}
#[test]
fn needs_rebuild_stale() {
let dir = temp_dir("rebuild_stale");
let src = dir.join("test.c");
let obj = dir.join("test.o");
fs::write(&obj, "").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&src, "int main() { return 1; }").unwrap();
assert!(needs_rebuild(
&src.to_string_lossy(),
&obj.to_string_lossy()
));
}
#[test]
fn is_excluded_match() {
let include: Vec<String> = Vec::new();
let excluded = vec![PathBuf::from("/project/src/vendor")];
assert!(is_excluded(
Path::new("/project/src/vendor"),
&excluded,
&include
));
assert!(is_excluded(
Path::new("/project/src/vendor/lib.c"),
&excluded,
&include
));
}
#[test]
fn is_excluded_no_match() {
let include: Vec<String> = Vec::new();
let excluded = vec![PathBuf::from("/project/src/vendor")];
assert!(!is_excluded(
Path::new("/project/src/main.c"),
&excluded,
&include
));
assert!(!is_excluded(
Path::new("/other/vendor"),
&excluded,
&include
));
}
#[test]
fn collect_sources_c_files() {
let dir = temp_dir("collect_c");
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(src.join("utils.c"), "").unwrap();
fs::write(src.join("README.md"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 2);
assert!(sources.iter().any(|s| s.ends_with("main.c")));
assert!(sources.iter().any(|s| s.ends_with("utils.c")));
}
#[test]
fn collect_sources_cpp_files() {
let dir = temp_dir("collect_cpp");
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("main.cpp"), "").unwrap();
fs::write(src.join("helper.cxx"), "").unwrap();
fs::write(src.join("other.cc"), "").unwrap();
fs::write(src.join("skip.c"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["cpp", "cxx", "cc"], &[], &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 3);
}
#[test]
fn collect_sources_empty_dir() {
let dir = temp_dir("collect_empty");
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap();
assert!(result.is_err(), "empty src should return error");
}
#[test]
fn collect_sources_respects_excludes() {
let dir = temp_dir("collect_exclude");
let src = dir.join("src");
let vendor = src.join("vendor");
fs::create_dir_all(&vendor).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded
let exclude = vec![vendor.clone()];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("main.c"));
}
#[test]
fn collect_sources_nested() {
let dir = temp_dir("collect_nested");
let src = dir.join("src");
let sub = src.join("core").join("deep");
fs::create_dir_all(&sub).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(sub.join("nested.c"), "").unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 2);
}
#[test]
fn include_overrides_exclude() {
let dir = temp_dir("include_override");
let src = dir.join("src");
let boot = src.join("boot");
let arch = boot.join("arch");
fs::create_dir_all(&arch).unwrap();
fs::write(arch.join("start.s"), "").unwrap();
fs::write(boot.join("skip.c"), "").unwrap();
let exclude = vec![boot.clone()];
let include = vec!["src/boot/arch/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
let result = collect_sources(&roots, &["s", "c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("start.s"));
}
#[test]
fn exclude_glob_with_include_override() {
let dir = temp_dir("exclude_glob");
let src = dir.join("src");
let legacy = src.join("legacy");
let allow = legacy.join("allow");
fs::create_dir_all(&allow).unwrap();
fs::write(legacy.join("skip.c"), "").unwrap();
fs::write(allow.join("keep.c"), "").unwrap();
let exclude = vec![dir.join("src"), dir.join(".")];
let include = vec![
"!src/legacy/**".to_string(),
"src/legacy/allow/**".to_string(),
];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("keep.c"));
}
#[test]
fn exclude_glob_without_include_keeps_other_files() {
let dir = temp_dir("exclude_glob_only");
let src = dir.join("src");
let gen_dir = src.join("gen");
fs::create_dir_all(&gen_dir).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(gen_dir.join("skip.c"), "").unwrap();
let exclude: Vec<PathBuf> = Vec::new();
let include = vec!["!src/gen/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("main.c"));
}
#[test]
fn normalize_relative_path() {
let result = normalize_source_path(Path::new("./src/main.c"));
assert_eq!(result, "./src/main.c");
}
#[test]
fn parse_d_file_gcc_format() {
let content = "target/obj/main.o: src/main.c src/utils.h \\\n src/core/types.h";
let deps = parse_d_file(content);
assert_eq!(deps, vec!["src/main.c", "src/utils.h", "src/core/types.h"]);
}
#[test]
fn parse_d_file_msvc_format() {
let content = "target/obj/main.obj: \\\n C:/sdk/include/windows.h \\\n src/main.c";
let deps = parse_d_file(content);
assert_eq!(deps, vec!["C:/sdk/include/windows.h", "src/main.c"]);
}
#[test]
fn needs_rebuild_header_modified() {
let dir = temp_dir("rebuild_header");
let src = dir.join("test.c");
let header = dir.join("test.h");
let obj = dir.join("test.o");
let d_file = dir.join("test.d");
fs::write(&src, "int main() {}").unwrap();
fs::write(&header, "#define A 1").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap();
let d_content = format!(
"{}: {} {}",
obj.to_string_lossy(),
src.to_string_lossy(),
header.to_string_lossy()
);
fs::write(&d_file, d_content).unwrap();
assert!(
!needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
"should be fresh"
);
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&header, "#define A 2").unwrap(); // modify header
assert!(
needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
"changed header should trigger rebuild"
);
}
}

View File

@@ -1,12 +1,14 @@
use crate::core::builder::BuildContext; use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform; use crate::platform;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::Instant; use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> { pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.language.to_lowercase().as_str() != "asm" { let lang = ctx.language.to_lowercase();
if lang.split('+').any(|p| p.trim() != "asm") {
return Err("GAS backend requires build.language = \"asm\"".to_string()); return Err("GAS backend requires build.language = \"asm\"".to_string());
} }
let assembler = if ctx.compiler.is_empty() { let assembler = if ctx.compiler.is_empty() {
@@ -15,7 +17,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.compiler ctx.compiler
}; };
let start_time = Instant::now(); let start_time = Instant::now();
let sources = collect_sources(ctx.language, ctx.exclude_dirs)?; let sources = collect_sources(ctx)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?; let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
@@ -35,6 +37,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -72,6 +77,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg("-o").arg(out_path); cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -82,76 +90,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} }
} }
pub(crate) fn collect_sources( pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
language: &str, // GAS handles only lowercase .s files
exclude_dirs: &[PathBuf], common::collect_sources(
) -> Result<Vec<String>, String> { ctx.source_roots,
let lang = language.to_lowercase(); &["s"],
let mut sources = Vec::new(); ctx.exclude_dirs,
collect_sources_rec("./src", &lang, &mut sources, exclude_dirs)?; ctx.include_paths,
sources.sort(); )
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(
dir: &str,
lang: &str,
out: &mut Vec<String>,
exclude_dirs: &[PathBuf],
) -> Result<(), String> {
let dir_path = Path::new(dir);
let full_dir = if dir_path.is_absolute() {
dir_path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))?
.join(dir_path)
};
if is_excluded(&full_dir, exclude_dirs) {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs) {
continue;
}
collect_sources_rec(&path.to_string_lossy(), lang, out, exclude_dirs)?;
continue;
}
if !path.is_file() {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext = ext_raw.to_lowercase();
let file = normalize_source_path(&path);
let allowed = lang == "asm" && ext == "s" && ext_raw == "s";
if allowed {
out.push(file);
}
}
Ok(())
}
fn is_excluded(path: &Path, exclude_dirs: &[PathBuf]) -> bool {
exclude_dirs.iter().any(|dir| path.starts_with(dir))
}
fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
}
if let Ok(base) = std::env::current_dir()
&& let Ok(rel) = path.strip_prefix(&base)
{
return format!("./{}", rel.to_string_lossy());
}
path.to_string_lossy().to_string()
} }
fn build_objects( fn build_objects(
@@ -163,11 +109,11 @@ fn build_objects(
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let mut objects = Vec::new(); let mut objects = Vec::new();
for source in sources { for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext); let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() { if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
} }
if needs_rebuild(source, &obj_path) { if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler); let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(&obj_path); cmd.arg(source).arg("-o").arg(&obj_path);
for flag in ctx.cflags { for flag in ctx.cflags {
@@ -183,24 +129,3 @@ fn build_objects(
} }
Ok(objects) Ok(objects)
} }
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -1,250 +0,0 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx.language, ctx.exclude_dirs)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
cmd.arg("rcs").arg(&lib_path);
for obj in &objects {
cmd.arg(obj);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or(compiler));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
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 out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(
language: &str,
exclude_dirs: &[PathBuf],
) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources, exclude_dirs)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(
dir: &str,
lang: &str,
out: &mut Vec<String>,
exclude_dirs: &[PathBuf],
) -> Result<(), String> {
let dir_path = Path::new(dir);
let full_dir = if dir_path.is_absolute() {
dir_path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))?
.join(dir_path)
};
if is_excluded(&full_dir, exclude_dirs) {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs) {
continue;
}
collect_sources_rec(&path.to_string_lossy(), lang, out, exclude_dirs)?;
continue;
}
if !path.is_file() {
continue;
}
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or("")
.to_lowercase();
let file = normalize_source_path(&path);
let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed {
out.push(file);
}
}
Ok(())
}
fn is_excluded(path: &Path, exclude_dirs: &[PathBuf]) -> bool {
exclude_dirs.iter().any(|dir| path.starts_with(dir))
}
fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
}
if let Ok(base) = std::env::current_dir()
&& let Ok(rel) = path.strip_prefix(&base)
{
return format!("./{}", rel.to_string_lossy());
}
path.to_string_lossy().to_string()
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG", "-march=native"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDEBUG",
],
_ => &[],
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard));
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -1,8 +1,8 @@
pub mod clang; pub mod common;
pub mod gas; pub mod gas;
pub mod gcc;
pub mod msvc; pub mod msvc;
pub mod nasm; pub mod nasm;
pub mod unix_cc;
pub struct BuildContext<'a> { pub struct BuildContext<'a> {
pub profile: &'a str, pub profile: &'a str,
@@ -15,7 +15,9 @@ pub struct BuildContext<'a> {
pub platform: Option<&'a str>, pub platform: Option<&'a str>,
pub linker: Option<&'a str>, pub linker: Option<&'a str>,
pub archiver: Option<&'a str>, pub archiver: Option<&'a str>,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf], pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String], pub include_dirs: &'a [String],
pub lib_dirs: &'a [String], pub lib_dirs: &'a [String],
pub libs: &'a [String], pub libs: &'a [String],
@@ -34,37 +36,22 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if compiler.contains("nasm") { if compiler.contains("nasm") {
return nasm::build(ctx); return nasm::build(ctx);
} }
if compiler.contains("gcc") || compiler.contains("g++") {
return gcc::build(ctx);
}
if compiler.contains("clang") || compiler.contains("clang++") {
return clang::build(ctx);
}
if compiler == "cl" || compiler.contains("msvc") { if compiler == "cl" || compiler.contains("msvc") {
return msvc::build(ctx); return msvc::build(ctx);
} }
clang::build(ctx) unix_cc::build(ctx)
} }
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> { pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let compiler = ctx.compiler.to_lowercase(); let compiler = ctx.compiler.to_lowercase();
if compiler.contains("clang-cl") { if compiler.contains("clang-cl") || compiler == "cl" || compiler.contains("msvc") {
return msvc::collect_sources(ctx.language, ctx.exclude_dirs); return msvc::collect_sources(ctx);
} }
if compiler == "as" || compiler.contains("gas") { if compiler == "as" || compiler.contains("gas") {
return gas::collect_sources(ctx.language, ctx.exclude_dirs); return gas::collect_sources(ctx);
} }
if compiler.contains("nasm") { if compiler.contains("nasm") {
return nasm::collect_sources(ctx.language, ctx.exclude_dirs); return nasm::collect_sources(ctx);
} }
if compiler.contains("gcc") || compiler.contains("g++") { unix_cc::collect_sources(ctx)
return gcc::collect_sources(ctx.language, ctx.exclude_dirs);
}
if compiler.contains("clang") || compiler.contains("clang++") {
return clang::collect_sources(ctx.language, ctx.exclude_dirs);
}
if compiler == "cl" || compiler.contains("msvc") {
return msvc::collect_sources(ctx.language, ctx.exclude_dirs);
}
clang::collect_sources(ctx.language, ctx.exclude_dirs)
} }

View File

@@ -1,7 +1,8 @@
use crate::core::builder::BuildContext; use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform; use crate::platform;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::Instant; use std::time::Instant;
@@ -11,11 +12,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else { } else {
ctx.compiler ctx.compiler
}; };
if ctx.language.to_lowercase() == "asm" { let lang = ctx.language.to_lowercase();
return Err("MSVC backend does not support build.language = \"asm\"".to_string()); if lang.contains("asm") {
return Err("MSVC backend does not support build.language with asm".to_string());
} }
let start_time = Instant::now(); let start_time = Instant::now();
let sources = collect_sources(ctx.language, ctx.exclude_dirs)?; let sources = collect_sources(ctx)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?; let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
@@ -26,6 +28,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -90,6 +95,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg(format!("/Fe:{out_path}")); cmd.arg(format!("/Fe:{out_path}"));
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -100,82 +108,31 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} }
} }
pub(crate) fn collect_sources( pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
language: &str, let extensions = source_extensions(ctx.language);
exclude_dirs: &[PathBuf], common::collect_sources(
) -> Result<Vec<String>, String> { ctx.source_roots,
let lang = language.to_lowercase(); &extensions,
let mut sources = Vec::new(); ctx.exclude_dirs,
collect_sources_rec("./src", &lang, &mut sources, exclude_dirs)?; ctx.include_paths,
sources.sort(); )
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
} }
fn collect_sources_rec( fn source_extensions(language: &str) -> Vec<&str> {
dir: &str, let mut out = Vec::new();
lang: &str, for part in language.split('+') {
out: &mut Vec<String>, let lang = part.trim().to_lowercase();
exclude_dirs: &[PathBuf], match lang.as_str() {
) -> Result<(), String> { "c" => out.extend(["c"]),
let dir_path = Path::new(dir); "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
let full_dir = if dir_path.is_absolute() { "asm" => {}
dir_path.to_path_buf() _ => {}
} else {
std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))?
.join(dir_path)
};
if is_excluded(&full_dir, exclude_dirs) {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs) {
continue;
}
collect_sources_rec(&path.to_string_lossy(), lang, out, exclude_dirs)?;
continue;
}
if !path.is_file() {
continue;
}
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or("")
.to_lowercase();
let file = normalize_source_path(&path);
let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed {
out.push(file);
} }
} }
Ok(()) if out.is_empty() {
} out.extend(["c"]);
fn is_excluded(path: &Path, exclude_dirs: &[PathBuf]) -> bool {
exclude_dirs.iter().any(|dir| path.starts_with(dir))
}
fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
} }
if let Ok(base) = std::env::current_dir() out
&& let Ok(rel) = path.strip_prefix(&base)
{
return format!("./{}", rel.to_string_lossy());
}
path.to_string_lossy().to_string()
} }
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> { fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
@@ -221,7 +178,7 @@ fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
fn default_flags(profile: &str) -> &'static [&'static str] { fn default_flags(profile: &str) -> &'static [&'static str] {
match profile { match profile {
"release" => &["/O2", "/DNDEBUG"], "release" => &["/O2", "/DNDEBUG"],
"debug" => &["/Od", "/Zi", "/W4", "/DDEBUG", "/Oy-"], "debug" => &["/Od", "/Zi", "/W4", "/DDCR_DEBUG", "/Oy-"],
_ => &[], _ => &[],
} }
} }
@@ -235,11 +192,11 @@ fn build_objects(
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let mut objects = Vec::new(); let mut objects = Vec::new();
for source in sources { for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext); let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() { if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
} }
if needs_rebuild(source, &obj_path) { if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler); let mut cmd = Command::new(compiler);
cmd.arg("/nologo"); cmd.arg("/nologo");
match ctx.language.to_lowercase().as_str() { match ctx.language.to_lowercase().as_str() {
@@ -270,34 +227,57 @@ fn build_objects(
cmd.arg(format!("/I{dir}")); cmd.arg(format!("/I{dir}"));
} }
cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path)); cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path));
match cmd.status() { cmd.arg("/showIncludes");
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()), if std::env::var("DCR_DEBUG").is_ok() {
Err(err) => return Err(format!("Build failed: {err}")), eprintln!("[dcr] {:?}", cmd);
} }
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut headers = Vec::new();
let mut clean_stdout = String::new();
for line in stdout.lines() {
if let Some(stripped) = line.strip_prefix("Note: including file:") {
headers.push(stripped.trim().to_string());
} else if let Some(stripped) = line.strip_prefix("Примечание: включение файла:")
{
headers.push(stripped.trim().to_string());
} else {
clean_stdout.push_str(line);
clean_stdout.push('\n');
}
}
if !output.status.success() {
eprint!("{}", clean_stdout);
eprint!("{}", stderr);
return Err("Build failed".to_string());
} else {
let trimmed_out = clean_stdout.trim();
let trimmed_err = stderr.trim();
let src_filename = Path::new(source)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("");
if !trimmed_out.is_empty() && trimmed_out != src_filename {
print!("{}", clean_stdout);
}
if !trimmed_err.is_empty() {
eprintln!("{}", trimmed_err);
}
}
let d_path = Path::new(&obj_path).with_extension("d");
let mut d_content = format!("{}: \\\n", obj_path.replace('\\', "/"));
for h in headers {
let escaped = h.replace('\\', "/").replace(" ", "\\ ");
d_content.push_str(&format!(" {} \\\n", escaped));
}
std::fs::write(&d_path, d_content).map_err(|err| format!("d file error: {err}"))?;
} }
objects.push(obj_path); objects.push(obj_path);
} }
Ok(objects) Ok(objects)
} }
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -1,12 +1,14 @@
use crate::core::builder::BuildContext; use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform; use crate::platform;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::Instant; use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> { pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.language.to_lowercase().as_str() != "asm" { let lang = ctx.language.to_lowercase();
if lang.split('+').any(|p| p.trim() != "asm") {
return Err("NASM backend requires build.language = \"asm\"".to_string()); return Err("NASM backend requires build.language = \"asm\"".to_string());
} }
let assembler = if ctx.compiler.is_empty() { let assembler = if ctx.compiler.is_empty() {
@@ -15,7 +17,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.compiler ctx.compiler
}; };
let start_time = Instant::now(); let start_time = Instant::now();
let sources = collect_sources(ctx.language, ctx.exclude_dirs)?; let sources = collect_sources(ctx)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?; let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
@@ -35,6 +37,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -72,6 +77,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg("-o").arg(out_path); cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -82,76 +90,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} }
} }
pub(crate) fn collect_sources( pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
language: &str, // NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed)
exclude_dirs: &[PathBuf], common::collect_sources(
) -> Result<Vec<String>, String> { ctx.source_roots,
let lang = language.to_lowercase(); &["asm", "s"],
let mut sources = Vec::new(); ctx.exclude_dirs,
collect_sources_rec("./src", &lang, &mut sources, exclude_dirs)?; ctx.include_paths,
sources.sort(); )
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(
dir: &str,
lang: &str,
out: &mut Vec<String>,
exclude_dirs: &[PathBuf],
) -> Result<(), String> {
let dir_path = Path::new(dir);
let full_dir = if dir_path.is_absolute() {
dir_path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))?
.join(dir_path)
};
if is_excluded(&full_dir, exclude_dirs) {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs) {
continue;
}
collect_sources_rec(&path.to_string_lossy(), lang, out, exclude_dirs)?;
continue;
}
if !path.is_file() {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext = ext_raw.to_lowercase();
let file = normalize_source_path(&path);
let allowed = lang == "asm" && (ext == "asm" || (ext == "s" && ext_raw == "s"));
if allowed {
out.push(file);
}
}
Ok(())
}
fn is_excluded(path: &Path, exclude_dirs: &[PathBuf]) -> bool {
exclude_dirs.iter().any(|dir| path.starts_with(dir))
}
fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
}
if let Ok(base) = std::env::current_dir()
&& let Ok(rel) = path.strip_prefix(&base)
{
return format!("./{}", rel.to_string_lossy());
}
path.to_string_lossy().to_string()
} }
fn build_objects( fn build_objects(
@@ -164,11 +110,11 @@ fn build_objects(
let mut objects = Vec::new(); let mut objects = Vec::new();
let format = nasm_format(ctx.platform); let format = nasm_format(ctx.platform);
for source in sources { for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext); let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() { if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
} }
if needs_rebuild(source, &obj_path) { if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler); let mut cmd = Command::new(assembler);
cmd.arg("-f") cmd.arg("-f")
.arg(format) .arg(format)
@@ -178,6 +124,9 @@ fn build_objects(
for flag in ctx.cflags { for flag in ctx.cflags {
cmd.arg(flag); cmd.arg(flag);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => {} Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()), Ok(_) => return Err("Build failed".to_string()),
@@ -224,24 +173,3 @@ fn nasm_format(platform: Option<&str>) -> &'static str {
"elf64" "elf64"
} }
} }
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -1,7 +1,8 @@
use crate::core::builder::BuildContext; use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform; use crate::platform;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::time::Instant; use std::time::Instant;
@@ -12,7 +13,13 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.compiler ctx.compiler
}; };
let start_time = Instant::now(); let start_time = Instant::now();
let sources = collect_sources(ctx.language, ctx.exclude_dirs)?; let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?; let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
@@ -23,6 +30,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -44,12 +54,6 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.lib_dirs { for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}")); cmd.arg(format!("-L{dir}"));
} }
@@ -66,6 +70,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg("-o").arg(out_path); cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -76,82 +83,31 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} }
} }
pub(crate) fn collect_sources( pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
language: &str, let extensions = source_extensions(ctx.language);
exclude_dirs: &[PathBuf], common::collect_sources(
) -> Result<Vec<String>, String> { ctx.source_roots,
let lang = language.to_lowercase(); &extensions,
let mut sources = Vec::new(); ctx.exclude_dirs,
collect_sources_rec("./src", &lang, &mut sources, exclude_dirs)?; ctx.include_paths,
sources.sort(); )
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
} }
fn collect_sources_rec( fn source_extensions(language: &str) -> Vec<&str> {
dir: &str, let mut out = Vec::new();
lang: &str, for part in language.split('+') {
out: &mut Vec<String>, let lang = part.trim().to_lowercase();
exclude_dirs: &[PathBuf], match lang.as_str() {
) -> Result<(), String> { "c" => out.extend(["c"]),
let dir_path = Path::new(dir); "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
let full_dir = if dir_path.is_absolute() { "asm" => out.extend(["s", "S", "asm"]),
dir_path.to_path_buf() _ => {}
} else {
std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))?
.join(dir_path)
};
if is_excluded(&full_dir, exclude_dirs) {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs) {
continue;
}
collect_sources_rec(&path.to_string_lossy(), lang, out, exclude_dirs)?;
continue;
}
if !path.is_file() {
continue;
}
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or("")
.to_lowercase();
let file = normalize_source_path(&path);
let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed {
out.push(file);
} }
} }
Ok(()) if out.is_empty() {
} out.extend(["c"]);
fn is_excluded(path: &Path, exclude_dirs: &[PathBuf]) -> bool {
exclude_dirs.iter().any(|dir| path.starts_with(dir))
}
fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
} }
if let Ok(base) = std::env::current_dir() out
&& let Ok(rel) = path.strip_prefix(&base)
{
return format!("./{}", rel.to_string_lossy());
}
path.to_string_lossy().to_string()
} }
fn default_flags(profile: &str) -> &'static [&'static str] { fn default_flags(profile: &str) -> &'static [&'static str] {
@@ -163,7 +119,7 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-fno-omit-frame-pointer", "-fno-omit-frame-pointer",
"-DDEBUG", "-DDCR_DEBUG",
], ],
_ => &[], _ => &[],
} }
@@ -178,11 +134,11 @@ fn build_objects(
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let mut objects = Vec::new(); let mut objects = Vec::new();
for source in sources { for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext); let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() { if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
} }
if needs_rebuild(source, &obj_path) { if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler); let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path); cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
if ctx.kind == "sharedlib" { if ctx.kind == "sharedlib" {
@@ -200,6 +156,9 @@ fn build_objects(
cmd.arg(format!("-std={}", ctx.standard)); cmd.arg(format!("-std={}", ctx.standard));
} }
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {
if ctx.platform.is_some() && *flag == "-march=native" {
continue;
}
cmd.arg(flag); cmd.arg(flag);
} }
for flag in ctx.cflags { for flag in ctx.cflags {
@@ -208,6 +167,11 @@ fn build_objects(
for dir in ctx.include_dirs { for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}")); cmd.arg(format!("-I{dir}"));
} }
let d_path = Path::new(&obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => {} Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()), Ok(_) => return Err("Build failed".to_string()),
@@ -227,24 +191,3 @@ fn asm_lang_flag(source: &str) -> Option<&'static str> {
_ => None, _ => None,
} }
} }
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -115,7 +115,12 @@ impl Config {
.and_then(|v| v.as_table()) .and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?; .ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
for key in ["language", "standard", "compiler"] { if let Some(language) = build.get("language") {
self.validate_language(language)?;
} else {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
for key in ["standard", "compiler"] {
let value = build.get(key).and_then(|v| v.as_str()).unwrap_or(""); let value = build.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() { if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("build.{key} is empty"))); return Err(ConfigError::Invalid(format!("build.{key} is empty")));
@@ -144,6 +149,73 @@ impl Config {
return Err(ConfigError::Invalid("build.kind is invalid".into())); return Err(ConfigError::Invalid("build.kind is invalid".into()));
} }
} }
if let Some(exclude) = build.get("exclude") {
let arr = exclude.as_array().ok_or_else(|| {
ConfigError::Invalid("build.exclude must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.exclude contains empty value".into(),
));
}
}
}
if let Some(include) = build.get("include") {
let arr = include.as_array().ok_or_else(|| {
ConfigError::Invalid("build.include must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.include contains empty value".into(),
));
}
}
}
if let Some(roots) = build.get("roots") {
let arr = roots.as_array().ok_or_else(|| {
ConfigError::Invalid("build.roots must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.roots contains empty value".into(),
));
}
}
}
if let Some(src_disable) = build.get("src_disable")
&& !src_disable.is_bool()
{
return Err(ConfigError::Invalid(
"build.src_disable must be a boolean".into(),
));
}
if let Some(clean) = build.get("clean") {
let arr = clean.as_array().ok_or_else(|| {
ConfigError::Invalid("build.clean must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.clean contains empty value".into(),
));
}
}
}
for profile in ["release", "debug"] {
if let Some(section) = build.get(profile) {
let table = section.as_table().ok_or_else(|| {
ConfigError::Invalid(format!("build.{profile} must be a table"))
})?;
self.validate_profile_section(profile, table)?;
}
}
self.validate_workspace()?; self.validate_workspace()?;
Ok(()) Ok(())
} }
@@ -161,6 +233,115 @@ impl Config {
} }
impl Config { impl Config {
fn validate_language(&self, value: &Value) -> Result<(), ConfigError> {
if let Some(s) = value.as_str() {
if s.trim().is_empty() {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
return Ok(());
}
let arr = value
.as_array()
.ok_or_else(|| ConfigError::Invalid("build.language must be string or array".into()))?;
if arr.is_empty() {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.language contains empty value".into(),
));
}
}
Ok(())
}
fn validate_profile_section(
&self,
profile: &str,
table: &toml::value::Table,
) -> Result<(), ConfigError> {
if let Some(lang) = table.get("language") {
self.validate_language(lang)?;
}
for key in ["standard", "compiler", "kind", "target", "platform"] {
if let Some(value) = table.get(key) {
let s = value.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} is empty"
)));
}
}
}
if let Some(kind) = table.get("kind").and_then(|v| v.as_str()) {
let kind = kind.trim();
if !kind.is_empty() && kind != "bin" && kind != "staticlib" && kind != "sharedlib" {
return Err(ConfigError::Invalid(format!(
"build.{profile}.kind is invalid"
)));
}
}
if let Some(src_disable) = table.get("src_disable")
&& !src_disable.is_bool()
{
return Err(ConfigError::Invalid(format!(
"build.{profile}.src_disable must be boolean"
)));
}
for key in [
"cflags",
"ldflags",
"exclude",
"include",
"roots",
"pkg_config",
"generated",
"expect",
"clean",
] {
if let Some(val) = table.get(key) {
let arr = val.as_array().ok_or_else(|| {
ConfigError::Invalid(format!(
"build.{profile}.{key} must be an array of strings"
))
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} contains empty value"
)));
}
}
}
}
for key in ["steps", "post_steps"] {
if let Some(val) = table.get(key) {
let arr = val.as_array().ok_or_else(|| {
ConfigError::Invalid(format!("build.{profile}.{key} must be an array"))
})?;
for item in arr {
let tbl = item.as_table().ok_or_else(|| {
ConfigError::Invalid(format!(
"build.{profile}.{key} entries must be tables"
))
})?;
for req in ["name", "in", "out", "cmd"] {
let s = tbl.get(req).and_then(|v| v.as_str()).unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} missing {req}"
)));
}
}
}
}
}
Ok(())
}
fn validate_workspace(&self) -> Result<(), ConfigError> { fn validate_workspace(&self) -> Result<(), ConfigError> {
let Some(workspace) = self.get("workspace").and_then(|v| v.as_table()) else { let Some(workspace) = self.get("workspace").and_then(|v| v.as_table()) else {
return Ok(()); return Ok(());
@@ -264,7 +445,12 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let language = build.get("language").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 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 compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
@@ -274,7 +460,11 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
out.push_str(&format!("version = \"{version}\"\n\n")); out.push_str(&format!("version = \"{version}\"\n\n"));
out.push_str("[build]\n"); out.push_str("[build]\n");
out.push_str(&format!("language = \"{language}\"\n")); if language.starts_with('[') {
out.push_str(&format!("language = {language}\n"));
} else {
out.push_str(&format!("language = \"{language}\"\n"));
}
out.push_str(&format!("standard = \"{standard}\"\n")); out.push_str(&format!("standard = \"{standard}\"\n"));
out.push_str(&format!("compiler = \"{compiler}\"\n")); out.push_str(&format!("compiler = \"{compiler}\"\n"));
let kind = build let kind = build
@@ -393,3 +583,220 @@ fn set_path(value: &mut Value, path: &[&str], new_value: Value) -> Result<(), Co
Err(ConfigError::Invalid("empty key".into())) Err(ConfigError::Invalid("empty key".into()))
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("dcr_cfg_test_{prefix}_{n}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn write_toml_file(dir: &Path, content: &str) -> PathBuf {
let path = dir.join("dcr.toml");
fs::write(&path, content).unwrap();
path
}
fn minimal_valid_toml() -> &'static str {
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\n"
}
#[test]
fn open_valid_toml() {
let dir = temp_dir("open_valid");
let path = write_toml_file(&dir, minimal_valid_toml());
let config = Config::open(&path.to_string_lossy());
assert!(config.is_ok(), "should open valid toml");
}
#[test]
fn open_invalid_toml_syntax() {
let dir = temp_dir("open_invalid");
let path = write_toml_file(&dir, "this is not [valid toml !!!");
let config = Config::open(&path.to_string_lossy());
assert!(config.is_err(), "should fail on invalid TOML syntax");
}
#[test]
fn open_nonexistent_fails() {
let result = Config::open("/tmp/dcr_nonexistent_file_12345.toml");
assert!(result.is_err(), "should fail on nonexistent file");
}
#[test]
fn validate_missing_package_fails() {
let dir = temp_dir("no_package");
let path = write_toml_file(
&dir,
"[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "missing [package] should fail validation");
}
#[test]
fn validate_missing_build_fails() {
let dir = temp_dir("no_build");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "missing [build] should fail validation");
}
#[test]
fn validate_empty_language_fails() {
let dir = temp_dir("empty_lang");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"\"\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "empty language should fail validation");
}
#[test]
fn validate_language_array() {
let dir = temp_dir("lang_array");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = [\"c\", \"c++\", \"asm\"]\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_ok(), "language array should be valid");
}
#[test]
fn validate_language_array_empty_fails() {
let dir = temp_dir("lang_array_empty");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = []\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "empty language array should fail");
}
#[test]
fn validate_unknown_kind_fails() {
let dir = temp_dir("bad_kind");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"exe\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "unknown kind 'exe' should fail validation");
}
#[test]
fn validate_valid_kinds() {
for kind in ["bin", "staticlib", "sharedlib"] {
let dir = temp_dir(&format!("kind_{kind}"));
let content = format!(
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"{kind}\"\n\n[dependencies]\n"
);
let path = write_toml_file(&dir, &content);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_ok(), "kind '{kind}' should be valid");
}
}
#[test]
fn get_values() {
let dir = temp_dir("get_values");
let path = write_toml_file(&dir, minimal_valid_toml());
let config = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(
config.get("package.name").and_then(|v| v.as_str()),
Some("test")
);
assert_eq!(
config.get("build.language").and_then(|v| v.as_str()),
Some("c")
);
assert_eq!(
config.get("build.kind").and_then(|v| v.as_str()),
Some("bin")
);
assert!(config.get("nonexistent.key").is_none());
}
#[test]
fn set_and_read_back() {
let dir = temp_dir("set_value");
let path = write_toml_file(&dir, minimal_valid_toml());
let mut config = Config::open(&path.to_string_lossy()).unwrap();
config
.set("package.name", Value::String("newname".to_string()))
.unwrap();
assert_eq!(
config.get("package.name").and_then(|v| v.as_str()),
Some("newname")
);
// Verify persisted to disk
let config2 = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(
config2.get("package.name").and_then(|v| v.as_str()),
Some("newname")
);
}
#[test]
fn new_creates_default_config() {
let dir = temp_dir("new_default");
let path = dir.join("dcr.toml");
let config = Config::new(&path.to_string_lossy()).unwrap();
assert!(path.exists(), "dcr.toml should be created");
assert_eq!(
config.get("build.language").and_then(|v| v.as_str()),
Some("c")
);
assert_eq!(
config.get("build.standard").and_then(|v| v.as_str()),
Some("c11")
);
assert_eq!(
config.get("build.compiler").and_then(|v| v.as_str()),
Some("clang")
);
assert_eq!(
config.get("build.kind").and_then(|v| v.as_str()),
Some("bin")
);
}
#[test]
fn validate_workspace_empty_path_fails() {
let dir = temp_dir("ws_empty_path");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[workspace]\n[workspace.member1]\npath = \"\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(
result.is_err(),
"workspace member with empty path should fail"
);
}
#[test]
fn check_returns_bool() {
let dir = temp_dir("check_bool");
let path = write_toml_file(&dir, minimal_valid_toml());
let config = Config::open(&path.to_string_lossy()).unwrap();
assert!(config.check(), "valid config should return true");
}
}

View File

@@ -50,7 +50,8 @@ pub fn resolve_deps(
let mut lock_packages = Vec::new(); let mut lock_packages = Vec::new();
for dep in deps { for dep in deps {
let dep_path = resolve_path(project_root, &dep.path_raw)?; let dep_path_raw = expand_profile(&dep.path_raw, profile);
let dep_path = resolve_path(project_root, &dep_path_raw)?;
if !dep_path.is_dir() { if !dep_path.is_dir() {
return Err(format!( return Err(format!(
"Dependency '{}' path is not a directory: {}", "Dependency '{}' path is not a directory: {}",
@@ -59,8 +60,13 @@ pub fn resolve_deps(
)); ));
} }
let include = resolve_paths(&dep_path, dep.include_raw.as_deref(), &["include"])?; let include = resolve_paths(&dep_path, dep.include_raw.as_deref(), &["include"], profile)?;
let lib = resolve_paths(&dep_path, dep.lib_raw.as_deref(), &["lib", "lib64"])?; let lib = resolve_paths(
&dep_path,
dep.lib_raw.as_deref(),
&["lib", "lib64"],
profile,
)?;
let libs_list = dep let libs_list = dep
.libs_raw .libs_raw
.clone() .clone()
@@ -200,11 +206,13 @@ fn resolve_paths(
base: &Path, base: &Path,
raw: Option<&[String]>, raw: Option<&[String]>,
defaults: &[&str], defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> { ) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new(); let mut out = Vec::new();
if let Some(raw) = raw { if let Some(raw) = raw {
for r in raw { for r in raw {
let p = Path::new(r); let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() { let full = if p.is_absolute() {
p.to_path_buf() p.to_path_buf()
} else { } else {
@@ -227,6 +235,10 @@ fn resolve_paths(
Ok(out) Ok(out)
} }
fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> { fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() { if dst.exists() {
fs::remove_dir_all(dst)?; fs::remove_dir_all(dst)?;

View File

@@ -21,6 +21,7 @@ fn main() {
"build" => cli::build::build(rest), "build" => cli::build::build(rest),
"run" => cli::run::run(rest), "run" => cli::run::run(rest),
"clean" => cli::clean::clean(rest), "clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),
"--version" => { "--version" => {
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET")); println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET"));
0 0

View File

@@ -18,3 +18,57 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
None => format!("./target/{profile}/lib{name}.so"), None => format!("./target/{profile}/lib{name}.so"),
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bin_path_default() {
assert_eq!(bin_path("debug", "hello", None), "./target/debug/hello");
}
#[test]
fn bin_path_release() {
assert_eq!(bin_path("release", "hello", None), "./target/release/hello");
}
#[test]
fn bin_path_custom_target() {
assert_eq!(bin_path("debug", "hello", Some("out")), "out/hello");
}
#[test]
fn bin_path_custom_target_trailing_slash() {
assert_eq!(bin_path("debug", "hello", Some("out/")), "out/hello");
}
#[test]
fn lib_path_default() {
assert_eq!(
lib_path("debug", "mylib", None),
"./target/debug/libmylib.a"
);
}
#[test]
fn lib_path_custom_target() {
assert_eq!(lib_path("debug", "mylib", Some("out")), "out/libmylib.a");
}
#[test]
fn shared_lib_path_default() {
assert_eq!(
shared_lib_path("debug", "mylib", None),
"./target/debug/libmylib.so"
);
}
#[test]
fn shared_lib_path_custom_target() {
assert_eq!(
shared_lib_path("release", "mylib", Some("dist")),
"dist/libmylib.so"
);
}
}

297
tests/cli_errors.rs Normal file
View File

@@ -0,0 +1,297 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
cmd.output().expect("failed to run dcr")
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
fn available_compiler() -> Option<&'static str> {
for candidate in ["clang", "gcc", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if ok {
return Some(candidate);
}
}
None
}
// --- Tests ---
#[test]
fn no_args_shows_help() {
let dir = unique_sandbox_dir("noargs");
let out = run_dcr(&[], &dir);
// Should succeed (help returns 0) and produce some output
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("DCR") || stdout.contains("dcr") || stdout.contains("USAGE"),
"no args should show help text"
);
}
#[test]
fn unknown_command_fails() {
let dir = unique_sandbox_dir("unknown_cmd");
let out = run_dcr(&["foobar"], &dir);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("error") || stdout.contains("Unknown"),
"unknown command should print error"
);
}
#[test]
fn build_without_toml_fails() {
let dir = unique_sandbox_dir("build_no_toml");
let out = run_dcr(&["build"], &dir);
assert!(!out.status.success(), "build without dcr.toml should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("dcr.toml") || stdout.contains("not found"),
"should mention missing dcr.toml"
);
}
#[test]
fn run_without_toml_fails() {
let dir = unique_sandbox_dir("run_no_toml");
let out = run_dcr(&["run"], &dir);
assert!(!out.status.success(), "run without dcr.toml should fail");
}
#[test]
fn clean_without_toml_fails() {
let dir = unique_sandbox_dir("clean_no_toml");
let out = run_dcr(&["clean"], &dir);
assert!(!out.status.success(), "clean without dcr.toml should fail");
}
#[test]
fn new_existing_dir_fails() {
let dir = unique_sandbox_dir("new_exist");
// Create the project directory first
std::fs::create_dir_all(dir.join("hello")).expect("failed to create dir");
let out = run_dcr(&["new", "hello"], &dir);
assert!(!out.status.success(), "dcr new on existing dir should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("already exists") || stdout.contains("error"),
"should report dir already exists"
);
}
#[test]
fn init_nonempty_dir_fails() {
let dir = unique_sandbox_dir("init_nonempty");
// Put a file in the dir so it's not empty
std::fs::write(dir.join("dummy.txt"), "x").expect("failed to write");
let out = run_dcr(&["init"], &dir);
assert!(
!out.status.success(),
"dcr init in non-empty dir should fail"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("not empty") || stdout.contains("error"),
"should report dir not empty"
);
}
#[test]
fn build_unknown_flag_fails() {
let dir = unique_sandbox_dir("build_badflag");
// Init a valid project first
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
let out = run_dcr(&["build", "--foobar"], &dir);
assert!(!out.status.success(), "build with unknown flag should fail");
}
#[test]
fn run_library_project_fails() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping run_library test");
return;
};
let dir = unique_sandbox_dir("run_lib");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
// Edit dcr.toml to set kind = "staticlib"
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("kind = \"bin\"", "kind = \"staticlib\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["run"], &dir, &envs);
assert!(!out.status.success(), "dcr run on staticlib should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("library") || stdout.contains("Cannot run"),
"should report cannot run library"
);
}
#[test]
fn clean_specific_profile() {
let dir = unique_sandbox_dir("clean_profile");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
// Create target/debug and target/release dirs
std::fs::create_dir_all(dir.join("target").join("debug")).expect("create debug");
std::fs::write(dir.join("target").join("debug").join("dummy.o"), "x").expect("write");
std::fs::create_dir_all(dir.join("target").join("release")).expect("create release");
std::fs::write(dir.join("target").join("release").join("dummy.o"), "x").expect("write");
// Clean only release
let out = run_dcr(&["clean", "--release"], &dir);
assert!(out.status.success(), "clean --release should succeed");
assert!(
!dir.join("target").join("release").exists(),
"target/release should be removed"
);
assert!(
dir.join("target").join("debug").is_dir(),
"target/debug should remain"
);
}
#[test]
fn new_no_name_fails() {
let dir = unique_sandbox_dir("new_noname");
let out = run_dcr(&["new"], &dir);
assert!(!out.status.success(), "dcr new without name should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("not specified") || stdout.contains("error"),
"should report name not specified"
);
}
#[test]
fn staticlib_build() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping staticlib test");
return;
};
let dir = unique_sandbox_dir("staticlib");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("read dcr.toml");
let updated = toml.replace("kind = \"bin\"", "kind = \"staticlib\"");
std::fs::write(&toml_path, updated).expect("write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "staticlib build should succeed");
// Check that a .a file exists
let lib_path = dir.join("target").join("debug");
let has_lib = std::fs::read_dir(&lib_path)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.file_name().to_string_lossy().ends_with(".a"))
})
.unwrap_or(false);
assert!(has_lib, "staticlib should produce a .a file");
}
#[test]
fn build_release_profile() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping release build test");
return;
};
let dir = unique_sandbox_dir("release_build");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "release build should succeed");
assert!(
dir.join("target").join("release").is_dir(),
"target/release should exist"
);
}
#[test]
fn version_contains_version_string() {
let dir = unique_sandbox_dir("version");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
// Should contain version like "dcr 0.2.8 (target)"
assert!(
stdout.contains("dcr "),
"version output should start with 'dcr '"
);
assert!(
stdout.contains('.'),
"version output should contain a version number"
);
}