6 Commits

Author SHA1 Message Date
7cefbb3c3a feat: Implement cross-compilation with target inheritance and host defaults 2026-04-12 14:23:01 +08:00
045cc90d81 style: update code format 2026-04-11 13:37:29 +08:00
fd88e036ed feat: add cross-compilation support with --target flag
- Updated build and run commands to accept a --target option for specifying target platforms.
- Enhanced documentation to include cross-compilation usage and supported targets.
- Modified configuration to allow target-specific settings and toolchains.
- Introduced a new test command to run project tests with specified profiles and targets.
- Improved handling of target directories and output structure for different platforms.
- Added support for multiple targets in the configuration file.
- Implemented inheritance for workspace and dependencies based on target and profile.
2026-04-11 13:36:18 +08:00
613fbc6a6a refactor: run cargo fmt --all 2026-04-10 14:33:48 +08:00
03f72adade feat: Update version to 0.4.0, add test command, and improve documentation for testing functionality 2026-04-10 14:31:58 +08:00
cd5023243b feat: Update documentation with new sections for workspace guide, flags & environment, and troubleshooting map; enhance CLI reference and build/run notes for improved clarity. 2026-03-21 14:15:06 +08:00
44 changed files with 1582 additions and 196 deletions

View File

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

View File

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

2
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -27,7 +27,8 @@ kind = "bin"
- `[dependencies]` - `[dependencies]`
Workspace-only root: Workspace-only root:
- `[workspace]` may be used instead of `[package]`/`[build]` for a workspace root. - `[workspace]` can be added to a normal root config to define members and dependency order.
- Root config still requires `[package]`, `[build]`, and `[dependencies]`.
## Required keys ## Required keys
@@ -42,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)

View File

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

View File

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

View File

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

View File

@@ -8,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`.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}"))
} }
} }

View File

@@ -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)?;

View File

@@ -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,
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,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" => {

View File

@@ -59,7 +59,7 @@ fn ensure_bin_built() {
} }
fn available_compiler() -> Option<&'static str> { fn available_compiler() -> Option<&'static str> {
for candidate in ["clang", "gcc", "cc"] { for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate) let ok = Command::new(candidate)
.arg("--version") .arg("--version")
.output() .output()
@@ -133,19 +133,19 @@ fn build_run_clean_flags_normal_project() {
assert!(out.status.success(), "dcr build --release should succeed"); assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs); let out = run_dcr_env(&["run"], &dir, &envs);
assert!(out.status.success(), "dcr run should succeed");
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Hello World!"), "run output missing"); assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs); let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed"); assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!( assert!(
!dir.join("target").join("release").exists(), !dir.join(&target_dir).join("release").exists(),
"target/release should be removed" "target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
dir.join("target").join("debug").is_dir(), dir.join(&target_dir).join("debug").is_dir(),
"target/debug should remain" "target/x86_64-unknown-linux-gnu/debug should remain"
); );
let out = run_dcr_env(&["clean"], &dir, &envs); let out = run_dcr_env(&["clean"], &dir, &envs);
@@ -200,21 +200,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
); );
} }

View File

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