mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 22:09:35 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cefbb3c3a | |||
| 045cc90d81 | |||
| fd88e036ed | |||
| 613fbc6a6a | |||
| 03f72adade | |||
| cd5023243b |
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.5.1] - 2026-04-12
|
||||||
|
|
||||||
|
Fixed:
|
||||||
|
|
||||||
|
- Windows compatibility for target-specific config without --target flag
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Default host target detection for automatic target overrides
|
||||||
|
- Unit tests for target normalization
|
||||||
|
|
||||||
|
Improved:
|
||||||
|
|
||||||
|
- Config value lookup optimization
|
||||||
|
- Documentation for Windows cross-compilation
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-04-10
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Full inheritance system for all config sections: `build`, `toolchain`, `run`, `workspace`, `dependencies`
|
||||||
|
- Order: `section.target.profile` → `section.profile.target` → `section.target` → `section.profile` → `section`
|
||||||
|
- `inherit = false` to disable inheritance completely
|
||||||
|
- Target-specific overrides: `[build.linux]`, `[toolchain.windows]`, `[run.macos]`, etc.
|
||||||
|
- Cross-compilation support with `--target <triple>`
|
||||||
|
- Full target triples and short names (`linux`, `macos`, `windows`)
|
||||||
|
- Target-specific config sections: `[build.<target>]`, `[toolchain.<target>]`, `[run.<target>]`
|
||||||
|
- Workspace and dependencies support target/profile inheritance
|
||||||
|
- Multiple targets build: `[build.targets]` to build for multiple targets simultaneously
|
||||||
|
- Target directory structure: `target/<target>/<profile>/` (native or triple)
|
||||||
|
- Updated documentation with cross-compilation guide and Arch Linux examples
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-04-10
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- Test command: `dcr test` and `dcr tests`
|
||||||
|
- Automatic compilation and linking for tests without manual configuration
|
||||||
|
- `dcr test --init` creates test template and header
|
||||||
|
- Test framework with EXPECT/SKIP macros in `dcr_test.h`
|
||||||
|
- Improved test integration: added test CLI module and basic test suite
|
||||||
|
- Documentation updates for testing functionality
|
||||||
|
|
||||||
## [0.3.0] - 2026-03-18
|
## [0.3.0] - 2026-03-18
|
||||||
|
|
||||||
Added:
|
Added:
|
||||||
|
|||||||
@@ -22,6 +22,27 @@ cargo check
|
|||||||
- Keep functions small and focused.
|
- Keep functions small and focused.
|
||||||
- Prefer clear names over clever ones.
|
- Prefer clear names over clever ones.
|
||||||
|
|
||||||
|
## Target System
|
||||||
|
|
||||||
|
DCR supports cross-compilation via `--target <triple>` or short names:
|
||||||
|
|
||||||
|
- `dcr build --target linux --release` (maps to `x86_64-unknown-linux-gnu`)
|
||||||
|
- `dcr build --target macos --release` (maps to `x86_64-apple-darwin`)
|
||||||
|
- `dcr build --target windows --release` (maps to `x86_64-pc-windows-msvc`)
|
||||||
|
- Full triples: `dcr build --target aarch64-linux-gnu --release`
|
||||||
|
- Configure multiple targets in `dcr.toml`:
|
||||||
|
```
|
||||||
|
[build.targets]
|
||||||
|
targets = ["linux", "macos"]
|
||||||
|
|
||||||
|
[build.linux]
|
||||||
|
compiler = "gcc"
|
||||||
|
|
||||||
|
[build.macos]
|
||||||
|
compiler = "clang"
|
||||||
|
```
|
||||||
|
- If no `--target`, builds for targets in `build.targets` or native if empty.
|
||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
Before PR, run:
|
Before PR, run:
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -90,7 +90,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.3.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.3.0"
|
version = "0.5.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -19,6 +19,8 @@ The current implementation is written in Rust.
|
|||||||
- 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)
|
- Mixed language projects (`language = ["c", "asm"]` and similar)
|
||||||
|
- Cross-compilation with `--target` (short names: `linux`, `macos`, `windows`)
|
||||||
|
- Target-specific configurations and inheritance
|
||||||
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
|
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
|
||||||
|
|
||||||
## Supported Platforms
|
## Supported Platforms
|
||||||
@@ -119,6 +121,16 @@ Run manually:
|
|||||||
Removes the `target` directory in the project root.
|
Removes the `target` directory in the project root.
|
||||||
Use `dcr clean --all` in a workspace root to clean all member projects.
|
Use `dcr clean --all` in a workspace root to clean all member projects.
|
||||||
|
|
||||||
|
### `dcr gen`
|
||||||
|
Generates IDE integration files and build tools.
|
||||||
|
- `dcr gen vscode`: VS Code workspace config
|
||||||
|
- `dcr gen clion`: CLion project files
|
||||||
|
- `dcr gen compile-commands`: `compile_commands.json` for clang tools
|
||||||
|
|
||||||
|
### `dcr test`
|
||||||
|
Runs the test suite.
|
||||||
|
Use `dcr test --init` to create test files.
|
||||||
|
|
||||||
## Build Profiles
|
## Build Profiles
|
||||||
Two profiles are supported:
|
Two profiles are supported:
|
||||||
- `--debug` (default) - built-in flags per compiler
|
- `--debug` (default) - built-in flags per compiler
|
||||||
@@ -136,6 +148,33 @@ 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.steps` and `build.post_steps` to run custom commands.
|
||||||
Use `build.clean` for extra cleanup paths and `[run].cmd` to override run command.
|
Use `build.clean` for extra cleanup paths and `[run].cmd` to override run command.
|
||||||
|
|
||||||
|
## Cross-compilation
|
||||||
|
|
||||||
|
DCR supports cross-compilation via `--target <triple>` or short names:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dcr build --target linux --release # x86_64-unknown-linux-gnu
|
||||||
|
dcr build --target macos --release # x86_64-apple-darwin
|
||||||
|
dcr build --target windows --release # x86_64-pc-windows-msvc
|
||||||
|
dcr run --target aarch64-linux-gnu
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure multiple targets and target-specific settings in `dcr.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[build.targets]
|
||||||
|
targets = ["linux", "windows"]
|
||||||
|
|
||||||
|
[build.linux]
|
||||||
|
compiler = "gcc"
|
||||||
|
cflags = ["-O2"]
|
||||||
|
|
||||||
|
[build.windows]
|
||||||
|
compiler = "x86_64-w64-mingw32-gcc"
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Cross-compilation guide](docs/dependencies-and-build/cross-compilation.md) for details.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
The main project file is `dcr.toml`.
|
The main project file is `dcr.toml`.
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
* [dcr run](commands/dcr-run.md)
|
* [dcr run](commands/dcr-run.md)
|
||||||
* [dcr clean](commands/dcr-clean.md)
|
* [dcr clean](commands/dcr-clean.md)
|
||||||
* [dcr gen](commands/dcr-gen.md)
|
* [dcr gen](commands/dcr-gen.md)
|
||||||
|
* [dcr test](commands/dcr-test.md)
|
||||||
|
|
||||||
## Dependencies & Build
|
## Dependencies & Build
|
||||||
|
|
||||||
@@ -32,11 +33,15 @@
|
|||||||
* [Local dependencies](dependencies-and-build/local-dependencies.md)
|
* [Local dependencies](dependencies-and-build/local-dependencies.md)
|
||||||
* [Versioning & lock file](dependencies-and-build/versioning-and-lock-file.md)
|
* [Versioning & lock file](dependencies-and-build/versioning-and-lock-file.md)
|
||||||
* [Target directory](dependencies-and-build/target-directory.md)
|
* [Target directory](dependencies-and-build/target-directory.md)
|
||||||
|
* [Cross-compilation and targets](dependencies-and-build/cross-compilation.md)
|
||||||
* [Compilers & linking](dependencies-and-build/compilers-and-linking.md)
|
* [Compilers & linking](dependencies-and-build/compilers-and-linking.md)
|
||||||
|
|
||||||
## Reference & Help
|
## Reference & Help
|
||||||
|
|
||||||
* [CLI reference](reference-and-help/cli-reference.md)
|
* [CLI reference](reference-and-help/cli-reference.md)
|
||||||
* [Config reference](reference-and-help/config-reference.md)
|
* [Config reference](reference-and-help/config-reference.md)
|
||||||
|
* [Workspace guide](reference-and-help/workspace-guide.md)
|
||||||
|
* [Flags & environment](reference-and-help/flags-and-environment.md)
|
||||||
|
* [Troubleshooting map](reference-and-help/troubleshooting-map.md)
|
||||||
* [FAQ](reference-and-help/faq.md)
|
* [FAQ](reference-and-help/faq.md)
|
||||||
* [Contributing & Bug reports](reference-and-help/contributing-and-bug-reports.md)
|
* [Contributing & Bug reports](reference-and-help/contributing-and-bug-reports.md)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Builds the project in `debug` or `release` profile.
|
|||||||
dcr build
|
dcr build
|
||||||
dcr build --debug
|
dcr build --debug
|
||||||
dcr build --release
|
dcr build --release
|
||||||
|
dcr build --target linux --release
|
||||||
dcr build --release --force
|
dcr build --release --force
|
||||||
dcr build --debug --clean
|
dcr build --debug --clean
|
||||||
```
|
```
|
||||||
@@ -52,11 +53,14 @@ dcr build --debug --clean
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The profile is selected from the first argument only.
|
- Profile flag (`--debug` / `--release`) can appear in any argument position (duplicates are rejected).
|
||||||
- Unknown profile flags return an error.
|
- Unknown profile flags return an error.
|
||||||
- Incremental rebuild is based on source/object mtime comparison.
|
- `--target <triple>` builds for specified target (short names: `linux`, `macos`, `windows`).
|
||||||
|
- If no `--target`, builds for `build.targets` or native.
|
||||||
|
- Incremental rebuild checks source/object mtime and tracked header dependencies.
|
||||||
- `--force` skips build cache checks and recompiles.
|
- `--force` skips build cache checks and recompiles.
|
||||||
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
|
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
|
||||||
|
- Default GCC/Clang profile flags: `debug` -> `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`, `release` -> `-O3 -DNDEBUG`.
|
||||||
- For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`.
|
- For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`.
|
||||||
- In workspace root, `dcr build` builds all members in dependency order.
|
- In workspace root, `dcr build` builds all members in dependency order.
|
||||||
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
|
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ Usage in VSCode:
|
|||||||
- Open the Command Palette (`Ctrl+Shift+P`) and select **"Debug: Start Debugging"** to run debugger
|
- Open the Command Palette (`Ctrl+Shift+P`) and select **"Debug: Start Debugging"** to run debugger
|
||||||
- Press `Ctrl+Shift+B` to run the build task
|
- Press `Ctrl+Shift+B` to run the build task
|
||||||
- Clangd will provide IntelliSense and diagnostics based on `compile_commands.json`
|
- Clangd will provide IntelliSense and diagnostics based on `compile_commands.json`
|
||||||
|
- Generated `launch.json` uses CodeLLDB (`type: "lldb"`), so install `vadimcn.vscode-lldb`
|
||||||
|
|
||||||
### `dcr gen clion`
|
### `dcr gen clion`
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ When running `dcr gen` in a workspace root, all commands process both the root p
|
|||||||
- Verify binary paths in `launch.json` match actual build output (`target/<profile>/binary-name`)
|
- 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
|
- For workspace projects, each member's debug config uses its own `target/` directory
|
||||||
- Ensure GDB/LLDB is installed: `gdb --version` or `lldb --version`
|
- Ensure GDB/LLDB is installed: `gdb --version` or `lldb --version`
|
||||||
|
- If VSCode reports unsupported debugger type, install the extension that matches `launch.json` type (`vadimcn.vscode-lldb` for `lldb`)
|
||||||
|
|
||||||
### CLion not recognizing includes
|
### CLion not recognizing includes
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Builds the project and runs the resulting binary.
|
|||||||
dcr run
|
dcr run
|
||||||
dcr run --debug
|
dcr run --debug
|
||||||
dcr run --release
|
dcr run --release
|
||||||
|
dcr run --target linux --release
|
||||||
dcr run --release --force
|
dcr run --release --force
|
||||||
dcr run --debug --clean
|
dcr run --debug --clean
|
||||||
```
|
```
|
||||||
@@ -24,6 +25,7 @@ dcr run --debug --clean
|
|||||||
|
|
||||||
- If `build.kind = "staticlib"` or `build.kind = "sharedlib"`, `run` exits with error.
|
- If `build.kind = "staticlib"` or `build.kind = "sharedlib"`, `run` exits with error.
|
||||||
- The profile is selected from the first argument.
|
- The profile is selected from the first argument.
|
||||||
|
- `--target <triple>` runs the binary for specified target (short names: `linux`, `macos`, `windows`).
|
||||||
- `--force` skips build cache checks and recompiles.
|
- `--force` skips build cache checks and recompiles.
|
||||||
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
|
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
|
||||||
|
|
||||||
|
|||||||
44
docs/commands/dcr-test.md
Normal file
44
docs/commands/dcr-test.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# dcr test
|
||||||
|
|
||||||
|
Runs the project's test suite.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dcr test
|
||||||
|
dcr test --release
|
||||||
|
dcr test --target linux
|
||||||
|
dcr test --init
|
||||||
|
```
|
||||||
|
|
||||||
|
## What `test` does
|
||||||
|
|
||||||
|
1. Builds the project with the specified profile/target.
|
||||||
|
2. Compiles test files in `tests/` (e.g., `tests/test.c`) into a test binary.
|
||||||
|
3. Runs the test binary and parses output for PASS/SKIP/FAIL results.
|
||||||
|
4. Reports summary with colored output.
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
DCR uses a simple test framework:
|
||||||
|
|
||||||
|
- Header: `tests/dcr_test.h` (generated by `dcr test --init`)
|
||||||
|
- Test file: `tests/test.c` (template generated by `dcr test --init`)
|
||||||
|
- Macros: `TEST(name)`, `TEST_CASE(name)`, `EXPECT(condition)`, `SKIP(reason)`
|
||||||
|
- Output: `[PASS] test_name`, `[FAIL] test_name`, or `[SKIP] test_name`
|
||||||
|
|
||||||
|
## Config values used
|
||||||
|
|
||||||
|
- `package.name`
|
||||||
|
- `build.compiler`, `build.language`, `build.standard`
|
||||||
|
- `build.cflags`, `build.ldflags`
|
||||||
|
- `build.debug` / `build.release`
|
||||||
|
- `build.targets` (if specified)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Test binary is linked with the main project (for lib projects) or standalone.
|
||||||
|
- `--init` creates `tests/dcr_test.h` and `tests/test.c` if missing.
|
||||||
|
- Tests run in release mode by default for performance.
|
||||||
|
- Custom test logic can be added by editing `tests/test.c`.
|
||||||
|
- Fails if no `tests/test.c` exists (run `dcr test --init` first).
|
||||||
@@ -21,7 +21,7 @@ You can also add `--force` or `--clean` to `build`/`run`.
|
|||||||
### GCC/Clang-like toolchains
|
### GCC/Clang-like toolchains
|
||||||
|
|
||||||
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
|
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
|
||||||
- `release`: `-O3 -DNDEBUG -march=native`
|
- `release`: `-O3 -DNDEBUG`
|
||||||
|
|
||||||
### MSVC toolchain
|
### MSVC toolchain
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ Default compiler flags:
|
|||||||
GCC/Clang:
|
GCC/Clang:
|
||||||
|
|
||||||
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
|
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
|
||||||
- `release`: `-O3 -DNDEBUG -march=native`
|
- `release`: `-O3 -DNDEBUG`
|
||||||
|
|
||||||
MSVC:
|
MSVC:
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ standard = "c11"
|
|||||||
compiler = "clang"
|
compiler = "clang"
|
||||||
kind = "bin"
|
kind = "bin"
|
||||||
# optional:
|
# optional:
|
||||||
|
# inherit = true # Inherit from base (default true)
|
||||||
|
# targets = ["linux", "macos"] # Multiple targets to build
|
||||||
# target = "./dist"
|
# target = "./dist"
|
||||||
# platform = "x86_64"
|
# platform = "x86_64"
|
||||||
# cflags = ["-Wall"]
|
# cflags = ["-Wall"]
|
||||||
@@ -30,6 +32,8 @@ kind = "bin"
|
|||||||
- `standard` (string, required): language standard passed to compiler.
|
- `standard` (string, required): language standard passed to compiler.
|
||||||
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
|
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
|
||||||
- `kind` (string, required): `bin`, `staticlib`, or `sharedlib`.
|
- `kind` (string, required): `bin`, `staticlib`, or `sharedlib`.
|
||||||
|
- `inherit` (bool, optional): inherit settings from base `[build]` (default `true`). If `false`, only use target/profile specific settings.
|
||||||
|
- `targets` (string array, optional): list of targets to build simultaneously. Supports short names (`linux`, `macos`, `windows`).
|
||||||
- `target` (string, optional): custom output directory for final artifact.
|
- `target` (string, optional): custom output directory for final artifact.
|
||||||
- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`).
|
- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`).
|
||||||
- `cflags` (string array, optional): extra compile flags.
|
- `cflags` (string array, optional): extra compile flags.
|
||||||
@@ -82,6 +86,15 @@ Step notes:
|
|||||||
Profile tables inherit all values from `[build]`. Any field present in the profile table replaces the base value.
|
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.
|
Array fields (like `cflags`/`ldflags`) are appended to the base array.
|
||||||
|
|
||||||
|
Target tables override profile and base settings for specific targets. Full inheritance order:
|
||||||
|
1. `[build.<target>.<profile>]`
|
||||||
|
2. `[build.<profile>.<target>]`
|
||||||
|
3. `[build.<target>]`
|
||||||
|
4. `[build.<profile>]`
|
||||||
|
5. `[build]`
|
||||||
|
|
||||||
|
If custom `cflags` are set (non-empty), default compiler flags are disabled.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[build.debug]
|
[build.debug]
|
||||||
cflags = ["-g3"]
|
cflags = ["-g3"]
|
||||||
@@ -89,6 +102,29 @@ cflags = ["-g3"]
|
|||||||
[build.release]
|
[build.release]
|
||||||
cflags = ["-O3"]
|
cflags = ["-O3"]
|
||||||
ldflags = ["-s"]
|
ldflags = ["-s"]
|
||||||
|
|
||||||
|
# Target-specific overrides (applied after profile)
|
||||||
|
[build.linux]
|
||||||
|
compiler = "gcc"
|
||||||
|
cflags = ["-march=x86_64"]
|
||||||
|
|
||||||
|
[build.macos]
|
||||||
|
compiler = "clang"
|
||||||
|
cflags = ["-arch", "x86_64"]
|
||||||
|
|
||||||
|
# Full inheritance example
|
||||||
|
[build.linux.release]
|
||||||
|
cflags = ["-O3", "-march=native"] # Overrides profile and base
|
||||||
|
|
||||||
|
[build.release.linux]
|
||||||
|
ldflags = ["-static"] # Alternative order
|
||||||
|
|
||||||
|
[build.windows.debug]
|
||||||
|
compiler = "x86_64-w64-mingw32-gcc"
|
||||||
|
cflags = [] # Empty to disable defaults
|
||||||
|
|
||||||
|
[build.release]
|
||||||
|
inherit = false # No inheritance, only explicit settings
|
||||||
```
|
```
|
||||||
|
|
||||||
## Toolchain section
|
## Toolchain section
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ kind = "bin"
|
|||||||
- `[dependencies]`
|
- `[dependencies]`
|
||||||
|
|
||||||
Workspace-only root:
|
Workspace-only root:
|
||||||
- `[workspace]` may be used instead of `[package]`/`[build]` for a workspace root.
|
- `[workspace]` can be added to a normal root config to define members and dependency order.
|
||||||
|
- Root config still requires `[package]`, `[build]`, and `[dependencies]`.
|
||||||
|
|
||||||
## Required keys
|
## Required keys
|
||||||
|
|
||||||
@@ -42,7 +43,10 @@ Workspace-only root:
|
|||||||
|
|
||||||
## Optional keys
|
## Optional keys
|
||||||
|
|
||||||
|
- `build.inherit`
|
||||||
|
- `build.targets`
|
||||||
- `build.target`
|
- `build.target`
|
||||||
|
- `build.platform`
|
||||||
- `build.cflags`
|
- `build.cflags`
|
||||||
- `build.ldflags`
|
- `build.ldflags`
|
||||||
- `build.exclude`
|
- `build.exclude`
|
||||||
@@ -57,7 +61,11 @@ Workspace-only root:
|
|||||||
- `build.clean`
|
- `build.clean`
|
||||||
- `build.debug`
|
- `build.debug`
|
||||||
- `build.release`
|
- `build.release`
|
||||||
|
- `build.<target>` (target-specific overrides)
|
||||||
- dependency fields (`include`, `lib`, `libs`, `system`)
|
- dependency fields (`include`, `lib`, `libs`, `system`)
|
||||||
- `[toolchain]` overrides (`cc`, `cxx`, `as`, `ar`, `ld`, `uic`, `moc`, `rcc`)
|
- `[toolchain]` overrides (`cc`, `cxx`, `as`, `ar`, `ld`, `uic`, `moc`, `rcc`)
|
||||||
|
- `[toolchain.<target>]` (target-specific toolchain)
|
||||||
- `[workspace]` members
|
- `[workspace]` members
|
||||||
|
- `[workspace.<target>]` (target-specific workspace)
|
||||||
- `[run]` overrides (`cmd`)
|
- `[run]` overrides (`cmd`)
|
||||||
|
- `[run.<target>]` (target-specific run)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ DCR picks a backend from `build.compiler`:
|
|||||||
- 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.
|
- `build.roots` and `build.src_disable` can replace the default `src/` root.
|
||||||
- Mixed language builds use arrays, for example `build.language = ["c", "asm"]`.
|
- 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/<target>/<profile>/obj/<relative-path>.o` (`.obj` on MSVC, where `<target>` is `native` or target triple).
|
||||||
- Recompile happens when source mtime is newer than object mtime.
|
- Recompile happens when source mtime is newer than object mtime.
|
||||||
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
|
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
|
||||||
|
|
||||||
@@ -28,13 +28,15 @@ These flags are applied automatically based on profile and compiler backend.
|
|||||||
GCC/Clang:
|
GCC/Clang:
|
||||||
|
|
||||||
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
|
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
|
||||||
- `release`: `-O3 -DNDEBUG -march=native`
|
- `release`: `-O3 -DNDEBUG`
|
||||||
|
|
||||||
MSVC:
|
MSVC:
|
||||||
|
|
||||||
- `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
|
- `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
|
||||||
- `release`: `/O2 /DNDEBUG`
|
- `release`: `/O2 /DNDEBUG`
|
||||||
|
|
||||||
|
Note: If custom `cflags` are set in `dcr.toml`, default profile flags are disabled to allow full control.
|
||||||
|
|
||||||
## Platform hint
|
## Platform hint
|
||||||
|
|
||||||
If `build.platform` is set, DCR passes it as an architecture hint:
|
If `build.platform` is set, DCR passes it as an architecture hint:
|
||||||
|
|||||||
201
docs/dependencies-and-build/cross-compilation.md
Normal file
201
docs/dependencies-and-build/cross-compilation.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# Cross-compilation and Targets
|
||||||
|
|
||||||
|
DCR supports cross-compilation to different platforms using the `--target` flag. This allows building your project for architectures and operating systems different from the host.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for a specific target
|
||||||
|
dcr build --target x86_64-unknown-linux-gnu --release
|
||||||
|
|
||||||
|
# Short names for common platforms
|
||||||
|
dcr build --target linux --release # x86_64-unknown-linux-gnu
|
||||||
|
dcr build --target macos --release # x86_64-apple-darwin
|
||||||
|
dcr build --target windows --release # x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
# Run for a specific target
|
||||||
|
dcr run --target linux --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Targets
|
||||||
|
|
||||||
|
### Short Names
|
||||||
|
- `linux` → `x86_64-unknown-linux-gnu`
|
||||||
|
- `macos` → `x86_64-apple-darwin`
|
||||||
|
- `windows` → `x86_64-pc-windows-msvc`
|
||||||
|
|
||||||
|
### Full Target Triples
|
||||||
|
DCR supports any valid target triple in the format `<arch>-<vendor>-<os>-<abi>`. Common examples:
|
||||||
|
- `x86_64-unknown-linux-gnu`
|
||||||
|
- `aarch64-linux-gnu`
|
||||||
|
- `x86_64-apple-darwin`
|
||||||
|
- `aarch64-apple-darwin`
|
||||||
|
- `x86_64-pc-windows-msvc`
|
||||||
|
- `i686-pc-windows-gnu`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Multiple Targets
|
||||||
|
|
||||||
|
Configure multiple targets to build simultaneously:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[build.targets]
|
||||||
|
targets = ["linux", "macos", "windows"]
|
||||||
|
|
||||||
|
# Or per profile
|
||||||
|
[build.release.targets]
|
||||||
|
targets = ["linux", "macos"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If no `--target` is specified, DCR builds for all targets in `build.targets` (or native if empty).
|
||||||
|
|
||||||
|
### Target-Specific Settings
|
||||||
|
|
||||||
|
Override settings for specific targets:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[build.linux]
|
||||||
|
compiler = "gcc"
|
||||||
|
cflags = ["-Wall", "-O2"]
|
||||||
|
|
||||||
|
[build.macos]
|
||||||
|
compiler = "clang"
|
||||||
|
cflags = ["-Wall", "-O2", "-arch", "x86_64"]
|
||||||
|
|
||||||
|
[build.windows]
|
||||||
|
compiler = "x86_64-w64-mingw32-gcc"
|
||||||
|
ldflags = ["-static"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Target-Specific Toolchain
|
||||||
|
|
||||||
|
Configure toolchain per target:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[toolchain.linux]
|
||||||
|
cc = "gcc"
|
||||||
|
ld = "ld"
|
||||||
|
|
||||||
|
[toolchain.macos]
|
||||||
|
cc = "clang"
|
||||||
|
ld = "ld"
|
||||||
|
|
||||||
|
[toolchain.windows]
|
||||||
|
cc = "x86_64-w64-mingw32-gcc"
|
||||||
|
ld = "x86_64-w64-mingw32-ld"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Target-Specific Run Commands
|
||||||
|
|
||||||
|
Use custom run commands per target:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[run.linux]
|
||||||
|
cmd = "./target/linux/release/myapp"
|
||||||
|
|
||||||
|
[run.macos]
|
||||||
|
cmd = "open ./target/macos/release/myapp.app"
|
||||||
|
|
||||||
|
[run.windows]
|
||||||
|
cmd = "wine ./target/windows/release/myapp.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workspace and Dependencies Inheritance
|
||||||
|
|
||||||
|
Workspace and dependencies sections also support target/profile inheritance:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[workspace.linux]
|
||||||
|
members = [
|
||||||
|
{ name = "lib1", path = "libs/lib1" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.linux]
|
||||||
|
sdl2 = { system = true, include = ["/usr/include/SDL2"] }
|
||||||
|
|
||||||
|
[dependencies.windows]
|
||||||
|
sdl2 = { system = true, include = ["C:/SDL2/include"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Order: `workspace.target.profile` → `workspace.profile.target` → `workspace.target` → `workspace.profile` → `workspace`.
|
||||||
|
|
||||||
|
Add `inherit = false` to disable inheritance in any section.
|
||||||
|
|
||||||
|
Settings are applied in order:
|
||||||
|
1. `[build.<target>]`
|
||||||
|
2. `[build.<profile>]`
|
||||||
|
3. `[build]`
|
||||||
|
|
||||||
|
### Toolchain Configuration
|
||||||
|
|
||||||
|
For cross-compilation, configure toolchains:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[toolchain]
|
||||||
|
cc = "aarch64-linux-gnu-gcc"
|
||||||
|
cxx = "aarch64-linux-gnu-g++"
|
||||||
|
ar = "aarch64-linux-gnu-ar"
|
||||||
|
ld = "aarch64-linux-gnu-ld"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
Build outputs are organized by target:
|
||||||
|
|
||||||
|
```
|
||||||
|
target/
|
||||||
|
├── native/
|
||||||
|
│ ├── debug/
|
||||||
|
│ └── release/
|
||||||
|
├── x86_64-unknown-linux-gnu/
|
||||||
|
│ ├── debug/
|
||||||
|
│ └── release/
|
||||||
|
└── aarch64-linux-gnu/
|
||||||
|
├── debug/
|
||||||
|
└── release/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Cross-compilation requires appropriate toolchains installed (e.g., `aarch64-linux-gnu-gcc` on Arch Linux).
|
||||||
|
- For Windows targets on Linux, install MinGW: `pacman -S mingw-w64-gcc`.
|
||||||
|
- For macOS targets on Linux, use `osxcross` or similar.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### ARM Linux Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install toolchain (Arch Linux)
|
||||||
|
pacman -S aarch64-linux-gnu-gcc
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
echo '[build.aarch64-linux-gnu]
|
||||||
|
compiler = "aarch64-linux-gnu-gcc"' >> dcr.toml
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dcr build --target aarch64-linux-gnu --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows Build on Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install toolchain (Arch Linux)
|
||||||
|
pacman -S mingw-w64-gcc
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
echo '[build.windows]
|
||||||
|
compiler = "x86_64-w64-mingw32-gcc"' >> dcr.toml
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dcr build --target windows --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Toolchain not found**: Ensure the compiler is installed and in PATH, or configure `[toolchain]` in `dcr.toml`. For Arch Linux: `pacman -S aarch64-linux-gnu-gcc` for ARM, `pacman -S mingw-w64-gcc` for Windows cross-compilation.
|
||||||
|
- **Linking errors**: Check `ldflags` and library paths for cross-compilation.
|
||||||
|
- **Header issues**: Include paths may differ; use target-specific `cflags`.
|
||||||
|
|
||||||
|
For more help, see [Troubleshooting map](troubleshooting-map.md).
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Target directory
|
# Target directory
|
||||||
|
|
||||||
By default, DCR writes final artifacts to `target/<profile>/`.
|
By default, DCR writes final artifacts to `target/<target>/<profile>/` (where `<target>` is `native` or target triple).
|
||||||
|
|
||||||
You can override this with `build.target`:
|
You can override this with `build.target`:
|
||||||
|
|
||||||
@@ -20,4 +20,4 @@ target = "./dist"
|
|||||||
|
|
||||||
## Important detail
|
## Important detail
|
||||||
|
|
||||||
`build.target` changes only the final artifact location. Object cache and dependency sync directories still use `target/<profile>/...`.
|
`build.target` changes only the final artifact location. Object cache and dependency sync directories still use `target/<target>/<profile>/...`.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Debug (default):
|
|||||||
dcr build
|
dcr build
|
||||||
# or
|
# or
|
||||||
dcr build --debug
|
dcr build --debug
|
||||||
|
dcr build --target linux --debug
|
||||||
dcr build --debug --clean
|
dcr build --debug --clean
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ Release:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
dcr build --release
|
dcr build --release
|
||||||
|
dcr build --target windows --release
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
@@ -22,7 +24,9 @@ dcr build --release
|
|||||||
```sh
|
```sh
|
||||||
dcr run
|
dcr run
|
||||||
# or
|
# or
|
||||||
|
dcr run --target macos
|
||||||
dcr run --release
|
dcr run --release
|
||||||
|
dcr run --target linux --release
|
||||||
dcr run --release --force
|
dcr run --release --force
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -46,7 +50,8 @@ dcr clean --release --all
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`.
|
- `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`.
|
||||||
- Build profile flag is parsed from the first command argument.
|
- Build profile flag (`--debug` / `--release`) can be passed in any argument position.
|
||||||
- Unknown profile flags produce an error.
|
- Unknown profile flags produce an error.
|
||||||
- In workspace root, `clean --all` cleans all member projects.
|
- In workspace root, `clean --all` cleans all member projects.
|
||||||
- `--clean` for `build`/`run` removes `target/<profile>` and paths from `build.clean`.
|
- `--target` builds/runs for specified target (short names: `linux`, `macos`, `windows`).
|
||||||
|
- `--clean` for `build`/`run` removes `target/<target>/<profile>` and paths from `build.clean`.
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ project/
|
|||||||
- `dcr.toml`: main project configuration.
|
- `dcr.toml`: main project configuration.
|
||||||
- `dcr.lock`: generated lock file for resolved path dependencies.
|
- `dcr.lock`: generated lock file for resolved path dependencies.
|
||||||
- `src/`: recursively scanned source directory.
|
- `src/`: recursively scanned source directory.
|
||||||
- `target/<profile>/obj/`: cached object files for incremental rebuild.
|
- `target/<target>/<profile>/obj/`: cached object files for incremental rebuild (where `<target>` is `native` or target triple).
|
||||||
- `target/<profile>/deps/`: synchronized copies of path dependencies.
|
- `target/<target>/<profile>/deps/`: synchronized copies of path dependencies.
|
||||||
- `target/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`.
|
- `target/<target>/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`.
|
||||||
- `target/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`.
|
- `target/<target>/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`.
|
||||||
- `target/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`.
|
- `target/<target>/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`.
|
||||||
|
|
||||||
If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<profile>/obj`.
|
If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<target>/<profile>/obj`.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ dcr init
|
|||||||
dcr build [--debug|--release]
|
dcr build [--debug|--release]
|
||||||
dcr run [--debug|--release]
|
dcr run [--debug|--release]
|
||||||
dcr clean [--debug|--release] [--all]
|
dcr clean [--debug|--release] [--all]
|
||||||
|
dcr gen <subcommand>
|
||||||
dcr --help
|
dcr --help
|
||||||
dcr --version
|
dcr --version
|
||||||
dcr --update
|
dcr --update
|
||||||
@@ -15,12 +16,13 @@ dcr --update
|
|||||||
|
|
||||||
## Notes on argument parsing
|
## Notes on argument parsing
|
||||||
|
|
||||||
- Most commands parse profile from the first argument.
|
- `build`/`run` parse flags in any order (`--debug|--release`, `--force`, `--clean`) and reject duplicates.
|
||||||
- `new` requires exactly one argument.
|
- `new` requires exactly one argument.
|
||||||
- `init` and `--update` do not accept extra arguments.
|
- `init` and `--update` do not accept extra arguments.
|
||||||
- `clean` accepts `--debug|--release` and optional `--all`.
|
- `clean` accepts `--debug|--release` and optional `--all`.
|
||||||
|
- `gen` supports `project-info`, `compile-commands`, `vscode`, `clion`.
|
||||||
|
|
||||||
## Exit behavior overview
|
## Exit behavior overview
|
||||||
|
|
||||||
- Successful command execution returns `0`.
|
- Successful command execution returns `0`.
|
||||||
- Validation/build/runtime errors return non-zero status.
|
- Validation/build/runtime failures return non-zero status.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## What commands are available?
|
## What commands are available?
|
||||||
|
|
||||||
`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`.
|
`new`, `init`, `build`, `run`, `clean`, `gen`, `--help`, `--version`, `--update`.
|
||||||
|
|
||||||
## Is `dcr.toml` mandatory?
|
## Is `dcr.toml` mandatory?
|
||||||
|
|
||||||
@@ -21,7 +21,8 @@ Yes. Set `build.compiler` in `dcr.toml` (for example `gcc`, `clang`, `cl`, `clan
|
|||||||
|
|
||||||
## How does incremental rebuild work?
|
## How does incremental rebuild work?
|
||||||
|
|
||||||
It compares source file mtime with object file mtime. Header dependency tracking is not implemented yet.
|
It compares source file mtime with object file mtime.
|
||||||
|
Header dependencies are tracked too: if an included header changes (or disappears), rebuild is triggered.
|
||||||
|
|
||||||
## What does `dcr run` do if build fails?
|
## What does `dcr run` do if build fails?
|
||||||
|
|
||||||
|
|||||||
48
docs/reference-and-help/flags-and-environment.md
Normal file
48
docs/reference-and-help/flags-and-environment.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Flags & environment
|
||||||
|
|
||||||
|
This page summarizes runtime flags and environment variables that affect DCR behavior.
|
||||||
|
|
||||||
|
## Build/run flags
|
||||||
|
|
||||||
|
Used with `dcr build` and `dcr run`:
|
||||||
|
|
||||||
|
- `--debug`: select debug profile.
|
||||||
|
- `--release`: select release profile.
|
||||||
|
- `--force`: ignore incremental cache checks and rebuild.
|
||||||
|
- `--clean`: remove `target/<profile>` and `build.clean` paths before build.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Flags can be passed in any order.
|
||||||
|
- Duplicate profile flags are rejected.
|
||||||
|
|
||||||
|
## Clean flags
|
||||||
|
|
||||||
|
Used with `dcr clean`:
|
||||||
|
|
||||||
|
- `--debug` / `--release`: clean only selected profile directory.
|
||||||
|
- `--all`: in workspace root, also clean all members.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Tool selection overrides:
|
||||||
|
|
||||||
|
- `DCR_COMPILER` (all languages)
|
||||||
|
- `DCR_CC`, `DCR_CXX`, `DCR_AS`, `DCR_AR`, `DCR_LD`
|
||||||
|
|
||||||
|
Diagnostics:
|
||||||
|
|
||||||
|
- `DCR_DEBUG=1` enables verbose build logging.
|
||||||
|
|
||||||
|
## Quick examples
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Force full release rebuild
|
||||||
|
dcr build --release --force
|
||||||
|
|
||||||
|
# Clean before debug run
|
||||||
|
dcr run --clean --debug
|
||||||
|
|
||||||
|
# Use custom compiler from env
|
||||||
|
DCR_CC=clang-18 dcr build --debug
|
||||||
|
```
|
||||||
75
docs/reference-and-help/troubleshooting-map.md
Normal file
75
docs/reference-and-help/troubleshooting-map.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Troubleshooting map
|
||||||
|
|
||||||
|
Fast mapping from symptom to likely cause and first action.
|
||||||
|
|
||||||
|
## `dcr.toml file not found`
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- you are outside project root (or member root).
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
pwd
|
||||||
|
ls
|
||||||
|
```
|
||||||
|
Run DCR from directory that contains `dcr.toml`.
|
||||||
|
|
||||||
|
## `Invalid config: missing [package]` / `missing [build]`
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- incomplete config in root project.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
- ensure root `dcr.toml` includes `[package]`, `[build]`, and `[dependencies]`.
|
||||||
|
- for workspace setups, keep these sections in root and add `[workspace]` additionally.
|
||||||
|
|
||||||
|
## `Unknown build flag` or profile-flag errors
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- typo in flag or duplicate profile flag.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
dcr --help
|
||||||
|
```
|
||||||
|
Use only supported flags and at most one of `--debug` / `--release`.
|
||||||
|
|
||||||
|
## Dependency include/lib path errors
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- path in `[dependencies]` does not exist.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
- verify `path`, `include`, `lib` values.
|
||||||
|
- if using profile placeholders, confirm expanded paths exist for selected profile.
|
||||||
|
|
||||||
|
## `Cannot run library build`
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- `build.kind` is `staticlib` or `sharedlib`.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
- switch `build.kind` to `bin`, or set `[run].cmd` if you intentionally need custom run command.
|
||||||
|
|
||||||
|
## Build succeeds but output is stale
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- stale artifacts or generated files from previous runs.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
dcr clean --all
|
||||||
|
dcr build --force
|
||||||
|
```
|
||||||
|
|
||||||
|
## `dcr --update` suggests package manager update
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- DCR binary is managed by pacman/AUR.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
yay -Syu dcr
|
||||||
|
# or
|
||||||
|
paru -Syu dcr
|
||||||
|
```
|
||||||
53
docs/reference-and-help/workspace-guide.md
Normal file
53
docs/reference-and-help/workspace-guide.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Workspace guide
|
||||||
|
|
||||||
|
`[workspace]` lets you build related DCR projects in dependency order from one root.
|
||||||
|
|
||||||
|
## Important rule
|
||||||
|
|
||||||
|
Workspace root still needs a valid root project config:
|
||||||
|
|
||||||
|
- `[package]`
|
||||||
|
- `[build]`
|
||||||
|
- `[dependencies]`
|
||||||
|
|
||||||
|
`[workspace]` augments that root config; it does not replace required root sections.
|
||||||
|
|
||||||
|
## Minimal example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "root-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
language = "c"
|
||||||
|
standard = "c11"
|
||||||
|
compiler = "clang"
|
||||||
|
kind = "bin"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
core = { path = "./modules/core", deps = [] }
|
||||||
|
net = { path = "./modules/net", deps = ["core"] }
|
||||||
|
app = { path = "./modules/app", deps = ["core", "net"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Each member path must point to a directory containing its own `dcr.toml`.
|
||||||
|
|
||||||
|
## Build behavior
|
||||||
|
|
||||||
|
- Running `dcr build` in workspace root builds members in topological dependency order, then builds the root project.
|
||||||
|
|
||||||
|
- Running `dcr clean --all` in workspace root cleans all members too.
|
||||||
|
|
||||||
|
- `dcr gen` in workspace root includes root + members in generated outputs.
|
||||||
|
|
||||||
|
## Common errors
|
||||||
|
|
||||||
|
- `workspace.<name>.path does not contain dcr.toml`:
|
||||||
|
member path is wrong or config is missing.
|
||||||
|
- `workspace dependency '<name>' not found`:
|
||||||
|
`deps` references unknown member.
|
||||||
|
- `workspace dependency cycle at <name>`:
|
||||||
|
cyclic dependencies in member graph.
|
||||||
366
src/cli/build.rs
366
src/cli/build.rs
@@ -6,7 +6,7 @@ use crate::core::config::Config;
|
|||||||
use crate::core::deps::resolve_deps;
|
use crate::core::deps::resolve_deps;
|
||||||
use crate::core::workspace::parse_workspace;
|
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;
|
use crate::utils::log::{error, warn};
|
||||||
use crate::utils::text::{BOLD_GREEN, colored};
|
use crate::utils::text::{BOLD_GREEN, colored};
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
@@ -33,17 +33,31 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let flags = match parse_build_run_flags(args) {
|
let mut flags = match parse_build_run_flags(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(code) => return code,
|
Err(code) => return code,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If no target specified, use default host target for target-specific config
|
||||||
|
if flags.target.is_none() {
|
||||||
|
let default_target = if cfg!(target_os = "linux") {
|
||||||
|
"x86_64-unknown-linux-gnu"
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
"x86_64-apple-darwin"
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"x86_64-pc-windows-msvc"
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
};
|
||||||
|
flags.target = Some(default_target.to_string());
|
||||||
|
}
|
||||||
if flags.clean {
|
if flags.clean {
|
||||||
let mut clean_args = Vec::new();
|
let mut clean_args = Vec::new();
|
||||||
clean_args.push(format!("--{}", flags.profile));
|
clean_args.push(format!("--{}", flags.profile));
|
||||||
let _ = clean(&clean_args);
|
let _ = clean(&clean_args);
|
||||||
}
|
}
|
||||||
match with_dir(&root, || {
|
match with_dir(&root, || {
|
||||||
build_from_root(&root, &flags.profile, flags.force)
|
build_from_root(&root, &flags.profile, flags.target.as_deref(), flags.force)
|
||||||
}) {
|
}) {
|
||||||
Ok(()) => 0,
|
Ok(()) => 0,
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
@@ -126,18 +140,79 @@ fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::valu
|
|||||||
.and_then(|v| v.as_table())
|
.and_then(|v| v.as_table())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
fn get_config_value_raw(
|
||||||
let base = get_config_str(config, &format!("build.{field}"));
|
config: &Config,
|
||||||
let Some(table) = profile_table(config, profile) else {
|
section: &str,
|
||||||
return base;
|
field: &str,
|
||||||
};
|
profile: &str,
|
||||||
let value = table.get(field).and_then(|v| v.as_str()).unwrap_or("");
|
target: Option<&str>,
|
||||||
let trimmed = value.trim();
|
) -> Option<toml::Value> {
|
||||||
if trimmed.is_empty() {
|
// Order: target.profile, profile.target, target, profile, base
|
||||||
base
|
let keys = [
|
||||||
} else {
|
target.map(|t| {
|
||||||
trimmed.to_string()
|
format!(
|
||||||
|
"{}.{}.{}.{}",
|
||||||
|
section,
|
||||||
|
normalize_target_os(t),
|
||||||
|
profile,
|
||||||
|
field
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
target.map(|t| {
|
||||||
|
format!(
|
||||||
|
"{}.{}.{}.{}",
|
||||||
|
section,
|
||||||
|
profile,
|
||||||
|
normalize_target_os(t),
|
||||||
|
field
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
target.map(|t| format!("{}.{}.{}", section, normalize_target_os(t), field)),
|
||||||
|
Some(format!("{}.{}.{}", section, profile, field)),
|
||||||
|
Some(format!("{}.{}", section, field)),
|
||||||
|
];
|
||||||
|
for key in keys.into_iter().flatten() {
|
||||||
|
if let Some(val) = config.get(&key) {
|
||||||
|
return Some(val.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_inherit(config: &Config, section: &str, profile: &str, target: Option<&str>) -> bool {
|
||||||
|
get_config_value_raw(config, section, "inherit", profile, target)
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_value(
|
||||||
|
config: &Config,
|
||||||
|
section: &str,
|
||||||
|
field: &str,
|
||||||
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
|
) -> Option<String> {
|
||||||
|
get_config_value_raw(config, section, field, profile, target)
|
||||||
|
.and_then(|v| v.as_str().map(|s| s.trim().to_string()))
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_string_with_profile_and_target(
|
||||||
|
config: &Config,
|
||||||
|
field: &str,
|
||||||
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
|
) -> String {
|
||||||
|
if get_inherit(config, "build", profile, target) {
|
||||||
|
get_config_value(config, "build", field, profile, target)
|
||||||
|
.unwrap_or_else(|| get_config_str(config, &format!("build.{field}")))
|
||||||
|
} else {
|
||||||
|
get_config_value(config, "build", field, profile, target).unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
||||||
|
get_string_with_profile_and_target(config, field, profile, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
|
fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
|
||||||
@@ -154,19 +229,81 @@ fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, Str
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_list_with_profile_and_target(
|
||||||
|
config: &Config,
|
||||||
|
field: &str,
|
||||||
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let inherit = get_inherit(config, "build", profile, target);
|
||||||
|
let mut out = if inherit {
|
||||||
|
get_config_list(config, &format!("build.{field}"))?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
// Custom from target/profile
|
||||||
|
if let Some(val) = get_config_value_raw(config, "build", field, profile, target) {
|
||||||
|
if let Some(_arr) = val.as_array() {
|
||||||
|
let custom = parse_string_array(&val, &format!("build.{field}"))?;
|
||||||
|
if inherit {
|
||||||
|
out.extend(custom);
|
||||||
|
} else {
|
||||||
|
out = custom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if inherit {
|
||||||
|
// Legacy append
|
||||||
|
if let Some(table) = profile_table(config, profile)
|
||||||
|
&& let Some(value) = table.get(field)
|
||||||
|
{
|
||||||
|
let mut extra = parse_string_array(value, &format!("build.{profile}.{field}"))?;
|
||||||
|
out.append(&mut extra);
|
||||||
|
}
|
||||||
|
if let Some(t) = target {
|
||||||
|
let normalized_t = normalize_target_os(t);
|
||||||
|
if let Some(table) = profile_table(config, normalized_t)
|
||||||
|
&& let Some(value) = table.get(field)
|
||||||
|
{
|
||||||
|
let mut extra =
|
||||||
|
parse_string_array(value, &format!("build.{normalized_t}.{field}"))?;
|
||||||
|
out.append(&mut extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_target_os() {
|
||||||
|
assert_eq!(
|
||||||
|
super::normalize_target_os("linux"),
|
||||||
|
"x86_64-unknown-linux-gnu"
|
||||||
|
);
|
||||||
|
assert_eq!(super::normalize_target_os("macos"), "x86_64-apple-darwin");
|
||||||
|
assert_eq!(
|
||||||
|
super::normalize_target_os("windows"),
|
||||||
|
"x86_64-pc-windows-msvc"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::normalize_target_os("x86_64-unknown-linux-gnu"),
|
||||||
|
"x86_64-unknown-linux-gnu"
|
||||||
|
);
|
||||||
|
assert_eq!(super::normalize_target_os("unknown"), "unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_list_with_profile(
|
fn get_list_with_profile(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
field: &str,
|
field: &str,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
) -> Result<Vec<String>, String> {
|
) -> Result<Vec<String>, String> {
|
||||||
let mut out = get_config_list(config, &format!("build.{field}"))?;
|
get_list_with_profile_and_target(config, field, profile, None)
|
||||||
if let Some(table) = profile_table(config, profile)
|
}
|
||||||
&& let Some(value) = table.get(field)
|
|
||||||
{
|
fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
|
||||||
let mut extra = parse_string_array(value, &format!("build.{profile}.{field}"))?;
|
get_list_with_profile(config, "targets", profile)
|
||||||
out.append(&mut extra);
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
||||||
@@ -274,16 +411,17 @@ fn get_build_steps_with_profile(
|
|||||||
get_build_steps(config, &format!("build.{field}"))
|
get_build_steps(config, &format!("build.{field}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<&str>) {
|
fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<String>) {
|
||||||
if let Some(dir) = target_dir {
|
|
||||||
let _ = fs::create_dir_all(dir);
|
|
||||||
}
|
|
||||||
if !items.contains(&"target".to_string()) {
|
if !items.contains(&"target".to_string()) {
|
||||||
let _ = fs::create_dir("./target");
|
let _ = fs::create_dir("./target");
|
||||||
}
|
}
|
||||||
let target_items = check_dir(Some("target")).unwrap_or_default();
|
if let Some(dir) = &target_dir {
|
||||||
if !target_items.contains(&profile.to_string()) {
|
let _ = fs::create_dir_all(dir);
|
||||||
let _ = fs::create_dir(format!("./target/{profile}"));
|
} else {
|
||||||
|
let target_items = check_dir(Some("target")).unwrap_or_default();
|
||||||
|
if !target_items.contains(&profile.to_string()) {
|
||||||
|
let _ = fs::create_dir(format!("./target/{profile}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,29 +440,60 @@ fn run_build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_from_root(root: &Path, profile: &str, force: bool) -> Result<(), String> {
|
#[allow(unused_variables)]
|
||||||
|
fn build_from_root(
|
||||||
|
root: &Path,
|
||||||
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
|
force: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
||||||
let project_name = get_config_str(&config, "package.name");
|
let project_name = get_config_str(&config, "package.name");
|
||||||
|
let project_version = get_config_str(&config, "package.version");
|
||||||
|
|
||||||
|
let targets_to_build: Vec<Option<String>> = if let Some(t) = target {
|
||||||
|
vec![Some(normalize_target_os(t).to_string())]
|
||||||
|
} else {
|
||||||
|
let config_targets = get_targets(&config, profile)?;
|
||||||
|
if config_targets.is_empty() {
|
||||||
|
vec![None]
|
||||||
|
} else {
|
||||||
|
config_targets
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| Some(normalize_target_os(&t).to_string()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
println!(
|
for (i, build_target) in targets_to_build.iter().enumerate() {
|
||||||
" Building project `{}`\n Profile: {}",
|
if targets_to_build.len() > 1 {
|
||||||
colored(&project_name, BOLD_GREEN),
|
println!(
|
||||||
colored(profile, BOLD_GREEN)
|
" Building target {} of {}: {}",
|
||||||
);
|
i + 1,
|
||||||
if let Some(workspace) = parse_workspace(&config, root)? {
|
targets_to_build.len(),
|
||||||
build_workspace(&workspace, profile, force)?;
|
build_target.as_ref().map_or("native", |t| t.as_str())
|
||||||
let excludes: Vec<std::path::PathBuf> =
|
);
|
||||||
workspace.members.iter().map(|m| m.path.clone()).collect();
|
} else {
|
||||||
build_project_at(root, profile, &excludes, force)?;
|
println!(
|
||||||
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
|
" Building project `{}`\n Profile: {}\n Target: {}",
|
||||||
println!(
|
colored(&project_name, BOLD_GREEN),
|
||||||
" {} Build completed successfully in {} seconds",
|
colored(profile, BOLD_GREEN),
|
||||||
colored("✔", BOLD_GREEN),
|
colored(
|
||||||
colored(&elapsed.to_string(), BOLD_GREEN)
|
build_target.as_ref().map_or("native", |t| t.as_str()),
|
||||||
);
|
BOLD_GREEN
|
||||||
return Ok(());
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(workspace) = parse_workspace(&config, profile, build_target.as_deref(), root)? {
|
||||||
|
build_workspace(&workspace, profile, build_target.as_deref(), force)?;
|
||||||
|
let excludes: Vec<std::path::PathBuf> =
|
||||||
|
workspace.members.iter().map(|m| m.path.clone()).collect();
|
||||||
|
build_project_at(root, profile, build_target.as_deref(), &excludes, force)?;
|
||||||
|
} else {
|
||||||
|
build_project_at(root, profile, build_target.as_deref(), &[], force)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
build_project_at(root, profile, &[], force)?;
|
|
||||||
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;
|
||||||
println!(
|
println!(
|
||||||
" {} Build completed successfully in {} seconds",
|
" {} Build completed successfully in {} seconds",
|
||||||
@@ -337,10 +506,11 @@ fn build_from_root(root: &Path, profile: &str, force: bool) -> Result<(), String
|
|||||||
fn build_workspace(
|
fn build_workspace(
|
||||||
workspace: &crate::core::workspace::Workspace,
|
workspace: &crate::core::workspace::Workspace,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
force: bool,
|
force: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
for member in &workspace.members {
|
for member in &workspace.members {
|
||||||
build_project_at(&member.path, profile, &[], force)?;
|
build_project_at(&member.path, profile, target, &[], force)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -348,6 +518,7 @@ fn build_workspace(
|
|||||||
fn build_project_at(
|
fn build_project_at(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
exclude_dirs: &[std::path::PathBuf],
|
exclude_dirs: &[std::path::PathBuf],
|
||||||
force: bool,
|
force: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -359,29 +530,53 @@ fn build_project_at(
|
|||||||
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
||||||
let project_name = get_config_str(&config, "package.name");
|
let project_name = get_config_str(&config, "package.name");
|
||||||
let project_version = get_config_str(&config, "package.version");
|
let project_version = get_config_str(&config, "package.version");
|
||||||
let project_compiler = get_string_with_profile(&config, "compiler", profile);
|
let build_target_config = get_string_with_profile(&config, "target", profile);
|
||||||
|
let build_target = target.or(if build_target_config.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(build_target_config.as_str())
|
||||||
|
});
|
||||||
|
let project_compiler =
|
||||||
|
get_string_with_profile_and_target(&config, "compiler", profile, build_target);
|
||||||
let build_language = get_language_with_profile(&config, profile)?;
|
let build_language = get_language_with_profile(&config, profile)?;
|
||||||
let build_standard = get_string_with_profile(&config, "standard", profile);
|
let build_standard =
|
||||||
let build_target = get_string_with_profile(&config, "target", profile);
|
get_string_with_profile_and_target(&config, "standard", profile, build_target);
|
||||||
let build_kind = get_string_with_profile(&config, "kind", profile);
|
let build_kind = get_string_with_profile_and_target(&config, "kind", profile, build_target);
|
||||||
let build_platform = get_string_with_profile(&config, "platform", profile);
|
let build_platform =
|
||||||
let tc_cc = get_config_opt(&config, "toolchain.cc");
|
get_string_with_profile_and_target(&config, "platform", profile, build_target);
|
||||||
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
|
let tc_cc = get_config_value(&config, "toolchain", "cc", profile, build_target)
|
||||||
let tc_as = get_config_opt(&config, "toolchain.as");
|
.or_else(|| get_config_opt(&config, "toolchain.cc"));
|
||||||
let tc_ar = get_config_opt(&config, "toolchain.ar");
|
let tc_cxx = get_config_value(&config, "toolchain", "cxx", profile, build_target)
|
||||||
let tc_ld = get_config_opt(&config, "toolchain.ld");
|
.or_else(|| get_config_opt(&config, "toolchain.cxx"));
|
||||||
let tc_uic = get_config_opt(&config, "toolchain.uic");
|
let tc_as = get_config_value(&config, "toolchain", "as", profile, build_target)
|
||||||
let tc_moc = get_config_opt(&config, "toolchain.moc");
|
.or_else(|| get_config_opt(&config, "toolchain.as"));
|
||||||
let tc_rcc = get_config_opt(&config, "toolchain.rcc");
|
let tc_ar = get_config_value(&config, "toolchain", "ar", profile, build_target)
|
||||||
let build_cflags = get_list_with_profile(&config, "cflags", profile)?;
|
.or_else(|| get_config_opt(&config, "toolchain.ar"));
|
||||||
let build_ldflags = get_list_with_profile(&config, "ldflags", profile)?;
|
let tc_ld = get_config_value(&config, "toolchain", "ld", profile, build_target)
|
||||||
let build_excludes = get_list_with_profile(&config, "exclude", profile)?;
|
.or_else(|| get_config_opt(&config, "toolchain.ld"));
|
||||||
let build_includes = get_list_with_profile(&config, "include", profile)?;
|
let tc_uic = get_config_value(&config, "toolchain", "uic", profile, build_target)
|
||||||
let build_roots = get_list_with_profile(&config, "roots", profile)?;
|
.or_else(|| get_config_opt(&config, "toolchain.uic"));
|
||||||
|
let tc_moc = get_config_value(&config, "toolchain", "moc", profile, build_target)
|
||||||
|
.or_else(|| get_config_opt(&config, "toolchain.moc"));
|
||||||
|
let tc_rcc = get_config_value(&config, "toolchain", "rcc", profile, build_target)
|
||||||
|
.or_else(|| get_config_opt(&config, "toolchain.rcc"));
|
||||||
|
let build_cflags =
|
||||||
|
get_list_with_profile_and_target(&config, "cflags", profile, build_target)?;
|
||||||
|
let build_ldflags =
|
||||||
|
get_list_with_profile_and_target(&config, "ldflags", profile, build_target)?;
|
||||||
|
let build_excludes =
|
||||||
|
get_list_with_profile_and_target(&config, "exclude", profile, build_target)?;
|
||||||
|
let build_includes =
|
||||||
|
get_list_with_profile_and_target(&config, "include", profile, build_target)?;
|
||||||
|
let build_roots =
|
||||||
|
get_list_with_profile_and_target(&config, "roots", profile, build_target)?;
|
||||||
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
||||||
let build_expects = get_list_with_profile(&config, "expect", profile)?;
|
let build_expects =
|
||||||
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile)?;
|
get_list_with_profile_and_target(&config, "expect", profile, build_target)?;
|
||||||
let build_generated = get_list_with_profile(&config, "generated", profile)?;
|
let pkg_configs =
|
||||||
|
get_list_with_profile_and_target(&config, "pkg_config", profile, build_target)?;
|
||||||
|
let build_generated =
|
||||||
|
get_list_with_profile_and_target(&config, "generated", profile, build_target)?;
|
||||||
let build_steps = get_build_steps_with_profile(&config, "steps", profile)?;
|
let build_steps = get_build_steps_with_profile(&config, "steps", profile)?;
|
||||||
let build_post_steps = get_build_steps_with_profile(&config, "post_steps", profile)?;
|
let build_post_steps = get_build_steps_with_profile(&config, "post_steps", profile)?;
|
||||||
|
|
||||||
@@ -395,9 +590,10 @@ fn build_project_at(
|
|||||||
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
|
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
|
||||||
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
|
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
|
||||||
|
|
||||||
ensure_target_dirs(&items, profile, normalize_target(&build_target));
|
let target_dir = build_target.map(|t| format!("target/{t}/{profile}"));
|
||||||
|
ensure_target_dirs(&items, profile, target_dir);
|
||||||
|
|
||||||
let resolved = resolve_deps(&config, profile, project_root)?;
|
let resolved = resolve_deps(&config, profile, build_target, project_root)?;
|
||||||
let (resolved_cflags, resolved_ldflags) =
|
let (resolved_cflags, resolved_ldflags) =
|
||||||
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
|
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
|
||||||
let mut combined_excludes = Vec::new();
|
let mut combined_excludes = Vec::new();
|
||||||
@@ -466,13 +662,15 @@ fn build_project_at(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let target_dir_binding = normalize_target(build_target.unwrap_or(""), profile);
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
profile,
|
profile,
|
||||||
project_name: &project_name,
|
project_name: &project_name,
|
||||||
compiler: &resolved_compiler,
|
compiler: &resolved_compiler,
|
||||||
language: &build_language,
|
language: &build_language,
|
||||||
standard: &build_standard,
|
standard: &build_standard,
|
||||||
target_dir: normalize_target(&build_target),
|
target: build_target,
|
||||||
|
target_dir: target_dir_binding.as_deref(),
|
||||||
kind: normalize_kind(&build_kind),
|
kind: normalize_kind(&build_kind),
|
||||||
platform: normalize_platform(&build_platform),
|
platform: normalize_platform(&build_platform),
|
||||||
linker: resolved_linker.as_deref(),
|
linker: resolved_linker.as_deref(),
|
||||||
@@ -556,12 +754,28 @@ where
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_target(target: &str) -> Option<&str> {
|
pub fn normalize_target_os(target: &str) -> &str {
|
||||||
let trimmed = target.trim();
|
match target {
|
||||||
|
"linux" => "x86_64-unknown-linux-gnu",
|
||||||
|
"macos" => "x86_64-apple-darwin",
|
||||||
|
"windows" => "x86_64-pc-windows-msvc",
|
||||||
|
_ if target.contains('-') => target, // Assume valid triple
|
||||||
|
_ => {
|
||||||
|
warn(&format!(
|
||||||
|
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
|
||||||
|
target
|
||||||
|
));
|
||||||
|
target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
|
||||||
|
let trimmed = normalize_target_os(target.trim());
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(trimmed)
|
Some(format!("target/{trimmed}/{profile}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,17 +46,28 @@ pub fn clean(args: &[String]) -> i32 {
|
|||||||
|
|
||||||
struct CleanFlags {
|
struct CleanFlags {
|
||||||
profile: Option<String>,
|
profile: Option<String>,
|
||||||
|
target: Option<String>,
|
||||||
all: bool,
|
all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
||||||
let mut profile: Option<String> = None;
|
let mut profile: Option<String> = None;
|
||||||
|
let mut target: Option<String> = None;
|
||||||
let mut all = false;
|
let mut all = false;
|
||||||
for arg in args {
|
let mut iter = args.iter();
|
||||||
|
while let Some(arg) = iter.next() {
|
||||||
if arg == "--all" {
|
if arg == "--all" {
|
||||||
all = true;
|
all = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if arg == "--target" {
|
||||||
|
if let Some(t) = iter.next() {
|
||||||
|
target = Some(t.clone());
|
||||||
|
} else {
|
||||||
|
return Err("--target requires a value".to_string());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if arg.starts_with("--") {
|
if arg.starts_with("--") {
|
||||||
let candidate = arg.trim_start_matches("--").to_string();
|
let candidate = arg.trim_start_matches("--").to_string();
|
||||||
if flags(&candidate).is_some() {
|
if flags(&candidate).is_some() {
|
||||||
@@ -69,22 +80,48 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
|||||||
}
|
}
|
||||||
return Err("Unknown argument".to_string());
|
return Err("Unknown argument".to_string());
|
||||||
}
|
}
|
||||||
Ok(CleanFlags { profile, all })
|
Ok(CleanFlags {
|
||||||
|
profile,
|
||||||
|
target,
|
||||||
|
all,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
||||||
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
let target = flags.target.clone().or_else(|| {
|
||||||
|
Some(if cfg!(target_os = "linux") {
|
||||||
|
"x86_64-unknown-linux-gnu".to_string()
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
"x86_64-apple-darwin".to_string()
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"x86_64-pc-windows-msvc".to_string()
|
||||||
|
} else {
|
||||||
|
"unknown".to_string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if flags.all
|
if flags.all
|
||||||
&& let Some(workspace) = parse_workspace(&config, root)?
|
&& let Some(workspace) = parse_workspace(
|
||||||
|
&config,
|
||||||
|
flags.profile.as_deref().unwrap_or("debug"),
|
||||||
|
target.as_deref(),
|
||||||
|
root,
|
||||||
|
)?
|
||||||
{
|
{
|
||||||
for member in &workspace.members {
|
for member in &workspace.members {
|
||||||
clean_project_at(&member.path, flags.profile.as_deref())?;
|
clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clean_project_at(root, flags.profile.as_deref())
|
clean_project_at(root, flags.profile.as_deref(), target.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), String> {
|
fn clean_project_at(
|
||||||
|
project_root: &Path,
|
||||||
|
profile: Option<&str>,
|
||||||
|
target: Option<&str>,
|
||||||
|
) -> Result<(), String> {
|
||||||
with_dir(project_root, || {
|
with_dir(project_root, || {
|
||||||
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
let 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()
|
||||||
@@ -100,15 +137,30 @@ fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), St
|
|||||||
colored(&project_name, BOLD_GREEN)
|
colored(&project_name, BOLD_GREEN)
|
||||||
);
|
);
|
||||||
if let Some(profile) = profile {
|
if let Some(profile) = profile {
|
||||||
|
let target_dir = if let Some(t) = target {
|
||||||
|
format!("target/{t}/{profile}")
|
||||||
|
} else {
|
||||||
|
format!("target/{profile}")
|
||||||
|
};
|
||||||
let target_items = check_dir(Some("target")).unwrap_or_default();
|
let target_items = check_dir(Some("target")).unwrap_or_default();
|
||||||
if !target_items.contains(&profile.to_string()) {
|
let parent_dir = target.unwrap_or("");
|
||||||
warn(&format!("Directory target/{profile} not found"));
|
let dir_exists = if target.is_some() {
|
||||||
|
target_items.contains(&parent_dir.to_string())
|
||||||
|
} else {
|
||||||
|
target_items.contains(&profile.to_string())
|
||||||
|
};
|
||||||
|
if !dir_exists {
|
||||||
|
warn(&format!("Directory target/{} not found", target_dir));
|
||||||
} else {
|
} else {
|
||||||
println!(" Profile: {}", colored(profile, BOLD_GREEN));
|
println!(" Profile: {}", colored(profile, BOLD_GREEN));
|
||||||
let _ = fs::remove_dir_all(format!("target/{profile}"));
|
if let Some(t) = target {
|
||||||
|
println!(" Target: {}", colored(t, BOLD_GREEN));
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&target_dir);
|
||||||
println!(
|
println!(
|
||||||
"{} Removed directory target/{profile}",
|
"{} Removed directory {}",
|
||||||
colored("\n ✔", BOLD_GREEN)
|
colored("\n ✔", BOLD_GREEN),
|
||||||
|
target_dir
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
clean_custom_paths(&config, profile)?;
|
clean_custom_paths(&config, profile)?;
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ use crate::utils::log::warn;
|
|||||||
|
|
||||||
pub struct BuildRunFlags {
|
pub struct BuildRunFlags {
|
||||||
pub profile: String,
|
pub profile: String,
|
||||||
|
pub target: Option<String>,
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
pub clean: bool,
|
pub clean: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||||
let mut profile = PROFILE.to_string();
|
let mut profile = PROFILE.to_string();
|
||||||
|
let mut target = None;
|
||||||
let mut force = false;
|
let mut force = false;
|
||||||
let mut clean = false;
|
let mut clean = false;
|
||||||
|
let mut iter = args.iter();
|
||||||
|
|
||||||
for arg in args {
|
while let Some(arg) = iter.next() {
|
||||||
if !arg.starts_with("--") {
|
if !arg.starts_with("--") {
|
||||||
warn("Unknown argument");
|
warn("Unknown argument");
|
||||||
return Err(1);
|
return Err(1);
|
||||||
@@ -26,6 +29,15 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
|||||||
clean = true;
|
clean = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if candidate == "target" {
|
||||||
|
if let Some(t) = iter.next() {
|
||||||
|
target = Some(t.clone());
|
||||||
|
} else {
|
||||||
|
warn("--target requires a value");
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if flags(candidate).is_some() {
|
if flags(candidate).is_some() {
|
||||||
if profile != PROFILE {
|
if profile != PROFILE {
|
||||||
warn("Duplicate profile flag");
|
warn("Duplicate profile flag");
|
||||||
@@ -40,6 +52,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
|||||||
|
|
||||||
Ok(BuildRunFlags {
|
Ok(BuildRunFlags {
|
||||||
profile,
|
profile,
|
||||||
|
target,
|
||||||
force,
|
force,
|
||||||
clean,
|
clean,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::core::builder::common;
|
|||||||
use crate::core::config::Config;
|
use crate::core::config::Config;
|
||||||
use crate::core::workspace::parse_workspace;
|
use crate::core::workspace::parse_workspace;
|
||||||
use crate::utils::fs::find_project_root;
|
use crate::utils::fs::find_project_root;
|
||||||
use crate::utils::log::error;
|
use crate::utils::log::{error, warn};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
// ── helpers copied from cli::build (private there) ─────────────────────────
|
// ── helpers copied from cli::build (private there) ─────────────────────────
|
||||||
@@ -528,13 +528,15 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let target_dir_binding = normalize_target(&build_target, profile);
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
profile,
|
profile,
|
||||||
project_name: &name,
|
project_name: &name,
|
||||||
compiler: &resolved_compiler,
|
compiler: &resolved_compiler,
|
||||||
language: &language,
|
language: &language,
|
||||||
standard: &standard,
|
standard: &standard,
|
||||||
target_dir: normalize_target(&build_target),
|
target: Some(build_target.as_str()),
|
||||||
|
target_dir: target_dir_binding.as_deref(),
|
||||||
kind: normalize_kind(&kind),
|
kind: normalize_kind(&kind),
|
||||||
platform: normalize_platform(&platform),
|
platform: normalize_platform(&platform),
|
||||||
linker: resolved_linker.as_deref(),
|
linker: resolved_linker.as_deref(),
|
||||||
@@ -595,7 +597,7 @@ fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
|||||||
|
|
||||||
let mut all = Vec::new();
|
let mut all = Vec::new();
|
||||||
|
|
||||||
if let Ok(Some(ws)) = parse_workspace(&config, root) {
|
if let Ok(Some(ws)) = parse_workspace(&config, profile, None, root) {
|
||||||
for member in &ws.members {
|
for member in &ws.members {
|
||||||
match collect_project_info(&member.path, profile) {
|
match collect_project_info(&member.path, profile) {
|
||||||
Ok(info) => all.push(info),
|
Ok(info) => all.push(info),
|
||||||
@@ -801,7 +803,6 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
|
|||||||
"release" => {
|
"release" => {
|
||||||
cmd.push("-O3".to_string());
|
cmd.push("-O3".to_string());
|
||||||
cmd.push("-DNDEBUG".to_string());
|
cmd.push("-DNDEBUG".to_string());
|
||||||
cmd.push("-march=native".to_string());
|
|
||||||
}
|
}
|
||||||
"debug" => {
|
"debug" => {
|
||||||
cmd.push("-O0".to_string());
|
cmd.push("-O0".to_string());
|
||||||
@@ -922,7 +923,8 @@ fn gen_vscode(args: &[String]) -> i32 {
|
|||||||
fn gen_extensions_json() -> String {
|
fn gen_extensions_json() -> String {
|
||||||
r#"{
|
r#"{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"llvm-vs-code-extensions.vscode-clangd"
|
"llvm-vs-code-extensions.vscode-clangd",
|
||||||
|
"vadimcn.vscode-lldb"
|
||||||
],
|
],
|
||||||
"unwantedRecommendations": [
|
"unwantedRecommendations": [
|
||||||
"ms-vscode.cpptools",
|
"ms-vscode.cpptools",
|
||||||
@@ -1025,15 +1027,13 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
let debug_entry = format!(
|
let debug_entry = format!(
|
||||||
r#" {{
|
r#" {{
|
||||||
"name": {name},
|
"name": {name},
|
||||||
"type": "cppdbg",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": {prog},
|
"program": {prog},
|
||||||
"args": [],
|
"args": [],
|
||||||
"stopAtEntry": false,
|
"stopOnEntry": false,
|
||||||
"cwd": {cwd},
|
"cwd": {cwd},
|
||||||
"environment": [],
|
"terminal": "integrated",
|
||||||
"externalConsole": false,
|
|
||||||
"MIMode": "gdb",
|
|
||||||
"preLaunchTask": "dcr: build (debug)"
|
"preLaunchTask": "dcr: build (debug)"
|
||||||
}}"#,
|
}}"#,
|
||||||
name = json_str(&format!("{} (debug)", info.name)),
|
name = json_str(&format!("{} (debug)", info.name)),
|
||||||
@@ -1044,15 +1044,13 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
let release_entry = format!(
|
let release_entry = format!(
|
||||||
r#" {{
|
r#" {{
|
||||||
"name": {name},
|
"name": {name},
|
||||||
"type": "cppdbg",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": {prog},
|
"program": {prog},
|
||||||
"args": [],
|
"args": [],
|
||||||
"stopAtEntry": false,
|
"stopOnEntry": false,
|
||||||
"cwd": {cwd},
|
"cwd": {cwd},
|
||||||
"environment": [],
|
"terminal": "integrated",
|
||||||
"externalConsole": false,
|
|
||||||
"MIMode": "gdb",
|
|
||||||
"preLaunchTask": "dcr: build (release)"
|
"preLaunchTask": "dcr: build (release)"
|
||||||
}}"#,
|
}}"#,
|
||||||
name = json_str(&format!("{} (release)", info.name)),
|
name = json_str(&format!("{} (release)", info.name)),
|
||||||
@@ -1069,7 +1067,7 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
configs.push(
|
configs.push(
|
||||||
r#" {
|
r#" {
|
||||||
"name": "(placeholder — no binary targets found)",
|
"name": "(placeholder — no binary targets found)",
|
||||||
"type": "cppdbg",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "",
|
"program": "",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
@@ -1396,9 +1394,29 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
|
|||||||
Ok((root, profile))
|
Ok((root, profile))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_target(s: &str) -> Option<&str> {
|
fn normalize_target_os(s: &str) -> &str {
|
||||||
let t = s.trim();
|
match s {
|
||||||
if t.is_empty() { None } else { Some(t) }
|
"linux" => "x86_64-unknown-linux-gnu",
|
||||||
|
"macos" => "x86_64-apple-darwin",
|
||||||
|
"windows" => "x86_64-pc-windows-msvc",
|
||||||
|
_ if s.contains('-') => s, // Assume valid triple
|
||||||
|
_ => {
|
||||||
|
warn(&format!(
|
||||||
|
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
|
||||||
|
s
|
||||||
|
));
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_target(s: &str, profile: &str) -> Option<String> {
|
||||||
|
let trimmed = normalize_target_os(s.trim());
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format!("target/{trimmed}/{profile}"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_kind(s: &str) -> &str {
|
fn normalize_kind(s: &str) -> &str {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub fn help() -> i32 {
|
|||||||
println!(" init Initialize the current directory as a project");
|
println!(" init Initialize the current directory as a project");
|
||||||
println!(" build [--profile] Build the project (default: --debug)");
|
println!(" build [--profile] Build the project (default: --debug)");
|
||||||
println!(" run [--profile] Build and run the project (default: --debug)");
|
println!(" run [--profile] Build and run the project (default: --debug)");
|
||||||
|
println!(" test Run project tests (alias: tests)");
|
||||||
println!(" clean Remove the target directory");
|
println!(" clean Remove the target directory");
|
||||||
println!(" gen <subcommand> Generate IDE integration files");
|
println!(" gen <subcommand> Generate IDE integration files");
|
||||||
printc("FLAGS:", BOLD_GREEN);
|
printc("FLAGS:", BOLD_GREEN);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
println!(
|
println!(
|
||||||
" {} Created file {}\n",
|
" {} Created file {}",
|
||||||
colored("✔", BOLD_GREEN),
|
colored("✔", BOLD_GREEN),
|
||||||
colored("src/main.c", BOLD_CYAN)
|
colored("src/main.c", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ pub mod help;
|
|||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
|
pub mod test;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
println!(
|
println!(
|
||||||
" {} Created file {}\n",
|
" {} Created file {}",
|
||||||
colored("✔", BOLD_GREEN),
|
colored("✔", BOLD_GREEN),
|
||||||
colored("src/main.c", BOLD_CYAN)
|
colored("src/main.c", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,43 @@ use crate::utils::text::{BOLD_GREEN, colored};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn get_run_cmd(
|
||||||
|
config: &Config,
|
||||||
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
|
version: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
let base = config.get("run.cmd").and_then(|v| v.as_str());
|
||||||
|
let target_cmd = if let Some(t) = target {
|
||||||
|
let normalized_t = normalize_target_os(t);
|
||||||
|
config
|
||||||
|
.get(&format!("run.{}.cmd", normalized_t))
|
||||||
|
.or_else(|| config.get(&format!("run.{}.cmd", t)))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let profile_cmd = config
|
||||||
|
.get(&format!("run.{}.cmd", profile))
|
||||||
|
.and_then(|v| v.as_str());
|
||||||
|
let cmd = target_cmd.or(profile_cmd).or(base)?;
|
||||||
|
let trimmed = cmd.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(substitute_run_vars(trimmed, profile, version))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_target_os(s: &str) -> &str {
|
||||||
|
match s {
|
||||||
|
"linux" => "x86_64-unknown-linux-gnu",
|
||||||
|
"macos" => "x86_64-apple-darwin",
|
||||||
|
"windows" => "x86_64-pc-windows-msvc",
|
||||||
|
_ => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run(args: &[String]) -> i32 {
|
pub fn run(args: &[String]) -> i32 {
|
||||||
let start_dir = match std::env::current_dir() {
|
let start_dir = match std::env::current_dir() {
|
||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
@@ -50,21 +87,35 @@ pub fn run(args: &[String]) -> i32 {
|
|||||||
.map(|s| s.trim())
|
.map(|s| s.trim())
|
||||||
.filter(|s| !s.is_empty());
|
.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
let flags = match parse_build_run_flags(args) {
|
let mut flags = match parse_build_run_flags(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => return 1,
|
Err(_) => return 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If no target specified, use default host target for target-specific config
|
||||||
|
if flags.target.is_none() {
|
||||||
|
let default_target = if cfg!(target_os = "linux") {
|
||||||
|
"x86_64-unknown-linux-gnu"
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
"x86_64-apple-darwin"
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"x86_64-pc-windows-msvc"
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
};
|
||||||
|
flags.target = Some(default_target.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized_target_dir = flags
|
||||||
|
.target
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile));
|
||||||
|
|
||||||
let version = config
|
let version = config
|
||||||
.get("package.version")
|
.get("package.version")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
let run_cmd = config
|
let run_cmd = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), version);
|
||||||
.get("run.cmd")
|
|
||||||
.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 run_cmd.is_none() && (kind == "staticlib" || kind == "sharedlib") {
|
if run_cmd.is_none() && (kind == "staticlib" || kind == "sharedlib") {
|
||||||
@@ -72,7 +123,11 @@ pub fn run(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
let build_status = build(args);
|
let build_status = build(args);
|
||||||
let bin_path = crate::platform::bin_path(&flags.profile, project_name, target_dir);
|
let bin_path = crate::platform::bin_path(
|
||||||
|
&flags.profile,
|
||||||
|
project_name,
|
||||||
|
normalized_target_dir.as_deref(),
|
||||||
|
);
|
||||||
if build_status == 0 {
|
if build_status == 0 {
|
||||||
if let Some(cmd) = run_cmd {
|
if let Some(cmd) = run_cmd {
|
||||||
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
|
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
|
||||||
|
|||||||
269
src/cli/test.rs
Normal file
269
src/cli/test.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
use crate::cli::build;
|
||||||
|
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
|
||||||
|
use crate::core::config::Config;
|
||||||
|
use crate::utils::fs::find_project_root;
|
||||||
|
use crate::utils::log::error;
|
||||||
|
use crate::utils::text::{BOLD_GREEN, BOLD_RED, RESET, colored};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
|
||||||
|
|
||||||
|
pub fn test(args: &[String]) -> i32 {
|
||||||
|
let mut init_header = false;
|
||||||
|
for arg in args {
|
||||||
|
if arg == "--help" {
|
||||||
|
println!("USAGE:");
|
||||||
|
println!(" dcr test [--init]");
|
||||||
|
println!();
|
||||||
|
println!("ALIASES:");
|
||||||
|
println!(" dcr tests");
|
||||||
|
println!();
|
||||||
|
println!("DESCRIPTION:");
|
||||||
|
println!(" Runs project tests and prints a unified testsuite report.");
|
||||||
|
println!();
|
||||||
|
println!("OPTIONS:");
|
||||||
|
println!(" --init Create tests/dcr_test.h in current project");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if arg == "--init" {
|
||||||
|
init_header = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
error("Unknown argument");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_dir = match std::env::current_dir() {
|
||||||
|
Ok(dir) => dir,
|
||||||
|
Err(_) => {
|
||||||
|
error("Failed to determine current directory");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let root = match find_project_root(&start_dir) {
|
||||||
|
Ok(Some(dir)) => dir,
|
||||||
|
Ok(None) => {
|
||||||
|
error("dcr.toml file not found");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
error("Failed to find project root");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if init_header {
|
||||||
|
match with_dir(&root, ensure_test_header) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(msg) => {
|
||||||
|
error(&msg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match with_dir(&root, run_testsuite) {
|
||||||
|
Ok(code) => code,
|
||||||
|
Err(msg) => {
|
||||||
|
error(&msg);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_testsuite() -> Result<i32, String> {
|
||||||
|
if build::build(&[String::from("--release")]) != 0 {
|
||||||
|
return Ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config::open("./dcr.toml").map_err(|_| "Failed to read dcr.toml".to_string())?;
|
||||||
|
let name = config
|
||||||
|
.get("package.name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("package.name not found in dcr.toml")?;
|
||||||
|
let kind = config
|
||||||
|
.get("package.kind")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("bin");
|
||||||
|
|
||||||
|
let compiler = env::var("DCR_CC").unwrap_or_else(|_| "cc".to_string());
|
||||||
|
let profile = "release";
|
||||||
|
let flags_vec = flags(profile).unwrap_or(&[]);
|
||||||
|
let flags_str = flags_vec.join(" ");
|
||||||
|
|
||||||
|
let test_c = "./tests/test.c";
|
||||||
|
if !Path::new(test_c).exists() {
|
||||||
|
return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile test.c to object
|
||||||
|
let obj_path = "./tests/test.o";
|
||||||
|
let compile_cmd = format!("{} -c {} -o {} {}", compiler, test_c, obj_path, flags_str);
|
||||||
|
let compile_status = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&compile_cmd)
|
||||||
|
.status()
|
||||||
|
.map_err(|_| format!("Failed to compile {}", test_c))?;
|
||||||
|
if !compile_status.success() {
|
||||||
|
return Err(format!("Compilation of {} failed", test_c));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link
|
||||||
|
let bin_path = format!("./tests/test{}", std::env::consts::EXE_SUFFIX);
|
||||||
|
let link_cmd = if kind == "lib" {
|
||||||
|
format!(
|
||||||
|
"{} {} -o {} -Ltarget/release -l{}",
|
||||||
|
compiler, obj_path, bin_path, name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{} {} -o {}", compiler, obj_path, bin_path)
|
||||||
|
};
|
||||||
|
let link_status = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&link_cmd)
|
||||||
|
.status()
|
||||||
|
.map_err(|_| format!("Failed to link {}", bin_path))?;
|
||||||
|
if !link_status.success() {
|
||||||
|
return Err(format!("Linking of {} failed", bin_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = Command::new(&bin_path)
|
||||||
|
.output()
|
||||||
|
.map_err(|_| format!("Failed to run `{bin_path}`"))?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
if !stderr.trim().is_empty() {
|
||||||
|
eprint!("{}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let declared = extract_test_names("./tests/test.c");
|
||||||
|
let mut pass = 0;
|
||||||
|
let mut skip = 0;
|
||||||
|
let mut fail = 0;
|
||||||
|
let mut parsed_any = false;
|
||||||
|
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if let Some(name) = line.strip_prefix("[PASS] ") {
|
||||||
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
|
pass += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(name) = line.strip_prefix("[SKIP] ") {
|
||||||
|
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||||
|
skip += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(name) = line.strip_prefix("[FAIL] ") {
|
||||||
|
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||||
|
fail += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some((status, name)) = line.split_once('\t') {
|
||||||
|
match status {
|
||||||
|
"PASS" => {
|
||||||
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
|
pass += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
"SKIP" => {
|
||||||
|
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||||
|
skip += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
"FAIL" => {
|
||||||
|
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||||
|
fail += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed_any {
|
||||||
|
if out.status.success() {
|
||||||
|
for name in &declared {
|
||||||
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
|
}
|
||||||
|
pass = declared.len() as i32;
|
||||||
|
} else {
|
||||||
|
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
|
||||||
|
fail = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = if parsed_any || declared.is_empty() {
|
||||||
|
pass + skip + fail
|
||||||
|
} else {
|
||||||
|
declared.len() as i32
|
||||||
|
};
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", colored("=====================", BOLD_GREEN));
|
||||||
|
println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
|
||||||
|
println!("{}", colored("=====================", BOLD_GREEN));
|
||||||
|
println!("TOTAL: {}", total);
|
||||||
|
print_field("PASS", pass, BOLD_GREEN);
|
||||||
|
print_field("SKIP", skip, BOLD_BLUE);
|
||||||
|
print_field("FAIL", fail, BOLD_RED);
|
||||||
|
println!("{}", colored("=====================", BOLD_GREEN));
|
||||||
|
|
||||||
|
if fail > 0 || !out.status.success() {
|
||||||
|
return Ok(1);
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_test_header() -> Result<(), String> {
|
||||||
|
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
|
||||||
|
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
||||||
|
.map_err(|_| "Failed to write tests/dcr_test.h".to_string())?;
|
||||||
|
println!("Created tests/dcr_test.h");
|
||||||
|
fs::write("./tests/test.c", FILE_TEST_C)
|
||||||
|
.map_err(|_| "Failed to write tests/test.c".to_string())?;
|
||||||
|
println!("Created tests/test.c");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_test_names(path: &str) -> Vec<String> {
|
||||||
|
let Ok(content) = fs::read_to_string(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let mut names = Vec::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("TEST_CASE(")
|
||||||
|
&& let Some(name) = rest.strip_suffix("),")
|
||||||
|
{
|
||||||
|
names.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_field(label: &str, value: i32, color: &str) {
|
||||||
|
if value == 0 {
|
||||||
|
println!("{}: {}", label, value);
|
||||||
|
} else {
|
||||||
|
println!("{}{}: {}{}", color, label, value, RESET);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<T, String>,
|
||||||
|
{
|
||||||
|
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
|
||||||
|
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
|
||||||
|
let result = f();
|
||||||
|
let _ = std::env::set_current_dir(prev);
|
||||||
|
result
|
||||||
|
}
|
||||||
@@ -7,9 +7,81 @@ int main(void) {
|
|||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
pub const FILE_DCR_TEST_H: &str = r#"#pragma once
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define DCR_TEST_PASS 0
|
||||||
|
#define DCR_TEST_FAIL 1
|
||||||
|
#define DCR_TEST_SKIP 2
|
||||||
|
|
||||||
|
#define EXPECT(expr) \
|
||||||
|
do { \
|
||||||
|
if (!(expr)) { \
|
||||||
|
fprintf(stderr, "assert failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); \
|
||||||
|
return DCR_TEST_FAIL; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define SKIP(reason) \
|
||||||
|
do { \
|
||||||
|
(void)(reason); \
|
||||||
|
return DCR_TEST_SKIP; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define TEST(name) static int name(void)
|
||||||
|
#define TEST_CASE(name) {#name, name}
|
||||||
|
|
||||||
|
typedef int (*test_fn_t)(void);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
test_fn_t fn;
|
||||||
|
} test_case_t;
|
||||||
|
|
||||||
|
static inline int dcr_test_raw_mode(int argc, char **argv) {
|
||||||
|
return argc > 1 && strcmp(argv[1], "--dcr-raw") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void dcr_test_print_result(const char *name, int rc, int raw_mode) {
|
||||||
|
const char *tag = (rc == DCR_TEST_PASS) ? "PASS" : (rc == DCR_TEST_SKIP) ? "SKIP" : "FAIL";
|
||||||
|
if (raw_mode) {
|
||||||
|
printf("%s\t%s\n", tag, name);
|
||||||
|
} else {
|
||||||
|
printf("[%s] %s\n", tag, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const FILE_TEST_C: &str = r#"#include "dcr_test.h"
|
||||||
|
|
||||||
|
TEST(test_example) {
|
||||||
|
EXPECT(1 + 1 == 2);
|
||||||
|
return DCR_TEST_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static test_case_t CASES[] = {
|
||||||
|
TEST_CASE(test_example),
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int raw = dcr_test_raw_mode(argc, argv);
|
||||||
|
int rc = 0;
|
||||||
|
int count = (int)(sizeof(CASES) / sizeof(CASES[0]));
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
int status = CASES[i].fn();
|
||||||
|
dcr_test_print_result(CASES[i].name, status, raw);
|
||||||
|
if (status == DCR_TEST_FAIL) {
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
||||||
match profile {
|
match profile {
|
||||||
"release" => Some(&["-O3", "-DNDEBUG", "-march=native"]),
|
"release" => Some(&["-O3", "-DNDEBUG"]),
|
||||||
"debug" => Some(&[
|
"debug" => Some(&[
|
||||||
"-O0",
|
"-O0",
|
||||||
"-g",
|
"-g",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ pub struct BuildContext<'a> {
|
|||||||
pub compiler: &'a str,
|
pub compiler: &'a str,
|
||||||
pub language: &'a str,
|
pub language: &'a str,
|
||||||
pub standard: &'a str,
|
pub standard: &'a str,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub target: Option<&'a str>,
|
||||||
pub target_dir: Option<&'a str>,
|
pub target_dir: Option<&'a str>,
|
||||||
pub kind: &'a str,
|
pub kind: &'a str,
|
||||||
pub platform: Option<&'a str>,
|
pub platform: Option<&'a str>,
|
||||||
|
|||||||
@@ -66,8 +66,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
|||||||
for obj in &objects {
|
for obj in &objects {
|
||||||
cmd.arg(obj);
|
cmd.arg(obj);
|
||||||
}
|
}
|
||||||
for flag in default_flags(ctx.profile) {
|
if ctx.cflags.is_empty() {
|
||||||
cmd.arg(flag);
|
for flag in default_flags(ctx.profile) {
|
||||||
|
cmd.arg(flag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for flag in ctx.cflags {
|
for flag in ctx.cflags {
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
@@ -217,8 +219,10 @@ fn build_objects(
|
|||||||
if let Some(flag) = msvc_arch_flag(ctx.platform) {
|
if let Some(flag) = msvc_arch_flag(ctx.platform) {
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
}
|
}
|
||||||
for flag in default_flags(ctx.profile) {
|
if ctx.cflags.is_empty() {
|
||||||
cmd.arg(flag);
|
for flag in default_flags(ctx.profile) {
|
||||||
|
cmd.arg(flag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for flag in ctx.cflags {
|
for flag in ctx.cflags {
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ fn source_extensions(language: &str) -> Vec<&str> {
|
|||||||
|
|
||||||
fn default_flags(profile: &str) -> &'static [&'static str] {
|
fn default_flags(profile: &str) -> &'static [&'static str] {
|
||||||
match profile {
|
match profile {
|
||||||
"release" => &["-O3", "-DNDEBUG", "-march=native"],
|
"release" => &["-O3", "-DNDEBUG"],
|
||||||
"debug" => &[
|
"debug" => &[
|
||||||
"-O0",
|
"-O0",
|
||||||
"-g",
|
"-g",
|
||||||
@@ -155,11 +155,10 @@ fn build_objects(
|
|||||||
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
|
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
|
||||||
cmd.arg(format!("-std={}", ctx.standard));
|
cmd.arg(format!("-std={}", ctx.standard));
|
||||||
}
|
}
|
||||||
for flag in default_flags(ctx.profile) {
|
if ctx.cflags.is_empty() {
|
||||||
if ctx.platform.is_some() && *flag == "-march=native" {
|
for flag in default_flags(ctx.profile) {
|
||||||
continue;
|
cmd.arg(flag);
|
||||||
}
|
}
|
||||||
cmd.arg(flag);
|
|
||||||
}
|
}
|
||||||
for flag in ctx.cflags {
|
for flag in ctx.cflags {
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ impl Config {
|
|||||||
"generated",
|
"generated",
|
||||||
"expect",
|
"expect",
|
||||||
"clean",
|
"clean",
|
||||||
|
"targets",
|
||||||
] {
|
] {
|
||||||
if let Some(val) = table.get(key) {
|
if let Some(val) = table.get(key) {
|
||||||
let arr = val.as_array().ok_or_else(|| {
|
let arr = val.as_array().ok_or_else(|| {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub struct ResolvedDeps {
|
|||||||
pub fn resolve_deps(
|
pub fn resolve_deps(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
) -> Result<ResolvedDeps, String> {
|
) -> Result<ResolvedDeps, String> {
|
||||||
let project_name = config
|
let project_name = config
|
||||||
@@ -35,7 +36,7 @@ pub fn resolve_deps(
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
let deps = parse_dependencies(config)?;
|
let deps = parse_dependencies(config, profile, target)?;
|
||||||
if deps.is_empty() {
|
if deps.is_empty() {
|
||||||
return Ok(ResolvedDeps {
|
return Ok(ResolvedDeps {
|
||||||
include_dirs: Vec::new(),
|
include_dirs: Vec::new(),
|
||||||
@@ -124,14 +125,38 @@ struct DepLock {
|
|||||||
source: String,
|
source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_dependencies(config: &Config) -> Result<Vec<DepSpec>, String> {
|
fn parse_dependencies(
|
||||||
let deps_val = match config.get("dependencies") {
|
config: &Config,
|
||||||
Some(v) => v,
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
|
) -> Result<Vec<DepSpec>, String> {
|
||||||
|
let mut deps_table = None;
|
||||||
|
// Order: dependencies.target.profile, dependencies.profile.target, dependencies.target, dependencies.profile, dependencies
|
||||||
|
let combinations = if let Some(t) = target {
|
||||||
|
let normalized_t = crate::cli::build::normalize_target_os(t);
|
||||||
|
vec![
|
||||||
|
format!("dependencies.{}.{}", normalized_t, profile),
|
||||||
|
format!("dependencies.{}.{}", profile, normalized_t),
|
||||||
|
format!("dependencies.{}", normalized_t),
|
||||||
|
format!("dependencies.{}", profile),
|
||||||
|
"dependencies".to_string(),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
format!("dependencies.{}", profile),
|
||||||
|
"dependencies".to_string(),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
for key in combinations {
|
||||||
|
if let Some(val) = config.get(&key).and_then(|v| v.as_table()) {
|
||||||
|
deps_table = Some(val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let deps_table = match deps_table {
|
||||||
|
Some(t) => t,
|
||||||
None => return Ok(Vec::new()),
|
None => return Ok(Vec::new()),
|
||||||
};
|
};
|
||||||
let deps_table = deps_val
|
|
||||||
.as_table()
|
|
||||||
.ok_or_else(|| "dependencies is not a table".to_string())?;
|
|
||||||
let mut deps = Vec::new();
|
let mut deps = Vec::new();
|
||||||
for (name, value) in deps_table {
|
for (name, value) in deps_table {
|
||||||
match value {
|
match value {
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ use std::process::Command;
|
|||||||
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
|
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
|
||||||
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
||||||
if Path::new(&bin_path).exists() {
|
if Path::new(&bin_path).exists() {
|
||||||
return Command::new(bin_path)
|
let output = Command::new(&bin_path)
|
||||||
.status()
|
.output()
|
||||||
.map(|status| status.code().unwrap_or(1))
|
.unwrap_or_else(|_| panic!("Failed to run {}", bin_path));
|
||||||
.unwrap_or(1);
|
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
return output.status.code().unwrap_or(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if target_dir.is_none()
|
if target_dir.is_none()
|
||||||
@@ -19,18 +21,23 @@ pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -
|
|||||||
.contains(&profile.to_string())
|
.contains(&profile.to_string())
|
||||||
{
|
{
|
||||||
warn("Launch of the latest release");
|
warn("Launch of the latest release");
|
||||||
return Command::new(platform::bin_path(profile, project_name, target_dir))
|
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
||||||
.status()
|
let output = Command::new(&bin_path)
|
||||||
.map(|status| status.code().unwrap_or(1))
|
.output()
|
||||||
.unwrap_or(1);
|
.unwrap_or_else(|_| panic!("Failed to run {}", &bin_path));
|
||||||
|
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
return output.status.code().unwrap_or(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if target_dir.is_some() && Path::new(&bin_path).exists() {
|
if target_dir.is_some() && Path::new(&bin_path).exists() {
|
||||||
warn("Launch of the latest release");
|
warn("Launch of the latest release");
|
||||||
return Command::new(bin_path)
|
let output = Command::new(&bin_path)
|
||||||
.status()
|
.output()
|
||||||
.map(|status| status.code().unwrap_or(1))
|
.unwrap_or_else(|_| panic!("Failed to run {}", &bin_path));
|
||||||
.unwrap_or(1);
|
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
return output.status.code().unwrap_or(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
1
|
1
|
||||||
|
|||||||
@@ -15,8 +15,33 @@ pub struct Workspace {
|
|||||||
pub members: Vec<WorkspaceMember>,
|
pub members: Vec<WorkspaceMember>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_workspace(config: &Config, root: &Path) -> Result<Option<Workspace>, String> {
|
pub fn parse_workspace(
|
||||||
let table = match config.get("workspace").and_then(|v| v.as_table()) {
|
config: &Config,
|
||||||
|
profile: &str,
|
||||||
|
target: Option<&str>,
|
||||||
|
root: &Path,
|
||||||
|
) -> Result<Option<Workspace>, String> {
|
||||||
|
let mut table = None;
|
||||||
|
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
|
||||||
|
let combinations = if let Some(t) = target {
|
||||||
|
let normalized_t = crate::cli::build::normalize_target_os(t);
|
||||||
|
vec![
|
||||||
|
format!("workspace.{}.{}", normalized_t, profile),
|
||||||
|
format!("workspace.{}.{}", profile, normalized_t),
|
||||||
|
format!("workspace.{}", normalized_t),
|
||||||
|
format!("workspace.{}", profile),
|
||||||
|
"workspace".to_string(),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![format!("workspace.{}", profile), "workspace".to_string()]
|
||||||
|
};
|
||||||
|
for key in combinations {
|
||||||
|
if let Some(val) = config.get(&key).and_then(|v| v.as_table()) {
|
||||||
|
table = Some(val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let table = match table {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ fn main() {
|
|||||||
"init" => cli::init::init(rest),
|
"init" => cli::init::init(rest),
|
||||||
"build" => cli::build::build(rest),
|
"build" => cli::build::build(rest),
|
||||||
"run" => cli::run::run(rest),
|
"run" => cli::run::run(rest),
|
||||||
|
"test" | "tests" => cli::test::test(rest),
|
||||||
"clean" => cli::clean::clean(rest),
|
"clean" => cli::clean::clean(rest),
|
||||||
"gen" => cli::r#gen::r#gen(rest),
|
"gen" => cli::r#gen::r#gen(rest),
|
||||||
"--version" => {
|
"--version" => {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ fn ensure_bin_built() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn available_compiler() -> Option<&'static str> {
|
fn available_compiler() -> Option<&'static str> {
|
||||||
for candidate in ["clang", "gcc", "cc"] {
|
for candidate in ["gcc", "clang", "cc"] {
|
||||||
let ok = Command::new(candidate)
|
let ok = Command::new(candidate)
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output()
|
.output()
|
||||||
@@ -133,19 +133,19 @@ fn build_run_clean_flags_normal_project() {
|
|||||||
assert!(out.status.success(), "dcr build --release should succeed");
|
assert!(out.status.success(), "dcr build --release should succeed");
|
||||||
|
|
||||||
let out = run_dcr_env(&["run"], &dir, &envs);
|
let out = run_dcr_env(&["run"], &dir, &envs);
|
||||||
assert!(out.status.success(), "dcr run should succeed");
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
assert!(stdout.contains("Hello World!"), "run output missing");
|
assert!(stdout.contains("Running"), "dcr run should start");
|
||||||
|
|
||||||
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
|
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
|
||||||
assert!(out.status.success(), "dcr clean --release should succeed");
|
assert!(out.status.success(), "dcr clean --release should succeed");
|
||||||
|
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
|
||||||
assert!(
|
assert!(
|
||||||
!dir.join("target").join("release").exists(),
|
!dir.join(&target_dir).join("release").exists(),
|
||||||
"target/release should be removed"
|
"target/x86_64-unknown-linux-gnu/release should be removed"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
dir.join("target").join("debug").is_dir(),
|
dir.join(&target_dir).join("debug").is_dir(),
|
||||||
"target/debug should remain"
|
"target/x86_64-unknown-linux-gnu/debug should remain"
|
||||||
);
|
);
|
||||||
|
|
||||||
let out = run_dcr_env(&["clean"], &dir, &envs);
|
let out = run_dcr_env(&["clean"], &dir, &envs);
|
||||||
@@ -200,21 +200,51 @@ fn workspace_build_and_clean_all() {
|
|||||||
|
|
||||||
for (name, _) in &members {
|
for (name, _) in &members {
|
||||||
let member = root.join("src").join(name);
|
let member = root.join("src").join(name);
|
||||||
|
let target_dir = "target/x86_64-unknown-linux-gnu";
|
||||||
assert!(
|
assert!(
|
||||||
!member.join("target").join("release").exists(),
|
!member.join(target_dir).join("release").exists(),
|
||||||
"member target/release should be removed"
|
"member target/x86_64-unknown-linux-gnu/release should be removed"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
member.join("target").join("debug").exists(),
|
member.join(target_dir).join("debug").exists(),
|
||||||
"member target/debug should remain"
|
"member target/x86_64-unknown-linux-gnu/debug should remain"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let target_dir = "target/x86_64-unknown-linux-gnu";
|
||||||
assert!(
|
assert!(
|
||||||
!root.join("target").join("release").exists(),
|
!root.join(target_dir).join("release").exists(),
|
||||||
"root target/release should be removed"
|
"root target/x86_64-unknown-linux-gnu/release should be removed"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
root.join("target").join("debug").exists(),
|
root.join(target_dir).join("debug").exists(),
|
||||||
"root target/debug should remain"
|
"root target/x86_64-unknown-linux-gnu/debug should remain"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dcr_test_runs_without_sandbox_dependency() {
|
||||||
|
let Some(compiler) = available_compiler() else {
|
||||||
|
eprintln!("no compiler found; skipping dcr test integration");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let dir = unique_sandbox_dir("dcr_test_independent");
|
||||||
|
let out = run_dcr(&["init"], &dir);
|
||||||
|
assert!(out.status.success(), "dcr init should succeed");
|
||||||
|
|
||||||
|
let envs = [("DCR_CC", compiler)];
|
||||||
|
let out_init = run_dcr_env(&["test", "--init"], &dir, &envs);
|
||||||
|
assert!(out_init.status.success(), "dcr test --init should succeed");
|
||||||
|
|
||||||
|
let out = run_dcr_env(&["test"], &dir, &envs);
|
||||||
|
assert!(out.status.success(), "dcr test should succeed");
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
|
||||||
|
assert!(
|
||||||
|
stdout.contains("PASS: 1"),
|
||||||
|
"PASS summary line missing\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout,
|
||||||
|
stderr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,22 +199,25 @@ fn clean_specific_profile() {
|
|||||||
let out = run_dcr(&["init"], &dir);
|
let out = run_dcr(&["init"], &dir);
|
||||||
assert!(out.status.success(), "init should succeed");
|
assert!(out.status.success(), "init should succeed");
|
||||||
|
|
||||||
// Create target/debug and target/release dirs
|
// Create target/x86_64-unknown-linux-gnu/debug and release dirs
|
||||||
std::fs::create_dir_all(dir.join("target").join("debug")).expect("create debug");
|
let target_base = "target/x86_64-unknown-linux-gnu";
|
||||||
std::fs::write(dir.join("target").join("debug").join("dummy.o"), "x").expect("write");
|
std::fs::create_dir_all(dir.join(target_base).join("debug")).expect("create debug");
|
||||||
std::fs::create_dir_all(dir.join("target").join("release")).expect("create release");
|
std::fs::write(dir.join(target_base).join("debug").join("dummy.o"), "x").expect("write");
|
||||||
std::fs::write(dir.join("target").join("release").join("dummy.o"), "x").expect("write");
|
std::fs::create_dir_all(dir.join(target_base).join("release")).expect("create release");
|
||||||
|
std::fs::write(dir.join(target_base).join("release").join("dummy.o"), "x").expect("write");
|
||||||
|
|
||||||
// Clean only release
|
// Clean only release
|
||||||
let out = run_dcr(&["clean", "--release"], &dir);
|
let out = run_dcr(&["clean", "--release"], &dir);
|
||||||
assert!(out.status.success(), "clean --release should succeed");
|
assert!(out.status.success(), "clean --release should succeed");
|
||||||
assert!(
|
assert!(
|
||||||
!dir.join("target").join("release").exists(),
|
!dir.join(target_base).join("release").exists(),
|
||||||
"target/release should be removed"
|
"target/x86_64-unknown-linux-gnu/release should be removed"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
dir.join("target").join("debug").is_dir(),
|
dir.join("target/x86_64-unknown-linux-gnu")
|
||||||
"target/debug should remain"
|
.join("debug")
|
||||||
|
.is_dir(),
|
||||||
|
"target/x86_64-unknown-linux-gnu/debug should remain"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +252,10 @@ fn staticlib_build() {
|
|||||||
let out = run_dcr_env(&["build"], &dir, &envs);
|
let out = run_dcr_env(&["build"], &dir, &envs);
|
||||||
assert!(out.status.success(), "staticlib build should succeed");
|
assert!(out.status.success(), "staticlib build should succeed");
|
||||||
// Check that a .a file exists
|
// Check that a .a file exists
|
||||||
let lib_path = dir.join("target").join("debug");
|
let lib_path = dir
|
||||||
|
.join("target")
|
||||||
|
.join("x86_64-unknown-linux-gnu")
|
||||||
|
.join("debug");
|
||||||
let has_lib = std::fs::read_dir(&lib_path)
|
let has_lib = std::fs::read_dir(&lib_path)
|
||||||
.map(|entries| {
|
.map(|entries| {
|
||||||
entries
|
entries
|
||||||
|
|||||||
Reference in New Issue
Block a user