mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 19:29:30 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 613fbc6a6a | |||
| 03f72adade | |||
| cd5023243b |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -90,7 +90,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@@ -38,5 +38,8 @@
|
|||||||
|
|
||||||
* [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)
|
||||||
|
|||||||
@@ -52,11 +52,12 @@ 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.
|
- Incremental rebuild checks source/object mtime and tracked header dependencies.
|
||||||
- `--force` skips build cache checks and recompiles.
|
- `--force` skips build cache checks and recompiles.
|
||||||
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
|
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
|
||||||
|
- Default GCC/Clang profile flags: `debug` -> `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`, `release` -> `-O3 -DNDEBUG`.
|
||||||
- For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`.
|
- For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`.
|
||||||
- In workspace root, `dcr build` builds all members in dependency order.
|
- In workspace root, `dcr build` builds all members in dependency order.
|
||||||
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
|
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ Usage in VSCode:
|
|||||||
- Open the Command Palette (`Ctrl+Shift+P`) and select **"Debug: Start Debugging"** to run debugger
|
- Open the Command Palette (`Ctrl+Shift+P`) and select **"Debug: Start Debugging"** to run debugger
|
||||||
- Press `Ctrl+Shift+B` to run the build task
|
- Press `Ctrl+Shift+B` to run the build task
|
||||||
- Clangd will provide IntelliSense and diagnostics based on `compile_commands.json`
|
- Clangd will provide IntelliSense and diagnostics based on `compile_commands.json`
|
||||||
|
- Generated `launch.json` uses CodeLLDB (`type: "lldb"`), so install `vadimcn.vscode-lldb`
|
||||||
|
|
||||||
### `dcr gen clion`
|
### `dcr gen clion`
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ When running `dcr gen` in a workspace root, all commands process both the root p
|
|||||||
- Verify binary paths in `launch.json` match actual build output (`target/<profile>/binary-name`)
|
- Verify binary paths in `launch.json` match actual build output (`target/<profile>/binary-name`)
|
||||||
- For workspace projects, each member's debug config uses its own `target/` directory
|
- For workspace projects, each member's debug config uses its own `target/` directory
|
||||||
- Ensure GDB/LLDB is installed: `gdb --version` or `lldb --version`
|
- Ensure GDB/LLDB is installed: `gdb --version` or `lldb --version`
|
||||||
|
- If VSCode reports unsupported debugger type, install the extension that matches `launch.json` type (`vadimcn.vscode-lldb` for `lldb`)
|
||||||
|
|
||||||
### CLion not recognizing includes
|
### CLion not recognizing includes
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ 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:
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ 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`.
|
- `--clean` for `build`/`run` removes `target/<profile>` and paths from `build.clean`.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ dcr init
|
|||||||
dcr build [--debug|--release]
|
dcr build [--debug|--release]
|
||||||
dcr run [--debug|--release]
|
dcr run [--debug|--release]
|
||||||
dcr clean [--debug|--release] [--all]
|
dcr clean [--debug|--release] [--all]
|
||||||
|
dcr gen <subcommand>
|
||||||
dcr --help
|
dcr --help
|
||||||
dcr --version
|
dcr --version
|
||||||
dcr --update
|
dcr --update
|
||||||
@@ -15,12 +16,13 @@ dcr --update
|
|||||||
|
|
||||||
## Notes on argument parsing
|
## Notes on argument parsing
|
||||||
|
|
||||||
- Most commands parse profile from the first argument.
|
- `build`/`run` parse flags in any order (`--debug|--release`, `--force`, `--clean`) and reject duplicates.
|
||||||
- `new` requires exactly one argument.
|
- `new` requires exactly one argument.
|
||||||
- `init` and `--update` do not accept extra arguments.
|
- `init` and `--update` do not accept extra arguments.
|
||||||
- `clean` accepts `--debug|--release` and optional `--all`.
|
- `clean` accepts `--debug|--release` and optional `--all`.
|
||||||
|
- `gen` supports `project-info`, `compile-commands`, `vscode`, `clion`.
|
||||||
|
|
||||||
## Exit behavior overview
|
## Exit behavior overview
|
||||||
|
|
||||||
- Successful command execution returns `0`.
|
- Successful command execution returns `0`.
|
||||||
- Validation/build/runtime errors return non-zero status.
|
- Validation/build/runtime failures return non-zero status.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## What commands are available?
|
## What commands are available?
|
||||||
|
|
||||||
`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`.
|
`new`, `init`, `build`, `run`, `clean`, `gen`, `--help`, `--version`, `--update`.
|
||||||
|
|
||||||
## Is `dcr.toml` mandatory?
|
## Is `dcr.toml` mandatory?
|
||||||
|
|
||||||
@@ -21,7 +21,8 @@ Yes. Set `build.compiler` in `dcr.toml` (for example `gcc`, `clang`, `cl`, `clan
|
|||||||
|
|
||||||
## How does incremental rebuild work?
|
## How does incremental rebuild work?
|
||||||
|
|
||||||
It compares source file mtime with object file mtime. Header dependency tracking is not implemented yet.
|
It compares source file mtime with object file mtime.
|
||||||
|
Header dependencies are tracked too: if an included header changes (or disappears), rebuild is triggered.
|
||||||
|
|
||||||
## What does `dcr run` do if build fails?
|
## What does `dcr run` do if build fails?
|
||||||
|
|
||||||
|
|||||||
48
docs/reference-and-help/flags-and-environment.md
Normal file
48
docs/reference-and-help/flags-and-environment.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Flags & environment
|
||||||
|
|
||||||
|
This page summarizes runtime flags and environment variables that affect DCR behavior.
|
||||||
|
|
||||||
|
## Build/run flags
|
||||||
|
|
||||||
|
Used with `dcr build` and `dcr run`:
|
||||||
|
|
||||||
|
- `--debug`: select debug profile.
|
||||||
|
- `--release`: select release profile.
|
||||||
|
- `--force`: ignore incremental cache checks and rebuild.
|
||||||
|
- `--clean`: remove `target/<profile>` and `build.clean` paths before build.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Flags can be passed in any order.
|
||||||
|
- Duplicate profile flags are rejected.
|
||||||
|
|
||||||
|
## Clean flags
|
||||||
|
|
||||||
|
Used with `dcr clean`:
|
||||||
|
|
||||||
|
- `--debug` / `--release`: clean only selected profile directory.
|
||||||
|
- `--all`: in workspace root, also clean all members.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Tool selection overrides:
|
||||||
|
|
||||||
|
- `DCR_COMPILER` (all languages)
|
||||||
|
- `DCR_CC`, `DCR_CXX`, `DCR_AS`, `DCR_AR`, `DCR_LD`
|
||||||
|
|
||||||
|
Diagnostics:
|
||||||
|
|
||||||
|
- `DCR_DEBUG=1` enables verbose build logging.
|
||||||
|
|
||||||
|
## Quick examples
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Force full release rebuild
|
||||||
|
dcr build --release --force
|
||||||
|
|
||||||
|
# Clean before debug run
|
||||||
|
dcr run --clean --debug
|
||||||
|
|
||||||
|
# Use custom compiler from env
|
||||||
|
DCR_CC=clang-18 dcr build --debug
|
||||||
|
```
|
||||||
75
docs/reference-and-help/troubleshooting-map.md
Normal file
75
docs/reference-and-help/troubleshooting-map.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Troubleshooting map
|
||||||
|
|
||||||
|
Fast mapping from symptom to likely cause and first action.
|
||||||
|
|
||||||
|
## `dcr.toml file not found`
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- you are outside project root (or member root).
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
pwd
|
||||||
|
ls
|
||||||
|
```
|
||||||
|
Run DCR from directory that contains `dcr.toml`.
|
||||||
|
|
||||||
|
## `Invalid config: missing [package]` / `missing [build]`
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- incomplete config in root project.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
- ensure root `dcr.toml` includes `[package]`, `[build]`, and `[dependencies]`.
|
||||||
|
- for workspace setups, keep these sections in root and add `[workspace]` additionally.
|
||||||
|
|
||||||
|
## `Unknown build flag` or profile-flag errors
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- typo in flag or duplicate profile flag.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
dcr --help
|
||||||
|
```
|
||||||
|
Use only supported flags and at most one of `--debug` / `--release`.
|
||||||
|
|
||||||
|
## Dependency include/lib path errors
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- path in `[dependencies]` does not exist.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
- verify `path`, `include`, `lib` values.
|
||||||
|
- if using profile placeholders, confirm expanded paths exist for selected profile.
|
||||||
|
|
||||||
|
## `Cannot run library build`
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- `build.kind` is `staticlib` or `sharedlib`.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
- switch `build.kind` to `bin`, or set `[run].cmd` if you intentionally need custom run command.
|
||||||
|
|
||||||
|
## Build succeeds but output is stale
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- stale artifacts or generated files from previous runs.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
dcr clean --all
|
||||||
|
dcr build --force
|
||||||
|
```
|
||||||
|
|
||||||
|
## `dcr --update` suggests package manager update
|
||||||
|
|
||||||
|
Likely cause:
|
||||||
|
- DCR binary is managed by pacman/AUR.
|
||||||
|
|
||||||
|
First action:
|
||||||
|
```sh
|
||||||
|
yay -Syu dcr
|
||||||
|
# or
|
||||||
|
paru -Syu dcr
|
||||||
|
```
|
||||||
53
docs/reference-and-help/workspace-guide.md
Normal file
53
docs/reference-and-help/workspace-guide.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Workspace guide
|
||||||
|
|
||||||
|
`[workspace]` lets you build related DCR projects in dependency order from one root.
|
||||||
|
|
||||||
|
## Important rule
|
||||||
|
|
||||||
|
Workspace root still needs a valid root project config:
|
||||||
|
|
||||||
|
- `[package]`
|
||||||
|
- `[build]`
|
||||||
|
- `[dependencies]`
|
||||||
|
|
||||||
|
`[workspace]` augments that root config; it does not replace required root sections.
|
||||||
|
|
||||||
|
## Minimal example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "root-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
language = "c"
|
||||||
|
standard = "c11"
|
||||||
|
compiler = "clang"
|
||||||
|
kind = "bin"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
core = { path = "./modules/core", deps = [] }
|
||||||
|
net = { path = "./modules/net", deps = ["core"] }
|
||||||
|
app = { path = "./modules/app", deps = ["core", "net"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Each member path must point to a directory containing its own `dcr.toml`.
|
||||||
|
|
||||||
|
## Build behavior
|
||||||
|
|
||||||
|
- Running `dcr build` in workspace root builds members in topological dependency order, then builds the root project.
|
||||||
|
|
||||||
|
- Running `dcr clean --all` in workspace root cleans all members too.
|
||||||
|
|
||||||
|
- `dcr gen` in workspace root includes root + members in generated outputs.
|
||||||
|
|
||||||
|
## Common errors
|
||||||
|
|
||||||
|
- `workspace.<name>.path does not contain dcr.toml`:
|
||||||
|
member path is wrong or config is missing.
|
||||||
|
- `workspace dependency '<name>' not found`:
|
||||||
|
`deps` references unknown member.
|
||||||
|
- `workspace dependency cycle at <name>`:
|
||||||
|
cyclic dependencies in member graph.
|
||||||
@@ -801,7 +801,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 +921,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 +1025,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 +1042,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 +1065,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}"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub fn help() -> i32 {
|
|||||||
println!(" init Initialize the current directory as a project");
|
println!(" init Initialize the current directory as a project");
|
||||||
println!(" build [--profile] Build the project (default: --debug)");
|
println!(" build [--profile] Build the project (default: --debug)");
|
||||||
println!(" run [--profile] Build and run the project (default: --debug)");
|
println!(" run [--profile] Build and run the project (default: --debug)");
|
||||||
|
println!(" test Run project tests (alias: tests)");
|
||||||
println!(" clean Remove the target directory");
|
println!(" clean Remove the target directory");
|
||||||
println!(" gen <subcommand> Generate IDE integration files");
|
println!(" gen <subcommand> Generate IDE integration files");
|
||||||
printc("FLAGS:", BOLD_GREEN);
|
printc("FLAGS:", BOLD_GREEN);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
println!(
|
println!(
|
||||||
" {} Created file {}\n",
|
" {} Created file {}",
|
||||||
colored("✔", BOLD_GREEN),
|
colored("✔", BOLD_GREEN),
|
||||||
colored("src/main.c", BOLD_CYAN)
|
colored("src/main.c", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ pub mod help;
|
|||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
|
pub mod test;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
println!(
|
println!(
|
||||||
" {} Created file {}\n",
|
" {} Created file {}",
|
||||||
colored("✔", BOLD_GREEN),
|
colored("✔", BOLD_GREEN),
|
||||||
colored("src/main.c", BOLD_CYAN)
|
colored("src/main.c", BOLD_CYAN)
|
||||||
);
|
);
|
||||||
|
|||||||
269
src/cli/test.rs
Normal file
269
src/cli/test.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
use crate::cli::build;
|
||||||
|
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
|
||||||
|
use crate::core::config::Config;
|
||||||
|
use crate::utils::fs::find_project_root;
|
||||||
|
use crate::utils::log::error;
|
||||||
|
use crate::utils::text::{BOLD_GREEN, BOLD_RED, RESET, colored};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
|
||||||
|
|
||||||
|
pub fn test(args: &[String]) -> i32 {
|
||||||
|
let mut init_header = false;
|
||||||
|
for arg in args {
|
||||||
|
if arg == "--help" {
|
||||||
|
println!("USAGE:");
|
||||||
|
println!(" dcr test [--init]");
|
||||||
|
println!();
|
||||||
|
println!("ALIASES:");
|
||||||
|
println!(" dcr tests");
|
||||||
|
println!();
|
||||||
|
println!("DESCRIPTION:");
|
||||||
|
println!(" Runs project tests and prints a unified testsuite report.");
|
||||||
|
println!();
|
||||||
|
println!("OPTIONS:");
|
||||||
|
println!(" --init Create tests/dcr_test.h in current project");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if arg == "--init" {
|
||||||
|
init_header = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
error("Unknown argument");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_dir = match std::env::current_dir() {
|
||||||
|
Ok(dir) => dir,
|
||||||
|
Err(_) => {
|
||||||
|
error("Failed to determine current directory");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let root = match find_project_root(&start_dir) {
|
||||||
|
Ok(Some(dir)) => dir,
|
||||||
|
Ok(None) => {
|
||||||
|
error("dcr.toml file not found");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
error("Failed to find project root");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if init_header {
|
||||||
|
match with_dir(&root, ensure_test_header) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(msg) => {
|
||||||
|
error(&msg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match with_dir(&root, run_testsuite) {
|
||||||
|
Ok(code) => code,
|
||||||
|
Err(msg) => {
|
||||||
|
error(&msg);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_testsuite() -> Result<i32, String> {
|
||||||
|
if build::build(&[String::from("--release")]) != 0 {
|
||||||
|
return Ok(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config::open("./dcr.toml").map_err(|_| "Failed to read dcr.toml".to_string())?;
|
||||||
|
let name = config
|
||||||
|
.get("package.name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or("package.name not found in dcr.toml")?;
|
||||||
|
let kind = config
|
||||||
|
.get("package.kind")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("bin");
|
||||||
|
|
||||||
|
let compiler = env::var("DCR_CC").unwrap_or_else(|_| "cc".to_string());
|
||||||
|
let profile = "release";
|
||||||
|
let flags_vec = flags(profile).unwrap_or(&[]);
|
||||||
|
let flags_str = flags_vec.join(" ");
|
||||||
|
|
||||||
|
let test_c = "./tests/test.c";
|
||||||
|
if !Path::new(test_c).exists() {
|
||||||
|
return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile test.c to object
|
||||||
|
let obj_path = "./tests/test.o";
|
||||||
|
let compile_cmd = format!("{} -c {} -o {} {}", compiler, test_c, obj_path, flags_str);
|
||||||
|
let compile_status = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&compile_cmd)
|
||||||
|
.status()
|
||||||
|
.map_err(|_| format!("Failed to compile {}", test_c))?;
|
||||||
|
if !compile_status.success() {
|
||||||
|
return Err(format!("Compilation of {} failed", test_c));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link
|
||||||
|
let bin_path = format!("./tests/test{}", std::env::consts::EXE_SUFFIX);
|
||||||
|
let link_cmd = if kind == "lib" {
|
||||||
|
format!(
|
||||||
|
"{} {} -o {} -Ltarget/release -l{}",
|
||||||
|
compiler, obj_path, bin_path, name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{} {} -o {}", compiler, obj_path, bin_path)
|
||||||
|
};
|
||||||
|
let link_status = Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&link_cmd)
|
||||||
|
.status()
|
||||||
|
.map_err(|_| format!("Failed to link {}", bin_path))?;
|
||||||
|
if !link_status.success() {
|
||||||
|
return Err(format!("Linking of {} failed", bin_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = Command::new(&bin_path)
|
||||||
|
.output()
|
||||||
|
.map_err(|_| format!("Failed to run `{bin_path}`"))?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
if !stderr.trim().is_empty() {
|
||||||
|
eprint!("{}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let declared = extract_test_names("./tests/test.c");
|
||||||
|
let mut pass = 0;
|
||||||
|
let mut skip = 0;
|
||||||
|
let mut fail = 0;
|
||||||
|
let mut parsed_any = false;
|
||||||
|
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if let Some(name) = line.strip_prefix("[PASS] ") {
|
||||||
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
|
pass += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(name) = line.strip_prefix("[SKIP] ") {
|
||||||
|
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||||
|
skip += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(name) = line.strip_prefix("[FAIL] ") {
|
||||||
|
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||||
|
fail += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some((status, name)) = line.split_once('\t') {
|
||||||
|
match status {
|
||||||
|
"PASS" => {
|
||||||
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
|
pass += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
"SKIP" => {
|
||||||
|
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
|
||||||
|
skip += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
"FAIL" => {
|
||||||
|
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
|
||||||
|
fail += 1;
|
||||||
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed_any {
|
||||||
|
if out.status.success() {
|
||||||
|
for name in &declared {
|
||||||
|
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
|
||||||
|
}
|
||||||
|
pass = declared.len() as i32;
|
||||||
|
} else {
|
||||||
|
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
|
||||||
|
fail = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = if parsed_any || declared.is_empty() {
|
||||||
|
pass + skip + fail
|
||||||
|
} else {
|
||||||
|
declared.len() as i32
|
||||||
|
};
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", colored("=====================", BOLD_GREEN));
|
||||||
|
println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
|
||||||
|
println!("{}", colored("=====================", BOLD_GREEN));
|
||||||
|
println!("TOTAL: {}", total);
|
||||||
|
print_field("PASS", pass, BOLD_GREEN);
|
||||||
|
print_field("SKIP", skip, BOLD_BLUE);
|
||||||
|
print_field("FAIL", fail, BOLD_RED);
|
||||||
|
println!("{}", colored("=====================", BOLD_GREEN));
|
||||||
|
|
||||||
|
if fail > 0 || !out.status.success() {
|
||||||
|
return Ok(1);
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_test_header() -> Result<(), String> {
|
||||||
|
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
|
||||||
|
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
|
||||||
|
.map_err(|_| "Failed to write tests/dcr_test.h".to_string())?;
|
||||||
|
println!("Created tests/dcr_test.h");
|
||||||
|
fs::write("./tests/test.c", FILE_TEST_C)
|
||||||
|
.map_err(|_| "Failed to write tests/test.c".to_string())?;
|
||||||
|
println!("Created tests/test.c");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_test_names(path: &str) -> Vec<String> {
|
||||||
|
let Ok(content) = fs::read_to_string(path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let mut names = Vec::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("TEST_CASE(")
|
||||||
|
&& let Some(name) = rest.strip_suffix("),")
|
||||||
|
{
|
||||||
|
names.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_field(label: &str, value: i32, color: &str) {
|
||||||
|
if value == 0 {
|
||||||
|
println!("{}: {}", label, value);
|
||||||
|
} else {
|
||||||
|
println!("{}{}: {}{}", color, label, value, RESET);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<T, String>,
|
||||||
|
{
|
||||||
|
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
|
||||||
|
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
|
||||||
|
let result = f();
|
||||||
|
let _ = std::env::set_current_dir(prev);
|
||||||
|
result
|
||||||
|
}
|
||||||
@@ -7,9 +7,81 @@ int main(void) {
|
|||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
pub const FILE_DCR_TEST_H: &str = r#"#pragma once
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define DCR_TEST_PASS 0
|
||||||
|
#define DCR_TEST_FAIL 1
|
||||||
|
#define DCR_TEST_SKIP 2
|
||||||
|
|
||||||
|
#define EXPECT(expr) \
|
||||||
|
do { \
|
||||||
|
if (!(expr)) { \
|
||||||
|
fprintf(stderr, "assert failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); \
|
||||||
|
return DCR_TEST_FAIL; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define SKIP(reason) \
|
||||||
|
do { \
|
||||||
|
(void)(reason); \
|
||||||
|
return DCR_TEST_SKIP; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define TEST(name) static int name(void)
|
||||||
|
#define TEST_CASE(name) {#name, name}
|
||||||
|
|
||||||
|
typedef int (*test_fn_t)(void);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
test_fn_t fn;
|
||||||
|
} test_case_t;
|
||||||
|
|
||||||
|
static inline int dcr_test_raw_mode(int argc, char **argv) {
|
||||||
|
return argc > 1 && strcmp(argv[1], "--dcr-raw") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void dcr_test_print_result(const char *name, int rc, int raw_mode) {
|
||||||
|
const char *tag = (rc == DCR_TEST_PASS) ? "PASS" : (rc == DCR_TEST_SKIP) ? "SKIP" : "FAIL";
|
||||||
|
if (raw_mode) {
|
||||||
|
printf("%s\t%s\n", tag, name);
|
||||||
|
} else {
|
||||||
|
printf("[%s] %s\n", tag, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const FILE_TEST_C: &str = r#"#include "dcr_test.h"
|
||||||
|
|
||||||
|
TEST(test_example) {
|
||||||
|
EXPECT(1 + 1 == 2);
|
||||||
|
return DCR_TEST_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static test_case_t CASES[] = {
|
||||||
|
TEST_CASE(test_example),
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int raw = dcr_test_raw_mode(argc, argv);
|
||||||
|
int rc = 0;
|
||||||
|
int count = (int)(sizeof(CASES) / sizeof(CASES[0]));
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
int status = CASES[i].fn();
|
||||||
|
dcr_test_print_result(CASES[i].name, status, raw);
|
||||||
|
if (status == DCR_TEST_FAIL) {
|
||||||
|
rc = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
|
||||||
match profile {
|
match profile {
|
||||||
"release" => Some(&["-O3", "-DNDEBUG", "-march=native"]),
|
"release" => Some(&["-O3", "-DNDEBUG"]),
|
||||||
"debug" => Some(&[
|
"debug" => Some(&[
|
||||||
"-O0",
|
"-O0",
|
||||||
"-g",
|
"-g",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -156,9 +156,6 @@ fn build_objects(
|
|||||||
cmd.arg(format!("-std={}", ctx.standard));
|
cmd.arg(format!("-std={}", ctx.standard));
|
||||||
}
|
}
|
||||||
for flag in default_flags(ctx.profile) {
|
for flag in default_flags(ctx.profile) {
|
||||||
if ctx.platform.is_some() && *flag == "-march=native" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cmd.arg(flag);
|
cmd.arg(flag);
|
||||||
}
|
}
|
||||||
for flag in ctx.cflags {
|
for flag in ctx.cflags {
|
||||||
|
|||||||
@@ -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" => {
|
||||||
|
|||||||
@@ -218,3 +218,31 @@ fn workspace_build_and_clean_all() {
|
|||||||
"root target/debug should remain"
|
"root target/debug should remain"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dcr_test_runs_without_sandbox_dependency() {
|
||||||
|
let Some(compiler) = available_compiler() else {
|
||||||
|
eprintln!("no compiler found; skipping dcr test integration");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let dir = unique_sandbox_dir("dcr_test_independent");
|
||||||
|
let out = run_dcr(&["init"], &dir);
|
||||||
|
assert!(out.status.success(), "dcr init should succeed");
|
||||||
|
|
||||||
|
let envs = [("DCR_CC", compiler)];
|
||||||
|
let out_init = run_dcr_env(&["test", "--init"], &dir, &envs);
|
||||||
|
assert!(out_init.status.success(), "dcr test --init should succeed");
|
||||||
|
|
||||||
|
let out = run_dcr_env(&["test"], &dir, &envs);
|
||||||
|
assert!(out.status.success(), "dcr test should succeed");
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
|
||||||
|
assert!(
|
||||||
|
stdout.contains("PASS: 1"),
|
||||||
|
"PASS summary line missing\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout,
|
||||||
|
stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user