mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 16:39:37 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d08ceaf31d | |||
| 5f0dca4bb3 | |||
| 0e93a1aa0f |
56
CHANGELOG.md
56
CHANGELOG.md
@@ -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
9
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -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]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
168
docs/commands/dcr-gen.md
Normal 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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|
||||||
|
|||||||
922
src/cli/build.rs
922
src/cli/build.rs
File diff suppressed because it is too large
Load Diff
121
src/cli/clean.rs
121
src/cli/clean.rs
@@ -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
46
src/cli/flags.rs
Normal 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
1440
src/cli/gen.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
115
src/cli/run.rs
115
src/cli/run.rs
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user