7 Commits

Author SHA1 Message Date
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
48 changed files with 4415 additions and 274 deletions

View File

@@ -1,7 +1,47 @@
# Changelog # Changelog
## [0.2.10] - 2026-03-16 ## [0.5.0] - 2026-04-10
Added: 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 +49,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 +76,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 +93,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 +125,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 +153,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:

2
Cargo.lock generated
View File

@@ -90,7 +90,7 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.2.10" version = "0.5.0"
dependencies = [ dependencies = [
"glob", "glob",
"reqwest", "reqwest",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.2.10" version = "0.5.0"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@@ -14,9 +14,13 @@ The current implementation is written in Rust.
- Build a project with `debug` and `release` profiles - Build a project with `debug` and `release` profiles
- Run the compiled binary - Run the compiled binary
- Clean build artifacts - Clean build artifacts
- Generate IDE integration files (VS Code, CLion) and compilation databases
- Generate a minimal C project template - Generate a minimal C project template
- Build static and shared libraries - Build static and shared libraries
- ASM projects with NASM/GAS or via GCC/Clang - ASM projects with NASM/GAS or via GCC/Clang
- Mixed language projects (`language = ["c", "asm"]` and similar)
- Cross-compilation with `--target` (short names: `linux`, `macos`, `windows`)
- Target-specific configurations and inheritance
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs) - Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
## Supported Platforms ## Supported Platforms
@@ -104,9 +108,11 @@ Creates a project in the current directory. The project name is taken from the d
### `dcr build [profile]` ### `dcr build [profile]`
Builds the project. If no profile is specified, `--debug` is used. Builds the project. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
### `dcr run [profile]` ### `dcr run [profile]`
Builds the project and runs the binary. If no profile is specified, `--debug` is used. Builds the project and runs the binary. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
Run manually: Run manually:
`./target/<profile>/<name>` `./target/<profile>/<name>`
@@ -115,17 +121,59 @@ 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`.
@@ -146,6 +194,10 @@ compiler = "clang"
# Optional custom flags # Optional custom flags
cflags = ["-Wall", "-Wextra"] cflags = ["-Wall", "-Wextra"]
ldflags = ["-lm"] ldflags = ["-lm"]
#
# Optional include/exclude:
# exclude = ["src/vendor", "src/legacy/**"]
# include = ["src/boot/arch/**"]
[toolchain] [toolchain]
# Optional tool overrides: # Optional tool overrides:
@@ -154,6 +206,9 @@ ldflags = ["-lm"]
# as = "as" # as = "as"
# ar = "ar" # ar = "ar"
# ld = "ld" # ld = "ld"
# uic = "uic"
# moc = "moc"
# rcc = "rcc"
[dependencies] [dependencies]
``` ```

View File

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

View File

@@ -24,6 +24,8 @@
* [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 +33,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)

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

@@ -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

@@ -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

@@ -8,6 +8,7 @@ dcr init
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 +16,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.

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;
@@ -74,7 +75,12 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
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())?;
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"),
None,
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())?;
@@ -85,6 +91,7 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), String> { fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), String> {
with_dir(project_root, || { with_dir(project_root, || {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let project_name = std::env::current_dir() let project_name = std::env::current_dir()
.ok() .ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string())) .and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
@@ -97,31 +104,33 @@ fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), St
" Cleaning project `{}`", " Cleaning project `{}`",
colored(&project_name, BOLD_GREEN) colored(&project_name, BOLD_GREEN)
); );
if !items.contains(&"target".to_string()) {
warn("Directory target not found");
return Ok(());
}
if let Some(profile) = profile { if let Some(profile) = profile {
let target_items = check_dir(Some("target")).unwrap_or_default(); let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&profile.to_string()) { if !target_items.contains(&profile.to_string()) {
warn(&format!("Directory target/{profile} not found")); warn(&format!("Directory target/{profile} not found"));
return Ok(()); } else {
}
println!(" Profile: {}", colored(profile, BOLD_GREEN)); println!(" Profile: {}", colored(profile, BOLD_GREEN));
let _ = fs::remove_dir_all(format!("target/{profile}")); let _ = fs::remove_dir_all(format!("target/{profile}"));
println!( println!(
"{} Removed directory target/{profile}", "{} Removed directory target/{profile}",
colored("\n ✔", BOLD_GREEN) colored("\n ✔", BOLD_GREEN)
); );
}
clean_custom_paths(&config, profile)?;
return Ok(()); return Ok(());
} }
if items.contains(&"target".to_string()) {
let _ = fs::remove_dir_all("target"); let _ = fs::remove_dir_all("target");
println!( println!(
"{} Removed directory target", "{} Removed directory target",
colored("\n ✔", BOLD_GREEN) colored("\n ✔", BOLD_GREEN)
); );
} else {
warn("Directory target not found");
}
clean_custom_paths(&config, "debug")?;
clean_custom_paths(&config, "release")?;
Ok(()) Ok(())
}) })
} }
@@ -136,3 +145,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,
})
}

1458
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,10 @@
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 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,40 @@ pub fn run(args: &[String]) -> i32 {
.map(|s| s.trim()) .map(|s| s.trim())
.filter(|s| !s.is_empty()); .filter(|s| !s.is_empty());
if let Some(first_arg) = args.first() { let flags = match parse_build_run_flags(args) {
if first_arg.starts_with("--") { Ok(v) => v,
let candidate = first_arg.trim_start_matches("--"); Err(_) => return 1,
if flags(candidate).is_some() { };
active_profile = candidate.to_string();
} else { let version = config
warn("Unknown build flag"); .get("package.version")
return 1; .and_then(|v| v.as_str())
} .unwrap_or("");
} else { let run_cmd = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), version);
warn("Unknown argument");
return 1;
}
}
let kind = build_kind.trim(); let kind = build_kind.trim();
if kind == "staticlib" || kind == "sharedlib" { if run_cmd.is_none() && (kind == "staticlib" || kind == "sharedlib") {
error("Cannot run library build"); error("Cannot run library build");
return 1; return 1;
} }
let build_status = build(args); let build_status = build(args);
let bin_path = crate::platform::bin_path(&active_profile, project_name, target_dir); let bin_path = crate::platform::bin_path(&flags.profile, project_name, target_dir);
if build_status == 0 { if build_status == 0 {
if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------");
return run_shell(&cmd);
}
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------"); println!("--------------------------------");
return run_binary(project_name, &active_profile, target_dir); return run_binary(project_name, &flags.profile, target_dir);
} }
let fallback_code = run_binary(project_name, &active_profile, target_dir); let fallback_code = if let Some(cmd) = run_cmd {
run_shell(&cmd)
} else {
run_binary(project_name, &flags.profile, target_dir)
};
if fallback_code != 1 { if fallback_code != 1 {
return fallback_code; return fallback_code;
} }
@@ -98,3 +139,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,
}
}

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,16 @@ 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 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,9 +66,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {
cmd.arg(flag); 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();
for part in language.split('+') {
let lang = part.trim().to_lowercase();
match lang.as_str() { match lang.as_str() {
"c" => vec!["c"], "c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => vec!["cpp", "cxx", "cc"], "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
_ => vec!["c"], "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,9 +219,11 @@ 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);
} }
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {
cmd.arg(flag); 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();
for part in language.split('+') {
let lang = part.trim().to_lowercase();
match lang.as_str() { match lang.as_str() {
"c" => vec!["c"], "c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => vec!["cpp", "cxx", "cc"], "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => vec!["s", "S", "asm"], "asm" => out.extend(["s", "S", "asm"]),
_ => vec!["c"], _ => {}
} }
}
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,9 +155,11 @@ 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));
} }
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {
cmd.arg(flag); cmd.arg(flag);
} }
}
for flag in ctx.cflags { for flag in ctx.cflags {
cmd.arg(flag); cmd.arg(flag);
} }

View File

@@ -115,7 +115,12 @@ impl Config {
.and_then(|v| v.as_table()) .and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?; .ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
for key in ["language", "standard", "compiler"] { if let Some(language) = build.get("language") {
self.validate_language(language)?;
} else {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
for key in ["standard", "compiler"] {
let value = build.get(key).and_then(|v| v.as_str()).unwrap_or(""); let value = build.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() { if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("build.{key} is empty"))); return Err(ConfigError::Invalid(format!("build.{key} is empty")));
@@ -144,6 +149,73 @@ impl Config {
return Err(ConfigError::Invalid("build.kind is invalid".into())); return Err(ConfigError::Invalid("build.kind is invalid".into()));
} }
} }
if let Some(exclude) = build.get("exclude") {
let arr = exclude.as_array().ok_or_else(|| {
ConfigError::Invalid("build.exclude must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.exclude contains empty value".into(),
));
}
}
}
if let Some(include) = build.get("include") {
let arr = include.as_array().ok_or_else(|| {
ConfigError::Invalid("build.include must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.include contains empty value".into(),
));
}
}
}
if let Some(roots) = build.get("roots") {
let arr = roots.as_array().ok_or_else(|| {
ConfigError::Invalid("build.roots must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.roots contains empty value".into(),
));
}
}
}
if let Some(src_disable) = build.get("src_disable")
&& !src_disable.is_bool()
{
return Err(ConfigError::Invalid(
"build.src_disable must be a boolean".into(),
));
}
if let Some(clean) = build.get("clean") {
let arr = clean.as_array().ok_or_else(|| {
ConfigError::Invalid("build.clean must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.clean contains empty value".into(),
));
}
}
}
for profile in ["release", "debug"] {
if let Some(section) = build.get(profile) {
let table = section.as_table().ok_or_else(|| {
ConfigError::Invalid(format!("build.{profile} must be a table"))
})?;
self.validate_profile_section(profile, table)?;
}
}
self.validate_workspace()?; self.validate_workspace()?;
Ok(()) Ok(())
} }
@@ -161,6 +233,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(());
@@ -264,7 +446,12 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let language = build.get("language").and_then(|v| v.as_str()).unwrap_or(""); let language_value = build.get("language");
let language = match language_value {
Some(Value::String(s)) => s.to_string(),
Some(Value::Array(arr)) => format_string_array(&Value::Array(arr.clone())),
_ => "".to_string(),
};
let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or(""); let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or("");
let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or(""); let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
@@ -274,7 +461,11 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
out.push_str(&format!("version = \"{version}\"\n\n")); out.push_str(&format!("version = \"{version}\"\n\n"));
out.push_str("[build]\n"); out.push_str("[build]\n");
if language.starts_with('[') {
out.push_str(&format!("language = {language}\n"));
} else {
out.push_str(&format!("language = \"{language}\"\n")); 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
@@ -474,6 +665,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

@@ -23,6 +23,7 @@ pub struct ResolvedDeps {
pub fn resolve_deps( pub fn resolve_deps(
config: &Config, config: &Config,
profile: &str, profile: &str,
target: Option<&str>,
project_root: &Path, project_root: &Path,
) -> Result<ResolvedDeps, String> { ) -> Result<ResolvedDeps, String> {
let project_name = config let project_name = config
@@ -35,7 +36,7 @@ pub fn resolve_deps(
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
let deps = parse_dependencies(config)?; let deps = parse_dependencies(config, profile, target)?;
if deps.is_empty() { if deps.is_empty() {
return Ok(ResolvedDeps { return Ok(ResolvedDeps {
include_dirs: Vec::new(), include_dirs: Vec::new(),
@@ -124,14 +125,38 @@ struct DepLock {
source: String, source: String,
} }
fn parse_dependencies(config: &Config) -> Result<Vec<DepSpec>, String> { fn parse_dependencies(
let deps_val = match config.get("dependencies") { config: &Config,
Some(v) => v, profile: &str,
target: Option<&str>,
) -> Result<Vec<DepSpec>, String> {
let mut deps_table = None;
// Order: dependencies.target.profile, dependencies.profile.target, dependencies.target, dependencies.profile, dependencies
let combinations = if let Some(t) = target {
let normalized_t = crate::cli::build::normalize_target_os(t);
vec![
format!("dependencies.{}.{}", normalized_t, profile),
format!("dependencies.{}.{}", profile, normalized_t),
format!("dependencies.{}", normalized_t),
format!("dependencies.{}", profile),
"dependencies".to_string(),
]
} else {
vec![
format!("dependencies.{}", profile),
"dependencies".to_string(),
]
};
for key in combinations {
if let Some(val) = config.get(&key).and_then(|v| v.as_table()) {
deps_table = Some(val);
break;
}
}
let deps_table = match deps_table {
Some(t) => t,
None => return Ok(Vec::new()), 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(); let mut deps = Vec::new();
for (name, value) in deps_table { for (name, value) in deps_table {
match value { match value {

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

@@ -20,7 +20,9 @@ fn main() {
"init" => cli::init::init(rest), "init" => cli::init::init(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

@@ -218,3 +218,31 @@ fn workspace_build_and_clean_all() {
"root target/debug should remain" "root target/debug should remain"
); );
} }
#[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
);
}