3 Commits

Author SHA1 Message Date
d08ceaf31d Added:
- `dcr gen` command for IDE integration and build tool support
    - `dcr gen project-info`: Output project metadata as JSON
    - `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
    - `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
    - `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
2026-03-19 00:03:39 +08:00
5f0dca4bb3 Added:
- `dcr gen` command for IDE integration and build tool support
    - `dcr gen project-info`: Output project metadata as JSON
    - `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
    - `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
    - `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
2026-03-19 00:02:04 +08:00
0e93a1aa0f Added:
- `build.steps` for pre-build generators (e.g., `moc`/`uic`/`rcc`)
- `build.pkg_config` for `pkg-config`-driven cflags/ldflags resolution
- `[toolchain]` support for `uic`, `moc`, `rcc`
- Step command variables (`{uic}`, `{moc}`, `{rcc}`, `{cflags}`, `{stem}`, `{in}`, `{out}`)
- `{profile}` placeholder support for dependency paths
2026-03-16 20:50:32 +08:00
34 changed files with 3573 additions and 162 deletions

View File

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

9
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

1440
src/cli/gen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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();
@@ -175,9 +267,11 @@ mod tests {
use super::*; use super::*;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0); static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
static CWD_LOCK: Mutex<()> = Mutex::new(());
fn temp_dir(prefix: &str) -> PathBuf { fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst); let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
@@ -187,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");
@@ -258,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]
@@ -282,9 +395,12 @@ mod tests {
fs::write(src.join("utils.c"), "").unwrap(); fs::write(src.join("utils.c"), "").unwrap();
fs::write(src.join("README.md"), "").unwrap(); // should be ignored fs::write(src.join("README.md"), "").unwrap(); // should be ignored
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");
@@ -303,9 +419,12 @@ mod tests {
fs::write(src.join("other.cc"), "").unwrap(); fs::write(src.join("other.cc"), "").unwrap();
fs::write(src.join("skip.c"), "").unwrap(); // should be ignored fs::write(src.join("skip.c"), "").unwrap(); // should be ignored
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");
@@ -318,9 +437,12 @@ mod tests {
let src = dir.join("src"); let src = dir.join("src");
fs::create_dir_all(&src).unwrap(); fs::create_dir_all(&src).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");
@@ -336,9 +458,12 @@ mod tests {
fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded
let exclude = vec![vendor.clone()]; let exclude = vec![vendor.clone()];
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");
@@ -355,15 +480,92 @@ mod tests {
fs::write(src.join("main.c"), "").unwrap(); fs::write(src.join("main.c"), "").unwrap();
fs::write(sub.join("nested.c"), "").unwrap(); fs::write(sub.join("nested.c"), "").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() {
@@ -36,6 +37,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -73,6 +77,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg("-o").arg(out_path); cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -85,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

@@ -15,7 +15,9 @@ pub struct BuildContext<'a> {
pub platform: Option<&'a str>, pub platform: Option<&'a str>,
pub linker: Option<&'a str>, pub linker: Option<&'a str>,
pub archiver: Option<&'a str>, pub archiver: Option<&'a str>,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf], pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String], pub include_dirs: &'a [String],
pub lib_dirs: &'a [String], pub lib_dirs: &'a [String],
pub libs: &'a [String], pub libs: &'a [String],

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)?;
@@ -27,6 +28,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -91,6 +95,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg(format!("/Fe:{out_path}")); cmd.arg(format!("/Fe:{out_path}"));
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -103,16 +110,29 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> { pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language); let extensions = source_extensions(ctx.language);
common::collect_sources(&extensions, ctx.exclude_dirs) common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
} }
fn source_extensions(language: &str) -> Vec<&str> { fn source_extensions(language: &str) -> Vec<&str> {
let lang = language.to_lowercase(); let mut out = Vec::new();
match lang.as_str() { for part in language.split('+') {
"c" => vec!["c"], let lang = part.trim().to_lowercase();
"c++" | "cpp" | "cxx" => vec!["cpp", "cxx", "cc"], match lang.as_str() {
_ => vec!["c"], "c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => {}
_ => {}
}
} }
if out.is_empty() {
out.extend(["c"]);
}
out
} }
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> { fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
@@ -158,7 +178,7 @@ fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
fn default_flags(profile: &str) -> &'static [&'static str] { fn default_flags(profile: &str) -> &'static [&'static str] {
match profile { match profile {
"release" => &["/O2", "/DNDEBUG"], "release" => &["/O2", "/DNDEBUG"],
"debug" => &["/Od", "/Zi", "/W4", "/DDEBUG", "/Oy-"], "debug" => &["/Od", "/Zi", "/W4", "/DDCR_DEBUG", "/Oy-"],
_ => &[], _ => &[],
} }
} }
@@ -209,6 +229,9 @@ fn build_objects(
cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path)); cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path));
cmd.arg("/showIncludes"); cmd.arg("/showIncludes");
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?; let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);

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() {
@@ -36,6 +37,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -73,6 +77,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg("-o").arg(out_path); cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -85,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(
@@ -112,6 +124,9 @@ fn build_objects(
for flag in ctx.cflags { for flag in ctx.cflags {
cmd.arg(flag); cmd.arg(flag);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => {} Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()), Ok(_) => return Err("Build failed".to_string()),

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")?;
@@ -25,6 +30,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -62,6 +70,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}; };
cmd.arg("-o").arg(out_path); cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -74,17 +85,29 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> { pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language); let extensions = source_extensions(ctx.language);
common::collect_sources(&extensions, ctx.exclude_dirs) common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
} }
fn source_extensions(language: &str) -> Vec<&str> { fn source_extensions(language: &str) -> Vec<&str> {
let lang = language.to_lowercase(); let mut out = Vec::new();
match lang.as_str() { for part in language.split('+') {
"c" => vec!["c"], let lang = part.trim().to_lowercase();
"c++" | "cpp" | "cxx" => vec!["cpp", "cxx", "cc"], match lang.as_str() {
"asm" => vec!["s", "S", "asm"], "c" => out.extend(["c"]),
_ => vec!["c"], "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => out.extend(["s", "S", "asm"]),
_ => {}
}
} }
if out.is_empty() {
out.extend(["c"]);
}
out
} }
fn default_flags(profile: &str) -> &'static [&'static str] { fn default_flags(profile: &str) -> &'static [&'static str] {
@@ -96,7 +119,7 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-fno-omit-frame-pointer", "-fno-omit-frame-pointer",
"-DDEBUG", "-DDCR_DEBUG",
], ],
_ => &[], _ => &[],
} }
@@ -133,6 +156,9 @@ fn build_objects(
cmd.arg(format!("-std={}", ctx.standard)); cmd.arg(format!("-std={}", ctx.standard));
} }
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {
if ctx.platform.is_some() && *flag == "-march=native" {
continue;
}
cmd.arg(flag); cmd.arg(flag);
} }
for flag in ctx.cflags { for flag in ctx.cflags {
@@ -143,6 +169,9 @@ fn build_objects(
} }
let d_path = Path::new(&obj_path).with_extension("d"); let d_path = Path::new(&obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path); cmd.arg("-MMD").arg("-MF").arg(&d_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => {} Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()), Ok(_) => return Err("Build failed".to_string()),

View File

@@ -115,7 +115,12 @@ impl Config {
.and_then(|v| v.as_table()) .and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?; .ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
for key in ["language", "standard", "compiler"] { if let Some(language) = build.get("language") {
self.validate_language(language)?;
} else {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
for key in ["standard", "compiler"] {
let value = build.get(key).and_then(|v| v.as_str()).unwrap_or(""); let value = build.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() { if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("build.{key} is empty"))); return Err(ConfigError::Invalid(format!("build.{key} is empty")));
@@ -144,6 +149,73 @@ impl Config {
return Err(ConfigError::Invalid("build.kind is invalid".into())); return Err(ConfigError::Invalid("build.kind is invalid".into()));
} }
} }
if let Some(exclude) = build.get("exclude") {
let arr = exclude.as_array().ok_or_else(|| {
ConfigError::Invalid("build.exclude must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.exclude contains empty value".into(),
));
}
}
}
if let Some(include) = build.get("include") {
let arr = include.as_array().ok_or_else(|| {
ConfigError::Invalid("build.include must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.include contains empty value".into(),
));
}
}
}
if let Some(roots) = build.get("roots") {
let arr = roots.as_array().ok_or_else(|| {
ConfigError::Invalid("build.roots must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.roots contains empty value".into(),
));
}
}
}
if let Some(src_disable) = build.get("src_disable")
&& !src_disable.is_bool()
{
return Err(ConfigError::Invalid(
"build.src_disable must be a boolean".into(),
));
}
if let Some(clean) = build.get("clean") {
let arr = clean.as_array().ok_or_else(|| {
ConfigError::Invalid("build.clean must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.clean contains empty value".into(),
));
}
}
}
for profile in ["release", "debug"] {
if let Some(section) = build.get(profile) {
let table = section.as_table().ok_or_else(|| {
ConfigError::Invalid(format!("build.{profile} must be a table"))
})?;
self.validate_profile_section(profile, table)?;
}
}
self.validate_workspace()?; self.validate_workspace()?;
Ok(()) Ok(())
} }
@@ -161,6 +233,115 @@ impl Config {
} }
impl Config { impl Config {
fn validate_language(&self, value: &Value) -> Result<(), ConfigError> {
if let Some(s) = value.as_str() {
if s.trim().is_empty() {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
return Ok(());
}
let arr = value
.as_array()
.ok_or_else(|| ConfigError::Invalid("build.language must be string or array".into()))?;
if arr.is_empty() {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.language contains empty value".into(),
));
}
}
Ok(())
}
fn validate_profile_section(
&self,
profile: &str,
table: &toml::value::Table,
) -> Result<(), ConfigError> {
if let Some(lang) = table.get("language") {
self.validate_language(lang)?;
}
for key in ["standard", "compiler", "kind", "target", "platform"] {
if let Some(value) = table.get(key) {
let s = value.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} is empty"
)));
}
}
}
if let Some(kind) = table.get("kind").and_then(|v| v.as_str()) {
let kind = kind.trim();
if !kind.is_empty() && kind != "bin" && kind != "staticlib" && kind != "sharedlib" {
return Err(ConfigError::Invalid(format!(
"build.{profile}.kind is invalid"
)));
}
}
if let Some(src_disable) = table.get("src_disable")
&& !src_disable.is_bool()
{
return Err(ConfigError::Invalid(format!(
"build.{profile}.src_disable must be boolean"
)));
}
for key in [
"cflags",
"ldflags",
"exclude",
"include",
"roots",
"pkg_config",
"generated",
"expect",
"clean",
] {
if let Some(val) = table.get(key) {
let arr = val.as_array().ok_or_else(|| {
ConfigError::Invalid(format!(
"build.{profile}.{key} must be an array of strings"
))
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} contains empty value"
)));
}
}
}
}
for key in ["steps", "post_steps"] {
if let Some(val) = table.get(key) {
let arr = val.as_array().ok_or_else(|| {
ConfigError::Invalid(format!("build.{profile}.{key} must be an array"))
})?;
for item in arr {
let tbl = item.as_table().ok_or_else(|| {
ConfigError::Invalid(format!(
"build.{profile}.{key} entries must be tables"
))
})?;
for req in ["name", "in", "out", "cmd"] {
let s = tbl.get(req).and_then(|v| v.as_str()).unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} missing {req}"
)));
}
}
}
}
}
Ok(())
}
fn validate_workspace(&self) -> Result<(), ConfigError> { fn validate_workspace(&self) -> Result<(), ConfigError> {
let Some(workspace) = self.get("workspace").and_then(|v| v.as_table()) else { let Some(workspace) = self.get("workspace").and_then(|v| v.as_table()) else {
return Ok(()); return Ok(());
@@ -264,7 +445,12 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let language = build.get("language").and_then(|v| v.as_str()).unwrap_or(""); let language_value = build.get("language");
let language = match language_value {
Some(Value::String(s)) => s.to_string(),
Some(Value::Array(arr)) => format_string_array(&Value::Array(arr.clone())),
_ => "".to_string(),
};
let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or(""); let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or("");
let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or(""); let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
@@ -274,7 +460,11 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
out.push_str(&format!("version = \"{version}\"\n\n")); out.push_str(&format!("version = \"{version}\"\n\n"));
out.push_str("[build]\n"); out.push_str("[build]\n");
out.push_str(&format!("language = \"{language}\"\n")); if language.starts_with('[') {
out.push_str(&format!("language = {language}\n"));
} else {
out.push_str(&format!("language = \"{language}\"\n"));
}
out.push_str(&format!("standard = \"{standard}\"\n")); out.push_str(&format!("standard = \"{standard}\"\n"));
out.push_str(&format!("compiler = \"{compiler}\"\n")); out.push_str(&format!("compiler = \"{compiler}\"\n"));
let kind = build let kind = build
@@ -474,6 +664,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

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

View File

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