14 Commits

Author SHA1 Message Date
2de836a421 feat: Add installation of Linux cross compilation tools for builds 2026-05-13 20:39:03 +08:00
ed85f801e9 refactor: Improve registry sorting logic for package resolution 2026-05-13 19:43:25 +08:00
9d8dcda366 style: Update macOS target descriptions for clarity 2026-05-13 19:41:26 +08:00
0193834bfd feat: Enhance dependency management and project setup
- Added support for registry-based dependencies in the dependencies section.
- Updated the package section to include a project type field.
- Introduced a new CLI command `dcr setup` to load and display registry configurations.
- Implemented a new module for handling registry operations, including loading and resolving packages from registries.
- Enhanced the build process to handle library packaging, including copying headers and libraries to the appropriate target directories.
- Updated the CLI to support building library packages and added tests for library package builds.
- Improved error handling and output messages during dependency resolution and package fetching.
2026-05-13 19:24:53 +08:00
debc5d2044 Added:
- Full Git dependency support in `dcr.toml`
  - Support for `git`, `branch`, `tag`, and `rev` fields
  - Automatic cloning and checkout via `git2`
  - Dependencies are stored in `target/<profile>/deps/git/`
- New `dcr add` command for easy dependency management
  - Short syntax: `github:user/repo`, `gitlab:user/repo`, `path:/to/lib`
  - Automatic GitHub resolution for `git:user/repo`
- Visual feedback for dependency fetching with in-place status updates ("Fetching" → "Fetched")
- Modular dependency management architecture in `src/core/deps/`
2026-05-12 01:29:26 +08:00
00ed418291 New TODO.md 2026-05-11 23:22:12 +08:00
7cefbb3c3a feat: Implement cross-compilation with target inheritance and host defaults 2026-04-12 14:23:01 +08:00
045cc90d81 style: update code format 2026-04-11 13:37:29 +08:00
fd88e036ed feat: add cross-compilation support with --target flag
- Updated build and run commands to accept a --target option for specifying target platforms.
- Enhanced documentation to include cross-compilation usage and supported targets.
- Modified configuration to allow target-specific settings and toolchains.
- Introduced a new test command to run project tests with specified profiles and targets.
- Improved handling of target directories and output structure for different platforms.
- Added support for multiple targets in the configuration file.
- Implemented inheritance for workspace and dependencies based on target and profile.
2026-04-11 13:36:18 +08:00
613fbc6a6a refactor: run cargo fmt --all 2026-04-10 14:33:48 +08:00
03f72adade feat: Update version to 0.4.0, add test command, and improve documentation for testing functionality 2026-04-10 14:31:58 +08:00
cd5023243b feat: Update documentation with new sections for workspace guide, flags & environment, and troubleshooting map; enhance CLI reference and build/run notes for improved clarity. 2026-03-21 14:15:06 +08:00
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
67 changed files with 5989 additions and 815 deletions

View File

@@ -42,19 +42,68 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
# GNU/Linux
# x86_64
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
asset_name: dcr-x86_64-unknown-linux-gnu asset_name: dcr-x86_64-unknown-linux-gnu
# ARM64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
asset_name: dcr-aarch64-unknown-linux-gnu
# Musl/Linux
# x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
asset_name: dcr-x86_64-unknown-linux-musl
# ARM64
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
asset_name: dcr-aarch64-unknown-linux-musl
# MacOS
# x86_64 (Intel Mac, macOS 10.12+)
- os: macos-15-intel - os: macos-15-intel
target: x86_64-apple-darwin target: x86_64-apple-darwin
asset_name: dcr-x86_64-apple-darwin asset_name: dcr-x86_64-apple-darwin
- os: macos-15
# ARM64 (Apple Silicon Mac, macOS 11+)
- os: macos-latest
target: aarch64-apple-darwin target: aarch64-apple-darwin
asset_name: dcr-aarch64-apple-darwin asset_name: dcr-aarch64-apple-darwin
# Windows MSVC
# x86_64
- os: windows-latest - os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
asset_name: dcr-x86_64-pc-windows-msvc.exe asset_name: dcr-x86_64-pc-windows-msvc.exe
# ARM64
- os: windows-latest
target: aarch64-pc-windows-msvc
asset_name: dcr-aarch64-pc-windows-msvc.exe
# Windows GNU
# x86_64
- os: windows-latest
target: x86_64-pc-windows-gnu
asset_name: dcr-x86_64-pc-windows-gnu.exe
# Windows LLVM
# x86_64
- os: windows-latest
target: x86_64-pc-windows-gnullvm
asset_name: dcr-x86_64-pc-windows-gnullvm.exe
# ARM64
- os: windows-latest
target: aarch64-pc-windows-gnullvm
asset_name: dcr-aarch64-pc-windows-gnullvm.exe
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: notes needs: notes
steps: steps:
@@ -65,6 +114,12 @@ jobs:
name: release-notes name: release-notes
path: .github path: .github
- name: Install Linux Cross Compilation Tools
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu musl-tools
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:

121
AGENT.md
View File

@@ -2,11 +2,11 @@
## Project Overview ## Project Overview
**DCR (Dexoron Cargo Realization)** — CLI-утилита для управления C/C++/ASM проектами в стиле Cargo. **DCR (Dexoron Cargo Realization)** — CLI tool for managing C/C++/ASM projects in a Cargo-style.
- **Язык реализации**: Rust (edition 2024) - **Implementation Language**: Rust (edition 2024)
- **Лицензия**: MIT - **License**: MIT
- **Репозиторий**: [github.com/dexoron/dcr](https://github.com/dexoron/dcr) - **Repository**: [github.com/dexoron/dcr](https://github.com/dexoron/dcr)
## Architecture ## Architecture
@@ -14,47 +14,35 @@
``` ```
src/ src/
├── main.rs # Точка входа, маршрутизация CLI-команд ├── main.rs # Entry point, CLI command routing
├── config.rs # Глобальные константы: профили сборки, шаблон main.c ├── config.rs # Global constants
├── cli/ # Реализация CLI-команд ├── cli/ # CLI command implementation
│ ├── new.rs # dcr new <name> │ ├── new.rs # dcr new <name>
│ ├── init.rs # dcr init │ ├── init.rs # dcr init
│ ├── build.rs # dcr build [--debug|--release] │ ├── setup.rs # dcr setup (registry initialization)
│ ├── run.rs # dcr run [--debug|--release] │ ├── add.rs # dcr add <name>
│ ├── clean.rs # dcr clean [--all] │ ├── build.rs # dcr build
│ ├── run.rs # dcr run
│ ├── clean.rs # dcr clean
│ ├── help.rs # dcr --help │ ├── help.rs # dcr --help
│ └── flag_update.rs # dcr --update (self-update через GitHub Releases) │ └── flag_update.rs # dcr --update
├── core/ # Бизнес-логика ├── core/ # Business logic
│ ├── config.rs # Парсинг/валидация/форматирование dcr.toml (struct Config) │ ├── config.rs # dcr.toml parsing
│ ├── deps.rs # Разрешение path-зависимостей, генерация dcr.lock │ ├── registry.rs # Package index management (RegistryManager)
│ ├── runner.rs # Запуск скомпилированного бинарника │ ├── deps/ # Dependency resolution (path, git)
│ ├── workspace.rs # Поддержка [workspace] и упорядочивание сборки │ ├── runner.rs # Binary execution
│ └── builder/ # Бэкенды компиляторов/ассемблеров │ ├── workspace.rs # [workspace] support
│ ├── mod.rs # Общий интерфейс сборки │ └── builder/ # Compiler backends (gcc, clang, msvc, nasm, gas)
│ ├── gcc.rs # GCC backend ├── platform/ # Platform-specific logic
│ ├── clang.rs # Clang backend └── utils/ # Helper utilities
│ ├── 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 ### Key Concepts
- **`dcr.toml`** — конфигурационный файл проекта (аналог `Cargo.toml`). Секции: `[package]`, `[build]`, `[toolchain]`, `[dependencies]`, `[workspace]`. - **`~/.dcr/`**: Centralized storage for registries and configurations.
- **Профили сборки**: `debug` (по умолчанию) и `release`. Встроенные флаги зависят от компилятора. Пользовательские флаги через `build.cflags` / `build.ldflags`. - **`config.toml`**: File in `~/.dcr/` defining available package indices and their priorities.
- **Инкрементальная сборка**: объектные файлы кешируются по mtime. Отслеживание заголовков пока не реализовано (см. `TODO.md`). - **`dcr.toml`**: Local project configuration.
- **Workspace**: корневой `dcr.toml` с секцией `[workspace]` описывает member-проекты и их зависимости; сборка происходит в топологическом порядке. - **Registry System**: `RegistryManager` handles package discovery across multiple indices based on priority.
- **Path dependencies**: зависимости по путям автоматически добавляют include/lib-пути; генерируется `dcr.lock`.
- **Build kinds**: `bin` (бинарник), `staticlib` (`.a`), `sharedlib` (`.so`/`.dylib`/`.dll`).
## Supported Platforms ## Supported Platforms
@@ -69,58 +57,21 @@ src/
| Crate | Purpose | | Crate | Purpose |
|---|---| |---|---|
| `reqwest` | HTTP-запросы для `--update` (blocking, rustls-tls) | | `reqwest` | HTTP requests |
| `serde` | Сериализация/десериализация | | `serde` | Serialization |
| `toml` | Парсинг `dcr.toml` | | `toml` | Configuration parsing |
| `sha2` | Хеширование при обновлении | | `git2` | Git dependency handling |
| `self-replace` | Замена бинарника при `--update` |
## Development ## Development
### Build & Run ### Build & Run
```bash ```bash
cargo build # debug build cargo build --release
cargo build --release # release build ./target/release/dcr setup
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 ## Code Conventions
- Rust stable edition 2024.
- Rust stable edition 2024 - `cargo fmt` and `cargo clippy -- -D warnings`.
- Форматирование: `cargo fmt` - CLI output in English.
- Линтинг: `cargo clippy` без warnings - Use `utils::log` for messaging.
- Функции — маленькие и сфокусированные
- Имена — понятные и описательные
- Пользовательские сообщения 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,7 +1,94 @@
# Changelog # Changelog
## [0.2.10] - 2026-03-16 ## [0.6.5] - 2026-05-13
Added: Added:
- New dependency registry system
- Package type support (`lib`, `app`, `none`) in `dcr.toml`
- Library packaging functionality: automatically generates `include` and `lib` directories in `target/` for `lib` type projects
- Expanded CI build targets:
- Linux: `aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`
- Windows: `aarch64-pc-windows-msvc`, `x86_64-pc-windows-gnu`, `aarch64-pc-windows-gnullvm`
Changed:
- `src/core/deps/` modular architecture overhaul
- Native platform pathing defaults for Linux (now dynamic architecture detection)
- Improved `run` command output handling
## [0.6.0] - 2026-05-12
Added:
- Full Git dependency support in `dcr.toml`
- Support for `git`, `branch`, `tag`, and `rev` fields
- Automatic cloning and checkout via `git2`
- Dependencies are stored in `target/<profile>/deps/git/`
- New `dcr add` command for easy dependency management
- Short syntax: `github:user/repo`, `gitlab:user/repo`, `path:/to/lib`
- Automatic GitHub resolution for `git:user/repo`
- Visual feedback for dependency fetching with in-place status updates ("Fetching" → "Fetched")
- Modular dependency management architecture in `src/core/deps/`
## [0.5.1] - 2026-04-12
Fixed:
- Windows compatibility for target-specific config without --target flag
Added:
- Default host target detection for automatic target overrides
- Unit tests for target normalization
Improved:
- Config value lookup optimization
- Documentation for Windows cross-compilation
## [0.5.0] - 2026-04-10
Added:
- Full inheritance system for all config sections: `build`, `toolchain`, `run`, `workspace`, `dependencies`
- Order: `section.target.profile` → `section.profile.target` → `section.target` → `section.profile` → `section`
- `inherit = false` to disable inheritance completely
- Target-specific overrides: `[build.linux]`, `[toolchain.windows]`, `[run.macos]`, etc.
- Cross-compilation support with `--target <triple>`
- Full target triples and short names (`linux`, `macos`, `windows`)
- Target-specific config sections: `[build.<target>]`, `[toolchain.<target>]`, `[run.<target>]`
- Workspace and dependencies support target/profile inheritance
- Multiple targets build: `[build.targets]` to build for multiple targets simultaneously
- Target directory structure: `target/<target>/<profile>/` (native or triple)
- Updated documentation with cross-compilation guide and Arch Linux examples
## [0.4.0] - 2026-04-10
Added:
- Test command: `dcr test` and `dcr tests`
- Automatic compilation and linking for tests without manual configuration
- `dcr test --init` creates test template and header
- Test framework with EXPECT/SKIP macros in `dcr_test.h`
- Improved test integration: added test CLI module and basic test suite
- Documentation updates for testing functionality
## [0.3.0] - 2026-03-18
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.steps` for pre-build generators (e.g., `moc`/`uic`/`rcc`)
- `build.pkg_config` for `pkg-config`-driven cflags/ldflags resolution - `build.pkg_config` for `pkg-config`-driven cflags/ldflags resolution
- `[toolchain]` support for `uic`, `moc`, `rcc` - `[toolchain]` support for `uic`, `moc`, `rcc`
@@ -9,20 +96,25 @@ Added:
- `{profile}` placeholder support for dependency paths - `{profile}` placeholder support for dependency paths
## [0.2.9] - 2026-03-14 ## [0.2.9] - 2026-03-14
Added: Added:
- Header dependency tracking for fine-grained incremental builds (rebuilds when `#include` files change) - Header dependency tracking for fine-grained incremental builds (rebuilds when `#include` files change)
- Complete internal rewrite of the builder module: merged duplicate compilation backends - Complete internal rewrite of the builder module: merged duplicate compilation backends
Changed: Changed:
- `cflags` and `default_flags` no longer incorrectly leak to the linker stage in GCC/Clang - `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 - 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 - Greatly expanded test suite with 34 new tests covering configuration, CLI errors, and path operations
## [0.2.8] - 2026-03-06 ## [0.2.8] - 2026-03-06
Added: 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
@@ -31,12 +123,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
@@ -44,24 +140,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
@@ -69,20 +172,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
@@ -91,31 +200,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

View File

@@ -22,6 +22,27 @@ cargo check
- Keep functions small and focused. - Keep functions small and focused.
- Prefer clear names over clever ones. - Prefer clear names over clever ones.
## Target System
DCR supports cross-compilation via `--target <triple>` or short names:
- `dcr build --target linux --release` (maps to `x86_64-unknown-linux-gnu`)
- `dcr build --target macos --release` (maps to `x86_64-apple-darwin`)
- `dcr build --target windows --release` (maps to `x86_64-pc-windows-msvc`)
- Full triples: `dcr build --target aarch64-linux-gnu --release`
- Configure multiple targets in `dcr.toml`:
```
[build.targets]
targets = ["linux", "macos"]
[build.linux]
compiler = "gcc"
[build.macos]
compiler = "clang"
```
- If no `--target`, builds for targets in `build.targets` or native if empty.
## Checks ## Checks
Before PR, run: Before PR, run:

152
Cargo.lock generated
View File

@@ -54,6 +54,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver",
"libc",
"shlex", "shlex",
] ]
@@ -69,6 +71,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "console"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
dependencies = [
"encode_unicode",
"libc",
"unicode-width",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@@ -90,12 +104,16 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.2.10" version = "0.6.5"
dependencies = [ dependencies = [
"console",
"git2",
"glob", "glob",
"indicatif",
"reqwest", "reqwest",
"self-replace", "self-replace",
"serde", "serde",
"serde_json",
"sha2", "sha2",
"toml", "toml",
] ]
@@ -121,6 +139,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -263,6 +287,21 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "git2"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"openssl-probe",
"openssl-sys",
"url",
]
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.3" version = "0.3.3"
@@ -510,6 +549,19 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "indicatif"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
dependencies = [
"console",
"portable-atomic",
"unicode-width",
"unit-prefix",
"web-time",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
@@ -532,6 +584,16 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.85" version = "0.3.85"
@@ -554,6 +616,46 @@ version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libgit2-sys"
version = "0.18.4+1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7"
dependencies = [
"cc",
"libc",
"libssh2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libssh2-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.11.0"
@@ -601,6 +703,24 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -619,6 +739,18 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.4" version = "0.1.4"
@@ -1241,12 +1373,24 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unit-prefix"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@@ -1271,6 +1415,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.2.10" version = "0.6.5"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
@@ -11,3 +11,7 @@ serde = { version = "1", features = ["derive"] }
self-replace = "1" self-replace = "1"
sha2 = "0.10" sha2 = "0.10"
toml = "1.0.3" toml = "1.0.3"
indicatif = "0.18.4"
console = "0.16.3"
git2 = "0.20.4"
serde_json = "1"

View File

@@ -14,16 +14,23 @@ 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
- Dependency management:
- Git and Path dependencies with `dcr.lock`
- Registry-based dependency resolution
- Library packaging functionality (auto-generate `include/` and `lib/` for `type = "lib"` packages)
- ASM projects with NASM/GAS or via GCC/Clang - ASM projects with NASM/GAS or via GCC/Clang
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs) - Mixed language projects
- Cross-compilation with `--target` (full triple support and short names)
- Target-specific configurations and inheritance
- Update the binary via `dcr --update` (GitHub Releases)
## Supported Platforms ## Supported Platforms
- Linux: `x86_64-unknown-linux-gnu` - Linux: x86_64/aarch64 (GNU and Musl)
- macOS Intel: `x86_64-apple-darwin` - macOS: x86_64/aarch64
- macOS Apple Silicon: `aarch64-apple-darwin` - Windows: x86_64/aarch64 (MSVC, GNU, LLVM)
- Windows: `x86_64-pc-windows-msvc`
## Installation ## Installation
@@ -102,11 +109,22 @@ Creates a project with the specified name in the current directory.
### `dcr init` ### `dcr init`
Creates a project in the current directory. The project name is taken from the directory name. The directory must be empty. Creates a project in the current directory. The project name is taken from the directory name. The directory must be empty.
### `dcr setup`
Initializes the registry system by reading `~/.dcr/config.toml` and verifying the availability of configured indices.
### `dcr add <name> <source>`
Adds a dependency to `dcr.toml`.
If a registry is configured, `dcr` automatically finds the package.
Git/Path syntax: `path:`, `github:`, `gitlab:`, `git:`, or a full URL.
Use `--branch`, `--tag`, or `--rev` for Git dependencies.
### `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>`
@@ -115,20 +133,63 @@ Run manually:
Removes the `target` directory in the project root. Removes the `target` directory in the project root.
Use `dcr clean --all` in a workspace root to clean all member projects. Use `dcr clean --all` in a workspace root to clean all member projects.
### `dcr gen`
Generates IDE integration files and build tools.
- `dcr gen vscode`: VS Code workspace config
- `dcr gen clion`: CLion project files
- `dcr gen compile-commands`: `compile_commands.json` for clang tools
### `dcr test`
Runs the test suite.
Use `dcr test --init` to create test files.
## Build Profiles ## Build Profiles
Two profiles are supported: Two profiles are supported:
- `--debug` (default) - built-in flags per compiler - `--debug` (default) - built-in flags per compiler
- `--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.
## Cross-compilation
DCR supports cross-compilation via `--target <triple>` or short names:
```sh
dcr build --target linux --release # x86_64-unknown-linux-gnu
dcr build --target macos --release # x86_64-apple-darwin
dcr build --target windows --release # x86_64-pc-windows-msvc
dcr run --target aarch64-linux-gnu
```
Configure multiple targets and target-specific settings in `dcr.toml`:
```toml
[build.targets]
targets = ["linux", "windows"]
[build.linux]
compiler = "gcc"
cflags = ["-O2"]
[build.windows]
compiler = "x86_64-w64-mingw32-gcc"
```
See [Cross-compilation guide](docs/dependencies-and-build/cross-compilation.md) for details.
## Configuration ## Configuration
The main project file is `dcr.toml`. The main project file is `dcr.toml`.
For multi-registry management, use `~/.dcr/config.toml`.
Example `dcr.toml`: Example `dcr.toml`:
@@ -136,16 +197,22 @@ Example `dcr.toml`:
[package] [package]
name = "hello" name = "hello"
version = "0.1.0" version = "0.1.0"
type = "app" # or "lib", "none"
[build] [build]
language = "c" language = "c"
standard = "c11" standard = "c11"
compiler = "clang" compiler = "clang"
kind = "bin" # "bin", "staticlib", "sharedlib"
# Optional platform hint # Optional platform hint
# platform = "x86_64" # platform = "x86_64"
# 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,22 +221,27 @@ ldflags = ["-lm"]
# as = "as" # as = "as"
# ar = "ar" # ar = "ar"
# ld = "ld" # ld = "ld"
# uic = "uic"
# moc = "moc"
# rcc = "rcc"
[dependencies] [dependencies]
my_lib = "path:./libs/my_lib"
cool_lib = { git = "https://github.com/user/cool_lib", tag = "v1.0" }
``` ```
Workspace example: Workspace example:
...
```toml
[workspace] [workspace]
kernel = { path = "src/kernel", deps = ["core", "userspace"] } kernel = { path = "src/kernel", deps = ["core", "userspace"] }
core = { path = "src/core", deps = ["userspace"] } core = { path = "src/core", deps = ["userspace"] }
userspace = { path = "src/userspace" } userspace = { path = "src/userspace" }
``` ```
Path dependencies are supported. DCR will resolve them on build and generate `dcr.lock`. Git and Path dependencies are supported. DCR will resolve them on build and generate `dcr.lock`.
Incremental build note: object files are rebuilt when source `.c/.cpp` or included header files change.
Incremental build note: object files are rebuilt when source `.c/.cpp` files change. Header dependency tracking is not implemented yet.
## Requirements ## Requirements
- Rust toolchain (`rustc`, `cargo`) - for building DCR from source - Rust toolchain (`rustc`, `cargo`) - for building DCR from source

74
TODO.md
View File

@@ -1,9 +1,67 @@
# TODO # DCR TODO.md
- Add external package managers (Conan, vcpkg) Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers.
- Linker script + direct linker control
- Custom targets/steps ## 🔥 High Priority (Next Releases)
- 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) - [x] **Git dependencies support**
- Per-member include directories in workspace (e.g. include paths for subprojects) `git = "https://github.com/user/repo.git#branch"` and `git = "..."@tag`
- Build-time code generation (auto-generated headers, pre-build scripts) - [ ] **Conan and vcpkg integration**
Automatic download, installation and linking of packages
- [ ] **Full freestanding / embedded support**
`-nostdlib`, linker scripts, bare-metal targets (i686-elf, aarch64-none-elf, etc.)
- [ ] **EFI support**
Dedicated platform configuration for EFI targets
- [ ] **Custom project name**
Override binary name in `dcr.toml`
- [ ] **Custom build steps / targets**
Ability to define custom pre-build, post-build, codegen steps in `dcr.toml`
- [ ] **Improved mixed C/C++/ASM projects**
Better handling of `.S` and `.asm` files with correct flags
- [ ] **dcr publish + official package registry**
Publish and fetch your own libraries easily
## ✅ Medium Priority
- [ ] **Static analysis** (`dcr check`)
Integration with clang-tidy, cppcheck, include-what-you-use
- [ ] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
- [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.)
- [ ] **Better cross-compilation experience**
Ready-to-use toolchains + automatic download
- [ ] **Linker script support** + direct linker control
- [ ] **Workspaces improvements**
Per-member include directories, selective builds, better dependency graph
- [ ] **Build-time code generation**
Support for protobuf, flatbuffers, Qt moc, custom generators
## 📌 Low Priority / Nice to Have
- [ ] Support for Meson and Ninja as alternative backends
- [ ] Dockerfile generation (`dcr docker init`)
- [ ] First-class Windows MSVC support (currently works but rough)
- [ ] Documentation generation (`dcr doc`)
- [ ] Sanitizers integration (`dcr sanitize`)
- [ ] Benchmarks and comparison with CMake/Meson
- [ ] C++20 modules support (when it becomes stable)
## 🧠 Future Ideas
- Official DCR package registry (dcrhub)
- GUI wrapper (dcr-gui)
- Plugin system
- Toolchain manager (like rustup)
- Zig as alternative compiler
- deb/rpm/flatpak/AppImage packaging support
---
**Currently in progress:**
- ...
**Completed in recent releases:**
- Workspaces
- Cross-compilation
- IDE config generation (VS Code, CLion, compile_commands.json)
- Self-update (`dcr --update`)
- Header tracking

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

@@ -21,9 +21,12 @@
* [Global options](commands/global-options.md) * [Global options](commands/global-options.md)
* [dcr init/new](commands/dcr-init-or-new.md) * [dcr init/new](commands/dcr-init-or-new.md)
* [dcr add](commands/dcr-add.md)
* [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)
* [dcr test](commands/dcr-test.md)
## Dependencies & Build ## Dependencies & Build
@@ -31,11 +34,15 @@
* [Local dependencies](dependencies-and-build/local-dependencies.md) * [Local dependencies](dependencies-and-build/local-dependencies.md)
* [Versioning & lock file](dependencies-and-build/versioning-and-lock-file.md) * [Versioning & lock file](dependencies-and-build/versioning-and-lock-file.md)
* [Target directory](dependencies-and-build/target-directory.md) * [Target directory](dependencies-and-build/target-directory.md)
* [Cross-compilation and targets](dependencies-and-build/cross-compilation.md)
* [Compilers & linking](dependencies-and-build/compilers-and-linking.md) * [Compilers & linking](dependencies-and-build/compilers-and-linking.md)
## Reference & Help ## Reference & Help
* [CLI reference](reference-and-help/cli-reference.md) * [CLI reference](reference-and-help/cli-reference.md)
* [Config reference](reference-and-help/config-reference.md) * [Config reference](reference-and-help/config-reference.md)
* [Workspace guide](reference-and-help/workspace-guide.md)
* [Flags & environment](reference-and-help/flags-and-environment.md)
* [Troubleshooting map](reference-and-help/troubleshooting-map.md)
* [FAQ](reference-and-help/faq.md) * [FAQ](reference-and-help/faq.md)
* [Contributing & Bug reports](reference-and-help/contributing-and-bug-reports.md) * [Contributing & Bug reports](reference-and-help/contributing-and-bug-reports.md)

32
docs/commands/dcr-add.md Normal file
View File

@@ -0,0 +1,32 @@
# dcr add
Add a dependency to `dcr.toml`.
## Usage
```bash
dcr add <name> <source> [options]
```
## Sources
- `path:./path/to/lib`: Local directory.
- `github:user/repo`: GitHub repository.
- `gitlab:user/repo`: GitLab repository.
- `git:host.com/user/repo`: Generic Git repository (HTTPS).
- `https://...`: Full Git URL.
## Options
- `--branch <name>`: Use a specific branch.
- `--tag <name>`: Use a specific tag.
- `--rev <sha>`: Use a specific commit hash.
## Examples
```bash
dcr add my_lib path:./libs/my_lib
dcr add cool_lib github:user/cool_lib
dcr add shared_lib gitlab:org/shared --branch develop
dcr add custom_lib git:git.internal.com/r/lib --tag v1.0.0
```

View File

@@ -8,6 +8,9 @@ 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 --target linux --release
dcr build --release --force
dcr build --debug --clean
``` ```
## What `build` does ## What `build` does
@@ -30,17 +33,36 @@ 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. - Profile flag (`--debug` / `--release`) can appear in any argument position (duplicates are rejected).
- Unknown profile flags return an error. - Unknown profile flags return an error.
- Incremental rebuild is based on source/object mtime comparison. - `--target <triple>` builds for specified target (short names: `linux`, `macos`, `windows`).
- If no `--target`, builds for `build.targets` or native.
- Incremental rebuild checks source/object mtime and tracked header dependencies.
- `--force` skips build cache checks and recompiles.
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
- Default GCC/Clang profile flags: `debug` -> `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`, `release` -> `-O3 -DNDEBUG`.
- 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

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

@@ -0,0 +1,170 @@
# 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`
- Generated `launch.json` uses CodeLLDB (`type: "lldb"`), so install `vadimcn.vscode-lldb`
### `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`
- If VSCode reports unsupported debugger type, install the extension that matches `launch.json` type (`vadimcn.vscode-lldb` for `lldb`)
### 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,9 @@ 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 --target linux --release
dcr run --release --force
dcr run --debug --clean
``` ```
## Behavior ## Behavior
@@ -16,11 +19,21 @@ 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.
- `--target <triple>` runs the binary for specified target (short names: `linux`, `macos`, `windows`).
- `--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

44
docs/commands/dcr-test.md Normal file
View File

@@ -0,0 +1,44 @@
# dcr test
Runs the project's test suite.
## Usage
```sh
dcr test
dcr test --release
dcr test --target linux
dcr test --init
```
## What `test` does
1. Builds the project with the specified profile/target.
2. Compiles test files in `tests/` (e.g., `tests/test.c`) into a test binary.
3. Runs the test binary and parses output for PASS/SKIP/FAIL results.
4. Reports summary with colored output.
## Test Structure
DCR uses a simple test framework:
- Header: `tests/dcr_test.h` (generated by `dcr test --init`)
- Test file: `tests/test.c` (template generated by `dcr test --init`)
- Macros: `TEST(name)`, `TEST_CASE(name)`, `EXPECT(condition)`, `SKIP(reason)`
- Output: `[PASS] test_name`, `[FAIL] test_name`, or `[SKIP] test_name`
## Config values used
- `package.name`
- `build.compiler`, `build.language`, `build.standard`
- `build.cflags`, `build.ldflags`
- `build.debug` / `build.release`
- `build.targets` (if specified)
## Notes
- Test binary is linked with the main project (for lib projects) or standalone.
- `--init` creates `tests/dcr_test.h` and `tests/test.c` if missing.
- Tests run in release mode by default for performance.
- Custom test logic can be added by editing `tests/test.c`.
- Fails if no `tests/test.c` exists (run `dcr test --init` first).

View File

@@ -14,12 +14,14 @@ 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 -DDCR_DEBUG` - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG -march=native` - `release`: `-O3 -DNDEBUG`
### MSVC toolchain ### MSVC toolchain
@@ -38,7 +40,7 @@ Default compiler flags:
GCC/Clang: GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG -march=native` - `release`: `-O3 -DNDEBUG`
MSVC: MSVC:
@@ -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

@@ -9,22 +9,123 @@ standard = "c11"
compiler = "clang" compiler = "clang"
kind = "bin" kind = "bin"
# optional: # optional:
# inherit = true # Inherit from base (default true)
# targets = ["linux", "macos"] # Multiple targets to build
# target = "./dist" # target = "./dist"
# 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`.
- `inherit` (bool, optional): inherit settings from base `[build]` (default `true`). If `false`, only use target/profile specific settings.
- `targets` (string array, optional): list of targets to build simultaneously. Supports short names (`linux`, `macos`, `windows`).
- `target` (string, optional): custom output directory for final artifact. - `target` (string, optional): custom output directory for final artifact.
- `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.
Target tables override profile and base settings for specific targets. Full inheritance order:
1. `[build.<target>.<profile>]`
2. `[build.<profile>.<target>]`
3. `[build.<target>]`
4. `[build.<profile>]`
5. `[build]`
If custom `cflags` are set (non-empty), default compiler flags are disabled.
```toml
[build.debug]
cflags = ["-g3"]
[build.release]
cflags = ["-O3"]
ldflags = ["-s"]
# Target-specific overrides (applied after profile)
[build.linux]
compiler = "gcc"
cflags = ["-march=x86_64"]
[build.macos]
compiler = "clang"
cflags = ["-arch", "x86_64"]
# Full inheritance example
[build.linux.release]
cflags = ["-O3", "-march=native"] # Overrides profile and base
[build.release.linux]
ldflags = ["-static"] # Alternative order
[build.windows.debug]
compiler = "x86_64-w64-mingw32-gcc"
cflags = [] # Empty to disable defaults
[build.release]
inherit = false # No inheritance, only explicit settings
```
## Toolchain section ## Toolchain section
@@ -35,6 +136,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

@@ -27,7 +27,8 @@ kind = "bin"
- `[dependencies]` - `[dependencies]`
Workspace-only root: Workspace-only root:
- `[workspace]` may be used instead of `[package]`/`[build]` for a workspace root. - `[workspace]` can be added to a normal root config to define members and dependency order.
- Root config still requires `[package]`, `[build]`, and `[dependencies]`.
## Required keys ## Required keys
@@ -42,9 +43,29 @@ Workspace-only root:
## Optional keys ## Optional keys
- `build.inherit`
- `build.targets`
- `build.target` - `build.target`
- `build.platform`
- `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`
- `build.<target>` (target-specific overrides)
- 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`)
- `[toolchain.<target>]` (target-specific toolchain)
- `[workspace]` members - `[workspace]` members
- `[workspace.<target>]` (target-specific workspace)
- `[run]` overrides (`cmd`)
- `[run.<target>]` (target-specific run)

View File

@@ -1,23 +1,36 @@
# Dependencies section # Dependencies section
DCR currently supports path dependencies. DCR supports registry-based dependencies and path dependencies.
## Example ## Example
```toml ```toml
[dependencies] [dependencies]
# Registry dependency
mylib = "0.1.0"
# Git dependency
json = { git = "https://github.com/nlohmann/json", tag = "v3.11.2" }
# Path dependency
frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"] } frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"] }
``` ```
## Supported fields per dependency ## Supported dependency types
### Registry dependencies
Defined by name and version string. DCR automatically resolves the package from the configured registry index.
### Git dependencies
Defined by repository URL and optionally branch, tag, or revision.
- `git` (string, required): repository URL.
- `branch`, `tag`, `rev` (string, optional): Git reference.
### Path dependencies
- `path` (string, required): absolute path or path relative to project root. - `path` (string, required): absolute path or path relative to project root.
- `include` (string array, optional): include directories inside dependency. - `include` (string array, optional): include directories inside dependency.
- `lib` (string array, optional): library directories inside dependency. - `lib` (string array, optional): library directories inside dependency.
- `libs` (string array, optional): library names for linker. - `libs` (string array, optional): library names for linker.
- `system` (bool, optional): currently `system = true` is rejected.
## Defaults ## Defaults for path dependencies
- If `include` is omitted, DCR tries `<dep>/include`. - If `include` is omitted, DCR tries `<dep>/include`.
- If `lib` is omitted, DCR tries `<dep>/lib`, then `<dep>/lib64`. - If `lib` is omitted, DCR tries `<dep>/lib`, then `<dep>/lib64`.
@@ -25,6 +38,7 @@ frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"
## Validation ## Validation
- Dependency value must be a TOML table. - Dependency value must be a string (registry) or a TOML table (path).
- `path` is mandatory. - Registry dependencies use the version string to match versions in the registry.
- `path` is mandatory for path dependencies.
- `include`/`lib`/`libs` must be arrays of strings if provided. - `include`/`lib`/`libs` must be arrays of strings if provided.

View File

@@ -6,15 +6,18 @@
[package] [package]
name = "my-app" name = "my-app"
version = "0.1.0" version = "0.1.0"
type = "app"
``` ```
## Fields ## Fields
- `name` (string, required): used as output artifact base name. - `name` (string, required): used as output artifact base name.
- `version` (string, required): used in `dcr.lock` project entry. - `version` (string, required): used in `dcr.lock` project entry.
- `type` (string, optional): project type (`app`, `lib`, `none`).
## Notes ## Notes
- Empty `name` or `version` makes config invalid. - Empty `name` or `version` makes config invalid.
- `dcr new` sets `name` from the passed project name. - `dcr new` sets `name` from the passed project name.
- `dcr init` sets `name` from current directory name. - `dcr init` sets `name` from current directory name.
- If `type = "lib"`, DCR generates `include/` and `lib/` directories in the target path, copying public headers and built libraries.

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`.
- Each source is compiled to `target/<profile>/obj/<relative-path>.o` (`.obj` on MSVC). - `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/<target>/<profile>/obj/<relative-path>.o` (`.obj` on MSVC, where `<target>` is `native` or target triple).
- 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
@@ -25,13 +28,15 @@ These flags are applied automatically based on profile and compiler backend.
GCC/Clang: GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` - `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG -march=native` - `release`: `-O3 -DNDEBUG`
MSVC: MSVC:
- `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-` - `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
- `release`: `/O2 /DNDEBUG` - `release`: `/O2 /DNDEBUG`
Note: If custom `cflags` are set in `dcr.toml`, default profile flags are disabled to allow full control.
## Platform hint ## Platform hint
If `build.platform` is set, DCR passes it as an architecture hint: If `build.platform` is set, DCR passes it as an architecture hint:
@@ -46,10 +51,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

@@ -0,0 +1,201 @@
# Cross-compilation and Targets
DCR supports cross-compilation to different platforms using the `--target` flag. This allows building your project for architectures and operating systems different from the host.
## Basic Usage
```bash
# Build for a specific target
dcr build --target x86_64-unknown-linux-gnu --release
# Short names for common platforms
dcr build --target linux --release # x86_64-unknown-linux-gnu
dcr build --target macos --release # x86_64-apple-darwin
dcr build --target windows --release # x86_64-pc-windows-msvc
# Run for a specific target
dcr run --target linux --release
```
## Supported Targets
### Short Names
- `linux` → `x86_64-unknown-linux-gnu`
- `macos` → `x86_64-apple-darwin`
- `windows` → `x86_64-pc-windows-msvc`
### Full Target Triples
DCR supports any valid target triple in the format `<arch>-<vendor>-<os>-<abi>`. Common examples:
- `x86_64-unknown-linux-gnu`
- `aarch64-linux-gnu`
- `x86_64-apple-darwin`
- `aarch64-apple-darwin`
- `x86_64-pc-windows-msvc`
- `i686-pc-windows-gnu`
## Configuration
### Multiple Targets
Configure multiple targets to build simultaneously:
```toml
[build.targets]
targets = ["linux", "macos", "windows"]
# Or per profile
[build.release.targets]
targets = ["linux", "macos"]
```
If no `--target` is specified, DCR builds for all targets in `build.targets` (or native if empty).
### Target-Specific Settings
Override settings for specific targets:
```toml
[build.linux]
compiler = "gcc"
cflags = ["-Wall", "-O2"]
[build.macos]
compiler = "clang"
cflags = ["-Wall", "-O2", "-arch", "x86_64"]
[build.windows]
compiler = "x86_64-w64-mingw32-gcc"
ldflags = ["-static"]
```
### Target-Specific Toolchain
Configure toolchain per target:
```toml
[toolchain.linux]
cc = "gcc"
ld = "ld"
[toolchain.macos]
cc = "clang"
ld = "ld"
[toolchain.windows]
cc = "x86_64-w64-mingw32-gcc"
ld = "x86_64-w64-mingw32-ld"
```
### Target-Specific Run Commands
Use custom run commands per target:
```toml
[run.linux]
cmd = "./target/linux/release/myapp"
[run.macos]
cmd = "open ./target/macos/release/myapp.app"
[run.windows]
cmd = "wine ./target/windows/release/myapp.exe"
```
## Workspace and Dependencies Inheritance
Workspace and dependencies sections also support target/profile inheritance:
```toml
[workspace.linux]
members = [
{ name = "lib1", path = "libs/lib1" },
]
[dependencies.linux]
sdl2 = { system = true, include = ["/usr/include/SDL2"] }
[dependencies.windows]
sdl2 = { system = true, include = ["C:/SDL2/include"] }
```
Order: `workspace.target.profile` → `workspace.profile.target` → `workspace.target` → `workspace.profile` → `workspace`.
Add `inherit = false` to disable inheritance in any section.
Settings are applied in order:
1. `[build.<target>]`
2. `[build.<profile>]`
3. `[build]`
### Toolchain Configuration
For cross-compilation, configure toolchains:
```toml
[toolchain]
cc = "aarch64-linux-gnu-gcc"
cxx = "aarch64-linux-gnu-g++"
ar = "aarch64-linux-gnu-ar"
ld = "aarch64-linux-gnu-ld"
```
## Output Structure
Build outputs are organized by target:
```
target/
├── native/
│ ├── debug/
│ └── release/
├── x86_64-unknown-linux-gnu/
│ ├── debug/
│ └── release/
└── aarch64-linux-gnu/
├── debug/
└── release/
```
## Requirements
- Cross-compilation requires appropriate toolchains installed (e.g., `aarch64-linux-gnu-gcc` on Arch Linux).
- For Windows targets on Linux, install MinGW: `pacman -S mingw-w64-gcc`.
- For macOS targets on Linux, use `osxcross` or similar.
## Examples
### ARM Linux Build
```bash
# Install toolchain (Arch Linux)
pacman -S aarch64-linux-gnu-gcc
# Configure
echo '[build.aarch64-linux-gnu]
compiler = "aarch64-linux-gnu-gcc"' >> dcr.toml
# Build
dcr build --target aarch64-linux-gnu --release
```
### Windows Build on Linux
```bash
# Install toolchain (Arch Linux)
pacman -S mingw-w64-gcc
# Configure
echo '[build.windows]
compiler = "x86_64-w64-mingw32-gcc"' >> dcr.toml
# Build
dcr build --target windows --release
```
## Troubleshooting
- **Toolchain not found**: Ensure the compiler is installed and in PATH, or configure `[toolchain]` in `dcr.toml`. For Arch Linux: `pacman -S aarch64-linux-gnu-gcc` for ARM, `pacman -S mingw-w64-gcc` for Windows cross-compilation.
- **Linking errors**: Check `ldflags` and library paths for cross-compilation.
- **Header issues**: Include paths may differ; use target-specific `cflags`.
For more help, see [Troubleshooting map](troubleshooting-map.md).

View File

@@ -2,25 +2,39 @@
Dependencies are defined in `dcr.toml` under `[dependencies]`. Dependencies are defined in `dcr.toml` under `[dependencies]`.
## Full example ## Full example (Path)
```toml ```toml
[dependencies] [dependencies]
fmt = { path = "./third_party/fmt", include = ["include"], lib = ["lib"], libs = ["fmt"] } fmt = { path = "./third_party/fmt", include = ["include"], lib = ["lib"], libs = ["fmt"] }
``` ```
## Full example (Git)
```toml
[dependencies]
json = { git = "https://github.com/nlohmann/json", tag = "v3.11.2" }
```
## Field reference ## Field reference
- `path`: required dependency root path. - `path`: local path to dependency root.
- `git`: URL of Git repository.
- `branch`: Git branch name.
- `tag`: Git tag name.
- `rev`: Git commit SHA.
- `include`: optional include subpaths (absolute or relative to dep root). - `include`: optional include subpaths (absolute or relative to dep root).
- `lib`: optional library subpaths (absolute or relative to dep root). - `lib`: optional library subpaths (absolute or relative to dep root).
- `libs`: optional linker library names. - `libs`: optional linker library names.
## Common valid compact form ## Common valid compact forms
```toml ```toml
[dependencies] [dependencies]
mylib = { path = "./libs/mylib" } mylib = { path = "./libs/mylib" }
gitlib = { git = "https://github.com/user/repo" }
# Shortest form for path
simple = "./libs/simple"
``` ```
In this form DCR will try defaults for include/lib dirs and use `mylib` as the linker library name. In these forms DCR will try defaults for include/lib dirs and use the dependency name as the linker library name.

View File

@@ -1,6 +1,6 @@
# Target directory # Target directory
By default, DCR writes final artifacts to `target/<profile>/`. By default, DCR writes final artifacts to `target/<target>/<profile>/` (where `<target>` is `native` or target triple).
You can override this with `build.target`: You can override this with `build.target`:
@@ -20,4 +20,4 @@ target = "./dist"
## Important detail ## Important detail
`build.target` changes only the final artifact location. Object cache and dependency sync directories still use `target/<profile>/...`. `build.target` changes only the final artifact location. Object cache and dependency sync directories still use `target/<target>/<profile>/...`.

View File

@@ -8,12 +8,15 @@ Debug (default):
dcr build dcr build
# or # or
dcr build --debug dcr build --debug
dcr build --target linux --debug
dcr build --debug --clean
``` ```
Release: Release:
```sh ```sh
dcr build --release dcr build --release
dcr build --target windows --release
``` ```
## Run ## Run
@@ -21,7 +24,10 @@ dcr build --release
```sh ```sh
dcr run dcr run
# or # or
dcr run --target macos
dcr run --release dcr run --release
dcr run --target linux --release
dcr run --release --force
``` ```
`dcr run` first builds, then launches the produced binary. `dcr run` first builds, then launches the produced binary.
@@ -44,6 +50,8 @@ dcr clean --release --all
## Notes ## Notes
- `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`. - `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`.
- Build profile flag is parsed from the first command argument. - Build profile flag (`--debug` / `--release`) can be passed in any argument position.
- 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.
- `--target` builds/runs for specified target (short names: `linux`, `macos`, `windows`).
- `--clean` for `build`/`run` removes `target/<target>/<profile>` and paths from `build.clean`.

View File

@@ -31,10 +31,10 @@ project/
- `dcr.toml`: main project configuration. - `dcr.toml`: main project configuration.
- `dcr.lock`: generated lock file for resolved path dependencies. - `dcr.lock`: generated lock file for resolved path dependencies.
- `src/`: recursively scanned source directory. - `src/`: recursively scanned source directory.
- `target/<profile>/obj/`: cached object files for incremental rebuild. - `target/<target>/<profile>/obj/`: cached object files for incremental rebuild (where `<target>` is `native` or target triple).
- `target/<profile>/deps/`: synchronized copies of path dependencies. - `target/<target>/<profile>/deps/`: synchronized copies of path dependencies.
- `target/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`. - `target/<target>/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`.
- `target/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`. - `target/<target>/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`.
- `target/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`. - `target/<target>/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`.
If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<profile>/obj`. If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<target>/<profile>/obj`.

View File

@@ -5,9 +5,12 @@
```text ```text
dcr new <name> dcr new <name>
dcr init dcr init
dcr setup
dcr add <name> <source>(optional)
dcr build [--debug|--release] dcr build [--debug|--release]
dcr run [--debug|--release] dcr run [--debug|--release]
dcr clean [--debug|--release] [--all] dcr clean [--debug|--release] [--all]
dcr gen <subcommand>
dcr --help dcr --help
dcr --version dcr --version
dcr --update dcr --update
@@ -15,12 +18,13 @@ dcr --update
## Notes on argument parsing ## Notes on argument parsing
- Most commands parse profile from the first argument. - `build`/`run` parse flags in any order (`--debug|--release`, `--force`, `--clean`) and reject duplicates.
- `new` requires exactly one argument. - `new` requires exactly one argument.
- `init` and `--update` do not accept extra arguments. - `init` and `--update` do not accept extra arguments.
- `clean` accepts `--debug|--release` and optional `--all`. - `clean` accepts `--debug|--release` and optional `--all`.
- `gen` supports `project-info`, `compile-commands`, `vscode`, `clion`.
## Exit behavior overview ## Exit behavior overview
- Successful command execution returns `0`. - Successful command execution returns `0`.
- Validation/build/runtime errors return non-zero status. - Validation/build/runtime failures return non-zero status.

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`)

View File

@@ -2,7 +2,7 @@
## What commands are available? ## What commands are available?
`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`. `new`, `init`, `build`, `run`, `clean`, `gen`, `--help`, `--version`, `--update`.
## Is `dcr.toml` mandatory? ## Is `dcr.toml` mandatory?
@@ -21,7 +21,8 @@ Yes. Set `build.compiler` in `dcr.toml` (for example `gcc`, `clang`, `cl`, `clan
## How does incremental rebuild work? ## How does incremental rebuild work?
It compares source file mtime with object file mtime. Header dependency tracking is not implemented yet. It compares source file mtime with object file mtime.
Header dependencies are tracked too: if an included header changes (or disappears), rebuild is triggered.
## What does `dcr run` do if build fails? ## What does `dcr run` do if build fails?

View File

@@ -0,0 +1,48 @@
# Flags & environment
This page summarizes runtime flags and environment variables that affect DCR behavior.
## Build/run flags
Used with `dcr build` and `dcr run`:
- `--debug`: select debug profile.
- `--release`: select release profile.
- `--force`: ignore incremental cache checks and rebuild.
- `--clean`: remove `target/<profile>` and `build.clean` paths before build.
Notes:
- Flags can be passed in any order.
- Duplicate profile flags are rejected.
## Clean flags
Used with `dcr clean`:
- `--debug` / `--release`: clean only selected profile directory.
- `--all`: in workspace root, also clean all members.
## Environment variables
Tool selection overrides:
- `DCR_COMPILER` (all languages)
- `DCR_CC`, `DCR_CXX`, `DCR_AS`, `DCR_AR`, `DCR_LD`
Diagnostics:
- `DCR_DEBUG=1` enables verbose build logging.
## Quick examples
```sh
# Force full release rebuild
dcr build --release --force
# Clean before debug run
dcr run --clean --debug
# Use custom compiler from env
DCR_CC=clang-18 dcr build --debug
```

View File

@@ -0,0 +1,75 @@
# Troubleshooting map
Fast mapping from symptom to likely cause and first action.
## `dcr.toml file not found`
Likely cause:
- you are outside project root (or member root).
First action:
```sh
pwd
ls
```
Run DCR from directory that contains `dcr.toml`.
## `Invalid config: missing [package]` / `missing [build]`
Likely cause:
- incomplete config in root project.
First action:
- ensure root `dcr.toml` includes `[package]`, `[build]`, and `[dependencies]`.
- for workspace setups, keep these sections in root and add `[workspace]` additionally.
## `Unknown build flag` or profile-flag errors
Likely cause:
- typo in flag or duplicate profile flag.
First action:
```sh
dcr --help
```
Use only supported flags and at most one of `--debug` / `--release`.
## Dependency include/lib path errors
Likely cause:
- path in `[dependencies]` does not exist.
First action:
- verify `path`, `include`, `lib` values.
- if using profile placeholders, confirm expanded paths exist for selected profile.
## `Cannot run library build`
Likely cause:
- `build.kind` is `staticlib` or `sharedlib`.
First action:
- switch `build.kind` to `bin`, or set `[run].cmd` if you intentionally need custom run command.
## Build succeeds but output is stale
Likely cause:
- stale artifacts or generated files from previous runs.
First action:
```sh
dcr clean --all
dcr build --force
```
## `dcr --update` suggests package manager update
Likely cause:
- DCR binary is managed by pacman/AUR.
First action:
```sh
yay -Syu dcr
# or
paru -Syu dcr
```

View File

@@ -0,0 +1,53 @@
# Workspace guide
`[workspace]` lets you build related DCR projects in dependency order from one root.
## Important rule
Workspace root still needs a valid root project config:
- `[package]`
- `[build]`
- `[dependencies]`
`[workspace]` augments that root config; it does not replace required root sections.
## Minimal example
```toml
[package]
name = "root-app"
version = "0.1.0"
[build]
language = "c"
standard = "c11"
compiler = "clang"
kind = "bin"
[dependencies]
[workspace]
core = { path = "./modules/core", deps = [] }
net = { path = "./modules/net", deps = ["core"] }
app = { path = "./modules/app", deps = ["core", "net"] }
```
Each member path must point to a directory containing its own `dcr.toml`.
## Build behavior
- Running `dcr build` in workspace root builds members in topological dependency order, then builds the root project.
- Running `dcr clean --all` in workspace root cleans all members too.
- `dcr gen` in workspace root includes root + members in generated outputs.
## Common errors
- `workspace.<name>.path does not contain dcr.toml`:
member path is wrong or config is missing.
- `workspace dependency '<name>' not found`:
`deps` references unknown member.
- `workspace dependency cycle at <name>`:
cyclic dependencies in member graph.

199
src/cli/add.rs Normal file
View File

@@ -0,0 +1,199 @@
use crate::core::config::Config;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use toml::Value;
use toml::map::Map;
pub struct AddArgs {
pub name: String,
pub path: Option<String>,
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
}
pub fn add(args: &[String]) -> i32 {
let add_args = match parse_add_args(args) {
Ok(a) => a,
Err(code) => return code,
};
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
return 1;
}
};
let mut config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(cfg) => cfg,
Err(err) => {
error(&err.to_string());
return 1;
}
};
let dep_value = if let Some(git_url) = add_args.git {
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
let mut table = Map::new();
table.insert("git".to_string(), Value::String(git_url));
Value::Table(table)
} else {
let mut table = Map::new();
table.insert("git".to_string(), Value::String(git_url));
if let Some(b) = add_args.branch {
table.insert("branch".to_string(), Value::String(b));
}
if let Some(t) = add_args.tag {
table.insert("tag".to_string(), Value::String(t));
}
if let Some(r) = add_args.rev {
table.insert("rev".to_string(), Value::String(r));
}
Value::Table(table)
}
} else if let Some(path) = add_args.path {
Value::String(path)
} else {
error("Dependency source (path or git) must be provided");
return 1;
};
let key = format!("dependencies.{}", add_args.name);
if let Err(err) = config.edit(&key, dep_value) {
error(&format!("Failed to update dcr.toml: {}", err));
return 1;
}
println!(
" {} dependency `{}` to dcr.toml",
colored("Added", BOLD_GREEN),
add_args.name
);
0
}
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:");
error(" user/repo -> github.com/user/repo");
error(" github:user/repo -> github.com/user/repo");
error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo");
error(" path:./path/to/lib -> local path");
return Err(1);
}
let mut name = String::new();
let source_spec: String;
let mut branch = None;
let mut tag = None;
let mut rev = None;
let mut iter = args.iter();
if let Some(n) = iter.next() {
if n.starts_with("--") {
error("First argument must be the dependency name");
return Err(1);
}
name = n.clone();
}
if let Some(s) = iter.next() {
if s.starts_with("--") {
error("Second argument must be the dependency source");
return Err(1);
}
source_spec = s.clone();
} else {
error("Dependency source is required");
return Err(1);
}
while let Some(arg) = iter.next() {
match arg.as_str() {
"--branch" => {
branch = iter.next().cloned();
if branch.is_none() {
error("--branch requires a value");
return Err(1);
}
}
"--tag" => {
tag = iter.next().cloned();
if tag.is_none() {
error("--tag requires a value");
return Err(1);
}
}
"--rev" => {
rev = iter.next().cloned();
if rev.is_none() {
error("--rev requires a value");
return Err(1);
}
}
_ => {
error(&format!("Unknown argument: {}", arg));
return Err(1);
}
}
}
let mut path = None;
let mut git = None;
if let Some(p) = source_spec.strip_prefix("path:") {
path = Some(p.to_string());
} else if let Some(g) = source_spec.strip_prefix("github:") {
git = Some(format!("https://github.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("gitlab:") {
git = Some(format!("https://gitlab.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("git:") {
if g.contains('/') && !g.contains('.') {
// git:user/repo -> default to github
git = Some(format!("https://github.com/{}", g));
} else {
// git:host.com/user/repo
let url = if g.starts_with("http") || g.starts_with("git@") {
g.to_string()
} else {
format!("https://{}", g)
};
git = Some(url);
}
} else if source_spec.starts_with("http://")
|| source_spec.starts_with("https://")
|| source_spec.starts_with("git@")
{
git = Some(source_spec);
} else {
error("Source must have a prefix (path:, git:, github:, gitlab:) or be a full URL");
return Err(1);
}
Ok(AddArgs {
name,
path,
git,
branch,
tag,
rev,
})
}

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;
@@ -45,17 +46,28 @@ pub fn clean(args: &[String]) -> i32 {
struct CleanFlags { struct CleanFlags {
profile: Option<String>, profile: Option<String>,
target: Option<String>,
all: bool, all: bool,
} }
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> { fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
let mut profile: Option<String> = None; let mut profile: Option<String> = None;
let mut target: Option<String> = None;
let mut all = false; let mut all = false;
for arg in args { let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--all" { if arg == "--all" {
all = true; all = true;
continue; continue;
} }
if arg == "--target" {
if let Some(t) = iter.next() {
target = Some(t.clone());
} else {
return Err("--target requires a value".to_string());
}
continue;
}
if arg.starts_with("--") { if arg.starts_with("--") {
let candidate = arg.trim_start_matches("--").to_string(); let candidate = arg.trim_start_matches("--").to_string();
if flags(&candidate).is_some() { if flags(&candidate).is_some() {
@@ -68,23 +80,50 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
} }
return Err("Unknown argument".to_string()); return Err("Unknown argument".to_string());
} }
Ok(CleanFlags { profile, all }) Ok(CleanFlags {
profile,
target,
all,
})
} }
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> { fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let target = flags.target.clone().or_else(|| {
Some(if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu".to_string()
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else {
"unknown".to_string()
})
});
if flags.all if flags.all
&& let Some(workspace) = parse_workspace(&config, root)? && let Some(workspace) = parse_workspace(
&config,
flags.profile.as_deref().unwrap_or("debug"),
target.as_deref(),
root,
)?
{ {
for member in &workspace.members { for member in &workspace.members {
clean_project_at(&member.path, flags.profile.as_deref())?; clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
} }
} }
clean_project_at(root, flags.profile.as_deref()) clean_project_at(root, flags.profile.as_deref(), target.as_deref())
} }
fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), String> { fn clean_project_at(
project_root: &Path,
profile: Option<&str>,
target: 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 +136,48 @@ 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()) { if let Some(profile) = profile {
warn("Directory target not found"); let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}")
} else {
format!("target/{profile}")
};
let target_items = check_dir(Some("target")).unwrap_or_default();
let parent_dir = target.unwrap_or("");
let dir_exists = if target.is_some() {
target_items.contains(&parent_dir.to_string())
} else {
target_items.contains(&profile.to_string())
};
if !dir_exists {
warn(&format!("Directory target/{} not found", target_dir));
} else {
println!(" Profile: {}", colored(profile, BOLD_GREEN));
if let Some(t) = target {
println!(" Target: {}", colored(t, BOLD_GREEN));
}
let _ = fs::remove_dir_all(&target_dir);
println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
}
clean_custom_paths(&config, profile)?;
return Ok(()); return Ok(());
} }
if let Some(profile) = profile { if items.contains(&"target".to_string()) {
let target_items = check_dir(Some("target")).unwrap_or_default(); let _ = fs::remove_dir_all("target");
if !target_items.contains(&profile.to_string()) {
warn(&format!("Directory target/{profile} not found"));
return Ok(());
}
println!(" Profile: {}", colored(profile, BOLD_GREEN));
let _ = fs::remove_dir_all(format!("target/{profile}"));
println!( println!(
"{} Removed directory target/{profile}", "{} Removed directory target",
colored("\n ✔", BOLD_GREEN) colored("\n ✔", BOLD_GREEN)
); );
return Ok(()); } else {
warn("Directory target not found");
} }
clean_custom_paths(&config, "debug")?;
let _ = fs::remove_dir_all("target"); clean_custom_paths(&config, "release")?;
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
Ok(()) Ok(())
}) })
} }
@@ -136,3 +192,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,
}
}

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

@@ -0,0 +1,59 @@
use crate::config::{PROFILE, flags};
use crate::utils::log::warn;
pub struct BuildRunFlags {
pub profile: String,
pub target: Option<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 target = None;
let mut force = false;
let mut clean = false;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
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 candidate == "target" {
if let Some(t) = iter.next() {
target = Some(t.clone());
} else {
warn("--target requires a value");
return Err(1);
}
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,
target,
force,
clean,
})
}

1459
src/cli/gen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,15 +12,20 @@ pub fn help() -> i32 {
println!(" init Initialize the current directory as a project"); println!(" init Initialize the current directory as a project");
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!(" test Run project tests (alias: tests)");
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

@@ -65,7 +65,7 @@ pub fn init(args: &[String]) -> i32 {
return 1; return 1;
} }
println!( println!(
" {} Created file {}\n", " {} Created file {}",
colored("✔", BOLD_GREEN), colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );

View File

@@ -1,7 +1,12 @@
pub mod add;
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;
pub mod run; pub mod run;
pub mod setup;
pub mod test;

View File

@@ -86,7 +86,7 @@ pub fn new(args: &[String]) -> i32 {
return 1; return 1;
} }
println!( println!(
" {} Created file {}\n", " {} Created file {}",
colored("✔", BOLD_GREEN), colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );

View File

@@ -1,15 +1,51 @@
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;
fn get_run_cmd(
config: &Config,
profile: &str,
target: Option<&str>,
version: &str,
) -> Option<String> {
let base = config.get("run.cmd").and_then(|v| v.as_str());
let target_cmd = if let Some(t) = target {
let normalized_t = normalize_target_os(t);
config
.get(&format!("run.{}.cmd", normalized_t))
.or_else(|| config.get(&format!("run.{}.cmd", t)))
.and_then(|v| v.as_str())
} else {
None
};
let profile_cmd = config
.get(&format!("run.{}.cmd", profile))
.and_then(|v| v.as_str());
let cmd = target_cmd.or(profile_cmd).or(base)?;
let trimmed = cmd.trim();
if trimmed.is_empty() {
None
} else {
Some(substitute_run_vars(trimmed, profile, version))
}
}
fn normalize_target_os(s: &str) -> &str {
match s {
"linux" => "x86_64-unknown-linux-gnu",
"macos" => "x86_64-apple-darwin",
"windows" => "x86_64-pc-windows-msvc",
_ => s,
}
}
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 +87,63 @@ 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 mut 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 { // If no target specified, use default host target for target-specific config
warn("Unknown build flag"); if flags.target.is_none() {
return 1; let default_target = if cfg!(target_os = "linux") {
} "x86_64-unknown-linux-gnu"
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin"
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc"
} else { } else {
warn("Unknown argument"); "unknown"
return 1; };
} flags.target = Some(default_target.to_string());
} }
let normalized_target_dir = flags
.target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile));
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("");
let run_cmd = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), 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,
normalized_target_dir.as_deref(),
);
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 +162,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,
}
}

16
src/cli/setup.rs Normal file
View File

@@ -0,0 +1,16 @@
pub fn setup(_args: &[String]) -> i32 {
println!("Setting up DCR registries...");
match crate::core::registry::RegistryManager::load() {
Ok(manager) => {
println!("Loaded {} registries.", manager.config.registry.len());
for (name, reg) in manager.config.registry {
println!("- {}: {} (priority {})", name, reg.url, reg.priority);
}
0
}
Err(e) => {
println!("Error: {}", e);
1
}
}
}

269
src/cli/test.rs Normal file
View File

@@ -0,0 +1,269 @@
use crate::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::config::Config;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, BOLD_RED, RESET, colored};
use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
pub fn test(args: &[String]) -> i32 {
let mut init_header = false;
for arg in args {
if arg == "--help" {
println!("USAGE:");
println!(" dcr test [--init]");
println!();
println!("ALIASES:");
println!(" dcr tests");
println!();
println!("DESCRIPTION:");
println!(" Runs project tests and prints a unified testsuite report.");
println!();
println!("OPTIONS:");
println!(" --init Create tests/dcr_test.h in current project");
return 0;
}
if arg == "--init" {
init_header = true;
continue;
}
error("Unknown argument");
return 1;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
return 1;
}
};
if init_header {
match with_dir(&root, ensure_test_header) {
Ok(()) => {}
Err(msg) => {
error(&msg);
return 1;
}
}
}
match with_dir(&root, run_testsuite) {
Ok(code) => code,
Err(msg) => {
error(&msg);
1
}
}
}
fn run_testsuite() -> Result<i32, String> {
if build::build(&[String::from("--release")]) != 0 {
return Ok(1);
}
let config = Config::open("./dcr.toml").map_err(|_| "Failed to read dcr.toml".to_string())?;
let name = config
.get("package.name")
.and_then(|v| v.as_str())
.ok_or("package.name not found in dcr.toml")?;
let kind = config
.get("package.kind")
.and_then(|v| v.as_str())
.unwrap_or("bin");
let compiler = env::var("DCR_CC").unwrap_or_else(|_| "cc".to_string());
let profile = "release";
let flags_vec = flags(profile).unwrap_or(&[]);
let flags_str = flags_vec.join(" ");
let test_c = "./tests/test.c";
if !Path::new(test_c).exists() {
return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
}
// Compile test.c to object
let obj_path = "./tests/test.o";
let compile_cmd = format!("{} -c {} -o {} {}", compiler, test_c, obj_path, flags_str);
let compile_status = Command::new("sh")
.arg("-c")
.arg(&compile_cmd)
.status()
.map_err(|_| format!("Failed to compile {}", test_c))?;
if !compile_status.success() {
return Err(format!("Compilation of {} failed", test_c));
}
// Link
let bin_path = format!("./tests/test{}", std::env::consts::EXE_SUFFIX);
let link_cmd = if kind == "lib" {
format!(
"{} {} -o {} -Ltarget/release -l{}",
compiler, obj_path, bin_path, name
)
} else {
format!("{} {} -o {}", compiler, obj_path, bin_path)
};
let link_status = Command::new("sh")
.arg("-c")
.arg(&link_cmd)
.status()
.map_err(|_| format!("Failed to link {}", bin_path))?;
if !link_status.success() {
return Err(format!("Linking of {} failed", bin_path));
}
let out = Command::new(&bin_path)
.output()
.map_err(|_| format!("Failed to run `{bin_path}`"))?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.trim().is_empty() {
eprint!("{}", stderr);
}
let declared = extract_test_names("./tests/test.c");
let mut pass = 0;
let mut skip = 0;
let mut fail = 0;
let mut parsed_any = false;
for line in stdout.lines() {
let line = line.trim();
if let Some(name) = line.strip_prefix("[PASS] ") {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
pass += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[SKIP] ") {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
skip += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[FAIL] ") {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
fail += 1;
parsed_any = true;
continue;
}
if let Some((status, name)) = line.split_once('\t') {
match status {
"PASS" => {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
pass += 1;
parsed_any = true;
}
"SKIP" => {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
skip += 1;
parsed_any = true;
}
"FAIL" => {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
fail += 1;
parsed_any = true;
}
_ => {}
}
}
}
if !parsed_any {
if out.status.success() {
for name in &declared {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
}
pass = declared.len() as i32;
} else {
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
fail = 1;
}
}
let total = if parsed_any || declared.is_empty() {
pass + skip + fail
} else {
declared.len() as i32
};
println!();
println!("{}", colored("=====================", BOLD_GREEN));
println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
println!("{}", colored("=====================", BOLD_GREEN));
println!("TOTAL: {}", total);
print_field("PASS", pass, BOLD_GREEN);
print_field("SKIP", skip, BOLD_BLUE);
print_field("FAIL", fail, BOLD_RED);
println!("{}", colored("=====================", BOLD_GREEN));
if fail > 0 || !out.status.success() {
return Ok(1);
}
Ok(0)
}
fn ensure_test_header() -> Result<(), String> {
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
.map_err(|_| "Failed to write tests/dcr_test.h".to_string())?;
println!("Created tests/dcr_test.h");
fs::write("./tests/test.c", FILE_TEST_C)
.map_err(|_| "Failed to write tests/test.c".to_string())?;
println!("Created tests/test.c");
Ok(())
}
fn extract_test_names(path: &str) -> Vec<String> {
let Ok(content) = fs::read_to_string(path) else {
return Vec::new();
};
let mut names = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("TEST_CASE(")
&& let Some(name) = rest.strip_suffix("),")
{
names.push(name.to_string());
}
}
names
}
fn print_field(label: &str, value: i32, color: &str) {
if value == 0 {
println!("{}: {}", label, value);
} else {
println!("{}{}: {}{}", color, label, value, RESET);
}
}
fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
where
F: FnOnce() -> Result<T, String>,
{
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
let result = f();
let _ = std::env::set_current_dir(prev);
result
}

View File

@@ -7,9 +7,81 @@ int main(void) {
} }
"#; "#;
pub const FILE_DCR_TEST_H: &str = r#"#pragma once
#include <stdio.h>
#include <string.h>
#define DCR_TEST_PASS 0
#define DCR_TEST_FAIL 1
#define DCR_TEST_SKIP 2
#define EXPECT(expr) \
do { \
if (!(expr)) { \
fprintf(stderr, "assert failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); \
return DCR_TEST_FAIL; \
} \
} while (0)
#define SKIP(reason) \
do { \
(void)(reason); \
return DCR_TEST_SKIP; \
} while (0)
#define TEST(name) static int name(void)
#define TEST_CASE(name) {#name, name}
typedef int (*test_fn_t)(void);
typedef struct {
const char *name;
test_fn_t fn;
} test_case_t;
static inline int dcr_test_raw_mode(int argc, char **argv) {
return argc > 1 && strcmp(argv[1], "--dcr-raw") == 0;
}
static inline void dcr_test_print_result(const char *name, int rc, int raw_mode) {
const char *tag = (rc == DCR_TEST_PASS) ? "PASS" : (rc == DCR_TEST_SKIP) ? "SKIP" : "FAIL";
if (raw_mode) {
printf("%s\t%s\n", tag, name);
} else {
printf("[%s] %s\n", tag, name);
}
}
"#;
pub const FILE_TEST_C: &str = r#"#include "dcr_test.h"
TEST(test_example) {
EXPECT(1 + 1 == 2);
return DCR_TEST_PASS;
}
static test_case_t CASES[] = {
TEST_CASE(test_example),
};
int main(int argc, char **argv) {
int raw = dcr_test_raw_mode(argc, argv);
int rc = 0;
int count = (int)(sizeof(CASES) / sizeof(CASES[0]));
for (int i = 0; i < count; ++i) {
int status = CASES[i].fn();
dcr_test_print_result(CASES[i].name, status, raw);
if (status == DCR_TEST_FAIL) {
rc = 1;
}
}
return rc;
}
"#;
pub fn flags(profile: &str) -> Option<&'static [&'static str]> { pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
match profile { match profile {
"release" => Some(&["-O3", "-DNDEBUG", "-march=native"]), "release" => Some(&["-O3", "-DNDEBUG"]),
"debug" => Some(&[ "debug" => Some(&[
"-O0", "-O0",
"-g", "-g",

View File

@@ -2,33 +2,37 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub fn collect_sources( pub fn collect_sources(
roots: &[PathBuf],
extensions: &[&str], extensions: &[&str],
exclude_dirs: &[PathBuf], exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let mut sources = Vec::new(); let mut sources = Vec::new();
collect_sources_rec("./src", extensions, &mut sources, exclude_dirs)?; for root in roots {
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
}
sources.sort(); sources.sort();
if sources.is_empty() { if sources.is_empty() {
return Err("No source files found in ./src".to_string()); return Err(format!("No source files found in {}", format_roots(roots)));
} }
Ok(sources) Ok(sources)
} }
fn collect_sources_rec( fn collect_sources_rec(
dir: &str, dir: &Path,
extensions: &[&str], extensions: &[&str],
out: &mut Vec<String>, out: &mut Vec<String>,
exclude_dirs: &[PathBuf], exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<(), String> { ) -> Result<(), String> {
let dir_path = Path::new(dir); let full_dir = if dir.is_absolute() {
let full_dir = if dir_path.is_absolute() { dir.to_path_buf()
dir_path.to_path_buf()
} else { } else {
std::env::current_dir() std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))? .map_err(|err| format!("src dir error: {err}"))?
.join(dir_path) .join(dir)
}; };
if is_excluded(&full_dir, exclude_dirs) { if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(()); return Ok(());
} }
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?; let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
@@ -36,15 +40,18 @@ fn collect_sources_rec(
let entry = entry.map_err(|err| format!("src dir error: {err}"))?; let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
if is_excluded(&path, exclude_dirs) { if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
continue; continue;
} }
collect_sources_rec(&path.to_string_lossy(), extensions, out, exclude_dirs)?; collect_sources_rec(&path, extensions, out, exclude_dirs, include_paths)?;
continue; continue;
} }
if !path.is_file() { if !path.is_file() {
continue; 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_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext_lower = ext_raw.to_lowercase(); let ext_lower = ext_raw.to_lowercase();
let matched = extensions let matched = extensions
@@ -57,10 +64,95 @@ fn collect_sources_rec(
Ok(()) Ok(())
} }
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf]) -> bool { 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)) 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 { pub fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() { if !path.is_absolute() {
return path.to_string_lossy().to_string(); return path.to_string_lossy().to_string();
@@ -189,6 +281,10 @@ mod tests {
dir dir
} }
fn default_roots() -> Vec<PathBuf> {
vec![PathBuf::from("src")]
}
#[test] #[test]
fn object_path_basic() { fn object_path_basic() {
let obj_dir = Path::new("target/debug/obj"); let obj_dir = Path::new("target/debug/obj");
@@ -260,19 +356,34 @@ mod tests {
#[test] #[test]
fn is_excluded_match() { fn is_excluded_match() {
let include: Vec<String> = Vec::new();
let excluded = vec![PathBuf::from("/project/src/vendor")]; let excluded = vec![PathBuf::from("/project/src/vendor")];
assert!(is_excluded(Path::new("/project/src/vendor"), &excluded)); assert!(is_excluded(
Path::new("/project/src/vendor"),
&excluded,
&include
));
assert!(is_excluded( assert!(is_excluded(
Path::new("/project/src/vendor/lib.c"), Path::new("/project/src/vendor/lib.c"),
&excluded &excluded,
&include
)); ));
} }
#[test] #[test]
fn is_excluded_no_match() { fn is_excluded_no_match() {
let include: Vec<String> = Vec::new();
let excluded = vec![PathBuf::from("/project/src/vendor")]; let excluded = vec![PathBuf::from("/project/src/vendor")];
assert!(!is_excluded(Path::new("/project/src/main.c"), &excluded)); assert!(!is_excluded(
assert!(!is_excluded(Path::new("/other/vendor"), &excluded)); Path::new("/project/src/main.c"),
&excluded,
&include
));
assert!(!is_excluded(
Path::new("/other/vendor"),
&excluded,
&include
));
} }
#[test] #[test]
@@ -287,7 +398,9 @@ mod tests {
let _guard = CWD_LOCK.lock().unwrap(); let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let result = collect_sources(&["c"], &[]); let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap(); std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources"); let sources = result.expect("should find sources");
@@ -309,7 +422,9 @@ mod tests {
let _guard = CWD_LOCK.lock().unwrap(); let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let result = collect_sources(&["cpp", "cxx", "cc"], &[]); 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(); std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources"); let sources = result.expect("should find sources");
@@ -325,7 +440,9 @@ mod tests {
let _guard = CWD_LOCK.lock().unwrap(); let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let result = collect_sources(&["c"], &[]); let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap(); std::env::set_current_dir(prev).unwrap();
assert!(result.is_err(), "empty src should return error"); assert!(result.is_err(), "empty src should return error");
@@ -344,7 +461,9 @@ mod tests {
let _guard = CWD_LOCK.lock().unwrap(); let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let result = collect_sources(&["c"], &exclude); 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(); std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources"); let sources = result.expect("should find sources");
@@ -364,13 +483,89 @@ mod tests {
let _guard = CWD_LOCK.lock().unwrap(); let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap(); let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap(); std::env::set_current_dir(&dir).unwrap();
let result = collect_sources(&["c"], &[]); let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap(); std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources"); let sources = result.expect("should find sources");
assert_eq!(sources.len(), 2); 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] #[test]
fn normalize_relative_path() { fn normalize_relative_path() {
let result = normalize_source_path(Path::new("./src/main.c")); let result = normalize_source_path(Path::new("./src/main.c"));

View File

@@ -7,7 +7,8 @@ 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() {
@@ -91,7 +92,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> { pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// GAS handles only lowercase .s files // GAS handles only lowercase .s files
common::collect_sources(&["s"], ctx.exclude_dirs) common::collect_sources(
ctx.source_roots,
&["s"],
ctx.exclude_dirs,
ctx.include_paths,
)
} }
fn build_objects( fn build_objects(

View File

@@ -10,12 +10,17 @@ pub struct BuildContext<'a> {
pub compiler: &'a str, pub compiler: &'a str,
pub language: &'a str, pub language: &'a str,
pub standard: &'a str, pub standard: &'a str,
#[allow(dead_code)]
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>, pub target_dir: Option<&'a str>,
pub kind: &'a str, pub kind: &'a str,
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 package_type: 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],

View File

@@ -12,8 +12,9 @@ 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)?; let sources = collect_sources(ctx)?;
@@ -65,8 +66,10 @@ 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) { if ctx.cflags.is_empty() {
cmd.arg(flag); for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
} }
for flag in ctx.cflags { for flag in ctx.cflags {
cmd.arg(flag); cmd.arg(flag);
@@ -109,16 +112,29 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> { pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language); let extensions = source_extensions(ctx.language);
common::collect_sources(&extensions, ctx.exclude_dirs) common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
} }
fn source_extensions(language: &str) -> Vec<&str> { fn source_extensions(language: &str) -> Vec<&str> {
let lang = language.to_lowercase(); let mut out = Vec::new();
match lang.as_str() { for part in language.split('+') {
"c" => vec!["c"], let lang = part.trim().to_lowercase();
"c++" | "cpp" | "cxx" => vec!["cpp", "cxx", "cc"], match lang.as_str() {
_ => vec!["c"], "c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => {}
_ => {}
}
} }
if out.is_empty() {
out.extend(["c"]);
}
out
} }
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> { fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
@@ -203,8 +219,10 @@ fn build_objects(
if let Some(flag) = msvc_arch_flag(ctx.platform) { if let Some(flag) = msvc_arch_flag(ctx.platform) {
cmd.arg(flag); cmd.arg(flag);
} }
for flag in default_flags(ctx.profile) { if ctx.cflags.is_empty() {
cmd.arg(flag); for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
} }
for flag in ctx.cflags { for flag in ctx.cflags {
cmd.arg(flag); cmd.arg(flag);

View File

@@ -7,7 +7,8 @@ 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() {
@@ -91,7 +92,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> { pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed) // NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed)
common::collect_sources(&["asm", "s"], ctx.exclude_dirs) common::collect_sources(
ctx.source_roots,
&["asm", "s"],
ctx.exclude_dirs,
ctx.include_paths,
)
} }
fn build_objects( fn build_objects(

View File

@@ -14,7 +14,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
let start_time = Instant::now(); let start_time = Instant::now();
let extensions = source_extensions(ctx.language); let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(&extensions, ctx.exclude_dirs)?; 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")?;
@@ -80,22 +85,34 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> { pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language); let extensions = source_extensions(ctx.language);
common::collect_sources(&extensions, ctx.exclude_dirs) common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
} }
fn source_extensions(language: &str) -> Vec<&str> { fn source_extensions(language: &str) -> Vec<&str> {
let lang = language.to_lowercase(); let mut out = Vec::new();
match lang.as_str() { for part in language.split('+') {
"c" => vec!["c"], let lang = part.trim().to_lowercase();
"c++" | "cpp" | "cxx" => vec!["cpp", "cxx", "cc"], match lang.as_str() {
"asm" => vec!["s", "S", "asm"], "c" => out.extend(["c"]),
_ => vec!["c"], "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => out.extend(["s", "S", "asm"]),
_ => {}
}
} }
if out.is_empty() {
out.extend(["c"]);
}
out
} }
fn default_flags(profile: &str) -> &'static [&'static str] { fn default_flags(profile: &str) -> &'static [&'static str] {
match profile { match profile {
"release" => &["-O3", "-DNDEBUG", "-march=native"], "release" => &["-O3", "-DNDEBUG"],
"debug" => &[ "debug" => &[
"-O0", "-O0",
"-g", "-g",
@@ -138,8 +155,10 @@ fn build_objects(
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" { if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard)); cmd.arg(format!("-std={}", ctx.standard));
} }
for flag in default_flags(ctx.profile) { if ctx.cflags.is_empty() {
cmd.arg(flag); for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
} }
for flag in ctx.cflags { for flag in ctx.cflags {
cmd.arg(flag); cmd.arg(flag);

View File

@@ -110,12 +110,27 @@ impl Config {
} }
} }
if let Some(pkg_type) = package.get("type").and_then(|v| v.as_str()) {
let pkg_type = pkg_type.trim();
if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none"
{
return Err(ConfigError::Invalid(
"package.type must be 'lib', 'app', or 'none'".into(),
));
}
}
let build = self let build = self
.get("build") .get("build")
.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 +159,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 +243,116 @@ 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",
"targets",
] {
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(());
@@ -216,6 +408,11 @@ fn default_toml() -> Result<Value, ConfigError> {
"version".to_string(), "version".to_string(),
Value::String(DEFAULT_VERSION.to_string()), Value::String(DEFAULT_VERSION.to_string()),
); );
package.insert("type".to_string(), Value::String("none".to_string()));
package.insert("description".to_string(), Value::String("".to_string()));
package.insert("author".to_string(), Value::String("".to_string()));
package.insert("license".to_string(), Value::String("MIT".to_string()));
let mut build = Map::new(); let mut build = Map::new();
build.insert( build.insert(
"language".to_string(), "language".to_string(),
@@ -264,17 +461,48 @@ 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("");
let mut out = String::new(); let mut out = String::new();
out.push_str("[package]\n"); out.push_str("[package]\n");
out.push_str(&format!("name = \"{name}\"\n")); out.push_str(&format!("name = \"{name}\"\n"));
out.push_str(&format!("version = \"{version}\"\n\n")); out.push_str(&format!("version = \"{version}\"\n"));
for key in [
"type",
"description",
"author",
"authors",
"homepage",
"license",
"repository",
"readme",
"keywords",
"categories",
] {
if let Some(val) = package.get(key) {
match val {
Value::String(s) => out.push_str(&format!("{key} = \"{s}\"\n")),
Value::Array(_) => out.push_str(&format!("{key} = {}\n", format_string_array(val))),
_ => {}
}
}
}
out.push('\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
@@ -331,21 +559,36 @@ fn format_dep_value(value: &Value) -> String {
Value::String(s) => format!("\"{s}\""), Value::String(s) => format!("\"{s}\""),
Value::Table(tbl) => { Value::Table(tbl) => {
let mut parts = Vec::new(); let mut parts = Vec::new();
if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) {
parts.push(format!("version = \"{v}\""));
}
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) { if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
parts.push(format!("path = \"{v}\"")); parts.push(format!("path = \"{v}\""));
} }
if let Some(v) = tbl.get("git").and_then(|v| v.as_str()) {
parts.push(format!("git = \"{v}\""));
}
if let Some(v) = tbl.get("branch").and_then(|v| v.as_str()) {
parts.push(format!("branch = \"{v}\""));
}
if let Some(v) = tbl.get("tag").and_then(|v| v.as_str()) {
parts.push(format!("tag = \"{v}\""));
}
if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) {
parts.push(format!("rev = \"{v}\""));
}
if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) {
parts.push(format!(
"default-features = {}",
if v { "true" } else { "false" }
));
}
if let Some(v) = tbl.get("features") {
parts.push(format!("features = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) { if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) {
parts.push(format!("system = {}", if v { "true" } else { "false" })); parts.push(format!("system = {}", if v { "true" } else { "false" }));
} }
if let Some(v) = tbl.get("include") {
parts.push(format!("include = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("lib") {
parts.push(format!("lib = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("libs") {
parts.push(format!("libs = {}", format_string_array(v)));
}
format!("{{ {} }}", parts.join(", ")) format!("{{ {} }}", parts.join(", "))
} }
_ => "\"\"".to_string(), _ => "\"\"".to_string(),
@@ -474,6 +717,28 @@ mod tests {
assert!(result.is_err(), "empty language should fail validation"); 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] #[test]
fn validate_unknown_kind_fails() { fn validate_unknown_kind_fails() {
let dir = temp_dir("bad_kind"); let dir = temp_dir("bad_kind");

View File

@@ -1,365 +0,0 @@
use crate::core::config::Config;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub path_raw: String,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
pub fn resolve_deps(
config: &Config,
profile: &str,
project_root: &Path,
) -> Result<ResolvedDeps, String> {
let project_name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let project_version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let deps = parse_dependencies(config)?;
if deps.is_empty() {
return Ok(ResolvedDeps {
include_dirs: Vec::new(),
lib_dirs: Vec::new(),
libs: Vec::new(),
});
}
let mut include_dirs = Vec::new();
let mut lib_dirs = Vec::new();
let mut libs = Vec::new();
let mut lock_packages = Vec::new();
for dep in deps {
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() {
return Err(format!(
"Dependency '{}' path is not a directory: {}",
dep.name,
dep_path.display()
));
}
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"],
profile,
)?;
let libs_list = dep
.libs_raw
.clone()
.unwrap_or_else(|| vec![dep.name.clone()]);
if include.is_empty() || lib.is_empty() {
return Err(format!(
"Dependency '{}' missing include/lib dirs. Add include/lib in dcr.toml.",
dep.name
));
}
include_dirs.extend(include.iter().map(|p| p.to_string_lossy().to_string()));
lib_dirs.extend(lib.iter().map(|p| p.to_string_lossy().to_string()));
libs.extend(libs_list.iter().cloned());
let dep_cache = project_root
.join("target")
.join(profile)
.join("deps")
.join(&dep.name);
sync_dep_dir(&dep_path, &dep_cache)
.map_err(|err| format!("Failed to sync dep {}: {err}", dep.name))?;
let dep_version = read_dep_version(&dep_path).unwrap_or_else(|| "0.0.0".to_string());
let dep_checksum = compute_checksum(&dep_path)
.map_err(|err| format!("Failed to hash dep {}: {err}", dep.name))?;
lock_packages.push(DepLock {
name: dep.name.clone(),
version: dep_version,
checksum: dep_checksum,
source: format!("path+{}", dep.path_raw),
});
}
write_lock(
project_root,
&project_name,
&project_version,
&lock_packages,
)?;
Ok(ResolvedDeps {
include_dirs,
lib_dirs,
libs,
})
}
#[derive(Debug, Clone)]
struct DepLock {
name: String,
version: String,
checksum: String,
source: String,
}
fn parse_dependencies(config: &Config) -> Result<Vec<DepSpec>, String> {
let deps_val = match config.get("dependencies") {
Some(v) => v,
None => return Ok(Vec::new()),
};
let deps_table = deps_val
.as_table()
.ok_or_else(|| "dependencies is not a table".to_string())?;
let mut deps = Vec::new();
for (name, value) in deps_table {
match value {
Value::Table(tbl) => {
if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) {
return Err(format!(
"Dependency '{}' uses system=true, which is not supported yet",
name
));
}
let path = tbl
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("Dependency '{}' missing path", name))?
.to_string();
let include = parse_string_list(tbl.get("include"), name, "include")?;
let lib = parse_string_list(tbl.get("lib"), name, "lib")?;
let libs = parse_string_list(tbl.get("libs"), name, "libs")?;
deps.push(DepSpec {
name: name.to_string(),
path_raw: path,
include_raw: include,
lib_raw: lib,
libs_raw: libs,
});
}
_ => {
return Err(format!(
"Dependency '{}' must be a table with path/include/lib",
name
));
}
}
}
deps.sort_by(|a, b| a.name.cmp(&b.name));
Ok(deps)
}
fn parse_string_list(
value: Option<&Value>,
dep_name: &str,
key: &str,
) -> Result<Option<Vec<String>>, String> {
let Some(value) = value else { return Ok(None) };
let list = value.as_array().ok_or_else(|| {
format!(
"Dependency '{}' field '{}' must be an array of strings",
dep_name, key
)
})?;
let mut out = Vec::new();
for item in list {
let s = item
.as_str()
.ok_or_else(|| format!("Dependency '{}' field '{}' must be strings", dep_name, key))?;
out.push(s.to_string());
}
Ok(Some(out))
}
fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}
fn write_lock(
project_root: &Path,
project_name: &str,
project_version: &str,
packages: &[DepLock],
) -> Result<(), String> {
let mut out = String::new();
if !packages.is_empty() {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(project_name)));
out.push_str(&format!(
"version = \"{}\"\n",
escape_value(project_version)
));
out.push_str(&format!(
"dependencies = [{}]\n\n",
quote_list(&packages.iter().map(|p| p.name.clone()).collect::<Vec<_>>())
));
}
for pkg in packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
out.push_str(&format!("version = \"{}\"\n", escape_value(&pkg.version)));
out.push_str(&format!("source = \"{}\"\n", escape_value(&pkg.source)));
out.push_str(&format!("checksum = \"{}\"\n", escape_value(&pkg.checksum)));
out.push('\n');
}
fs::write(project_root.join("dcr.lock"), out)
.map_err(|err| format!("Failed to write dcr.lock: {err}"))?;
Ok(())
}
fn quote_list(items: &[String]) -> String {
items
.iter()
.map(|s| format!("\"{}\"", escape_value(s)))
.collect::<Vec<_>>()
.join(", ")
}
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

111
src/core/deps/common.rs Normal file
View File

@@ -0,0 +1,111 @@
#[allow(dead_code)]
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum DepSource {
Path(String),
Git {
url: String,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
},
Registry {
version: String,
features: Option<Vec<String>>,
},
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub source: DepSource,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
#[allow(dead_code)]
pub fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
#[allow(dead_code)]
pub fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
#[allow(dead_code)]
pub fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
#[allow(dead_code)]
pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
#[allow(dead_code)]
pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}

54
src/core/deps/git.rs Normal file
View File

@@ -0,0 +1,54 @@
use std::path::Path;
#[allow(dead_code)]
pub fn fetch_git_dep(
url: &str,
target_dir: &Path,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<(), String> {
if target_dir.exists() {
return Ok(());
}
let mut builder = git2::build::RepoBuilder::new();
if let Some(b) = branch {
builder.branch(b);
}
let repo = builder
.clone(url, target_dir)
.map_err(|e| format!("failed to clone {}: {}", url, e))?;
if let Some(t) = tag {
checkout_ref(&repo, &format!("refs/tags/{}", t))?;
} else if let Some(r) = rev {
checkout_ref(&repo, r)?;
}
Ok(())
}
#[allow(dead_code)]
fn checkout_ref(repo: &git2::Repository, reference_str: &str) -> Result<(), String> {
let (object, reference) = repo
.revparse_ext(reference_str)
.map_err(|e| format!("failed to find ref {}: {}", reference_str, e))?;
repo.checkout_tree(&object, None).map_err(|e| {
let ref_name = reference.as_ref().and_then(|r| r.name()).unwrap_or("HEAD");
format!("failed to checkout {}: {}", ref_name, e)
})?;
match reference {
Some(gref) => {
let ref_name = gref.name().unwrap_or("HEAD");
repo.set_head(ref_name)
}
None => repo.set_head_detached(object.id()),
}
.map_err(|e| format!("failed to set HEAD: {}", e))?;
Ok(())
}

120
src/core/deps/lock.rs Normal file
View File

@@ -0,0 +1,120 @@
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepLock {
pub name: String,
pub version: String,
pub checksum: String,
pub source: String,
}
pub fn write_lock(
project_root: &Path,
project_name: &str,
project_version: &str,
packages: &[DepLock],
) -> Result<(), String> {
let mut out = String::new();
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(project_name)));
out.push_str(&format!(
"version = \"{}\"\n",
escape_value(project_version)
));
if !packages.is_empty() {
out.push_str(&format!(
"dependencies = [{}]\n",
quote_list(&packages.iter().map(|p| p.name.clone()).collect::<Vec<_>>())
));
}
out.push('\n');
for pkg in packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
out.push_str(&format!("version = \"{}\"\n", escape_value(&pkg.version)));
out.push_str(&format!("source = \"{}\"\n", escape_value(&pkg.source)));
out.push_str(&format!("checksum = \"{}\"\n", escape_value(&pkg.checksum)));
out.push('\n');
}
fs::write(project_root.join("dcr.lock"), out)
.map_err(|err| format!("Failed to write dcr.lock: {err}"))?;
Ok(())
}
fn quote_list(items: &[String]) -> String {
items
.iter()
.map(|s| format!("\"{}\"", escape_value(s)))
.collect::<Vec<_>>()
.join(", ")
}
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
#[allow(dead_code)]
pub fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[allow(dead_code)]
pub fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
#[allow(dead_code)]
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
#[allow(dead_code)]
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

93
src/core/deps/mod.rs Normal file
View File

@@ -0,0 +1,93 @@
pub mod common;
pub mod git;
pub mod lock;
pub mod register;
use crate::core::config::Config;
use crate::core::deps::common::ResolvedDeps;
use std::path::Path;
pub fn resolve_deps(
config: &Config,
_profile: &str,
_target: Option<&str>,
project_root: &Path,
) -> Result<ResolvedDeps, String> {
let mut resolved = ResolvedDeps::default();
let project_name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let project_version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let lock_packages = Vec::new();
if let Some(deps) = deps_table {
for (name, value) in deps {
if register::is_registry_dep(value) {
let _pkg_info = register::resolve_package_from_registry(name)?;
// Используем путь относительно project_root или системный путь, если нужно
let dep_root = project_root.join("dcr-index");
resolved.include_dirs.push(
dep_root
.join("target")
.join("include")
.to_string_lossy()
.to_string(),
);
resolved.lib_dirs.push(
dep_root
.join("target")
.join("lib")
.to_string_lossy()
.to_string(),
);
resolved.libs.push(name.clone());
} else if let Some(table) = value.as_table()
&& let Some(path) = table.get("path").and_then(|v| v.as_str())
{
let dep_root = project_root.join(path);
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
for inc in includes {
if let Some(inc_str) = inc.as_str() {
resolved
.include_dirs
.push(dep_root.join(inc_str).to_string_lossy().to_string());
}
}
}
let lib_path = dep_root.join("target").join("lib");
if lib_path.exists() {
resolved
.lib_dirs
.push(lib_path.to_string_lossy().to_string());
}
if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) {
for lib in libs {
if let Some(lib_str) = lib.as_str() {
resolved.libs.push(lib_str.to_string());
}
}
} else {
resolved.libs.push(name.clone());
}
}
}
}
crate::core::deps::lock::write_lock(
project_root,
&project_name,
&project_version,
&lock_packages,
)?;
Ok(resolved)
}

76
src/core/deps/register.rs Normal file
View File

@@ -0,0 +1,76 @@
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value as TomlValue;
#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DcrConfig {
pub registry: std::collections::HashMap<String, Registry>,
}
pub fn get_registry_config() -> Option<DcrConfig> {
let home = std::env::var("HOME").ok()?;
let config_path = Path::new(&home).join(".dcr/config.toml");
if !config_path.exists() {
return None;
}
let content = fs::read_to_string(config_path).ok()?;
toml::from_str(&content).ok()
}
pub fn get_index_path() -> PathBuf {
std::env::var("DCR_INDEX_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
Path::new(&home).join(".dcr").join("index.json")
})
}
pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
let config = get_registry_config().ok_or("No registry config found")?;
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
registries.sort_by_key(|b| std::cmp::Reverse(b.1.priority));
for (_name_reg, _reg) in registries {
let index_path = get_index_path();
if index_path.exists() {
let index_content = fs::read_to_string(&index_path).map_err(|e| e.to_string())?;
let index: JsonValue =
serde_json::from_str(&index_content).map_err(|e| e.to_string())?;
if let Some(pkgs) = index.get("packages").and_then(|v| v.as_array()) {
for pkg in pkgs {
if pkg.get("name").and_then(|v| v.as_str()) == Some(name) {
return Ok(pkg.clone());
}
}
}
}
}
Err(format!(
"Package {} not found in registry (checked: {:?})",
name,
get_index_path()
))
}
pub fn is_registry_dep(value: &TomlValue) -> bool {
if value.is_str() {
return true;
}
if let Some(table) = value.as_table() {
return !table.contains_key("git")
&& !table.contains_key("path")
&& !table.contains_key("url")
&& !table.contains_key("version");
}
false
}

View File

@@ -1,5 +1,6 @@
pub mod builder; pub mod builder;
pub mod config; pub mod config;
pub mod deps; pub mod deps;
pub mod registry;
pub mod runner; pub mod runner;
pub mod workspace; pub mod workspace;

41
src/core/registry.rs Normal file
View File

@@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub registry: HashMap<String, Registry>,
}
pub struct RegistryManager {
pub config: Config,
#[allow(dead_code)]
pub path: PathBuf,
}
impl RegistryManager {
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let home = std::env::var("HOME")?;
let dcr_dir = PathBuf::from(home).join(".dcr");
let config_path = dcr_dir.join("config.toml");
if !config_path.exists() {
return Err("Config file ~/.dcr/config.toml not found".into());
}
let content = fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&content)?;
Ok(Self {
config,
path: dcr_dir,
})
}
}

View File

@@ -1,37 +1,21 @@
use crate::platform; use crate::platform;
use crate::utils::fs::check_dir;
use crate::utils::log::warn;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 { pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
let bin_path = platform::bin_path(profile, project_name, target_dir); let bin_path = platform::bin_path(profile, project_name, target_dir);
if Path::new(&bin_path).exists() { if Path::new(&bin_path).exists() {
return Command::new(bin_path) let output = Command::new(&bin_path).output();
.status() match output {
.map(|status| status.code().unwrap_or(1)) Ok(o) => {
.unwrap_or(1); print!("{}", String::from_utf8_lossy(&o.stdout));
eprint!("{}", String::from_utf8_lossy(&o.stderr));
return o.status.code().unwrap_or(0);
}
Err(_) => {
return 1;
}
}
} }
if target_dir.is_none()
&& check_dir(Some(&format!("target/{profile}")))
.unwrap_or_default()
.contains(&profile.to_string())
{
warn("Launch of the latest release");
return Command::new(platform::bin_path(profile, project_name, target_dir))
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
if target_dir.is_some() && Path::new(&bin_path).exists() {
warn("Launch of the latest release");
return Command::new(bin_path)
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
1 1
} }

View File

@@ -15,8 +15,33 @@ pub struct Workspace {
pub members: Vec<WorkspaceMember>, pub members: Vec<WorkspaceMember>,
} }
pub fn parse_workspace(config: &Config, root: &Path) -> Result<Option<Workspace>, String> { pub fn parse_workspace(
let table = match config.get("workspace").and_then(|v| v.as_table()) { config: &Config,
profile: &str,
target: Option<&str>,
root: &Path,
) -> Result<Option<Workspace>, String> {
let mut table = None;
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
let combinations = if let Some(t) = target {
let normalized_t = crate::cli::build::normalize_target_os(t);
vec![
format!("workspace.{}.{}", normalized_t, profile),
format!("workspace.{}.{}", profile, normalized_t),
format!("workspace.{}", normalized_t),
format!("workspace.{}", profile),
"workspace".to_string(),
]
} else {
vec![format!("workspace.{}", profile), "workspace".to_string()]
};
for key in combinations {
if let Some(val) = config.get(&key).and_then(|v| v.as_table()) {
table = Some(val);
break;
}
}
let table = match table {
Some(t) => t, Some(t) => t,
None => return Ok(None), None => return Ok(None),
}; };

View File

@@ -18,9 +18,13 @@ fn main() {
let code = match cmd { let code = match cmd {
"new" => cli::r#new::new(rest), "new" => cli::r#new::new(rest),
"init" => cli::init::init(rest), "init" => cli::init::init(rest),
"setup" => cli::setup::setup(rest),
"add" => cli::add::add(rest),
"build" => cli::build::build(rest), "build" => cli::build::build(rest),
"run" => cli::run::run(rest), "run" => cli::run::run(rest),
"test" | "tests" => cli::test::test(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

@@ -1,7 +1,11 @@
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir { match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"), None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}")
}
} }
} }
@@ -25,12 +29,16 @@ mod tests {
#[test] #[test]
fn bin_path_default() { fn bin_path_default() {
assert_eq!(bin_path("debug", "hello", None), "./target/debug/hello"); let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/hello");
assert_eq!(bin_path("debug", "hello", None), expected);
} }
#[test] #[test]
fn bin_path_release() { fn bin_path_release() {
assert_eq!(bin_path("release", "hello", None), "./target/release/hello"); let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/release/hello");
assert_eq!(bin_path("release", "hello", None), expected);
} }
#[test] #[test]

View File

@@ -59,7 +59,7 @@ fn ensure_bin_built() {
} }
fn available_compiler() -> Option<&'static str> { fn available_compiler() -> Option<&'static str> {
for candidate in ["clang", "gcc", "cc"] { for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate) let ok = Command::new(candidate)
.arg("--version") .arg("--version")
.output() .output()
@@ -133,19 +133,19 @@ fn build_run_clean_flags_normal_project() {
assert!(out.status.success(), "dcr build --release should succeed"); assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs); let out = run_dcr_env(&["run"], &dir, &envs);
assert!(out.status.success(), "dcr run should succeed");
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Hello World!"), "run output missing"); assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs); let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed"); assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!( assert!(
!dir.join("target").join("release").exists(), !dir.join(&target_dir).join("release").exists(),
"target/release should be removed" "target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
dir.join("target").join("debug").is_dir(), dir.join(&target_dir).join("debug").is_dir(),
"target/debug should remain" "target/x86_64-unknown-linux-gnu/debug should remain"
); );
let out = run_dcr_env(&["clean"], &dir, &envs); let out = run_dcr_env(&["clean"], &dir, &envs);
@@ -200,21 +200,146 @@ fn workspace_build_and_clean_all() {
for (name, _) in &members { for (name, _) in &members {
let member = root.join("src").join(name); let member = root.join("src").join(name);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!( assert!(
!member.join("target").join("release").exists(), !member.join(target_dir).join("release").exists(),
"member target/release should be removed" "member target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
member.join("target").join("debug").exists(), member.join(target_dir).join("debug").exists(),
"member target/debug should remain" "member target/x86_64-unknown-linux-gnu/debug should remain"
); );
} }
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!( assert!(
!root.join("target").join("release").exists(), !root.join(target_dir).join("release").exists(),
"root target/release should be removed" "root target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
root.join("target").join("debug").exists(), root.join(target_dir).join("debug").exists(),
"root target/debug should remain" "root target/x86_64-unknown-linux-gnu/debug should remain"
);
}
#[test]
fn dcr_add_dependencies() {
let dir = unique_sandbox_dir("add_dep");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
// Test path: prefix
let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir);
assert!(out.status.success(), "dcr add path should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("mylib = \"./libs/mylib\""),
"path dep not found in toml"
);
// Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"),
"github dep not found in toml"
);
// Test git: prefix (generic)
let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir);
assert!(out.status.success(), "dcr add custom git should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"),
"custom git dep not found in toml"
);
// Test git: prefix (github default)
let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir);
assert!(out.status.success(), "dcr add git short should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("git_short = { git = \"https://github.com/user/repo\" }"),
"git short dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
&dir,
);
assert!(out.status.success(), "dcr add with branch should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"),
"branch lib not found in toml"
);
// Test failure on no prefix
let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir);
assert!(!out.status.success(), "dcr add without prefix should fail");
}
#[test]
fn dcr_builds_lib_package() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping lib package test");
return;
};
let dir = unique_sandbox_dir("lib_package");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
let updated_toml = toml
.replace("kind = \"bin\"", "kind = \"staticlib\"")
.replace("type = \"none\"", "type = \"lib\"");
std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml");
std::fs::write(dir.join("src").join("my_lib.h"), "void hello();")
.expect("failed to write header");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(out.status.success(), "dcr build should succeed");
let target_dir = dir.join("target");
assert!(
target_dir.join("include").join("my_lib.h").is_file(),
"include/my_lib.h missing"
);
assert!(target_dir.join("lib").exists(), "lib directory missing");
}
#[test]
fn dcr_test_runs_without_sandbox_dependency() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping dcr test integration");
return;
};
let dir = unique_sandbox_dir("dcr_test_independent");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_CC", compiler)];
let out_init = run_dcr_env(&["test", "--init"], &dir, &envs);
assert!(out_init.status.success(), "dcr test --init should succeed");
let out = run_dcr_env(&["test"], &dir, &envs);
assert!(out.status.success(), "dcr test should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
assert!(
stdout.contains("PASS: 1"),
"PASS summary line missing\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
); );
} }

View File

@@ -199,22 +199,25 @@ fn clean_specific_profile() {
let out = run_dcr(&["init"], &dir); let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed"); assert!(out.status.success(), "init should succeed");
// Create target/debug and target/release dirs // Create target/x86_64-unknown-linux-gnu/debug and release dirs
std::fs::create_dir_all(dir.join("target").join("debug")).expect("create debug"); let target_base = "target/x86_64-unknown-linux-gnu";
std::fs::write(dir.join("target").join("debug").join("dummy.o"), "x").expect("write"); std::fs::create_dir_all(dir.join(target_base).join("debug")).expect("create debug");
std::fs::create_dir_all(dir.join("target").join("release")).expect("create release"); std::fs::write(dir.join(target_base).join("debug").join("dummy.o"), "x").expect("write");
std::fs::write(dir.join("target").join("release").join("dummy.o"), "x").expect("write"); std::fs::create_dir_all(dir.join(target_base).join("release")).expect("create release");
std::fs::write(dir.join(target_base).join("release").join("dummy.o"), "x").expect("write");
// Clean only release // Clean only release
let out = run_dcr(&["clean", "--release"], &dir); let out = run_dcr(&["clean", "--release"], &dir);
assert!(out.status.success(), "clean --release should succeed"); assert!(out.status.success(), "clean --release should succeed");
assert!( assert!(
!dir.join("target").join("release").exists(), !dir.join(target_base).join("release").exists(),
"target/release should be removed" "target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
dir.join("target").join("debug").is_dir(), dir.join("target/x86_64-unknown-linux-gnu")
"target/debug should remain" .join("debug")
.is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
); );
} }
@@ -249,7 +252,10 @@ fn staticlib_build() {
let out = run_dcr_env(&["build"], &dir, &envs); let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "staticlib build should succeed"); assert!(out.status.success(), "staticlib build should succeed");
// Check that a .a file exists // Check that a .a file exists
let lib_path = dir.join("target").join("debug"); let lib_path = dir
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug");
let has_lib = std::fs::read_dir(&lib_path) let has_lib = std::fs::read_dir(&lib_path)
.map(|entries| { .map(|entries| {
entries entries