45 Commits

Author SHA1 Message Date
peoplemiau1
cf634d8a8b feat: add fmt command 2026-05-26 15:06:21 +03:00
b66c846956 feat: improve release workflow with enhanced tag handling and clearer input descriptions 2026-05-26 19:39:35 +08:00
2a9c918711 fix: add missing package metadata including description, license, and repository URL 2026-05-26 19:36:39 +08:00
2af3baf4bb feat: enhance release workflow with additional input options and build configurations for Linux, macOS, and Windows 2026-05-26 19:16:09 +08:00
4e3601b1df Changed: openssl dependency is now conditional (cfg(not(windows))) — no longer pulled in on Windows, fixing Windows CI builds that lack Perl Locale::Maketext::Simple. On Linux (including musl cross-compilation) the vendored OpenSSL build is unchanged. 2026-05-26 17:57:37 +08:00
7969209eae Added:
- `build.kind = "efi"` — UEFI PE32+ executable support. Links with `-shared -nostdlib -Wl,-dll -Wl,--subsystem,10`, output via `efi_path()` with `.efi` extension, rejected by `dcr run`.
- `build.kind = "elf"` — bare-metal ELF executable support. Uses `elf_path()` output path, rejected by `dcr run`, no extra linker flags.

Fixed:

- `dcr run` with `--target` or `build.target` now correctly finds the binary in the arch-specific target directory.
- `dcr run` checks profile-specific `build.{profile}.kind` before rejecting library builds (previously only checked `build.kind`).
- `dcr clean` now reads `build.target` from `dcr.toml` when no `--target` flag is given.
- Linux: target directory is now consistent — `target/<arch>-unknown-linux-gnu/<profile>/` (no stale `target/<profile>/`).
- `dcr.lock` is now populated with all resolved dependencies (registry, path, git) instead of being always empty.
- Git dependencies are now properly recognized and recorded in `dcr.lock`.
- `is_registry_dep` correctly identifies version-based registry dependency tables (`{ version = "..." }`) and rejects tables with unknown keys.
- `dcr test` accepts `--debug`/`--release` flags (defaults to `debug` profile instead of always `release`).
- Build fingerprint cache (`build_cache_path`) now includes `target_dir` — different targets no longer share a single cache entry.
- `object_path` no longer hardcodes the `src/` prefix — works correctly with custom `build.roots` (e.g. `roots = ["lib"]`).
- `dcr update` now generates `.exe` candidates for all Windows targets, not just `x86_64-pc-windows-msvc`.
- Compiler existence is verified before starting the build (clear error if compiler is not found in PATH).
- Consolidated duplicate `OUTPUT_MUTEX` definitions — MSVC backend now uses the shared mutex from `common.rs`.
- Registry cache root directory is created if missing (prevents cryptic errors when `~/.dcr/` does not exist).
2026-05-26 16:38:23 +08:00
adfa24f8f4 feat: add AGENTS.md for quick start and CI guidelines; update documentation for dcr add and dcr build commands 2026-05-22 15:14:56 +08:00
ef2fb1e8e4 feat: update version to 0.6.7, enhance build configuration, and add dcr tree command 2026-05-22 14:37:45 +08:00
9ad968374e fix: target system
add: new field in dcr.toml - filename, extension
2026-05-21 00:02:24 +08:00
c4a49cb345 feat: add openssl dependency and update git2 configuration 2026-05-18 16:58:44 +08:00
527a789410 fix: update git2 dependency to use vendored-openssl 2026-05-18 16:46:11 +08:00
6b9e731c7b fix: update git2 dependency to disable default features and enable https 2026-05-18 16:41:01 +08:00
b2854db91b refactor: simplify Linux cross-compilation setup and install cargo-zigbuild 2026-05-18 16:31:29 +08:00
fce9c44181 Merge pull request #7 from dexoron/dev
v0.6.6
2026-05-18 16:15:34 +08:00
3b54500d4d Merge branch 'master' into dev 2026-05-18 16:15:22 +08:00
933fae749d Fixed:
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
from path/git strings, and `deps/mod.rs` resolves both table-form and
legacy string-form path dependencies
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
now use `package_root_from_registry_info()` which resolves relative to the
registry cache root (`~/.dcr/`)
- Registry dependencies are now actually built: `build_project_at()` is
called when `include_dir` or `lib_dir` is missing

Added:

- `SIGINT`/`Ctrl+C` handler via `ctrlc` crate — `dcr build` now checks
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
- `utils/build.rs` — extracted shared utilities: `parse_version_info`,
`normalize_target_os`, `resolve_compiler`, `primary_language`,
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
- `run_command_sync_output` in `builder/common.rs` with global `OUTPUT_MUTEX`
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
- New helper functions in `deps/register.rs`: `get_registry_cache_root`,
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
`path_from_string_dep`
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)

Removed:

- Removed unused dependencies `indicatif` and `console` from `Cargo.toml`
2026-05-18 16:13:33 +08:00
kie
9ab7ce15db Merge pull request #6 from dexoron/feat-tree
feat: add dcr tree command
2026-05-15 12:44:56 +03:00
peoplemiau1
954f2870ba style: final formatting and clippy fixes for tree.rs 2026-05-15 12:39:28 +03:00
peoplemiau1
9a24a80d59 fix: address clippy warnings in tree.rs 2026-05-15 12:22:05 +03:00
peoplemiau1
a123f647c8 style: fix formatting in tree.rs 2026-05-15 12:10:09 +03:00
peoplemiau1
8f04a37eb0 feat: add dcr tree command to visualize dependencies 2026-05-15 12:06:41 +03:00
441c9f4df3 update: add dev release support and update cross build 2026-05-15 11:40:17 +08:00
60b9c4f983 Merge pull request #5 from peoplemiau1/master
refactor: optimize parallel build and fix architectural issues
2026-05-14 21:35:36 +08:00
peoplemiau1
f5fd273e29 refactor: optimize parallel build and fix architectural issues 2026-05-13 19:35:43 +03:00
d6840fc7a2 Merge pull request #4 from peoplemiau1/master
feat: parallel compilation for all builders
2026-05-14 00:02:02 +08:00
peoplemiau1
f507148849 style: format code according to project standards 2026-05-13 18:59:59 +03:00
peoplemiau1
cfc6be1e95 feat: parallel compilation for all builders 2026-05-13 18:50:42 +03:00
2de836a421 feat: Add installation of Linux cross compilation tools for builds 2026-05-13 20:39:03 +08:00
ed85f801e9 refactor: Improve registry sorting logic for package resolution 2026-05-13 19:43:25 +08:00
9d8dcda366 style: Update macOS target descriptions for clarity 2026-05-13 19:41:26 +08:00
0193834bfd feat: Enhance dependency management and project setup
- Added support for registry-based dependencies in the dependencies section.
- Updated the package section to include a project type field.
- Introduced a new CLI command `dcr setup` to load and display registry configurations.
- Implemented a new module for handling registry operations, including loading and resolving packages from registries.
- Enhanced the build process to handle library packaging, including copying headers and libraries to the appropriate target directories.
- Updated the CLI to support building library packages and added tests for library package builds.
- Improved error handling and output messages during dependency resolution and package fetching.
2026-05-13 19:24:53 +08:00
debc5d2044 Added:
- Full Git dependency support in `dcr.toml`
  - Support for `git`, `branch`, `tag`, and `rev` fields
  - Automatic cloning and checkout via `git2`
  - Dependencies are stored in `target/<profile>/deps/git/`
- New `dcr add` command for easy dependency management
  - Short syntax: `github:user/repo`, `gitlab:user/repo`, `path:/to/lib`
  - Automatic GitHub resolution for `git:user/repo`
- Visual feedback for dependency fetching with in-place status updates ("Fetching" → "Fetched")
- Modular dependency management architecture in `src/core/deps/`
2026-05-12 01:29:26 +08:00
00ed418291 New TODO.md 2026-05-11 23:22:12 +08:00
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
d08ceaf31d Added:
- `dcr gen` command for IDE integration and build tool support
    - `dcr gen project-info`: Output project metadata as JSON
    - `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
    - `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
    - `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
2026-03-19 00:03:39 +08:00
5f0dca4bb3 Added:
- `dcr gen` command for IDE integration and build tool support
    - `dcr gen project-info`: Output project metadata as JSON
    - `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
    - `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
    - `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
2026-03-19 00:02:04 +08:00
0e93a1aa0f Added:
- `build.steps` for pre-build generators (e.g., `moc`/`uic`/`rcc`)
- `build.pkg_config` for `pkg-config`-driven cflags/ldflags resolution
- `[toolchain]` support for `uic`, `moc`, `rcc`
- Step command variables (`{uic}`, `{moc}`, `{rcc}`, `{cflags}`, `{stem}`, `{in}`, `{out}`)
- `{profile}` placeholder support for dependency paths
2026-03-16 20:50:32 +08:00
dd996b92d1 fix code format 2026-03-14 17:56:12 +08:00
a0e4fdbacf feat: Refactor builder modules by consolidating Unix C/C++ compilers, extracting common logic, enhancing MSVC with dependency tracking, and adding new CLI error tests. 2026-03-14 16:43:00 +08:00
4ea6267a88 Added:
- Workspace support (`[workspace]` with deps ordering)
- `clean --all` for workspace members
Changed:
- Project root discovery for `build/run/clean` (searches parent dirs)
- Workspace paths are excluded from root source scan
- Build output now uses a Cargo-style `Compiling <name> v<version>` line
- Build time now reports total time for the whole build stage (workspace included)
- Cache hits are silent (no compile line when nothing is rebuilt)
- Safer build cache: fingerprints include headers and resolved library files
2026-03-06 21:58:54 +08:00
85 changed files with 10170 additions and 1594 deletions

View File

@@ -15,6 +15,8 @@ permissions:
jobs:
publish-aur:
# Не публикуем dev/pre-release теги в AUR
if: "!contains(github.ref_name, '-')"
runs-on: ubuntu-latest
environment: aur
steps:
@@ -133,4 +135,4 @@ jobs:
git add PKGBUILD .SRCINFO
git commit -m "dcr ${{ steps.meta.outputs.pkgver }}"
git push
git push

View File

@@ -4,21 +4,68 @@ on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to build (e.g. v0.6.7)"
required: true
run_linux:
description: "Build Linux"
type: boolean
default: true
run_macos:
description: "Build macOS"
type: boolean
default: true
run_windows:
description: "Build Windows"
type: boolean
default: true
run_publish:
description: "Publish to crates.io"
type: boolean
default: false
permissions:
contents: write
jobs:
# ── Resolve Tag ────────────────────────────────────────────────────────────
setup:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.value }}
version: ${{ steps.tag.outputs.version }}
prerelease: ${{ steps.tag.outputs.prerelease }}
steps:
- name: Resolve tag
id: tag
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi
VERSION="${TAG#v}"
[[ "$TAG" == *"-"* ]] && PRERELEASE="true" || PRERELEASE="false"
echo "value=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
# ── Release Notes ──────────────────────────────────────────────────────────
notes:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Prepare Release Notes
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
tag="${{ needs.setup.outputs.version }}"
notes_file=".github/release-notes.md"
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then
awk -v ver="$tag" '
@@ -37,28 +84,80 @@ jobs:
name: release-notes
path: .github/release-notes.md
build:
# ── Linux ──────────────────────────────────────────────────────────────────
build-linux:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }}
needs: [setup, notes]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
asset_name: dcr-x86_64-unknown-linux-gnu
- os: macos-15-intel
target: x86_64-apple-darwin
asset_name: dcr-x86_64-apple-darwin
- os: macos-15
target: aarch64-apple-darwin
asset_name: dcr-aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
asset_name: dcr-x86_64-pc-windows-msvc.exe
runs-on: ${{ matrix.os }}
needs: notes
- target: x86_64-unknown-linux-gnu
- target: aarch64-unknown-linux-gnu
- target: x86_64-unknown-linux-musl
- target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Install cargo-zigbuild
run: |
pip install cargo-zigbuild
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: |
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
- name: Prepare Artifact
run: |
ASSET="dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}"
cp target/${{ matrix.target }}/release/dcr "$ASSET"
echo "ASSET_NAME=${ASSET}" >> "$GITHUB_ENV"
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: ${{ env.ASSET_NAME }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── macOS ──────────────────────────────────────────────────────────────────
build-macos:
if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }}
needs: [setup, notes]
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
@@ -73,20 +172,79 @@ jobs:
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Prepare Artifact (Unix)
if: runner.os != 'Windows'
shell: bash
- name: Prepare Artifact
run: |
cp target/${{ matrix.target }}/release/dcr ${{ matrix.asset_name }}
- name: Prepare Artifact (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item "target/${{ matrix.target }}/release/dcr.exe" "${{ matrix.asset_name }}"
ASSET="dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}"
cp target/${{ matrix.target }}/release/dcr "$ASSET"
echo "ASSET_NAME=${ASSET}" >> "$GITHUB_ENV"
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.asset_name }}
tag_name: ${{ needs.setup.outputs.tag }}
files: ${{ env.ASSET_NAME }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Windows ────────────────────────────────────────────────────────────────
build-windows:
if: ${{ github.event_name == 'push' || github.event.inputs.run_windows == 'true' }}
needs: [setup, notes]
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
- target: aarch64-pc-windows-msvc
- target: x86_64-pc-windows-gnu
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Prepare Artifact
shell: pwsh
run: |
$asset = "dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}.exe"
Copy-Item "target/${{ matrix.target }}/release/dcr.exe" "$asset"
"ASSET_NAME=$asset" >> $env:GITHUB_ENV
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: ${{ env.ASSET_NAME }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── crates.io ──────────────────────────────────────────────────────────────
publish-crates:
if: |
(github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_publish == 'true')
needs: [setup, build-linux, build-macos, build-windows]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- uses: dtolnay/rust-toolchain@stable
- name: Publish to crates.io
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@ target/
sandbox/**
.idea/
.vscode/
.env
.env
personal/

56
AGENTS.md Normal file
View File

@@ -0,0 +1,56 @@
# AGENTS.md
## Quick start
```bash
cargo build --release && ./target/release/dcr setup
```
## Pre-PR (CI) gate
```bash
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo check --all-targets --all-features
cargo test
```
CI (`.github/workflows/ci.yml`) runs fmt, clippy, check — **not** tests.
## Tests
- Integration tests in `tests/` build the dcr binary and run it in temp dirs under `sandbox/cli-tests/` (gitignored).
- Some tests **skip** if no C compiler (`gcc`/`clang`/`cc`) is found at runtime.
- Compiler can be overridden via `DCR_COMPILER` or `DCR_CC` env vars.
- Tests use `$CARGO_BIN_EXE_dcr` when available, else `target/debug/dcr`.
## Architecture
- **Single Rust package** (edition 2024), not a workspace. Only one binary: `dcr`.
- **Entrypoint**: `src/main.rs` — matches `args[1]` against command names.
- **Compiler backends** in `src/core/builder/`: `unix_cc` (gcc/clang), `msvc` (cl/clang-cl), `gas`, `nasm`. Dispatched by matching compiler name string.
- **Config**: `dcr.toml` (per-project), `~/.dcr/config.toml` (registry), `~/.dcr/index.json` (index). Defaults: `language=c`, `standard=c11`, `compiler=clang`, `kind=bin`.
- **`build.rs`** embeds `TARGET` as `DCR_TARGET` env var (shown in `--version`).
- **CLI output** must be in English. Use `utils::log` for messaging.
## Key commands
| Command | Notes |
|---|---|
| `dcr new <name>` | Creates project dir |
| `dcr init` | Init from CWD (must be empty) |
| `dcr build [--debug\|--release] [--target <triple>]` | Default profile: `--debug` |
| `dcr run [--debug\|--release]` | Only for `kind = "bin"` |
| `dcr clean [--debug\|--release] [--all]` | `--all` for workspace roots |
| `dcr tree` | Dependency tree viewer |
| `dcr test [--init]` | Runs/test project tests |
| `dcr gen <vscode\|clion\|compile-commands>` | IDE integration |
| `dcr --update` | Self-update from GitHub Releases |
## Cross-compilation
`--target` accepts short names (`linux`, `macos`, `windows`) or full triples. In CI, Linux cross-builds use `cargo-zigbuild`. When `build.target` is set in `dcr.toml`, DCR auto-injects `--target=<value>` into cflags (clang). Bare-metal targets (containing `none`/`-elf`/`eabi`/`baremetal`) skip DCR's default compiler flags.
## Dependencies (Cargo)
reqwest (blocking+json+rustls-tls), serde, toml, git2, openssl (vendored), self-replace, sha2, ctrlc.

View File

@@ -1,12 +1,217 @@
# Changelog
## [0.2.7] - 2026-03-03
## [0.6.7] - 2026-05-26
Added:
- `build.kind = "efi"` — UEFI PE32+ executable support. Links with `-shared -nostdlib -Wl,-dll -Wl,--subsystem,10`, output via `efi_path()` with `.efi` extension, rejected by `dcr run`.
- `build.kind = "elf"` — bare-metal ELF executable support. Uses `elf_path()` output path, rejected by `dcr run`, no extra linker flags.
- `build.filename` and `build.extension` in `dcr.toml` — complete control over the final artifact name without relying on `package.name`.
Example:
```toml
[build]
filename = "KERNEL"
extension = "EFI"
```
Produces `KERNEL.EFI` (works for `bin`, `staticlib`, and `sharedlib`).
- Automatic injection of `--target=<build.target>` into compiler flags when `build.target` is set in `dcr.toml`. This greatly simplifies clang-based cross-compilation (especially for bare-metal targets like `aarch64-none-elf`).
- Bare-metal targets (containing `none`, `-elf`, `eabi`, `baremetal`) no longer receive DCR's internal default flags (`-g`, `-Wall`, `-Wextra`, `-fno-omit-frame-pointer`, `-DDCR_DEBUG`, etc.). Prevents unwanted sections (`.comment`, debug info, etc.) that break custom linker scripts when `inherit = true`.
- `build.ldscript` in `dcr.toml` — linker script path passed as `-T <path>` to the linker. Essential for bare-metal/embedded/freestanding targets.
- `dcr build --verbose` — prints compiler/linker command lines (also works with `DCR_DEBUG` env var).
- `dcr add <name>` without a source argument — if a registry is configured, DCR looks up the package by name and adds it as a version dependency.
Fixed:
- `build.target` declared in `dcr.toml` is now correctly used as the default target even when `--target` is not passed on the command line (previously the host target was always forced for config resolution).
- `dcr run` with `--target` or `build.target` now correctly finds the binary in the arch-specific target directory.
- `dcr run` checks profile-specific `build.{profile}.kind` before rejecting library builds (previously only checked `build.kind`).
- `dcr clean` now reads `build.target` from `dcr.toml` when no `--target` flag is given.
- Linux: target directory is now consistent — `target/<arch>-unknown-linux-gnu/<profile>/` (no stale `target/<profile>/`).
- `dcr.lock` is now populated with all resolved dependencies (registry, path, git) instead of being always empty.
- Git dependencies are now properly recognized and recorded in `dcr.lock`.
- `is_registry_dep` correctly identifies version-based registry dependency tables (`{ version = "..." }`) and rejects tables with unknown keys.
- `dcr test` accepts `--debug`/`--release` flags (defaults to `debug` profile instead of always `release`).
- Build fingerprint cache (`build_cache_path`) now includes `target_dir` — different targets no longer share a single cache entry.
- `object_path` no longer hardcodes the `src/` prefix — works correctly with custom `build.roots` (e.g. `roots = ["lib"]`).
- `dcr update` now generates `.exe` candidates for all Windows targets, not just `x86_64-pc-windows-msvc`.
- Compiler existence is verified before starting the build (clear error if compiler is not found in PATH).
- Consolidated duplicate `OUTPUT_MUTEX` definitions — MSVC backend now uses the shared mutex from `common.rs`.
- Registry cache root directory is created if missing (prevents cryptic errors when `~/.dcr/` does not exist).
Changed:
- `openssl` dependency is now conditional (`cfg(not(windows))`) — no longer pulled in on Windows, fixing Windows CI builds that lack Perl `Locale::Maketext::Simple`. On Linux (including musl cross-compilation) the vendored OpenSSL build is unchanged.
## [0.6.6] - 2026-05-18
Fixed:
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
from path/git strings, and `deps/mod.rs` resolves both table-form and
legacy string-form path dependencies
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
now use `package_root_from_registry_info()` which resolves relative to the
registry cache root (`~/.dcr/`)
- Registry dependencies are now actually built: `build_project_at()` is
called when `include_dir` or `lib_dir` is missing
Added:
- `SIGINT`/`Ctrl+C` handler via `ctrlc` crate — `dcr build` now checks
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
- `utils/build.rs` — extracted shared utilities: `parse_version_info`,
`normalize_target_os`, `resolve_compiler`, `primary_language`,
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
- `run_command_sync_output` in `builder/common.rs` with global `OUTPUT_MUTEX`
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
- New helper functions in `deps/register.rs`: `get_registry_cache_root`,
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
`path_from_string_dep`
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)
- `dcr tree` command — visual dependency tree viewer (similar to `cargo tree`)
Removed:
- Removed unused dependencies `indicatif` and `console` from `Cargo.toml`
## [0.6.5] - 2026-05-13
Added:
- New dependency registry system
- Package type support (`lib`, `app`, `none`) in `dcr.toml`
- Library packaging functionality: automatically generates `include` and `lib` directories in `target/` for `lib` type projects
- Expanded CI build targets:
- Linux: `aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`
- Windows: `aarch64-pc-windows-msvc`, `x86_64-pc-windows-gnu`, `aarch64-pc-windows-gnullvm`
Changed:
- `src/core/deps/` modular architecture overhaul
- Native platform pathing defaults for Linux (now dynamic architecture detection)
- Improved `run` command output handling
## [0.6.0] - 2026-05-12
Added:
- Full Git dependency support in `dcr.toml`
- Support for `git`, `branch`, `tag`, and `rev` fields
- Automatic cloning and checkout via `git2`
- Dependencies are stored in `target/<profile>/deps/git/`
- New `dcr add` command for easy dependency management
- Short syntax: `github:user/repo`, `gitlab:user/repo`, `path:/to/lib`
- Automatic GitHub resolution for `git:user/repo`
- Visual feedback for dependency fetching with in-place status updates ("Fetching" → "Fetched")
- Modular dependency management architecture in `src/core/deps/`
## [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
Added:
- `dcr gen` command for IDE integration and build tool support
- `dcr gen project-info`: Output project metadata as JSON
- `dcr gen compile-commands`: Generate `compile_commands.json` for clangd and clang-tidy
- `dcr gen vscode`: Generate VS Code `.vscode/` integration (launch.json, tasks.json, settings.json)
- `dcr gen clion`: Generate JetBrains CLion `.idea/` integration (run configurations, build targets)
- Full workspace support for all `gen` subcommands
## [0.2.10] - 2026-03-16
Added:
- `build.steps` for pre-build generators (e.g., `moc`/`uic`/`rcc`)
- `build.pkg_config` for `pkg-config`-driven cflags/ldflags resolution
- `[toolchain]` support for `uic`, `moc`, `rcc`
- Step command variables (`{uic}`, `{moc}`, `{rcc}`, `{cflags}`, `{stem}`, `{in}`, `{out}`)
- `{profile}` placeholder support for dependency paths
## [0.2.9] - 2026-03-14
Added:
- Header dependency tracking for fine-grained incremental builds (rebuilds when `#include` files change)
- Complete internal rewrite of the builder module: merged duplicate compilation backends
Changed:
- `cflags` and `default_flags` no longer incorrectly leak to the linker stage in GCC/Clang
- Reduced builder codebase size by over 20% while increasing reliability
- Greatly expanded test suite with 34 new tests covering configuration, CLI errors, and path operations
## [0.2.8] - 2026-03-06
Added:
- Workspace support (`[workspace]` with deps ordering)
- `clean --all` for workspace members
Changed:
- Project root discovery for `build/run/clean` (searches parent dirs)
- Workspace paths are excluded from root source scan
- Build output now uses a Cargo-style `Compiling <name> v<version>` line
- Build time now reports total time for the whole build stage (workspace included)
- Cache hits are silent (no compile line when nothing is rebuilt)
- Safer build cache: fingerprints include headers and resolved library files
## [0.2.7] - 2026-03-03
Added:
- Toolchain overrides via `[toolchain]` and env (`DCR_*`)
- ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`)
## [0.2.6] - 2026-03-01
Added:
- `sharedlib` build kind (shared library output)
- ASM projects (`build.language = "asm"`)
- NASM and GAS backends
@@ -14,24 +219,31 @@ Added:
- Basic CLI integration tests
## [0.2.5] - 2026-02-26
Added:
- Incremental builds (object caching by mtime)
- `build.kind` with `staticlib` support
- Custom output directory via `build.target`
## [0.2.4] - 2026-02-22
Added:
- Custom build flags: `build.cflags` and `build.ldflags`
- Recursive source discovery inside `src/`
- Path dependencies with auto include/lib resolution and `dcr.lock` generation (experimental)
- Add docs/index.md and docs/dcr-toml.md
## [0.2.3] - 2026-02-21
Added:
- Modular builders for `gcc`, `clang`, `msvc`
- Platform-specific binary path generation (Linux/macOS/Windows)
Changed:
- Build configuration moved to `[build]` (`language`, `standard`, `compiler`)
- `dcr.toml` formatting now includes the `[build]` section
- Build uses built-in `debug/release` flags per compiler
@@ -39,20 +251,26 @@ Changed:
- Updated `dcr.toml` examples in documentation
## [0.2.2] - 2026-02-20
Changed:
- Reworked `dcr.toml` handling: added read, validation, and edit through `core::config`
- `new` and `init` use the new config creation logic
- `build` and `run` now require `dcr.toml` and read `compiler` and `name` from it
## [0.2.1] - 2026-02-18
Changed:
- Translated user-facing CLI messages to English
- Unified error and warning output via `utils::log::{error, warn}`
- Updated `--help` output: translated headers and examples to English, used `printc`, and applied `BOLD_*` styles
- Translated installer script messages in `install.sh` and `install.ps1` to English
## [0.2.0] - 2026-02-17
Changed:
- Project migrated from Python to Rust
- CLI and commands (`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`) ported to Rust
- Updated `--update` flag: added support for GNU/Linux, Windows, macOS
@@ -61,31 +279,40 @@ Changed:
- Updated `install.sh` for GNU/Linux and macOS
Added:
- Added `install.ps1` for Windows
- Support for GNU/Linux, Windows, macOS (x86_64/arm)
Important:
- Code was ported with neural networks; future versions will include bug fixes and logic changes
## [0.1.2] - 2026-02-12
Added:
- Update command
- `install.sh` install script and README instructions
- `--version` flag
Changed:
- Improved CLI output, added colors, and updated `--help`
- Updated project run/build handling in `run.py`/`build.py`
## [0.1.1] - 2026-02-11
Changed:
- Updated `--help`
- `main.py` now runs correctly when executed directly
## [0.1.0] - 2026-02-11
First public release.
Added:
- Base commands `new`, `init`, `build`, `run`, `clean`
- Build profiles `debug` and `release`
- `dcr.toml` and `src/main.c` templates

View File

@@ -22,6 +22,27 @@ cargo check
- Keep functions small and focused.
- 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
Before PR, run:

211
Cargo.lock generated
View File

@@ -35,6 +35,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
@@ -54,6 +63,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@@ -89,12 +100,28 @@ dependencies = [
]
[[package]]
name = "dcr"
version = "0.2.7"
name = "ctrlc"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [
"dispatch2",
"nix",
"windows-sys 0.61.2",
]
[[package]]
name = "dcr"
version = "0.6.7"
dependencies = [
"ctrlc",
"git2",
"glob",
"openssl",
"reqwest",
"self-replace",
"serde",
"serde_json",
"sha2",
"toml",
]
@@ -109,6 +136,18 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"block2",
"libc",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -154,6 +193,21 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -262,6 +316,27 @@ dependencies = [
"wasip3",
]
[[package]]
name = "git2"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"openssl-probe",
"openssl-sys",
"url",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -525,6 +600,16 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.85"
@@ -547,6 +632,46 @@ version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libgit2-sys"
version = "0.18.4+1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7"
dependencies = [
"cc",
"libc",
"libssh2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libssh2-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -588,12 +713,82 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "nix"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -612,6 +807,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -1264,6 +1465,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"

View File

@@ -1,12 +1,22 @@
[package]
name = "dcr"
version = "0.2.7"
version = "0.6.7"
edition = "2024"
build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
license = "MIT"
repository = "https://github.com/dexoron/dcr"
[dependencies]
glob = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
self-replace = "1"
sha2 = "0.10"
toml = "1.0.3"
git2 = "0.20.4"
serde_json = "1"
ctrlc = "3"
[target.'cfg(not(windows))'.dependencies]
openssl = { version = "0.10" }

102
README.md
View File

@@ -14,16 +14,23 @@ The current implementation is written in Rust.
- Build a project with `debug` and `release` profiles
- Run the compiled binary
- Clean build artifacts
- Generate IDE integration files (VS Code, CLion) and compilation databases
- Generate a minimal C project template
- Build static and shared libraries
- Build static, shared libraries, UEFI PE32+ executables, and bare-metal ELF executables
- Dependency management:
- Git and Path dependencies with `dcr.lock`
- Registry-based dependency resolution
- Library packaging functionality (auto-generate `include/` and `lib/` for `type = "lib"` packages)
- ASM projects with NASM/GAS or via GCC/Clang
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
- Mixed language projects
- Cross-compilation with `--target` (full triple support and short names)
- Target-specific configurations and inheritance
- Update the binary via `dcr --update` (GitHub Releases)
## Supported Platforms
- Linux: `x86_64-unknown-linux-gnu`
- macOS Intel: `x86_64-apple-darwin`
- macOS Apple Silicon: `aarch64-apple-darwin`
- Windows: `x86_64-pc-windows-msvc`
- Linux: x86_64/aarch64 (GNU and Musl)
- macOS: x86_64/aarch64
- Windows: x86_64/aarch64 (MSVC, GNU, LLVM)
## Installation
@@ -102,17 +109,39 @@ Creates a project with the specified name in the current directory.
### `dcr init`
Creates a project in the current directory. The project name is taken from the directory name. The directory must be empty.
### `dcr setup`
Initializes the registry system by reading `~/.dcr/config.toml` and verifying the availability of configured indices.
### `dcr add <name> <source>`
Adds a dependency to `dcr.toml`.
If a registry is configured, `dcr` automatically finds the package.
Git/Path syntax: `path:`, `github:`, `gitlab:`, `git:`, or a full URL.
Use `--branch`, `--tag`, or `--rev` for Git dependencies.
### `dcr build [profile]`
Builds the project. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
### `dcr run [profile]`
Builds the project and runs the binary. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
Run manually:
`./target/<profile>/<name>`
### `dcr clean`
Removes the `target` directory in the project root.
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
Two profiles are supported:
@@ -120,14 +149,49 @@ Two profiles are supported:
- `--release` - built-in flags per compiler
Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`.
Profile-specific flags can be added via `[build.debug]` and `[build.release]`.
You can also set `build.target` to override the output directory (profile-independent).
You can set `build.platform` to pass `-march=<platform>` (GCC/Clang) or `/arch:*` (MSVC) where supported.
Use `build.kind = "staticlib"` to build a static library instead of a binary.
Use `build.kind = "sharedlib"` to build a shared library (`.so`/`.dylib`/`.dll`).
`dcr run` is only for `build.kind = "bin"` and will fail for libraries.
Use `build.kind = "efi"` to build a UEFI PE32+ executable (`.efi`).
Use `build.kind = "elf"` to build a bare-metal ELF executable.
`dcr run` is only for `build.kind = "bin"` and will fail for libraries, EFI, and ELF.
Use `build.exclude`/`build.include` to control source/header collection.
Use `build.roots` and `build.src_disable` to replace the default `src/` root.
Use `build.steps` and `build.post_steps` to run custom commands.
Use `build.clean` for extra cleanup paths and `[run].cmd` to override run command.
## 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
The main project file is `dcr.toml`.
For multi-registry management, use `~/.dcr/config.toml`.
Example `dcr.toml`:
@@ -135,16 +199,22 @@ Example `dcr.toml`:
[package]
name = "hello"
version = "0.1.0"
type = "app" # or "lib", "none"
[build]
language = "c"
standard = "c11"
compiler = "clang"
kind = "bin" # "bin", "staticlib", "sharedlib", "efi", "elf"
# Optional platform hint
# platform = "x86_64"
# Optional custom flags
cflags = ["-Wall", "-Wextra"]
ldflags = ["-lm"]
#
# Optional include/exclude:
# exclude = ["src/vendor", "src/legacy/**"]
# include = ["src/boot/arch/**"]
[toolchain]
# Optional tool overrides:
@@ -153,13 +223,27 @@ ldflags = ["-lm"]
# as = "as"
# ar = "ar"
# ld = "ld"
# uic = "uic"
# moc = "moc"
# rcc = "rcc"
[dependencies]
my_lib = "path:./libs/my_lib"
cool_lib = { git = "https://github.com/user/cool_lib", tag = "v1.0" }
```
Path dependencies are supported. DCR will resolve them on build and generate `dcr.lock`.
Workspace example:
...
[workspace]
kernel = { path = "src/kernel", deps = ["core", "userspace"] }
core = { path = "src/core", deps = ["userspace"] }
userspace = { path = "src/userspace" }
```
Git and Path dependencies are supported. DCR will resolve them on build and generate `dcr.lock`.
Incremental build note: object files are rebuilt when source `.c/.cpp` or included header files change.
Incremental build note: object files are rebuilt when source `.c/.cpp` files change. Header dependency tracking is not implemented yet.
## Requirements
- Rust toolchain (`rustc`, `cargo`) - for building DCR from source

72
TODO.md
View File

@@ -1,7 +1,67 @@
# TODO
# DCR TODO.md
- Add header dependency tracking for incremental builds (GCC/Clang: -MMD, MSVC: /showIncludes)
- Add external package managers (Conan, vcpkg)
- Multi-target
- Linker script + direct linker control
- Custom targets/steps
Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers.
## 🔥 High Priority (Next Releases)
- [x] **Git dependencies support**
`git = "https://github.com/user/repo.git#branch"` and `git = "..."@tag`
- [ ] **Conan and vcpkg integration**
Automatic download, installation and linking of packages
- [ ] **Full freestanding / embedded support**
`-nostdlib`, linker scripts, bare-metal targets (i686-elf, aarch64-none-elf, etc.)
- [ ] **EFI support**
Dedicated platform configuration for EFI targets
- [ ] **Custom project name**
Override binary name in `dcr.toml`
- [ ] **Custom build steps / targets**
Ability to define custom pre-build, post-build, codegen steps in `dcr.toml`
- [ ] **Improved mixed C/C++/ASM projects**
Better handling of `.S` and `.asm` files with correct flags
- [ ] **dcr publish + official package registry**
Publish and fetch your own libraries easily
## ✅ Medium Priority
- [ ] **Static analysis** (`dcr check`)
Integration with clang-tidy, cppcheck, include-what-you-use
- [ ] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
- [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.)
- [ ] **Better cross-compilation experience**
Ready-to-use toolchains + automatic download
- [ ] **Linker script support** + direct linker control
- [ ] **Workspaces improvements**
Per-member include directories, selective builds, better dependency graph
- [ ] **Build-time code generation**
Support for protobuf, flatbuffers, Qt moc, custom generators
## 📌 Low Priority / Nice to Have
- [ ] Support for Meson and Ninja as alternative backends
- [ ] Dockerfile generation (`dcr docker init`)
- [ ] First-class Windows MSVC support (currently works but rough)
- [ ] Documentation generation (`dcr doc`)
- [ ] Sanitizers integration (`dcr sanitize`)
- [ ] Benchmarks and comparison with CMake/Meson
- [ ] C++20 modules support (when it becomes stable)
## 🧠 Future Ideas
- Official DCR package registry (dcrhub)
- GUI wrapper (dcr-gui)
- Plugin system
- Toolchain manager (like rustup)
- Zig as alternative compiler
- deb/rpm/flatpak/AppImage packaging support
---
**Currently in progress:**
- ...
**Completed in recent releases:**
- Workspaces
- Cross-compilation
- IDE config generation (VS Code, CLion, compile_commands.json)
- Self-update (`dcr --update`)
- Header tracking

View File

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

View File

@@ -21,9 +21,13 @@
* [Global options](commands/global-options.md)
* [dcr init/new](commands/dcr-init-or-new.md)
* [dcr add](commands/dcr-add.md)
* [dcr build](commands/dcr-build.md)
* [dcr run](commands/dcr-run.md)
* [dcr clean](commands/dcr-clean.md)
* [dcr gen](commands/dcr-gen.md)
* [dcr test](commands/dcr-test.md)
* [dcr tree](commands/dcr-tree.md)
## Dependencies & Build
@@ -31,11 +35,15 @@
* [Local dependencies](dependencies-and-build/local-dependencies.md)
* [Versioning & lock file](dependencies-and-build/versioning-and-lock-file.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)
## Reference & Help
* [CLI reference](reference-and-help/cli-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)
* [Contributing & Bug reports](reference-and-help/contributing-and-bug-reports.md)

37
docs/commands/dcr-add.md Normal file
View File

@@ -0,0 +1,37 @@
# dcr add
Add a dependency to `dcr.toml`.
## Usage
```bash
dcr add <name> [source] [options]
```
If `<source>` is omitted and a registry is configured, DCR will look up the
package by name in the registry and add it as a version dependency.
## Sources
- *omitted*: Look up in the configured registry.
- `path:./path/to/lib`: Local directory.
- `github:user/repo`: GitHub repository.
- `gitlab:user/repo`: GitLab repository.
- `git:host.com/user/repo`: Generic Git repository (HTTPS).
- `https://...`: Full Git URL.
## Options
- `--branch <name>`: Use a specific branch.
- `--tag <name>`: Use a specific tag.
- `--rev <sha>`: Use a specific commit hash.
## Examples
```bash
dcr add my_lib path:./libs/my_lib
dcr add cool_lib github:user/cool_lib
dcr add shared_lib gitlab:org/shared --branch develop
dcr add custom_lib git:git.internal.com/r/lib --tag v1.0.0
dcr add mylib # Looks up in registry
```

View File

@@ -8,6 +8,9 @@ Builds the project in `debug` or `release` profile.
dcr build
dcr build --debug
dcr build --release
dcr build --target linux --release
dcr build --release --force
dcr build --debug --clean
```
## What `build` does
@@ -22,6 +25,8 @@ dcr build --release
## Config values used
- `package.name`
- `build.filename`
- `build.extension`
- `build.compiler`
- `build.language`
- `build.standard`
@@ -30,16 +35,42 @@ dcr build --release
- `build.platform`
- `build.cflags`
- `build.ldflags`
- `build.ldscript`
- `build.debug` / `build.release`
- `build.exclude`
- `build.include`
- `build.pkg_config`
- `build.steps`
- `build.post_steps`
- `build.generated`
- `build.expect`
- `build.clean`
## Source selection
- `language = "c"` -> `*.c`
- `language = "c++" | "cpp" | "cxx"` -> `*.cpp`, `*.cxx`, `*.cc`
- `language = "asm"` -> `*.s`, `*.S`, `*.asm`
- Mixed languages are supported with arrays, for example `language = ["c", "asm"]`.
- By default sources are searched in `src/`; use `build.roots` and `build.src_disable` to override.
## Notes
- 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.
- Incremental rebuild is based on source/object mtime comparison.
- `--target <triple>` builds for specified target (short names: `linux`, `macos`, `windows`).
- `--verbose` prints the compiler/linker command lines to stderr (also works via `DCR_DEBUG` env var).
- 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.
- `--clean` removes `target/<profile>` and `build.clean` paths before building.
- `build.filename` + `build.extension` allow full control over the final artifact name (e.g. `KERNEL.BIN` instead of using `package.name`). When `build.kind = "efi"`, the output is a PE32+ executable with `.efi` extension.
- `build.kind = "efi"` links with `-shared -nostdlib -Wl,-dll -Wl,--subsystem,10` for UEFI PE32+ format. Use `build.filename` and `build.extension` for full control over the artifact name.
- `build.kind = "elf"` produces a bare-metal ELF executable. Linking flags are user-controlled via `build.ldflags` (no PIE/CRT defaults are added).
- Default GCC/Clang profile flags: `debug` -> `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`, `release` -> `-O3 -DNDEBUG`.
These defaults are **not applied** for bare-metal targets (e.g. `aarch64-none-elf`, targets containing `none` / `-elf` / `eabi`).
- 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.
- `build.exclude` removes paths from source/header collection; `build.include` re-allows matching paths and has priority over `exclude`.
- `build.steps` run before compilation; `build.post_steps` run after linking.
- `build.generated` is cleaned when `build.steps` need to rerun; `build.expect` is verified after post-steps.

View File

@@ -8,16 +8,20 @@ Removes build artifacts in `target/`.
dcr clean
dcr clean --debug
dcr clean --release
dcr clean --all
dcr clean --release --all
```
## Behavior
- Without arguments: removes entire `target/`.
- With profile: removes only `target/<profile>/`.
- With profile: removes only the profile-specific directory. Respects `build.target` from config: if `build.target = "aarch64-unknown-linux-gnu"`, cleans `target/aarch64-unknown-linux-gnu/<profile>/`.
- With `--all`: also cleans all workspace members.
- Additionally removes paths from `build.clean` if configured.
- `build.clean` supports `{profile}` and version placeholders.
## Validation
- `dcr.toml` must exist.
- More than one argument is treated as error.
- Unknown profile is treated as error.
- Unknown flags are treated as error.
- If `target` or selected profile directory does not exist, command prints a warning.

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

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

View File

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

45
docs/commands/dcr-test.md Normal file
View File

@@ -0,0 +1,45 @@
# dcr test
Runs the project's test suite.
## Usage
```sh
dcr test
dcr test --debug
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 debug mode by default. Use `--release` for release mode.
- Custom test logic can be added by editing `tests/test.c`.
- Fails if no `tests/test.c` exists (run `dcr test --init` first).

22
docs/commands/dcr-tree.md Normal file
View File

@@ -0,0 +1,22 @@
# dcr tree
Displays a visual dependency tree for the current project (similar to `cargo tree`).
## Usage
```sh
dcr tree
dcr tree --invert
dcr tree -p some-package
```
## What it shows
- All direct and transitive dependencies
- Version of each dependency
- Whether a dependency is a registry, git, or path dependency
## Notes
- Useful for debugging dependency issues and understanding the build graph.
- Output format is inspired by `cargo tree`.

View File

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

View File

@@ -8,23 +8,130 @@ language = "c"
standard = "c11"
compiler = "clang"
kind = "bin"
# filename = "KERNEL"
# extension = "BIN"
# optional:
# inherit = true # Inherit from base (default true)
# targets = ["linux", "macos"] # Multiple targets to build
# target = "./dist"
# platform = "x86_64"
# cflags = ["-Wall"]
# ldflags = ["-lm"]
# exclude = ["src/vendor", "src/legacy/**"]
# include = ["src/legacy/boot/**"]
# roots = ["kernel", "drivers"]
# src_disable = true
# pkg_config = ["Qt6Core", "Qt6Widgets"]
# generated = ["src/ui/*.h"]
# expect = ["./target/{profile}/boot/*.bin"]
# clean = ["./target/{profile}/boot", "/tmp/image/*.img"]
```
## Fields
- `language` (string, required): `c`, `c++`, `cpp`, `cxx`, or `asm`.
- `language` (string or array, required): `c`, `c++`, `cpp`, `cxx`, or `asm`.
Mixed languages can be specified as an array, for example `["c", "c++", "asm"]`.
- `standard` (string, required): language standard passed to compiler.
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
- `kind` (string, required): `bin`, `staticlib`, or `sharedlib`.
- `kind` (string, required): `bin`, `staticlib`, `sharedlib`, `efi`, or `elf`.
- `filename` (string, optional): custom output filename (without extension). Overrides `package.name` for the final artifact.
- `extension` (string, optional): custom file extension without the leading dot (e.g. `BIN`, `efi`, `EFI`). Combined with `filename`.
- `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.
When set, DCR automatically injects `--target=<value>` into `cflags` (useful for clang cross-compilation).
- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`).
- `cflags` (string array, optional): extra compile flags.
- `ldflags` (string array, optional): extra link flags.
- `ldscript` (string, optional): linker script path. Passed as `-T <path>` to the linker. Useful for bare-metal/embedded/freestanding targets.
- `exclude` (string array, optional): paths or glob patterns to skip during source/header collection.
- `include` (string array, optional): allowlist paths or globs that override `exclude`.
- `include` directory entries (non-glob) are also passed to the compiler as `-I` include paths.
- `roots` (string array, optional): additional source roots to scan instead of `src/`.
- `src_disable` (bool, optional): disables default `src/` root when true.
- `pkg_config` (string array, optional): packages used to append `pkg-config --cflags/--libs`.
- `generated` (string array, optional): generated files to delete when build steps rerun.
- `expect` (string array, optional): artifacts that must exist after build/post-steps.
- `clean` (string array, optional): additional paths removed by `dcr clean` and `--clean`.
Notes for `exclude`/`include`:
- Supports `*` and `**` glob patterns.
- If `include` is set, it has priority over `exclude`.
- Use `include` to re-allow nested paths, for example exclude `src/boot` but allow `src/boot/arch/**`.
When using glob patterns in `exclude`, DCR converts them internally, so `exclude` globs still work as expected.
### Build steps
Build steps run shell commands before compilation. Post steps run after linking.
```toml
[[build.steps]]
name = "uic"
in = "src/ui/*.ui"
out = "src/ui/{stem}.h"
cmd = "{uic} {in} -o {out}"
[[build.post_steps]]
name = "image"
in = "target/{profile}/boot/*.bin"
out = "/tmp/image/os-{version}.img"
cmd = "dd if=/dev/zero of={out} bs=1M count=8"
```
Step notes:
- `in` supports glob patterns.
- If `in` expands to multiple files, `out` must include `{stem}`.
- Steps run only when inputs are newer than outputs.
- Variables: `{in}`, `{out}`, `{stem}`, `{cflags}`, `{uic}`, `{moc}`, `{rcc}`, `{profile}`,
`{version}`, `{version_major}`, `{version_minor}`, `{version_patch}`, `{version_suffix}`, `{version_suffix_dash}`.
`build.clean` supports `{profile}` and version placeholders (`{version}`, `{version_major}`, ...).
### Profile overrides
Profile tables inherit all values from `[build]`. Any field present in the profile table replaces the base value.
Array fields (like `cflags`/`ldflags`) are appended to the base array.
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
[build.debug]
cflags = ["-g3"]
[build.release]
cflags = ["-O3"]
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
@@ -35,10 +142,13 @@ cxx = "clang++"
as = "as"
ar = "ar"
ld = "ld"
uic = "uic"
moc = "moc"
rcc = "rcc"
```
## Behavior notes
- Compiler backend is selected by compiler string (`gcc`, `clang`, `cl`, `clang-cl`).
- Empty or unknown compiler value falls back to clang-like build path.
- `run` is not allowed for library kinds (`staticlib`, `sharedlib`).
- `run` is not allowed for library kinds (`staticlib`, `sharedlib`), `efi`, or `elf`.

View File

@@ -26,6 +26,10 @@ kind = "bin"
- `[build]`
- `[dependencies]`
Workspace-only 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
- `package.name`
@@ -39,7 +43,29 @@ kind = "bin"
## Optional keys
- `build.inherit`
- `build.targets`
- `build.target`
- `build.platform`
- `build.cflags`
- `build.ldflags`
- `build.exclude`
- `build.include`
- `build.roots`
- `build.src_disable`
- `build.pkg_config`
- `build.steps`
- `build.post_steps`
- `build.generated`
- `build.expect`
- `build.clean`
- `build.debug`
- `build.release`
- `build.<target>` (target-specific overrides)
- dependency fields (`include`, `lib`, `libs`, `system`)
- `[toolchain]` overrides (`cc`, `cxx`, `as`, `ar`, `ld`, `uic`, `moc`, `rcc`)
- `[toolchain.<target>]` (target-specific toolchain)
- `[workspace]` members
- `[workspace.<target>]` (target-specific workspace)
- `[run]` overrides (`cmd`)
- `[run.<target>]` (target-specific run)

View File

@@ -1,23 +1,36 @@
# Dependencies section
DCR currently supports path dependencies.
DCR supports registry-based dependencies and path dependencies.
## Example
```toml
[dependencies]
# Registry dependency
mylib = "0.1.0"
# Git dependency
json = { git = "https://github.com/nlohmann/json", tag = "v3.11.2" }
# Path dependency
frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"] }
```
## Supported fields per dependency
## Supported dependency types
### Registry dependencies
Defined by name and version string. DCR automatically resolves the package from the configured registry index.
### Git dependencies
Defined by repository URL and optionally branch, tag, or revision.
- `git` (string, required): repository URL.
- `branch`, `tag`, `rev` (string, optional): Git reference.
### Path dependencies
- `path` (string, required): absolute path or path relative to project root.
- `include` (string array, optional): include directories inside dependency.
- `lib` (string array, optional): library directories inside dependency.
- `libs` (string array, optional): library names for linker.
- `system` (bool, optional): currently `system = true` is rejected.
## Defaults
## Defaults for path dependencies
- If `include` is omitted, DCR tries `<dep>/include`.
- If `lib` is omitted, DCR tries `<dep>/lib`, then `<dep>/lib64`.
@@ -25,6 +38,7 @@ frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"
## Validation
- Dependency value must be a TOML table.
- `path` is mandatory.
- Dependency value must be a string (registry) or a TOML table (path).
- Registry dependencies use the version string to match versions in the registry.
- `path` is mandatory for path dependencies.
- `include`/`lib`/`libs` must be arrays of strings if provided.

View File

@@ -6,15 +6,18 @@
[package]
name = "my-app"
version = "0.1.0"
type = "app"
```
## Fields
- `name` (string, required): used as output artifact base name.
- `version` (string, required): used in `dcr.lock` project entry.
- `type` (string, optional): project type (`app`, `lib`, `none`).
## Notes
- Empty `name` or `version` makes config invalid.
- `dcr new` sets `name` from the passed project name.
- `dcr init` sets `name` from current directory name.
- If `type = "lib"`, DCR generates `include/` and `lib/` directories in the target path, copying public headers and built libraries.

View File

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

View File

@@ -0,0 +1,203 @@
# 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
```
**Note:** If you set `target = "aarch64-linux-gnu"` (or any value) directly under `[build]`, DCR will automatically add `--target=...` to cflags when using clang. You usually don't need to repeat it manually.
### 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

@@ -2,25 +2,39 @@
Dependencies are defined in `dcr.toml` under `[dependencies]`.
## Full example
## Full example (Path)
```toml
[dependencies]
fmt = { path = "./third_party/fmt", include = ["include"], lib = ["lib"], libs = ["fmt"] }
```
## Full example (Git)
```toml
[dependencies]
json = { git = "https://github.com/nlohmann/json", tag = "v3.11.2" }
```
## Field reference
- `path`: required dependency root path.
- `path`: local path to dependency root.
- `git`: URL of Git repository.
- `branch`: Git branch name.
- `tag`: Git tag name.
- `rev`: Git commit SHA.
- `include`: optional include subpaths (absolute or relative to dep root).
- `lib`: optional library subpaths (absolute or relative to dep root).
- `libs`: optional linker library names.
## Common valid compact form
## Common valid compact forms
```toml
[dependencies]
mylib = { path = "./libs/mylib" }
gitlib = { git = "https://github.com/user/repo" }
# Shortest form for path
simple = "./libs/simple"
```
In this form DCR will try defaults for include/lib dirs and use `mylib` as the linker library name.
In these forms DCR will try defaults for include/lib dirs and use the dependency name as the linker library name.

View File

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

View File

@@ -1,6 +1,6 @@
# 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`:
@@ -20,4 +20,4 @@ target = "./dist"
## 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,12 +8,15 @@ Debug (default):
dcr build
# or
dcr build --debug
dcr build --target linux --debug
dcr build --debug --clean
```
Release:
```sh
dcr build --release
dcr build --target windows --release
```
## Run
@@ -21,7 +24,10 @@ dcr build --release
```sh
dcr run
# or
dcr run --target macos
dcr run --release
dcr run --target linux --release
dcr run --release --force
```
`dcr run` first builds, then launches the produced binary.
@@ -37,10 +43,15 @@ Profile-only cleanup:
```sh
dcr clean --debug
dcr clean --release
dcr clean --all
dcr clean --release --all
```
## Notes
- `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`.
- Build profile flag is parsed from the first command argument.
- `run` is unavailable for `build.kind = "staticlib"`, `build.kind = "sharedlib"`, `build.kind = "efi"`, and `build.kind = "elf"`.
- Build profile flag (`--debug` / `--release`) can be passed in any argument position.
- Unknown profile flags produce an error.
- In workspace root, `clean --all` cleans all member projects.
- `--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.lock`: generated lock file for resolved path dependencies.
- `src/`: recursively scanned source directory.
- `target/<profile>/obj/`: cached object files for incremental rebuild.
- `target/<profile>/deps/`: synchronized copies of path dependencies.
- `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/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`.
- `target/<target>/<profile>/obj/`: cached object files for incremental rebuild (where `<target>` is `native` or target triple).
- `target/<target>/<profile>/deps/`: synchronized copies of path dependencies.
- `target/<target>/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`.
- `target/<target>/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`.
- `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

@@ -5,9 +5,14 @@
```text
dcr new <name>
dcr init
dcr build [--debug|--release]
dcr setup
dcr add <name> <source>(optional)
dcr build [--debug|--release] [--target <triple>]
dcr run [--debug|--release]
dcr clean [--debug|--release]
dcr clean [--debug|--release] [--all]
dcr tree
dcr test
dcr gen <subcommand>
dcr --help
dcr --version
dcr --update
@@ -15,12 +20,15 @@ dcr --update
## Notes on argument parsing
- Most commands parse profile from the first argument.
- `build`/`run` parse flags in any order (`--debug|--release`, `--force`, `--clean`, `--verbose`) and reject duplicates.
- `new` requires exactly one argument.
- `init` and `--update` do not accept extra arguments.
- `clean` accepts at most one argument.
- `clean` accepts `--debug|--release` and optional `--all`.
- `gen` supports `project-info`, `compile-commands`, `vscode`, `clion`.
- `tree` shows a dependency tree (similar to `cargo tree`).
- `test` / `tests` runs the project's tests.
## Exit behavior overview
- Successful command execution returns `0`.
- Validation/build/runtime errors return non-zero status.
- Validation/build/runtime failures return non-zero status.

View File

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

View File

@@ -2,7 +2,7 @@
## 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?
@@ -21,7 +21,8 @@ Yes. Set `build.compiler` in `dcr.toml` (for example `gcc`, `clang`, `cl`, `clan
## 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?

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

@@ -1,15 +1,16 @@
$ErrorActionPreference = 'Stop'
$RepoUrl = 'https://github.com/dexoron/dcr'
$ApiUrl = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
$RepoUrl = 'https://github.com/dexoron/dcr'
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
$InstallPath = Join-Path $env:LOCALAPPDATA 'dcr'
$BinPath = Join-Path $env:USERPROFILE '.local\bin'
$BinaryPath = Join-Path $InstallPath 'dcr.exe'
$BinPath = Join-Path $env:USERPROFILE '.local\bin'
$BinaryPath = Join-Path $InstallPath 'dcr.exe'
function Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Fail($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Fail($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Require-Cmd($name) {
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
@@ -17,6 +18,20 @@ function Require-Cmd($name) {
}
}
function Select-Channel {
Write-Host "Choose channel:"
Write-Host " 1) Latest stable release (default)"
Write-Host " 2) Latest dev (pre-release)"
$choice = Read-Host "Enter 1 or 2 [1]"
if ([string]::IsNullOrWhiteSpace($choice)) { $choice = '1' }
switch ($choice) {
'1' { return 'stable' }
'2' { return 'dev' }
default { Fail 'Unknown option' }
}
}
function Select-Mode {
Write-Host "Choose installation mode:"
Write-Host " 1) Download prebuilt binary from GitHub Release (recommended)"
@@ -26,7 +41,7 @@ function Select-Mode {
switch ($choice) {
'1' { return 'release' }
'2' { return 'build' }
'2' { return 'build' }
default { Fail 'Unknown option' }
}
}
@@ -38,18 +53,35 @@ function Get-Target {
return 'x86_64-pc-windows-msvc'
}
function Fetch-ReleaseJson($channel) {
if ($channel -eq 'dev') {
Info "Looking for latest dev (pre-release)..."
$releases = Invoke-RestMethod -Method Get -Uri $ApiAll
$pre = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1
if (-not $pre) { Fail "No dev (pre-release) found on GitHub" }
return $pre
} else {
return Invoke-RestMethod -Method Get -Uri $ApiLatest
}
}
function Install-FromRelease {
param([string]$target)
param([string]$target, [string]$channel)
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
$assetName = "dcr-$target.exe"
Info "Fetching latest release..."
$release = Invoke-RestMethod -Method Get -Uri $ApiUrl
$release = Fetch-ReleaseJson $channel
$tag = $release.tag_name
$version = $tag.TrimStart('v')
# Имя бинарника: dcr-<triple>-<version>.exe
$assetName = "dcr-$target-$version.exe"
Info "Fetching release $tag (channel: $channel)..."
$asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
if (-not $asset) {
Fail "Asset $assetName not found in release $($release.tag_name)"
Fail "Asset $assetName not found in release $tag"
}
Info "Downloading $assetName..."
@@ -58,7 +90,7 @@ function Install-FromRelease {
}
function Install-FromSource {
param([string]$target)
param([string]$target, [string]$channel)
Require-Cmd 'git'
Require-Cmd 'cargo'
@@ -67,7 +99,15 @@ function Install-FromSource {
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
Info 'Cloning repository...'
git clone --depth 1 $RepoUrl $tmp | Out-Null
if ($channel -eq 'dev') {
# Пробуем ветку dev
$result = git clone --depth 1 --branch dev $RepoUrl $tmp 2>&1
if ($LASTEXITCODE -ne 0) {
git clone --depth 1 $RepoUrl $tmp | Out-Null
}
} else {
git clone --depth 1 $RepoUrl $tmp | Out-Null
}
Info 'Building release binary...'
Push-Location $tmp
@@ -100,14 +140,15 @@ function Setup-Path {
}
Info 'Starting DCR installation'
$mode = Select-Mode
$target = Get-Target
$channel = Select-Channel
$mode = Select-Mode
$target = Get-Target
if ($mode -eq 'build') {
Install-FromSource -target $target
Install-FromSource -target $target -channel $channel
} else {
Install-FromRelease -target $target
Install-FromRelease -target $target -channel $channel
}
Setup-Path
Ok 'Installation completed successfully'
Ok 'Installation completed successfully'

View File

@@ -6,7 +6,8 @@ INSTALL_PATH="$HOME/.local/share/dcr"
BINPATH="$HOME/.local/bin"
LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr"
GITHUB_API="https://api.github.com/repos/dexoron/dcr/releases/latest"
GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
GITHUB_API_ALL="https://api.github.com/repos/dexoron/dcr/releases"
RED='\033[0;31m'
GREEN='\033[0;32m'
@@ -17,13 +18,16 @@ NC='\033[0m'
mkdir -p "$(dirname "$LOGFILE")"
exec > >(tee -a "$LOGFILE") 2>&1
log() { echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"; }
log() { echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"; }
success() { echo -e "${GREEN}✔ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
error() { echo -e "${RED}✖ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
error() { echo -e "${RED}✖ $1${NC}"; }
trap 'error "Error on line $LINENO"; exit 1' ERR
INSTALL_MODE=""
CHANNEL=""
check_os() {
case "$(uname -s)" in
Linux|Darwin) ;;
@@ -37,13 +41,11 @@ detect_target() {
arch="$(uname -m)"
case "$os:$arch" in
Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;;
Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;;
Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;;
Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;;
*) error "Unsupported platform: $os/$arch"; exit 1 ;;
esac
ASSET_NAME="dcr-${TARGET_TRIPLE}"
}
check_common_dependencies() {
@@ -51,14 +53,106 @@ check_common_dependencies() {
}
check_build_dependencies() {
command -v git >/dev/null 2>&1 || { error "git is not installed"; exit 1; }
command -v git >/dev/null 2>&1 || { error "git is not installed"; exit 1; }
command -v cargo >/dev/null 2>&1 || { error "cargo is not installed"; exit 1; }
}
select_channel() {
echo "Choose channel:"
echo " 1) Latest stable release (default)"
echo " 2) Latest dev (pre-release)"
read -r -p "Enter 1 or 2 [1]: " choice
case "${choice:-1}" in
1) CHANNEL="stable" ;;
2) CHANNEL="dev" ;;
*) error "Unknown option"; exit 1 ;;
esac
}
select_install_mode() {
echo "Choose installation mode:"
echo " 1) Download prebuilt binary from GitHub Release (recommended)"
echo " 2) Build from git"
read -r -p "Enter 1 or 2 [1]: " choice
case "${choice:-1}" in
1) INSTALL_MODE="release" ;;
2) INSTALL_MODE="build" ;;
*) error "Unknown option"; exit 1 ;;
esac
}
# Возвращает JSON нужного релиза в stdout
fetch_release_json() {
if [[ "$CHANNEL" == "dev" ]]; then
log "Looking for latest dev (pre-release)..."
local json
json="$(curl -fsSL "$GITHUB_API_ALL")"
# Первый pre-release в списке
local result
result="$(printf '%s' "$json" | python3 - <<'EOF'
import sys, json
releases = json.load(sys.stdin)
pre = [r for r in releases if r.get("prerelease", False)]
if not pre:
print("{}", end="")
else:
print(json.dumps(pre[0]), end="")
EOF
)"
if [[ -z "$result" || "$result" == "{}" ]]; then
error "No dev (pre-release) found on GitHub"
exit 1
fi
printf '%s' "$result"
else
curl -fsSL "$GITHUB_API_LATEST"
fi
}
download_binary() {
local release_json tag version asset_name download_url
release_json="$(fetch_release_json)"
tag="$(printf '%s\n' "$release_json" | \
sed -n 's/.*"tag_name": "\([^"]*\)".*/\1/p' | head -n1)"
if [[ -z "$tag" ]]; then
error "Failed to determine release version"
exit 1
fi
# Версия без ведущего 'v'
version="${tag#v}"
# Имя бинарника: dcr-<triple>-<version>
asset_name="dcr-${TARGET_TRIPLE}-${version}"
log "Fetching release ${tag} (channel: ${CHANNEL})..."
download_url="$(printf '%s\n' "$release_json" | \
sed -n "s#.*\"browser_download_url\": \"\([^\"]*/${asset_name}\)\".*#\1#p" | head -n1)"
if [[ -z "$download_url" ]]; then
error "Asset ${asset_name} not found in release ${tag}"
exit 1
fi
mkdir -p "$INSTALL_PATH"
curl -fL "$download_url" -o "$INSTALL_PATH/dcr"
chmod +x "$INSTALL_PATH/dcr"
success "Binary ${asset_name} downloaded (${tag})"
}
prepare_sources() {
log "Fetching sources..."
rm -rf "$TMPDIR"
git clone --depth 1 "$REPO_URL" "$TMPDIR"
if [[ "$CHANNEL" == "dev" ]]; then
# Клонируем dev ветку, если она есть
git clone --depth 1 --branch dev "$REPO_URL" "$TMPDIR" 2>/dev/null || \
git clone --depth 1 "$REPO_URL" "$TMPDIR"
fi
success "Sources fetched"
}
@@ -68,34 +162,6 @@ build_binary() {
success "Build completed"
}
fetch_latest_release_json() {
curl -fsSL "$GITHUB_API"
}
download_binary() {
log "Fetching latest release..."
local release_json download_url tag
release_json="$(fetch_latest_release_json)"
tag="$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name": "\([^"]*\)".*/\1/p' | head -n1)"
if [[ -z "$tag" ]]; then
error "Failed to determine release version"
exit 1
fi
download_url="$(printf '%s\n' "$release_json" | sed -n "s#.*\"browser_download_url\": \"\([^\"]*${ASSET_NAME}\)\".*#\1#p" | head -n1)"
if [[ -z "$download_url" ]]; then
error "Asset ${ASSET_NAME} not found in release ${tag}"
exit 1
fi
mkdir -p "$INSTALL_PATH"
curl -fL "$download_url" -o "$INSTALL_PATH/dcr"
chmod +x "$INSTALL_PATH/dcr"
success "Binary ${ASSET_NAME} downloaded (${tag})"
}
install_built_binary() {
mkdir -p "$INSTALL_PATH"
cp "$TMPDIR/target/release/dcr" "$INSTALL_PATH/dcr"
@@ -122,25 +188,13 @@ cleanup() {
rm -rf "$TMPDIR" 2>/dev/null || true
}
select_install_mode() {
echo "Choose installation mode:"
echo " 1) Download prebuilt binary from GitHub Release (recommended)"
echo " 2) Build from git"
read -r -p "Enter 1 or 2 [1]: " choice
case "${choice:-1}" in
1) INSTALL_MODE="release" ;;
2) INSTALL_MODE="build" ;;
*) error "Unknown option"; exit 1 ;;
esac
}
main() {
log "Starting DCR installation"
check_os
detect_target
check_common_dependencies
select_channel
select_install_mode
cleanup
@@ -161,4 +215,4 @@ main() {
log "Log saved to $LOGFILE"
}
main "$@"
main "$@"

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

236
src/cli/add.rs Normal file
View File

@@ -0,0 +1,236 @@
use crate::core::config::Config;
use crate::core::deps::register;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use toml::Value;
use toml::map::Map;
pub struct AddArgs {
pub name: String,
pub path: Option<String>,
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
pub version_from_registry: Option<String>,
}
pub fn add(args: &[String]) -> i32 {
let add_args = match parse_add_args(args) {
Ok(a) => a,
Err(code) => return code,
};
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;
}
};
let mut config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(cfg) => cfg,
Err(err) => {
error(&err.to_string());
return 1;
}
};
let dep_value = if let Some(git_url) = add_args.git {
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
let mut table = Map::new();
table.insert("git".to_string(), Value::String(git_url));
Value::Table(table)
} else {
let mut table = Map::new();
table.insert("git".to_string(), Value::String(git_url));
if let Some(b) = add_args.branch {
table.insert("branch".to_string(), Value::String(b));
}
if let Some(t) = add_args.tag {
table.insert("tag".to_string(), Value::String(t));
}
if let Some(r) = add_args.rev {
table.insert("rev".to_string(), Value::String(r));
}
Value::Table(table)
}
} else if let Some(path) = add_args.path {
let mut table = Map::new();
table.insert("path".to_string(), Value::String(path));
Value::Table(table)
} else if let Some(version) = add_args.version_from_registry {
Value::String(version)
} else {
error("Dependency source (path or git) must be provided");
return 1;
};
let key = format!("dependencies.{}", add_args.name);
if let Err(err) = config.edit(&key, dep_value) {
error(&format!("Failed to update dcr.toml: {}", err));
return 1;
}
println!(
" {} dependency `{}` to dcr.toml",
colored("Added", BOLD_GREEN),
add_args.name
);
0
}
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:");
error(" user/repo -> github.com/user/repo");
error(" github:user/repo -> github.com/user/repo");
error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo");
error(" path:./path/to/lib -> local path");
return Err(1);
}
let mut name = String::new();
let source_spec: String;
let mut branch = None;
let mut tag = None;
let mut rev = None;
let mut iter = args.iter();
if let Some(n) = iter.next() {
if n.starts_with("--") {
error("First argument must be the dependency name");
return Err(1);
}
name = n.clone();
}
if let Some(s) = iter.next() {
if s.starts_with("--") {
error("Second argument must be the dependency source");
return Err(1);
}
source_spec = s.clone();
} else {
// No source provided — try registry auto-lookup
match register::resolve_package_from_registry(&name) {
Ok(info) => {
let version = info
.get("latest_version")
.or_else(|| info.get("version"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
(
"Registry entry for `".to_string() + &name + "` has no version field",
1,
)
})
.map_err(|(msg, code)| {
error(&msg);
code
})?;
return Ok(AddArgs {
name,
path: None,
git: None,
branch: None,
tag: None,
rev: None,
version_from_registry: Some(version.to_string()),
});
}
Err(e) => {
error(&format!("Cannot add `{}` without source: {}", name, e));
return Err(1);
}
}
}
while let Some(arg) = iter.next() {
match arg.as_str() {
"--branch" => {
branch = iter.next().cloned();
if branch.is_none() {
error("--branch requires a value");
return Err(1);
}
}
"--tag" => {
tag = iter.next().cloned();
if tag.is_none() {
error("--tag requires a value");
return Err(1);
}
}
"--rev" => {
rev = iter.next().cloned();
if rev.is_none() {
error("--rev requires a value");
return Err(1);
}
}
_ => {
error(&format!("Unknown argument: {}", arg));
return Err(1);
}
}
}
let mut path = None;
let mut git = None;
if let Some(p) = source_spec.strip_prefix("path:") {
path = Some(p.to_string());
} else if let Some(g) = source_spec.strip_prefix("github:") {
git = Some(format!("https://github.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("gitlab:") {
git = Some(format!("https://gitlab.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("git:") {
if g.contains('/') && !g.contains('.') {
// git:user/repo -> default to github
git = Some(format!("https://github.com/{}", g));
} else {
// git:host.com/user/repo
let url = if g.starts_with("http") || g.starts_with("git@") {
g.to_string()
} else {
format!("https://{}", g)
};
git = Some(url);
}
} else if source_spec.starts_with("http://")
|| source_spec.starts_with("https://")
|| source_spec.starts_with("git@")
{
git = Some(source_spec);
} else {
error("Source must have a prefix (path:, git:, github:, gitlab:) or be a full URL");
return Err(1);
}
Ok(AddArgs {
name,
path,
git,
branch,
tag,
rev,
version_from_registry: None,
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +1,239 @@
use crate::config::flags;
use crate::utils::fs::check_dir;
use crate::core::config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::parse_version_info;
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored};
use glob::glob;
use std::fs;
use std::path::Path;
pub fn clean(args: &[String]) -> i32 {
let project_name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
.unwrap_or_else(|| "project".to_string());
let items = check_dir(None).unwrap_or_default();
if args.len() > 1 {
error("Unknown argument");
return 1;
}
if !items.contains(&"dcr.toml".to_string()) {
error("dcr.toml file not found");
return 1;
}
println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
if !items.contains(&"target".to_string()) {
warn("Directory target not found");
return 1;
}
if let Some(arg) = args.first() {
let mut profile = arg.clone();
if profile.starts_with("--") {
profile = profile.trim_start_matches("--").to_string();
}
if flags(&profile).is_none() {
error("Unknown profile");
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
return 1;
}
let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&profile) {
warn(&format!("Directory target/{profile} not found"));
};
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;
}
};
let flags = match parse_clean_flags(args) {
Ok(v) => v,
Err(msg) => {
error(&msg);
return 1;
}
};
println!(" Profile: {}", colored(&profile, BOLD_GREEN));
let _ = fs::remove_dir_all(format!("target/{profile}"));
println!(
"{} Removed directory target/{profile}",
colored("\n ✔", BOLD_GREEN)
);
return 0;
match with_dir(&root, || clean_from_root(&root, &flags)) {
Ok(()) => 0,
Err(msg) => {
error(&msg);
1
}
}
let _ = fs::remove_dir_all("target");
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
0
}
struct CleanFlags {
profile: Option<String>,
target: Option<String>,
all: bool,
}
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
let mut profile: Option<String> = None;
let mut target: Option<String> = None;
let mut all = false;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--all" {
all = true;
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("--") {
let candidate = arg.trim_start_matches("--").to_string();
if flags(&candidate).is_some() {
if profile.is_some() {
return Err("Duplicate profile flag".to_string());
}
profile = Some(candidate);
continue;
}
}
return Err("Unknown argument".to_string());
}
Ok(CleanFlags {
profile,
target,
all,
})
}
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let target = flags
.target
.clone()
.or_else(|| {
config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.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
&& let Some(workspace) = parse_workspace(
&config,
flags.profile.as_deref().unwrap_or("debug"),
target.as_deref(),
root,
)?
{
for member in &workspace.members {
clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
}
}
clean_project_at(root, flags.profile.as_deref(), target.as_deref())
}
fn clean_project_at(
project_root: &Path,
profile: Option<&str>,
target: Option<&str>,
) -> Result<(), String> {
with_dir(project_root, || {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let project_name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
.unwrap_or_else(|| "project".to_string());
let items = check_dir(None).map_err(|_| "Failed to read project directory".to_string())?;
if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string());
}
println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
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 parent_dir = target.unwrap_or("");
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 {
println!(" Profile: {}", colored(profile, BOLD_GREEN));
if let Some(t) = target {
println!(" Target: {}", colored(t, BOLD_GREEN));
}
let _ = fs::remove_dir_all(&target_dir);
println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
}
clean_custom_paths(&config, profile)?;
return Ok(());
}
if items.contains(&"target".to_string()) {
let _ = fs::remove_dir_all("target");
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
} else {
warn("Directory target not found");
}
clean_custom_paths(&config, "debug")?;
clean_custom_paths(&config, "release")?;
Ok(())
})
}
fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
let patterns = match config.get("build.clean") {
Some(v) => v
.as_array()
.ok_or_else(|| "build.clean must be an array of strings".to_string())?
.iter()
.filter_map(|item| item.as_str())
.map(|s| {
let value = s.replace("{profile}", profile);
substitute_version_vars(&value, config)
})
.collect::<Vec<String>>(),
None => Vec::new(),
};
if patterns.is_empty() {
return Ok(());
}
for pattern in patterns {
for entry in glob(&pattern).map_err(|err| format!("glob error: {err}"))? {
let path = entry.map_err(|err| format!("glob error: {err}"))?;
if path.is_dir() {
let _ = fs::remove_dir_all(&path);
} else if path.is_file() {
let _ = fs::remove_file(&path);
}
}
}
Ok(())
}
fn substitute_version_vars(value: &str, config: &Config) -> String {
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("");
let info = parse_version_info(version);
value
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}

View File

@@ -3,6 +3,7 @@ use reqwest::blocking::Client;
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -137,8 +138,8 @@ fn download_asset(client: &Client, url: &str) -> Result<Vec<u8>, String> {
fn asset_candidates(target: &str) -> Vec<String> {
let mut names = vec![format!("dcr-{target}")];
if target == "x86_64-pc-windows-msvc" {
names.push("dcr-x86_64-pc-windows-msvc.exe".to_string());
if target.contains("-windows-") || target.ends_with("-windows") {
names.push(format!("dcr-{target}.exe"));
}
names
@@ -156,14 +157,14 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf {
current_exe.with_extension(extension)
}
fn set_executable_permissions(path: &Path) {
fn set_executable_permissions(_path: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = fs::metadata(path) {
if let Ok(meta) = fs::metadata(_path) {
let mut perms = meta.permissions();
perms.set_mode(0o755);
let _ = fs::set_permissions(path, perms);
let _ = fs::set_permissions(_path, perms);
}
}
}

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

@@ -0,0 +1,66 @@
use crate::config::{PROFILE, flags};
use crate::utils::log::warn;
pub struct BuildRunFlags {
pub profile: String,
pub target: Option<String>,
pub force: bool,
pub clean: bool,
pub verbose: bool,
}
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
let mut profile = PROFILE.to_string();
let mut target = None;
let mut force = false;
let mut clean = false;
let mut verbose = false;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if !arg.starts_with("--") {
warn("Unknown argument");
return Err(1);
}
let candidate = arg.trim_start_matches("--");
if candidate == "force" {
force = true;
continue;
}
if candidate == "clean" {
clean = true;
continue;
}
if candidate == "verbose" {
verbose = true;
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 profile != PROFILE {
warn("Duplicate profile flag");
return Err(1);
}
profile = candidate.to_string();
continue;
}
warn("Unknown build flag");
return Err(1);
}
Ok(BuildRunFlags {
profile,
target,
force,
clean,
verbose,
})
}

56
src/cli/fmt.rs Normal file
View File

@@ -0,0 +1,56 @@
use crate::utils::log::error;
use crate::utils::text::{colored, BOLD_GREEN};
use glob::glob;
use std::process::Command;
pub fn fmt(_args: &[String]) -> i32 {
let patterns = [
"src/**/*.c",
"src/**/*.cpp",
"src/**/*.h",
"src/**/*.hpp",
"tests/**/*.c",
"tests/**/*.cpp",
"tests/**/*.h",
"tests/**/*.hpp",
];
let mut files = Vec::new();
for pattern in patterns {
if let Ok(paths) = glob(pattern) {
for path in paths.flatten() {
if let Some(s) = path.to_str() {
files.push(s.to_string());
}
}
}
}
if files.is_empty() {
println!(" {} No files to format", colored("Format", BOLD_GREEN));
return 0;
}
println!(
" {} {} files",
colored("Formatting", BOLD_GREEN),
files.len()
);
let status = Command::new("clang-format").arg("-i").args(&files).status();
match status {
Ok(s) if s.success() => {
println!(" {} successful", colored("Format", BOLD_GREEN));
0
}
Ok(s) => {
error(&format!("clang-format failed with status: {}", s));
1
}
Err(e) => {
error(&format!("Failed to execute clang-format: {}", e));
1
}
}
}

1233
src/cli/gen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,15 +12,22 @@ pub fn help() -> i32 {
println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run the project (default: --debug)");
println!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)");
println!(" fmt Format all C/C++ source files using clang-format");
println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN);
println!(" --help Show command help");
println!(" --update Update dcr to the latest version");
println!(" --version Show dcr version");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Build with debug profile");
println!(" --release Build with release profile");
println!(" --debug Use debug profile");
println!(" --release Use release profile");
println!(" --force Force rebuild (build/run)");
println!(" --clean Clean before build (build/run)");
println!(" --all Clean all workspace members (clean)");
println!();
printc("EXAMPLES:", BOLD_GREEN);
printc(" dcr new hello", BOLD_CYAN);

View File

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

View File

@@ -1,7 +1,14 @@
pub mod add;
pub mod build;
pub mod clean;
pub mod flag_update;
pub mod flags;
pub mod fmt;
pub mod r#gen;
pub mod help;
pub mod init;
pub mod new;
pub mod run;
pub mod setup;
pub mod test;
pub mod tree;

View File

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

View File

@@ -1,70 +1,148 @@
use crate::cli::build::build;
use crate::config::{PROFILE, flags};
use crate::cli::flags::parse_build_run_flags;
use crate::core::config::Config;
use crate::core::runner::run_binary;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::build::{normalize_target_os, parse_version_info};
use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
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))
}
}
pub fn run(args: &[String]) -> i32 {
let mut active_profile = PROFILE.to_string();
let items = check_dir(None).unwrap_or_default();
if !items.contains(&"dcr.toml".to_string()) {
error("dcr.toml file not found");
return 1;
}
let config = match Config::open("./dcr.toml") {
Ok(cfg) => cfg,
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;
}
};
let config = match with_dir(&root, || {
Config::open("./dcr.toml").map_err(|err| err.to_string())
}) {
Ok(cfg) => cfg,
Err(err) => {
error(&err);
return 1;
}
};
let project_name: &str = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("");
let build_kind = config
.get("build.kind")
.and_then(|v| v.as_str())
.unwrap_or("");
let target_dir = config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim())
.filter(|s| !s.is_empty());
if let Some(first_arg) = args.first() {
if first_arg.starts_with("--") {
let candidate = first_arg.trim_start_matches("--");
if flags(candidate).is_some() {
active_profile = candidate.to_string();
} else {
warn("Unknown build flag");
return 1;
}
let mut flags = match parse_build_run_flags(args) {
Ok(v) => v,
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 {
warn("Unknown argument");
return 1;
}
"unknown"
};
flags.target = Some(default_target.to_string());
}
let build_kind = config
.get(&format!("build.{}.kind", flags.profile))
.and_then(|v| v.as_str())
.or_else(|| config.get("build.kind").and_then(|v| v.as_str()))
.unwrap_or("");
let normalized_target_dir = flags
.target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile));
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("");
let run_cmd = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), version);
let kind = build_kind.trim();
if kind == "staticlib" || kind == "sharedlib" {
if run_cmd.is_none()
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf")
{
error("Cannot run library build");
return 1;
}
let build_status = build(args);
let bin_path = crate::platform::bin_path(&active_profile, project_name, target_dir);
let bin_path = crate::platform::bin_path(
&flags.profile,
project_name,
normalized_target_dir.as_deref(),
);
if build_status == 0 {
if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------");
return run_shell(&cmd);
}
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------");
return run_binary(project_name, &active_profile, target_dir);
return run_binary(
project_name,
&flags.profile,
normalized_target_dir.as_deref(),
);
}
let fallback_code = run_binary(project_name, &active_profile, target_dir);
let fallback_code = if let Some(cmd) = run_cmd {
run_shell(&cmd)
} else {
run_binary(
project_name,
&flags.profile,
normalized_target_dir.as_deref(),
)
};
if fallback_code != 1 {
return fallback_code;
}
@@ -72,3 +150,27 @@ pub fn run(args: &[String]) -> i32 {
error("Fix errors in the code to run the project");
1
}
fn run_shell(cmd: &str) -> i32 {
let status = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/C").arg(cmd).status()
} else {
Command::new("sh").arg("-c").arg(cmd).status()
};
match status {
Ok(s) if s.success() => 0,
Ok(s) => s.code().unwrap_or(1),
Err(_) => 1,
}
}
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
let info = parse_version_info(version);
cmd.replace("{profile}", profile)
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}

16
src/cli/setup.rs Normal file
View File

@@ -0,0 +1,16 @@
pub fn setup(_args: &[String]) -> i32 {
println!("Setting up DCR registries...");
match crate::core::registry::RegistryManager::load() {
Ok(manager) => {
println!("Loaded {} registries.", manager.config.registry.len());
for (name, reg) in manager.config.registry {
println!("- {}: {} (priority {})", name, reg.url, reg.priority);
}
0
}
Err(e) => {
println!("Error: {}", e);
1
}
}
}

371
src/cli/test.rs Normal file
View File

@@ -0,0 +1,371 @@
use crate::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::config::Config;
use crate::utils::build::{
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
};
use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, BOLD_RED, RESET, colored};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
pub fn test(args: &[String]) -> i32 {
let mut init_header = false;
let mut profile = "debug";
for arg in args {
if arg == "--help" {
println!("USAGE:");
println!(" dcr test [--init] [--debug | --release]");
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");
println!(" --debug Build and run tests with debug profile (default)");
println!(" --release Build and run tests with release profile");
return 0;
}
if arg == "--init" {
init_header = true;
continue;
}
if arg == "--debug" || arg == "--release" {
profile = arg.trim_start_matches("--");
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(profile)) {
Ok(code) => code,
Err(msg) => {
error(&msg);
1
}
}
}
fn run_testsuite(profile: &str) -> Result<i32, String> {
if build::build(&[format!("--{profile}")]) != 0 {
return Ok(1);
}
let config = Config::open("./dcr.toml").map_err(|_| "Failed to read dcr.toml".to_string())?;
let name = get_config_str(&config, "package.name");
let language = get_language_with_profile(&config, profile)?;
let compiler_cfg = get_string_with_profile(&config, "compiler", profile);
let standard = get_string_with_profile(&config, "standard", profile);
let package_type = get_config_str(&config, "package.type");
let build_kind = get_string_with_profile(&config, "kind", profile);
let tc_cc = get_config_opt(&config, "toolchain.cc");
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
let tc_as = get_config_opt(&config, "toolchain.as");
let compiler = resolve_compiler(
&language,
&compiler_cfg,
tc_cc.as_deref(),
tc_cxx.as_deref(),
tc_as.as_deref(),
);
let mut cflags: Vec<String> = flags(profile)
.unwrap_or(&[])
.iter()
.map(|v| v.to_string())
.collect();
cflags.extend(get_list_with_profile(&config, "cflags", profile));
let ldflags = get_list_with_profile(&config, "ldflags", profile);
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile);
let (cflags, ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
let includes = test_include_dirs(&config, profile);
let test_sources = collect_test_sources()?;
if test_sources.is_empty() {
return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
}
let link_project_lib =
package_type == "lib" || build_kind == "staticlib" || build_kind == "sharedlib";
let mut stdout = String::new();
let mut declared = Vec::new();
let mut suite_success = true;
fs::create_dir_all("./tests/target")
.map_err(|e| format!("Failed to create tests/target: {e}"))?;
for source in &test_sources {
declared.extend(extract_test_names_path(source));
let stem = source
.file_stem()
.and_then(|v| v.to_str())
.unwrap_or("test");
let obj_path = Path::new("./tests/target").join(format!("{stem}.o"));
let bin_path =
Path::new("./tests/target").join(format!("{stem}{}", std::env::consts::EXE_SUFFIX));
compile_test_source(&compiler, &standard, &cflags, &includes, source, &obj_path)?;
link_test_binary(
&compiler,
&ldflags,
link_project_lib.then_some(name.as_str()),
&obj_path,
&bin_path,
)?;
let out = Command::new(&bin_path)
.output()
.map_err(|_| format!("Failed to run `{}`", bin_path.display()))?;
stdout.push_str(&String::from_utf8_lossy(&out.stdout));
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.trim().is_empty() {
eprint!("{}", stderr);
}
if !out.status.success() {
suite_success = false;
}
}
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 suite_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 || !suite_success {
return Ok(1);
}
Ok(0)
}
fn collect_test_sources() -> Result<Vec<PathBuf>, String> {
let tests_dir = Path::new("./tests");
if !tests_dir.is_dir() {
return Ok(Vec::new());
}
let mut sources = Vec::new();
for entry in fs::read_dir(tests_dir).map_err(|e| format!("Failed to read tests/: {e}"))? {
let path = entry
.map_err(|e| format!("Failed to read tests/: {e}"))?
.path();
if path.extension().and_then(|v| v.to_str()) == Some("c") {
sources.push(path);
}
}
sources.sort();
Ok(sources)
}
fn test_include_dirs(config: &Config, profile: &str) -> Vec<String> {
let mut dirs = vec![
"./tests".to_string(),
"./src".to_string(),
"./target/include".to_string(),
];
dirs.extend(get_list_with_profile(config, "include", profile));
dirs
}
fn compile_test_source(
compiler: &str,
standard: &str,
cflags: &[String],
includes: &[String],
source: &Path,
obj_path: &Path,
) -> Result<(), String> {
let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(obj_path);
if !standard.trim().is_empty() {
cmd.arg(format!("-std={}", standard.trim()));
}
for flag in cflags {
cmd.arg(flag);
}
for dir in includes {
cmd.arg(format!("-I{dir}"));
}
let output = cmd
.output()
.map_err(|e| format!("Failed to compile {}: {e}", source.display()))?;
print_command_output(&output);
if !output.status.success() {
return Err(format!("Compilation of {} failed", source.display()));
}
Ok(())
}
fn link_test_binary(
compiler: &str,
ldflags: &[String],
project_lib: Option<&str>,
obj_path: &Path,
bin_path: &Path,
) -> Result<(), String> {
let mut cmd = Command::new(compiler);
cmd.arg(obj_path);
if let Some(name) = project_lib {
cmd.arg("-Ltarget/lib").arg(format!("-l{name}"));
}
for flag in ldflags {
cmd.arg(flag);
}
cmd.arg("-o").arg(bin_path);
let output = cmd
.output()
.map_err(|e| format!("Failed to link {}: {e}", bin_path.display()))?;
print_command_output(&output);
if !output.status.success() {
return Err(format!("Linking of {} failed", bin_path.display()));
}
Ok(())
}
fn print_command_output(output: &std::process::Output) {
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
}
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(path: &Path) -> 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);
}
}

118
src/cli/tree.rs Normal file
View File

@@ -0,0 +1,118 @@
use crate::core::config::Config;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, printc};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use toml::Value;
pub fn tree(_args: &[String]) -> i32 {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let config = match Config::open("dcr.toml") {
Ok(c) => c,
Err(_) => {
error("dcr.toml not found in current directory");
return 1;
}
};
let name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("0.0.0");
printc(&format!("{} v{}", name, version), BOLD_CYAN);
let mut seen = HashSet::new();
seen.insert(name.to_string());
if let Some(deps) = config.get("dependencies").and_then(|v| v.as_table()) {
print_deps(deps, &current_dir, "", &mut seen);
}
0
}
fn print_deps(
deps: &toml::value::Table,
base_path: &Path,
prefix: &str,
seen: &mut HashSet<String>,
) {
let mut dep_list: Vec<_> = deps.iter().collect();
dep_list.sort_by_key(|(name, _)| *name);
for (i, (name, value)) in dep_list.iter().enumerate() {
let is_last = i == dep_list.len() - 1;
let connector = if is_last { "└── " } else { "├── " };
let child_prefix = if is_last { " " } else { "│ " };
let dep_path = resolve_dep_path(value, base_path);
let mut version = String::new();
let mut sub_deps = None;
let mut resolved_path = None;
if let Some(path) = &dep_path {
let dcr_toml = path.join("dcr.toml");
if dcr_toml.exists()
&& let Ok(config) = Config::open(&dcr_toml.to_string_lossy())
{
if let Some(v) = config.get("package.version").and_then(|v| v.as_str()) {
version = format!(" v{}", v);
}
sub_deps = config
.get("dependencies")
.and_then(|v| v.as_table())
.cloned();
resolved_path = Some(path.clone());
}
}
if version.is_empty() {
version = match value {
Value::String(s) if s.starts_with("git:") || s.starts_with("github:") => {
format!(" ({})", s)
}
Value::Table(t) => {
if let Some(v) = t.get("version").and_then(|v| v.as_str()) {
format!(" v{}", v)
} else if let Some(git) = t.get("git").and_then(|v| v.as_str()) {
format!(" ({})", git)
} else {
"".to_string()
}
}
_ => "".to_string(),
};
}
println!("{}{}{}{}", prefix, connector, name, version);
if let Some(s_deps) = sub_deps
&& !seen.contains(*name)
{
seen.insert(name.to_string());
let new_prefix = format!("{}{}", prefix, child_prefix);
if let Some(path) = resolved_path {
print_deps(&s_deps, &path, &new_prefix, seen);
}
seen.remove(*name);
}
}
}
fn resolve_dep_path(value: &Value, base_path: &Path) -> Option<PathBuf> {
match value {
Value::String(s) => s
.strip_prefix("path:")
.map(|stripped| base_path.join(stripped)),
Value::Table(t) => t
.get("path")
.and_then(|v| v.as_str())
.map(|path| base_path.join(path)),
_ => None,
}
}

View File

@@ -7,16 +7,88 @@ 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]> {
match profile {
"release" => Some(&["-O3", "-DNDEBUG", "-march=native"]),
"release" => Some(&["-O3", "-DNDEBUG"]),
"debug" => Some(&[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDEBUG",
"-DDCR_DEBUG",
]),
_ => None,
}

View File

@@ -1,212 +0,0 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
cmd.arg("rcs").arg(&lib_path);
for obj in &objects {
cmd.arg(obj);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or(compiler));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or("")
.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed {
out.push(file);
}
}
Ok(())
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG", "-march=native"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDEBUG",
],
_ => &[],
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard));
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

696
src/core/builder/common.rs Normal file
View File

@@ -0,0 +1,696 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
pub fn get_output_lock() -> &'static Mutex<()> {
OUTPUT_MUTEX.get_or_init(|| Mutex::new(()))
}
pub fn collect_sources(
roots: &[PathBuf],
extensions: &[&str],
exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<Vec<String>, String> {
let mut sources = Vec::new();
for root in roots {
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
}
sources.sort();
if sources.is_empty() {
return Err(format!("No source files found in {}", format_roots(roots)));
}
Ok(sources)
}
fn collect_sources_rec(
dir: &Path,
extensions: &[&str],
out: &mut Vec<String>,
exclude_dirs: &[PathBuf],
include_paths: &[String],
) -> Result<(), String> {
let full_dir = if dir.is_absolute() {
dir.to_path_buf()
} else {
std::env::current_dir()
.map_err(|err| format!("src dir error: {err}"))?
.join(dir)
};
if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() {
return Ok(());
}
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
continue;
}
collect_sources_rec(&path, extensions, out, exclude_dirs, include_paths)?;
continue;
}
if !path.is_file() {
continue;
}
if is_excluded(&path, exclude_dirs, include_paths) {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext_lower = ext_raw.to_lowercase();
let matched = extensions
.iter()
.any(|allowed| *allowed == ext_raw || *allowed == ext_lower);
if matched {
out.push(normalize_source_path(&path));
}
}
Ok(())
}
fn format_roots(roots: &[PathBuf]) -> String {
if roots.is_empty() {
return "<none>".to_string();
}
let mut out = Vec::new();
for root in roots {
out.push(root.to_string_lossy().to_string());
}
out.join(", ")
}
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
if matches_patterns(path, include_paths, true) {
return false;
}
if matches_patterns(path, include_paths, false) {
return true;
}
exclude_dirs.iter().any(|dir| path.starts_with(dir))
}
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
if patterns.is_empty() {
return false;
}
let cwd = std::env::current_dir().ok();
let abs_path = if path.is_absolute() {
path.to_path_buf()
} else if let Some(base) = cwd.as_ref() {
base.join(path)
} else {
path.to_path_buf()
};
let abs = abs_path.to_string_lossy().replace('\\', "/");
let rel = cwd
.as_ref()
.and_then(|base| abs_path.strip_prefix(base).ok())
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default();
let rel_dot = if rel.is_empty() {
String::new()
} else {
format!("./{rel}")
};
for raw in patterns {
let trimmed = raw.trim();
if trimmed.is_empty() {
continue;
}
let is_neg = trimmed.starts_with('!');
if is_neg == positive {
continue;
}
let normalized = trimmed.trim_start_matches('!').replace('\\', "/");
if normalized.is_empty() {
continue;
}
if has_glob_magic(&normalized) {
let pat = match glob::Pattern::new(&normalized) {
Ok(p) => p,
Err(_) => continue,
};
if pat.matches(&abs)
|| (!rel.is_empty() && (pat.matches(&rel) || pat.matches(&rel_dot)))
{
return true;
}
continue;
}
let norm = normalized.trim_end_matches('/');
let candidate = Path::new(norm);
let full = if candidate.is_absolute() {
candidate.to_path_buf()
} else if let Some(base) = cwd.as_ref() {
base.join(candidate)
} else {
candidate.to_path_buf()
};
if abs_path.starts_with(&full) {
return true;
}
}
false
}
pub fn has_glob_magic(value: &str) -> bool {
value.chars().any(|c| matches!(c, '*' | '?' | '['))
}
pub fn parallel_build<F>(total_tasks: usize, task_fn: F) -> Result<(), String>
where
F: Fn(usize) -> Result<(), String> + Sync + Send,
{
let num_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let counter = std::sync::atomic::AtomicUsize::new(0);
let err_msg = std::sync::Mutex::new(None);
std::thread::scope(|s| {
for _ in 0..num_threads {
s.spawn(|| {
loop {
if err_msg.lock().unwrap().is_some() {
break;
}
let i = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if i >= total_tasks {
break;
}
if let Err(e) = task_fn(i) {
let mut err = err_msg.lock().unwrap();
if err.is_none() {
*err = Some(e);
}
break;
}
}
});
}
});
if let Some(err) = err_msg.into_inner().unwrap() {
return Err(err);
}
Ok(())
}
pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let _guard = OUTPUT_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap();
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
if output.status.success() {
Ok(())
} else {
Err("Build failed".to_string())
}
}
pub fn normalize_source_path(path: &Path) -> String {
if !path.is_absolute() {
return path.to_string_lossy().to_string();
}
if let Ok(base) = std::env::current_dir()
&& let Ok(rel) = path.strip_prefix(&base)
{
return format!("./{}", rel.to_string_lossy());
}
path.to_string_lossy().to_string()
}
pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let stripped = src_path.strip_prefix("./").unwrap_or(src_path);
let rel = stripped
.components()
.skip(1)
.collect::<std::path::PathBuf>();
let rel = if rel.components().next().is_some() {
rel
} else {
stripped.to_path_buf()
};
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
pub fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
let o_time = match obj_time {
Ok(t) => t,
Err(_) => return true,
};
match src_time {
Ok(s) if s > o_time => return true,
Err(_) => return true,
_ => {}
}
let d_file = PathBuf::from(object).with_extension("d");
if let Ok(content) = fs::read_to_string(&d_file) {
let deps = parse_d_file(&content);
for dep in deps {
let dep_path = Path::new(&dep);
if dep_path == Path::new(object) || dep_path == Path::new(source) {
continue;
}
if let Ok(dep_meta) = fs::metadata(dep_path) {
if let Ok(dep_time) = dep_meta.modified()
&& dep_time > o_time
{
return true;
}
} else {
return true; // Missing dependency triggers rebuild
}
}
}
false
}
fn parse_d_file(content: &str) -> Vec<String> {
let mut deps = Vec::new();
let text = content.replace("\\\n", " ").replace("\\\r\n", " ");
let mut target_end = 0;
let chars: Vec<char> = text.chars().collect();
for i in 0..chars.len() {
if chars[i] == ':' {
if i == 1
&& chars[0].is_ascii_alphabetic()
&& i + 1 < chars.len()
&& (chars[i + 1] == '\\' || chars[i + 1] == '/')
{
continue; // Windows drive letter
}
target_end = i + 1;
break;
}
}
let deps_str = if target_end > 0 {
&text[target_end..]
} else {
&text
};
let mut current_path = String::new();
let mut in_escape = false;
for c in deps_str.chars() {
if in_escape {
if c != '\n' && c != '\r' {
current_path.push(c);
}
in_escape = false;
} else if c == '\\' {
in_escape = true;
} else if c.is_whitespace() {
if !current_path.is_empty() {
deps.push(current_path.clone());
current_path.clear();
}
} else {
current_path.push(c);
}
}
if !current_path.is_empty() {
deps.push(current_path);
}
deps
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
static CWD_LOCK: Mutex<()> = Mutex::new(());
fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("dcr_test_{prefix}_{n}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn default_roots() -> Vec<PathBuf> {
vec![PathBuf::from("src")]
}
#[test]
fn object_path_basic() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "./src/main.c", "o");
assert_eq!(result, "target/debug/obj/main.o");
}
#[test]
fn object_path_nested() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "./src/core/utils.c", "o");
assert_eq!(result, "target/debug/obj/core/utils.o");
}
#[test]
fn object_path_no_prefix() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "src/main.c", "o");
assert_eq!(result, "target/debug/obj/main.o");
}
#[test]
fn object_path_msvc_ext() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "./src/main.c", "obj");
assert_eq!(result, "target/debug/obj/main.obj");
}
#[test]
fn needs_rebuild_no_object() {
let dir = temp_dir("rebuild_no_obj");
let src = dir.join("test.c");
fs::write(&src, "int main() {}").unwrap();
let obj = dir.join("test.o");
assert!(needs_rebuild(
&src.to_string_lossy(),
&obj.to_string_lossy()
));
}
#[test]
fn needs_rebuild_fresh() {
let dir = temp_dir("rebuild_fresh");
let src = dir.join("test.c");
let obj = dir.join("test.o");
fs::write(&src, "int main() {}").unwrap();
// Sleep briefly to ensure mtime difference
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap();
assert!(!needs_rebuild(
&src.to_string_lossy(),
&obj.to_string_lossy()
));
}
#[test]
fn needs_rebuild_stale() {
let dir = temp_dir("rebuild_stale");
let src = dir.join("test.c");
let obj = dir.join("test.o");
fs::write(&obj, "").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&src, "int main() { return 1; }").unwrap();
assert!(needs_rebuild(
&src.to_string_lossy(),
&obj.to_string_lossy()
));
}
#[test]
fn is_excluded_match() {
let include: Vec<String> = Vec::new();
let excluded = vec![PathBuf::from("/project/src/vendor")];
assert!(is_excluded(
Path::new("/project/src/vendor"),
&excluded,
&include
));
assert!(is_excluded(
Path::new("/project/src/vendor/lib.c"),
&excluded,
&include
));
}
#[test]
fn is_excluded_no_match() {
let include: Vec<String> = Vec::new();
let excluded = vec![PathBuf::from("/project/src/vendor")];
assert!(!is_excluded(
Path::new("/project/src/main.c"),
&excluded,
&include
));
assert!(!is_excluded(
Path::new("/other/vendor"),
&excluded,
&include
));
}
#[test]
fn collect_sources_c_files() {
let dir = temp_dir("collect_c");
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(src.join("utils.c"), "").unwrap();
fs::write(src.join("README.md"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 2);
assert!(sources.iter().any(|s| s.ends_with("main.c")));
assert!(sources.iter().any(|s| s.ends_with("utils.c")));
}
#[test]
fn collect_sources_cpp_files() {
let dir = temp_dir("collect_cpp");
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("main.cpp"), "").unwrap();
fs::write(src.join("helper.cxx"), "").unwrap();
fs::write(src.join("other.cc"), "").unwrap();
fs::write(src.join("skip.c"), "").unwrap(); // should be ignored
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["cpp", "cxx", "cc"], &[], &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 3);
}
#[test]
fn collect_sources_empty_dir() {
let dir = temp_dir("collect_empty");
let src = dir.join("src");
fs::create_dir_all(&src).unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap();
assert!(result.is_err(), "empty src should return error");
}
#[test]
fn collect_sources_respects_excludes() {
let dir = temp_dir("collect_exclude");
let src = dir.join("src");
let vendor = src.join("vendor");
fs::create_dir_all(&vendor).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded
let exclude = vec![vendor.clone()];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("main.c"));
}
#[test]
fn collect_sources_nested() {
let dir = temp_dir("collect_nested");
let src = dir.join("src");
let sub = src.join("core").join("deep");
fs::create_dir_all(&sub).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(sub.join("nested.c"), "").unwrap();
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let include: Vec<String> = Vec::new();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 2);
}
#[test]
fn include_overrides_exclude() {
let dir = temp_dir("include_override");
let src = dir.join("src");
let boot = src.join("boot");
let arch = boot.join("arch");
fs::create_dir_all(&arch).unwrap();
fs::write(arch.join("start.s"), "").unwrap();
fs::write(boot.join("skip.c"), "").unwrap();
let exclude = vec![boot.clone()];
let include = vec!["src/boot/arch/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
let result = collect_sources(&roots, &["s", "c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("start.s"));
}
#[test]
fn exclude_glob_with_include_override() {
let dir = temp_dir("exclude_glob");
let src = dir.join("src");
let legacy = src.join("legacy");
let allow = legacy.join("allow");
fs::create_dir_all(&allow).unwrap();
fs::write(legacy.join("skip.c"), "").unwrap();
fs::write(allow.join("keep.c"), "").unwrap();
let exclude = vec![dir.join("src"), dir.join(".")];
let include = vec![
"!src/legacy/**".to_string(),
"src/legacy/allow/**".to_string(),
];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("keep.c"));
}
#[test]
fn exclude_glob_without_include_keeps_other_files() {
let dir = temp_dir("exclude_glob_only");
let src = dir.join("src");
let gen_dir = src.join("gen");
fs::create_dir_all(&gen_dir).unwrap();
fs::write(src.join("main.c"), "").unwrap();
fs::write(gen_dir.join("skip.c"), "").unwrap();
let exclude: Vec<PathBuf> = Vec::new();
let include = vec!["!src/gen/**".to_string()];
let _guard = CWD_LOCK.lock().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let roots = default_roots();
let result = collect_sources(&roots, &["c"], &exclude, &include);
std::env::set_current_dir(prev).unwrap();
let sources = result.expect("should find sources");
assert_eq!(sources.len(), 1);
assert!(sources[0].ends_with("main.c"));
}
#[test]
fn normalize_relative_path() {
let result = normalize_source_path(Path::new("./src/main.c"));
assert_eq!(result, "./src/main.c");
}
#[test]
fn parse_d_file_gcc_format() {
let content = "target/obj/main.o: src/main.c src/utils.h \\\n src/core/types.h";
let deps = parse_d_file(content);
assert_eq!(deps, vec!["src/main.c", "src/utils.h", "src/core/types.h"]);
}
#[test]
fn parse_d_file_msvc_format() {
let content = "target/obj/main.obj: \\\n C:/sdk/include/windows.h \\\n src/main.c";
let deps = parse_d_file(content);
assert_eq!(deps, vec!["C:/sdk/include/windows.h", "src/main.c"]);
}
#[test]
fn needs_rebuild_header_modified() {
let dir = temp_dir("rebuild_header");
let src = dir.join("test.c");
let header = dir.join("test.h");
let obj = dir.join("test.o");
let d_file = dir.join("test.d");
fs::write(&src, "int main() {}").unwrap();
fs::write(&header, "#define A 1").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj, "").unwrap();
let d_content = format!(
"{}: {} {}",
obj.to_string_lossy(),
src.to_string_lossy(),
header.to_string_lossy()
);
fs::write(&d_file, d_content).unwrap();
assert!(
!needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
"should be fresh"
);
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&header, "#define A 2").unwrap(); // modify header
assert!(
needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
"changed header should trigger rebuild"
);
}
}

View File

@@ -1,4 +1,5 @@
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
@@ -6,7 +7,8 @@ use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.language.to_lowercase().as_str() != "asm" {
let lang = ctx.language.to_lowercase();
if lang.split('+').any(|p| p.trim() != "asm") {
return Err("GAS backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
@@ -15,8 +17,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
@@ -35,6 +40,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -65,13 +73,26 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -82,38 +103,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext = ext_raw.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = lang == "asm" && ext == "s" && ext_raw == "s";
if allowed {
out.push(file);
}
}
Ok(())
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// GAS handles only lowercase .s files
common::collect_sources(
ctx.source_roots,
&["s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_objects(
@@ -123,46 +120,38 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(&obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -1,212 +0,0 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
cmd.arg("rcs").arg(&lib_path);
for obj in &objects {
cmd.arg(obj);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or(compiler));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or("")
.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed {
out.push(file);
}
}
Ok(())
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG", "-march=native"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDEBUG",
],
_ => &[],
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard));
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -1,8 +1,8 @@
pub mod clang;
pub mod common;
pub mod gas;
pub mod gcc;
pub mod msvc;
pub mod nasm;
pub mod unix_cc;
pub struct BuildContext<'a> {
pub profile: &'a str,
@@ -10,20 +10,35 @@ pub struct BuildContext<'a> {
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
#[allow(dead_code)]
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub package_type: Option<&'a str>,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
pub include_dirs: &'a [String],
pub lib_dirs: &'a [String],
pub libs: &'a [String],
pub cflags: &'a [String],
pub ldflags: &'a [String],
pub output_filename: Option<&'a str>,
pub output_extension: Option<&'a str>,
pub verbose: bool,
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = ctx.compiler.to_lowercase();
if !check_compiler_exists(ctx.compiler) {
return Err(format!(
"Compiler not found: {}. Make sure it is installed and available in PATH.",
ctx.compiler
));
}
if compiler.contains("clang-cl") {
return msvc::build(ctx);
}
@@ -33,14 +48,30 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if compiler.contains("nasm") {
return nasm::build(ctx);
}
if compiler.contains("gcc") || compiler.contains("g++") {
return gcc::build(ctx);
}
if compiler.contains("clang") || compiler.contains("clang++") {
return clang::build(ctx);
}
if compiler == "cl" || compiler.contains("msvc") {
return msvc::build(ctx);
}
clang::build(ctx)
unix_cc::build(ctx)
}
fn check_compiler_exists(compiler: &str) -> bool {
let name = if compiler.is_empty() { "cc" } else { compiler };
std::process::Command::new(name)
.arg("--version")
.output()
.is_ok()
}
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let compiler = ctx.compiler.to_lowercase();
if compiler.contains("clang-cl") || compiler == "cl" || compiler.contains("msvc") {
return msvc::collect_sources(ctx);
}
if compiler == "as" || compiler.contains("gas") {
return gas::collect_sources(ctx);
}
if compiler.contains("nasm") {
return nasm::collect_sources(ctx);
}
unix_cc::collect_sources(ctx)
}

View File

@@ -1,4 +1,5 @@
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
@@ -11,12 +12,16 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else {
ctx.compiler
};
if ctx.language.to_lowercase() == "asm" {
return Err("MSVC backend does not support build.language = \"asm\"".to_string());
let lang = ctx.language.to_lowercase();
if lang.contains("asm") {
return Err("MSVC backend does not support build.language with asm".to_string());
}
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
if ctx.kind == "staticlib" {
@@ -26,6 +31,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -61,8 +69,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
@@ -85,11 +95,16 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
cmd.arg(format!("/Fe:{out_path}"));
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -100,44 +115,31 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or("")
.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed {
out.push(file);
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split('+') {
let lang = part.trim().to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => {}
_ => {}
}
}
Ok(())
if out.is_empty() {
out.extend(["c"]);
}
out
}
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
@@ -183,7 +185,7 @@ fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["/O2", "/DNDEBUG"],
"debug" => &["/Od", "/Zi", "/W4", "/DDEBUG", "/Oy-"],
"debug" => &["/Od", "/Zi", "/W4", "/DDCR_DEBUG", "/Oy-"],
_ => &[],
}
}
@@ -195,71 +197,124 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
match ctx.language.to_lowercase().as_str() {
"c" => {
cmd.arg("/TC");
}
"c++" | "cpp" | "cxx" => {
cmd.arg("/TP");
}
_ => {
return Err("Unsupported language".to_string());
}
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
if let Some(flag) = msvc_arch_flag(ctx.platform) {
cmd.arg(flag);
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("/I{dir}"));
}
cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path));
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
match ctx.language.to_lowercase().as_str() {
"c" => {
cmd.arg("/TC");
}
"c++" | "cpp" | "cxx" => {
cmd.arg("/TP");
}
_ => return Err("Unsupported language".to_string()),
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
if let Some(flag) = msvc_arch_flag(ctx.platform) {
cmd.arg(flag);
}
if ctx.cflags.is_empty() {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("/I{dir}"));
}
cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path));
cmd.arg("/showIncludes");
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut headers = Vec::new();
let mut clean_stdout = String::new();
for line in stdout.lines() {
if let Some(stripped) = line.strip_prefix("Note: including file:") {
headers.push(stripped.trim().to_string());
} else if let Some(stripped) = line.strip_prefix("Примечание: включение файла:")
{
headers.push(stripped.trim().to_string());
} else {
clean_stdout.push_str(line);
clean_stdout.push('\n');
}
}
let _lock = common::get_output_lock().lock().unwrap();
if !output.status.success() {
eprint!("{}", clean_stdout);
eprint!("{}", stderr);
return Err("Build failed".to_string());
}
let trimmed_out = clean_stdout.trim();
let trimmed_err = stderr.trim();
let src_filename = Path::new(source)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("");
if !trimmed_out.is_empty() && trimmed_out != src_filename {
print!("{}", clean_stdout);
}
if !trimmed_err.is_empty() {
eprintln!("{}", trimmed_err);
}
let d_path = Path::new(obj_path).with_extension("d");
let mut d_content = format!("{}: \\\n", obj_path.replace('\\', "/"));
for h in headers {
let escaped = h.replace('\\', "/").replace(" ", "\\ ");
d_content.push_str(&format!(" {} \\\n", escaped));
}
fs::write(&d_path, d_content).map_err(|err| format!("d file error: {err}"))?;
Ok(())
}

View File

@@ -1,4 +1,5 @@
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
@@ -6,7 +7,8 @@ use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.language.to_lowercase().as_str() != "asm" {
let lang = ctx.language.to_lowercase();
if lang.split('+').any(|p| p.trim() != "asm") {
return Err("NASM backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
@@ -15,8 +17,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
@@ -35,6 +40,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -65,13 +73,26 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -82,38 +103,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext = ext_raw.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = lang == "asm" && (ext == "asm" || (ext == "s" && ext_raw == "s"));
if allowed {
out.push(file);
}
}
Ok(())
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed)
common::collect_sources(
ctx.source_roots,
&["asm", "s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_objects(
@@ -123,34 +120,53 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
let format = nasm_format(ctx.platform);
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(&obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], format, ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
format: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {
let p = p.to_lowercase().replace('-', "_");
@@ -186,24 +202,3 @@ fn nasm_format(platform: Option<&str>) -> &'static str {
"elf64"
}
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

248
src/core/builder/unix_cc.rs Normal file
View File

@@ -0,0 +1,248 @@
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cc"
} else {
ctx.compiler
};
let start_time = Instant::now();
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
cmd.arg("rcs").arg(&lib_path);
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or(compiler));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
if ctx.kind == "efi" {
cmd.arg("-shared");
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "efi" {
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
let extensions = source_extensions(ctx.language);
common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split('+') {
let lang = part.trim().to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => out.extend(["s", "S", "asm"]),
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
],
_ => &[],
}
}
fn is_bare_metal_target(target: Option<&str>) -> bool {
if let Some(t) = target {
let lower = t.to_lowercase();
lower.contains("none")
|| lower.contains("-elf")
|| lower.contains("eabi")
|| lower.contains("baremetal")
|| lower.contains("bare-metal")
} else {
false
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
compiler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard));
}
let use_dcr_defaults = ctx.cflags.is_empty() && !is_bare_metal_target(ctx.target);
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("-I{dir}"));
}
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}

View File

@@ -1,3 +1,5 @@
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
@@ -8,6 +10,7 @@ const DEFAULT_LANGUAGE: &str = "c";
const DEFAULT_STANDARD: &str = "c11";
const DEFAULT_COMPILER: &str = "clang";
const DEFAULT_KIND: &str = "bin";
const VALID_KINDS: &[&str] = &["bin", "staticlib", "sharedlib", "efi", "elf"];
#[derive(Debug)]
pub enum ConfigError {
@@ -51,6 +54,98 @@ impl From<toml::ser::Error> for ConfigError {
pub struct Config {
path: PathBuf,
data: Value,
typed: DcrConfig,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct DcrConfig {
pub package: PackageConfig,
pub build: BuildConfig,
#[serde(default)]
pub dependencies: BTreeMap<String, DependencyConfig>,
#[serde(default)]
pub toolchain: Option<ToolchainConfig>,
#[serde(default)]
pub workspace: BTreeMap<String, WorkspaceMemberConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub version: String,
#[serde(default, rename = "type")]
pub pkg_type: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct BuildConfig {
pub language: LanguageConfig,
pub standard: String,
pub compiler: String,
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub target: Option<String>,
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub cflags: Vec<String>,
#[serde(default)]
pub ldflags: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub roots: Vec<String>,
#[serde(default)]
pub clean: Vec<String>,
#[serde(default)]
pub src_disable: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum LanguageConfig {
One(String),
Many(Vec<String>),
}
impl LanguageConfig {
fn values(&self) -> Vec<&str> {
match self {
LanguageConfig::One(value) => vec![value.as_str()],
LanguageConfig::Many(values) => values.iter().map(String::as_str).collect(),
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum DependencyConfig {
Version(String),
Table(BTreeMap<String, Value>),
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ToolchainConfig {
pub cc: Option<String>,
pub cxx: Option<String>,
#[serde(rename = "as")]
pub assembler: Option<String>,
pub ar: Option<String>,
pub ld: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct WorkspaceMemberConfig {
pub path: String,
#[serde(default)]
pub deps: Vec<String>,
}
impl Config {
@@ -63,7 +158,8 @@ impl Config {
write_toml(&path, &default_value)?;
default_value
};
let cfg = Self { path, data };
let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
cfg.validate()?;
Ok(cfg)
}
@@ -74,11 +170,27 @@ impl Config {
return Err(ConfigError::Invalid("dcr.toml not found".into()));
}
let data = read_toml(&path)?;
let cfg = Self { path, data };
let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
cfg.validate()?;
Ok(cfg)
}
#[allow(dead_code)]
pub fn typed(&self) -> &DcrConfig {
&self.typed
}
#[allow(dead_code)]
pub fn package(&self) -> &PackageConfig {
&self.typed.package
}
#[allow(dead_code)]
pub fn build_config(&self) -> &BuildConfig {
&self.typed.build
}
pub fn get(&self, key: &str) -> Option<&Value> {
let parts: Vec<&str> = key.split('.').collect();
get_path(&self.data, &parts)
@@ -98,52 +210,66 @@ impl Config {
}
pub fn validate(&self) -> Result<(), ConfigError> {
let package = self
.get("package")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
if self.typed.package.name.trim().is_empty() {
return Err(ConfigError::Invalid("package.name is empty".into()));
}
if self.typed.package.version.trim().is_empty() {
return Err(ConfigError::Invalid("package.version is empty".into()));
}
validate_package_name(&self.typed.package.name)?;
for key in ["name", "version"] {
let value = package.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("package.{key} is empty")));
if let Some(pkg_type) = &self.typed.package.pkg_type {
let pkg_type = pkg_type.trim();
if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none"
{
return Err(ConfigError::Invalid(
"package.type must be 'lib', 'app', or 'none'".into(),
));
}
}
let build = self
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
for key in ["language", "standard", "compiler", "kind"] {
let value = build.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("build.{key} is empty")));
}
validate_language_config(&self.typed.build.language, "build.language")?;
if self.typed.build.standard.trim().is_empty() {
return Err(ConfigError::Invalid("build.standard is empty".into()));
}
if let Some(platform) = build.get("platform")
&& !platform
.as_str()
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
if self.typed.build.compiler.trim().is_empty() {
return Err(ConfigError::Invalid("build.compiler is empty".into()));
}
if let Some(platform) = &self.typed.build.platform
&& platform.trim().is_empty()
{
return Err(ConfigError::Invalid("build.platform is empty".into()));
}
if let Some(toolchain) = self.get("toolchain").and_then(|v| v.as_table()) {
for key in ["cc", "cxx", "as", "ar", "ld"] {
if let Some(v) = toolchain.get(key)
&& !v.as_str().map(|s| !s.trim().is_empty()).unwrap_or(false)
{
return Err(ConfigError::Invalid(format!("toolchain.{key} is invalid")));
}
}
}
if let Some(kind) = build.get("kind").and_then(|v| v.as_str()) {
validate_toolchain(self.typed.toolchain.as_ref())?;
if let Some(kind) = &self.typed.build.kind {
let kind = kind.trim();
if kind != "bin" && kind != "staticlib" && kind != "sharedlib" {
if !kind.is_empty() && !VALID_KINDS.contains(&kind) {
return Err(ConfigError::Invalid("build.kind is invalid".into()));
}
}
validate_string_list(&self.typed.build.exclude, "build.exclude")?;
validate_string_list(&self.typed.build.include, "build.include")?;
validate_string_list(&self.typed.build.roots, "build.roots")?;
validate_string_list(&self.typed.build.clean, "build.clean")?;
validate_string_list(&self.typed.build.cflags, "build.cflags")?;
validate_string_list(&self.typed.build.ldflags, "build.ldflags")?;
if let Some(target) = &self.typed.build.target {
validate_non_empty_string(target, "build.target")?;
}
for profile in ["release", "debug"] {
if let Some(section) = self
.data
.get("build")
.and_then(|v| v.as_table())
.and_then(|build| build.get(profile))
{
let table = section.as_table().ok_or_else(|| {
ConfigError::Invalid(format!("build.{profile} must be a table"))
})?;
self.validate_profile_section(profile, table)?;
}
}
self.validate_workspace()?;
Ok(())
}
@@ -153,12 +279,236 @@ impl Config {
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
let parts: Vec<&str> = key.split('.').collect();
let previous = self.data.clone();
set_path(&mut self.data, &parts, value)?;
self.typed = match parse_typed_config(&self.data) {
Ok(typed) => typed,
Err(err) => {
self.data = previous;
return Err(err);
}
};
if let Err(err) = self.validate() {
self.data = previous;
self.typed = parse_typed_config(&self.data)?;
return Err(err);
}
self.save()?;
Ok(())
}
}
fn parse_typed_config(value: &Value) -> Result<DcrConfig, ConfigError> {
value.clone().try_into().map_err(ConfigError::TomlDe)
}
fn validate_package_name(name: &str) -> Result<(), ConfigError> {
let trimmed = name.trim();
if trimmed != name {
return Err(ConfigError::Invalid(
"package.name must not contain leading or trailing whitespace".into(),
));
}
if trimmed == "." || trimmed == ".." || trimmed.contains("..") {
return Err(ConfigError::Invalid("package.name is invalid".into()));
}
if !trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err(ConfigError::Invalid(
"package.name must contain only ASCII letters, digits, '_' or '-'".into(),
));
}
Ok(())
}
fn validate_language_config(language: &LanguageConfig, key: &str) -> Result<(), ConfigError> {
let values = language.values();
if values.is_empty() {
return Err(ConfigError::Invalid(format!("{key} is empty")));
}
for value in values {
validate_non_empty_string(value, key)?;
}
Ok(())
}
fn validate_toolchain(toolchain: Option<&ToolchainConfig>) -> Result<(), ConfigError> {
let Some(toolchain) = toolchain else {
return Ok(());
};
for (key, value) in [
("cc", toolchain.cc.as_deref()),
("cxx", toolchain.cxx.as_deref()),
("as", toolchain.assembler.as_deref()),
("ar", toolchain.ar.as_deref()),
("ld", toolchain.ld.as_deref()),
] {
if let Some(value) = value {
validate_non_empty_string(value, &format!("toolchain.{key}"))?;
}
}
Ok(())
}
fn validate_string_list(values: &[String], key: &str) -> Result<(), ConfigError> {
for value in values {
validate_non_empty_string(value, key)?;
}
Ok(())
}
fn validate_non_empty_string(value: &str, key: &str) -> Result<(), ConfigError> {
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("{key} contains empty value")));
}
Ok(())
}
impl Config {
fn validate_language(&self, value: &Value) -> Result<(), ConfigError> {
if let Some(s) = value.as_str() {
if s.trim().is_empty() {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
return Ok(());
}
let arr = value
.as_array()
.ok_or_else(|| ConfigError::Invalid("build.language must be string or array".into()))?;
if arr.is_empty() {
return Err(ConfigError::Invalid("build.language is empty".into()));
}
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.language contains empty value".into(),
));
}
}
Ok(())
}
fn validate_profile_section(
&self,
profile: &str,
table: &toml::value::Table,
) -> Result<(), ConfigError> {
if let Some(lang) = table.get("language") {
self.validate_language(lang)?;
}
for key in ["standard", "compiler", "kind", "target", "platform"] {
if let Some(value) = table.get(key) {
let s = value.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} is empty"
)));
}
}
}
if let Some(kind) = table.get("kind").and_then(|v| v.as_str()) {
let kind = kind.trim();
if !kind.is_empty() && !VALID_KINDS.contains(&kind) {
return Err(ConfigError::Invalid(format!(
"build.{profile}.kind is invalid"
)));
}
}
if let Some(src_disable) = table.get("src_disable")
&& !src_disable.is_bool()
{
return Err(ConfigError::Invalid(format!(
"build.{profile}.src_disable must be boolean"
)));
}
for key in [
"cflags",
"ldflags",
"exclude",
"include",
"roots",
"pkg_config",
"generated",
"expect",
"clean",
"targets",
] {
if let Some(val) = table.get(key) {
let arr = val.as_array().ok_or_else(|| {
ConfigError::Invalid(format!(
"build.{profile}.{key} must be an array of strings"
))
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} contains empty value"
)));
}
}
}
}
for key in ["steps", "post_steps"] {
if let Some(val) = table.get(key) {
let arr = val.as_array().ok_or_else(|| {
ConfigError::Invalid(format!("build.{profile}.{key} must be an array"))
})?;
for item in arr {
let tbl = item.as_table().ok_or_else(|| {
ConfigError::Invalid(format!(
"build.{profile}.{key} entries must be tables"
))
})?;
for req in ["name", "in", "out", "cmd"] {
let s = tbl.get(req).and_then(|v| v.as_str()).unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"build.{profile}.{key} missing {req}"
)));
}
}
}
}
}
Ok(())
}
fn validate_workspace(&self) -> Result<(), ConfigError> {
let Some(workspace) = self.get("workspace").and_then(|v| v.as_table()) else {
return Ok(());
};
for (name, value) in workspace {
let tbl = value
.as_table()
.ok_or_else(|| ConfigError::Invalid(format!("workspace.{name} must be a table")))?;
let path = tbl.get("path").and_then(|v| v.as_str()).unwrap_or("");
if path.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"workspace.{name}.path is empty"
)));
}
if let Some(deps) = tbl.get("deps") {
let arr = deps.as_array().ok_or_else(|| {
ConfigError::Invalid(format!("workspace.{name}.deps must be array"))
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(format!(
"workspace.{name}.deps contains empty value"
)));
}
}
}
}
Ok(())
}
}
fn read_toml(path: &Path) -> Result<Value, ConfigError> {
let content = fs::read_to_string(path)?;
Ok(toml::from_str(&content)?)
@@ -182,6 +532,11 @@ fn default_toml() -> Result<Value, ConfigError> {
"version".to_string(),
Value::String(DEFAULT_VERSION.to_string()),
);
package.insert("type".to_string(), Value::String("none".to_string()));
package.insert("description".to_string(), Value::String("".to_string()));
package.insert("author".to_string(), Value::String("".to_string()));
package.insert("license".to_string(), Value::String("MIT".to_string()));
let mut build = Map::new();
build.insert(
"language".to_string(),
@@ -230,17 +585,48 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
.and_then(|v| v.as_str())
.unwrap_or("");
let language = build.get("language").and_then(|v| v.as_str()).unwrap_or("");
let language_value = build.get("language");
let language = match language_value {
Some(Value::String(s)) => s.to_string(),
Some(Value::Array(arr)) => format_string_array(&Value::Array(arr.clone())),
_ => "".to_string(),
};
let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or("");
let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
let mut out = String::new();
out.push_str("[package]\n");
out.push_str(&format!("name = \"{name}\"\n"));
out.push_str(&format!("version = \"{version}\"\n\n"));
out.push_str(&format!("version = \"{version}\"\n"));
for key in [
"type",
"description",
"author",
"authors",
"homepage",
"license",
"repository",
"readme",
"keywords",
"categories",
] {
if let Some(val) = package.get(key) {
match val {
Value::String(s) => out.push_str(&format!("{key} = \"{s}\"\n")),
Value::Array(_) => out.push_str(&format!("{key} = {}\n", format_string_array(val))),
_ => {}
}
}
}
out.push('\n');
out.push_str("[build]\n");
out.push_str(&format!("language = \"{language}\"\n"));
if language.starts_with('[') {
out.push_str(&format!("language = {language}\n"));
} else {
out.push_str(&format!("language = \"{language}\"\n"));
}
out.push_str(&format!("standard = \"{standard}\"\n"));
out.push_str(&format!("compiler = \"{compiler}\"\n"));
let kind = build
@@ -297,21 +683,36 @@ fn format_dep_value(value: &Value) -> String {
Value::String(s) => format!("\"{s}\""),
Value::Table(tbl) => {
let mut parts = Vec::new();
if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) {
parts.push(format!("version = \"{v}\""));
}
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
parts.push(format!("path = \"{v}\""));
}
if let Some(v) = tbl.get("git").and_then(|v| v.as_str()) {
parts.push(format!("git = \"{v}\""));
}
if let Some(v) = tbl.get("branch").and_then(|v| v.as_str()) {
parts.push(format!("branch = \"{v}\""));
}
if let Some(v) = tbl.get("tag").and_then(|v| v.as_str()) {
parts.push(format!("tag = \"{v}\""));
}
if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) {
parts.push(format!("rev = \"{v}\""));
}
if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) {
parts.push(format!(
"default-features = {}",
if v { "true" } else { "false" }
));
}
if let Some(v) = tbl.get("features") {
parts.push(format!("features = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) {
parts.push(format!("system = {}", if v { "true" } else { "false" }));
}
if let Some(v) = tbl.get("include") {
parts.push(format!("include = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("lib") {
parts.push(format!("lib = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("libs") {
parts.push(format!("libs = {}", format_string_array(v)));
}
format!("{{ {} }}", parts.join(", "))
}
_ => "\"\"".to_string(),
@@ -359,3 +760,266 @@ fn set_path(value: &mut Value, path: &[&str], new_value: Value) -> Result<(), Co
Err(ConfigError::Invalid("empty key".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
fn temp_dir(prefix: &str) -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("dcr_cfg_test_{prefix}_{n}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn write_toml_file(dir: &Path, content: &str) -> PathBuf {
let path = dir.join("dcr.toml");
fs::write(&path, content).unwrap();
path
}
fn minimal_valid_toml() -> &'static str {
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\n"
}
#[test]
fn open_valid_toml() {
let dir = temp_dir("open_valid");
let path = write_toml_file(&dir, minimal_valid_toml());
let config = Config::open(&path.to_string_lossy());
assert!(config.is_ok(), "should open valid toml");
}
#[test]
fn exposes_typed_config() {
let dir = temp_dir("typed_config");
let path = write_toml_file(
&dir,
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
);
let config = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(config.package().name, "typed");
assert_eq!(config.typed().package.version, "1.2.3");
assert_eq!(config.build_config().compiler, "clang");
assert_eq!(config.build_config().cflags, ["-Wall"]);
assert!(config.typed().dependencies.contains_key("foo"));
}
#[test]
fn open_invalid_toml_syntax() {
let dir = temp_dir("open_invalid");
let path = write_toml_file(&dir, "this is not [valid toml !!!");
let config = Config::open(&path.to_string_lossy());
assert!(config.is_err(), "should fail on invalid TOML syntax");
}
#[test]
fn open_nonexistent_fails() {
let result = Config::open("/tmp/dcr_nonexistent_file_12345.toml");
assert!(result.is_err(), "should fail on nonexistent file");
}
#[test]
fn validate_missing_package_fails() {
let dir = temp_dir("no_package");
let path = write_toml_file(
&dir,
"[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "missing [package] should fail validation");
}
#[test]
fn validate_missing_build_fails() {
let dir = temp_dir("no_build");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "missing [build] should fail validation");
}
#[test]
fn validate_wrong_field_type_fails() {
let dir = temp_dir("wrong_type");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = [\"clang\"]\nkind = \"bin\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(
result.is_err(),
"typed config should reject wrong field types"
);
}
#[test]
fn validate_invalid_package_names_fail() {
for name in [
"../evil", "bad/name", "bad name", "", " name", "name ", "a.b",
] {
let dir = temp_dir("bad_name");
let content = format!(
"[package]\nname = \"{name}\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\n"
);
let path = write_toml_file(&dir, &content);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "package name `{name}` should fail");
}
}
#[test]
fn validate_empty_language_fails() {
let dir = temp_dir("empty_lang");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"\"\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "empty language should fail validation");
}
#[test]
fn validate_language_array() {
let dir = temp_dir("lang_array");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = [\"c\", \"c++\", \"asm\"]\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_ok(), "language array should be valid");
}
#[test]
fn validate_language_array_empty_fails() {
let dir = temp_dir("lang_array_empty");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = []\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "empty language array should fail");
}
#[test]
fn validate_unknown_kind_fails() {
let dir = temp_dir("bad_kind");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"exe\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_err(), "unknown kind 'exe' should fail validation");
}
#[test]
fn validate_valid_kinds() {
for &kind in VALID_KINDS {
let dir = temp_dir("valid_kind");
let toml = format!(
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"{kind}\"\n\n[dependencies]\n"
);
let path = write_toml_file(&dir, &toml);
assert!(
Config::open(&path.to_string_lossy()).is_ok(),
"kind '{kind}' should be valid"
);
}
}
#[test]
fn get_values() {
let dir = temp_dir("get_values");
let path = write_toml_file(&dir, minimal_valid_toml());
let config = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(
config.get("package.name").and_then(|v| v.as_str()),
Some("test")
);
assert_eq!(
config.get("build.language").and_then(|v| v.as_str()),
Some("c")
);
assert_eq!(
config.get("build.kind").and_then(|v| v.as_str()),
Some("bin")
);
assert!(config.get("nonexistent.key").is_none());
}
#[test]
fn set_and_read_back() {
let dir = temp_dir("set_value");
let path = write_toml_file(&dir, minimal_valid_toml());
let mut config = Config::open(&path.to_string_lossy()).unwrap();
config
.set("package.name", Value::String("newname".to_string()))
.unwrap();
assert_eq!(
config.get("package.name").and_then(|v| v.as_str()),
Some("newname")
);
// Verify persisted to disk
let config2 = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(
config2.get("package.name").and_then(|v| v.as_str()),
Some("newname")
);
}
#[test]
fn new_creates_default_config() {
let dir = temp_dir("new_default");
let path = dir.join("dcr.toml");
let config = Config::new(&path.to_string_lossy()).unwrap();
assert!(path.exists(), "dcr.toml should be created");
assert_eq!(
config.get("build.language").and_then(|v| v.as_str()),
Some("c")
);
assert_eq!(
config.get("build.standard").and_then(|v| v.as_str()),
Some("c11")
);
assert_eq!(
config.get("build.compiler").and_then(|v| v.as_str()),
Some("clang")
);
assert_eq!(
config.get("build.kind").and_then(|v| v.as_str()),
Some("bin")
);
}
#[test]
fn validate_workspace_empty_path_fails() {
let dir = temp_dir("ws_empty_path");
let path = write_toml_file(
&dir,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\n\n[workspace]\n[workspace.member1]\npath = \"\"\n\n[dependencies]\n",
);
let result = Config::open(&path.to_string_lossy());
assert!(
result.is_err(),
"workspace member with empty path should fail"
);
}
#[test]
fn check_returns_bool() {
let dir = temp_dir("check_bool");
let path = write_toml_file(&dir, minimal_valid_toml());
let config = Config::open(&path.to_string_lossy()).unwrap();
assert!(config.check(), "valid config should return true");
}
}

View File

@@ -1,353 +0,0 @@
use crate::core::config::Config;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub path_raw: String,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
pub fn resolve_deps(
config: &Config,
profile: &str,
project_root: &Path,
) -> Result<ResolvedDeps, String> {
let project_name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let project_version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let deps = parse_dependencies(config)?;
if deps.is_empty() {
return Ok(ResolvedDeps {
include_dirs: Vec::new(),
lib_dirs: Vec::new(),
libs: Vec::new(),
});
}
let mut include_dirs = Vec::new();
let mut lib_dirs = Vec::new();
let mut libs = Vec::new();
let mut lock_packages = Vec::new();
for dep in deps {
let dep_path = resolve_path(project_root, &dep.path_raw)?;
if !dep_path.is_dir() {
return Err(format!(
"Dependency '{}' path is not a directory: {}",
dep.name,
dep_path.display()
));
}
let include = resolve_paths(&dep_path, dep.include_raw.as_deref(), &["include"])?;
let lib = resolve_paths(&dep_path, dep.lib_raw.as_deref(), &["lib", "lib64"])?;
let libs_list = dep
.libs_raw
.clone()
.unwrap_or_else(|| vec![dep.name.clone()]);
if include.is_empty() || lib.is_empty() {
return Err(format!(
"Dependency '{}' missing include/lib dirs. Add include/lib in dcr.toml.",
dep.name
));
}
include_dirs.extend(include.iter().map(|p| p.to_string_lossy().to_string()));
lib_dirs.extend(lib.iter().map(|p| p.to_string_lossy().to_string()));
libs.extend(libs_list.iter().cloned());
let dep_cache = project_root
.join("target")
.join(profile)
.join("deps")
.join(&dep.name);
sync_dep_dir(&dep_path, &dep_cache)
.map_err(|err| format!("Failed to sync dep {}: {err}", dep.name))?;
let dep_version = read_dep_version(&dep_path).unwrap_or_else(|| "0.0.0".to_string());
let dep_checksum = compute_checksum(&dep_path)
.map_err(|err| format!("Failed to hash dep {}: {err}", dep.name))?;
lock_packages.push(DepLock {
name: dep.name.clone(),
version: dep_version,
checksum: dep_checksum,
source: format!("path+{}", dep.path_raw),
});
}
write_lock(
project_root,
&project_name,
&project_version,
&lock_packages,
)?;
Ok(ResolvedDeps {
include_dirs,
lib_dirs,
libs,
})
}
#[derive(Debug, Clone)]
struct DepLock {
name: String,
version: String,
checksum: String,
source: String,
}
fn parse_dependencies(config: &Config) -> Result<Vec<DepSpec>, String> {
let deps_val = match config.get("dependencies") {
Some(v) => v,
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();
for (name, value) in deps_table {
match value {
Value::Table(tbl) => {
if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) {
return Err(format!(
"Dependency '{}' uses system=true, which is not supported yet",
name
));
}
let path = tbl
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("Dependency '{}' missing path", name))?
.to_string();
let include = parse_string_list(tbl.get("include"), name, "include")?;
let lib = parse_string_list(tbl.get("lib"), name, "lib")?;
let libs = parse_string_list(tbl.get("libs"), name, "libs")?;
deps.push(DepSpec {
name: name.to_string(),
path_raw: path,
include_raw: include,
lib_raw: lib,
libs_raw: libs,
});
}
_ => {
return Err(format!(
"Dependency '{}' must be a table with path/include/lib",
name
));
}
}
}
deps.sort_by(|a, b| a.name.cmp(&b.name));
Ok(deps)
}
fn parse_string_list(
value: Option<&Value>,
dep_name: &str,
key: &str,
) -> Result<Option<Vec<String>>, String> {
let Some(value) = value else { return Ok(None) };
let list = value.as_array().ok_or_else(|| {
format!(
"Dependency '{}' field '{}' must be an array of strings",
dep_name, key
)
})?;
let mut out = Vec::new();
for item in list {
let s = item
.as_str()
.ok_or_else(|| format!("Dependency '{}' field '{}' must be strings", dep_name, key))?;
out.push(s.to_string());
}
Ok(Some(out))
}
fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let p = Path::new(r);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}
fn write_lock(
project_root: &Path,
project_name: &str,
project_version: &str,
packages: &[DepLock],
) -> Result<(), String> {
let mut out = String::new();
if !packages.is_empty() {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(project_name)));
out.push_str(&format!(
"version = \"{}\"\n",
escape_value(project_version)
));
out.push_str(&format!(
"dependencies = [{}]\n\n",
quote_list(&packages.iter().map(|p| p.name.clone()).collect::<Vec<_>>())
));
}
for pkg in packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
out.push_str(&format!("version = \"{}\"\n", escape_value(&pkg.version)));
out.push_str(&format!("source = \"{}\"\n", escape_value(&pkg.source)));
out.push_str(&format!("checksum = \"{}\"\n", escape_value(&pkg.checksum)));
out.push('\n');
}
fs::write(project_root.join("dcr.lock"), out)
.map_err(|err| format!("Failed to write dcr.lock: {err}"))?;
Ok(())
}
fn quote_list(items: &[String]) -> String {
items
.iter()
.map(|s| format!("\"{}\"", escape_value(s)))
.collect::<Vec<_>>()
.join(", ")
}
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

111
src/core/deps/common.rs Normal file
View File

@@ -0,0 +1,111 @@
#[allow(dead_code)]
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum DepSource {
Path(String),
Git {
url: String,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
},
Registry {
version: String,
features: Option<Vec<String>>,
},
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub source: DepSource,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
#[allow(dead_code)]
pub fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
#[allow(dead_code)]
pub fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
#[allow(dead_code)]
pub fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
#[allow(dead_code)]
pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
#[allow(dead_code)]
pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}

54
src/core/deps/git.rs Normal file
View File

@@ -0,0 +1,54 @@
use std::path::Path;
#[allow(dead_code)]
pub fn fetch_git_dep(
url: &str,
target_dir: &Path,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<(), String> {
if target_dir.exists() {
return Ok(());
}
let mut builder = git2::build::RepoBuilder::new();
if let Some(b) = branch {
builder.branch(b);
}
let repo = builder
.clone(url, target_dir)
.map_err(|e| format!("failed to clone {}: {}", url, e))?;
if let Some(t) = tag {
checkout_ref(&repo, &format!("refs/tags/{}", t))?;
} else if let Some(r) = rev {
checkout_ref(&repo, r)?;
}
Ok(())
}
#[allow(dead_code)]
fn checkout_ref(repo: &git2::Repository, reference_str: &str) -> Result<(), String> {
let (object, reference) = repo
.revparse_ext(reference_str)
.map_err(|e| format!("failed to find ref {}: {}", reference_str, e))?;
repo.checkout_tree(&object, None).map_err(|e| {
let ref_name = reference.as_ref().and_then(|r| r.name()).unwrap_or("HEAD");
format!("failed to checkout {}: {}", ref_name, e)
})?;
match reference {
Some(gref) => {
let ref_name = gref.name().unwrap_or("HEAD");
repo.set_head(ref_name)
}
None => repo.set_head_detached(object.id()),
}
.map_err(|e| format!("failed to set HEAD: {}", e))?;
Ok(())
}

120
src/core/deps/lock.rs Normal file
View File

@@ -0,0 +1,120 @@
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepLock {
pub name: String,
pub version: String,
pub checksum: String,
pub source: String,
}
pub fn write_lock(
project_root: &Path,
project_name: &str,
project_version: &str,
packages: &[DepLock],
) -> Result<(), String> {
let mut out = String::new();
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(project_name)));
out.push_str(&format!(
"version = \"{}\"\n",
escape_value(project_version)
));
if !packages.is_empty() {
out.push_str(&format!(
"dependencies = [{}]\n",
quote_list(&packages.iter().map(|p| p.name.clone()).collect::<Vec<_>>())
));
}
out.push('\n');
for pkg in packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
out.push_str(&format!("version = \"{}\"\n", escape_value(&pkg.version)));
out.push_str(&format!("source = \"{}\"\n", escape_value(&pkg.source)));
out.push_str(&format!("checksum = \"{}\"\n", escape_value(&pkg.checksum)));
out.push('\n');
}
fs::write(project_root.join("dcr.lock"), out)
.map_err(|err| format!("Failed to write dcr.lock: {err}"))?;
Ok(())
}
fn quote_list(items: &[String]) -> String {
items
.iter()
.map(|s| format!("\"{}\"", escape_value(s)))
.collect::<Vec<_>>()
.join(", ")
}
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
#[allow(dead_code)]
pub fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[allow(dead_code)]
pub fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
#[allow(dead_code)]
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
#[allow(dead_code)]
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

227
src/core/deps/mod.rs Normal file
View File

@@ -0,0 +1,227 @@
pub mod common;
pub mod git;
pub mod lock;
pub mod register;
use crate::core::config::Config;
use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock};
use std::path::Path;
fn dep_version(path: &Path) -> String {
Config::open(&path.join("dcr.toml").to_string_lossy())
.ok()
.and_then(|c| {
c.get("package.version")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.unwrap_or_default()
}
pub fn resolve_deps(
config: &Config,
_profile: &str,
_target: Option<&str>,
project_root: &Path,
) -> Result<ResolvedDeps, String> {
let mut resolved = ResolvedDeps::default();
let project_name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let project_version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let mut lock_packages: Vec<DepLock> = Vec::new();
if let Some(deps) = deps_table {
for (name, value) in deps {
if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
resolved.include_dirs.push(
register::registry_include_dir(&dep_root)
.to_string_lossy()
.to_string(),
);
resolved.lib_dirs.push(
register::registry_lib_dir(&dep_root)
.to_string_lossy()
.to_string(),
);
resolved.libs.push(name.clone());
lock_packages.push(DepLock {
name: name.clone(),
version: pkg_info
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
checksum: String::new(),
source: format!(
"registry+{}",
pkg_info
.get("registry_url")
.and_then(|v| v.as_str())
.unwrap_or("https://dcr-registry.pages.dev")
),
});
} else if let Some(path) = path_dep_path(value) {
let dep_root = project_root.join(path);
if let Some(table) = value.as_table() {
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
for inc in includes {
if let Some(inc_str) = inc.as_str() {
resolved
.include_dirs
.push(dep_root.join(inc_str).to_string_lossy().to_string());
}
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
}
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
for lib_dir in lib_dirs {
if let Some(lib_dir_str) = lib_dir.as_str() {
resolved
.lib_dirs
.push(dep_root.join(lib_dir_str).to_string_lossy().to_string());
}
}
} else {
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
}
if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) {
for lib in libs {
if let Some(lib_str) = lib.as_str() {
resolved.libs.push(lib_str.to_string());
}
}
} else {
resolved.libs.push(name.clone());
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
resolved.libs.push(name.clone());
}
lock_packages.push(DepLock {
name: name.clone(),
version: dep_version(&dep_root),
checksum: String::new(),
source: format!("path+{}", dep_root.display()),
});
} else if let Some(git_info) = git_dep(value) {
lock_packages.push(DepLock {
name: name.clone(),
version: git_info.version.unwrap_or_default(),
checksum: String::new(),
source: format!("git+{}", git_info.url),
});
}
}
}
write_lock(
project_root,
&project_name,
&project_version,
&lock_packages,
)?;
Ok(resolved)
}
struct GitDep<'a> {
url: &'a str,
version: Option<String>,
}
fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
if let Some(table) = value.as_table()
&& let Some(url) = table.get("git").and_then(|v| v.as_str())
{
let version = table
.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
return Some(GitDep { url, version });
}
None
}
fn path_dep_path(value: &toml::Value) -> Option<&str> {
if let Some(table) = value.as_table() {
return table.get("path").and_then(|v| v.as_str());
}
register::path_from_string_dep(value)
}
fn push_if_exists(paths: &mut Vec<String>, path: &Path) {
if path.exists() {
paths.push(path.to_string_lossy().to_string());
}
}
fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
for dir in ["lib", "lib64"] {
push_if_exists(paths, &dep_root.join(dir));
}
push_if_exists(paths, &dep_root.join("target").join("lib"));
}
#[cfg(test)]
mod tests {
use super::*;
use toml::Value;
#[test]
fn path_dep_path_supports_table_and_legacy_strings() {
let table = Value::Table(
[(
"path".to_string(),
Value::String("./libs/mylib".to_string()),
)]
.into_iter()
.collect(),
);
assert_eq!(path_dep_path(&table), Some("./libs/mylib"));
assert_eq!(
path_dep_path(&Value::String("path:./libs/mylib".to_string())),
Some("./libs/mylib")
);
assert_eq!(
path_dep_path(&Value::String("./libs/mylib".to_string())),
Some("./libs/mylib")
);
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
}
#[test]
fn default_lib_dirs_include_packaged_library_output() {
let root = std::env::temp_dir().join(format!(
"dcr_default_lib_dirs_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(root.join("target/lib")).unwrap();
let mut paths = Vec::new();
push_default_lib_dirs(&mut paths, &root);
assert!(paths.iter().any(|p| p.ends_with("target/lib")));
let _ = std::fs::remove_dir_all(root);
}
}

223
src/core/deps/register.rs Normal file
View File

@@ -0,0 +1,223 @@
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value as TomlValue;
#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DcrConfig {
pub registry: std::collections::HashMap<String, Registry>,
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
}
fn dcr_config_dir() -> Option<PathBuf> {
home_dir().map(|h| h.join(".dcr"))
}
pub fn get_registry_config() -> Option<DcrConfig> {
let home = home_dir()?;
let config_path = home.join(".dcr/config.toml");
if !config_path.exists() {
return None;
}
let content = fs::read_to_string(config_path).ok()?;
toml::from_str(&content).ok()
}
pub fn get_index_path() -> PathBuf {
std::env::var("DCR_INDEX_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dcr_config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("index.json")
})
}
pub fn get_registry_cache_root() -> PathBuf {
let index_path = get_index_path();
if let Some(parent) = index_path.parent() {
if parent.exists() {
return parent.to_path_buf();
}
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("Warning: failed to create registry cache directory: {e}");
}
parent.to_path_buf()
} else {
PathBuf::from(".")
}
}
pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf, String> {
let raw_path = pkg_info
.get("path")
.and_then(|v| v.as_str())
.ok_or("Registry package is missing path")?;
if raw_path.trim().is_empty() {
return Err("Registry package path is empty".to_string());
}
let path = Path::new(raw_path);
let full_path = if path.is_absolute() {
path.to_path_buf()
} else {
get_registry_cache_root().join(path)
};
if full_path.file_name().and_then(|v| v.to_str()) == Some("dcr.toml") {
return full_path
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| format!("Invalid registry package path: {}", full_path.display()));
}
Ok(full_path)
}
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("include")
}
pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("lib")
}
pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
let config = get_registry_config().ok_or("No registry config found")?;
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
registries.sort_by_key(|b| std::cmp::Reverse(b.1.priority));
for (_name_reg, _reg) in registries {
let index_path = get_index_path();
if index_path.exists() {
let index_content = fs::read_to_string(&index_path).map_err(|e| e.to_string())?;
let index: JsonValue =
serde_json::from_str(&index_content).map_err(|e| e.to_string())?;
if let Some(pkgs) = index.get("packages").and_then(|v| v.as_array()) {
for pkg in pkgs {
if pkg.get("name").and_then(|v| v.as_str()) == Some(name) {
return Ok(pkg.clone());
}
}
}
}
}
Err(format!(
"Package {} not found in registry (checked: {:?})",
name,
get_index_path()
))
}
pub fn is_registry_dep(value: &TomlValue) -> bool {
if let Some(raw) = value.as_str() {
let raw = raw.trim();
return !is_path_like_string(raw) && !is_git_like_string(raw);
}
if let Some(table) = value.as_table() {
if table.contains_key("git") || table.contains_key("path") || table.contains_key("url") {
return false;
}
table.contains_key("version")
|| table.contains_key("features")
|| table.contains_key("optional")
|| table.contains_key("registry")
} else {
false
}
}
pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
let raw = value.as_str()?.trim();
if let Some(path) = raw.strip_prefix("path:") {
return Some(path);
}
if is_path_like_string(raw) {
return Some(raw);
}
None
}
fn is_path_like_string(raw: &str) -> bool {
raw.starts_with("path:")
|| raw.starts_with("./")
|| raw.starts_with("../")
|| raw.starts_with('/')
|| raw.starts_with("~/")
|| raw.contains('\\')
}
fn is_git_like_string(raw: &str) -> bool {
raw.starts_with("git:")
|| raw.starts_with("github:")
|| raw.starts_with("gitlab:")
|| raw.starts_with("http://")
|| raw.starts_with("https://")
|| raw.starts_with("git@")
}
#[cfg(test)]
mod tests {
use super::*;
use toml::map::Map;
#[test]
fn registry_dep_detection_excludes_local_and_git_strings() {
assert!(is_registry_dep(&TomlValue::String("1.2.3".to_string())));
assert!(!is_registry_dep(&TomlValue::String(
"path:./libs/mylib".to_string()
)));
assert!(!is_registry_dep(&TomlValue::String(
"./libs/mylib".to_string()
)));
assert!(!is_registry_dep(&TomlValue::String(
"git:https://example.com/repo.git".to_string()
)));
}
#[test]
fn registry_dep_detection_excludes_source_tables() {
let mut path_table = Map::new();
path_table.insert(
"path".to_string(),
TomlValue::String("./libs/mylib".to_string()),
);
assert!(!is_registry_dep(&TomlValue::Table(path_table)));
let mut registry_table = Map::new();
registry_table.insert("features".to_string(), TomlValue::Array(Vec::new()));
assert!(is_registry_dep(&TomlValue::Table(registry_table)));
}
#[test]
fn registry_package_root_accepts_package_dir_or_manifest_path() {
let pkg = serde_json::json!({ "path": "/tmp/pkg" });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
);
let pkg = serde_json::json!({ "path": "/tmp/pkg/dcr.toml" });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
);
}
}

View File

@@ -1,4 +1,6 @@
pub mod builder;
pub mod config;
pub mod deps;
pub mod registry;
pub mod runner;
pub mod workspace;

52
src/core/registry.rs Normal file
View File

@@ -0,0 +1,52 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub registry: HashMap<String, Registry>,
}
pub struct RegistryManager {
pub config: Config,
#[allow(dead_code)]
pub path: PathBuf,
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
}
impl RegistryManager {
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let dcr_dir = home_dir()
.ok_or("Cannot determine home directory")?
.join(".dcr");
let config_path = dcr_dir.join("config.toml");
if !config_path.exists() {
return Err("Config file ~/.dcr/config.toml not found".into());
}
let content = fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&content)?;
Ok(Self {
config,
path: dcr_dir,
})
}
}

View File

@@ -1,37 +1,21 @@
use crate::platform;
use crate::utils::fs::check_dir;
use crate::utils::log::warn;
use std::path::Path;
use std::process::Command;
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);
if Path::new(&bin_path).exists() {
return Command::new(bin_path)
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
let output = Command::new(&bin_path).output();
match output {
Ok(o) => {
print!("{}", String::from_utf8_lossy(&o.stdout));
eprint!("{}", String::from_utf8_lossy(&o.stderr));
return o.status.code().unwrap_or(0);
}
Err(_) => {
return 1;
}
}
}
if target_dir.is_none()
&& check_dir(Some(&format!("target/{profile}")))
.unwrap_or_default()
.contains(&profile.to_string())
{
warn("Launch of the latest release");
return Command::new(platform::bin_path(profile, project_name, target_dir))
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
if target_dir.is_some() && Path::new(&bin_path).exists() {
warn("Launch of the latest release");
return Command::new(bin_path)
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
1
}

150
src/core/workspace.rs Normal file
View File

@@ -0,0 +1,150 @@
use crate::core::config::Config;
use crate::utils::build::normalize_target_os;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use toml::Value;
#[derive(Debug, Clone)]
pub struct WorkspaceMember {
pub name: String,
pub path: PathBuf,
pub deps: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Workspace {
pub members: Vec<WorkspaceMember>,
}
pub fn parse_workspace(
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 = 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,
None => return Ok(None),
};
let mut members = Vec::new();
for (name, value) in table {
let tbl = value
.as_table()
.ok_or_else(|| format!("workspace.{name} must be a table with path and deps"))?;
let path_raw = tbl
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("workspace.{name}.path is required"))?;
let path = resolve_path(root, path_raw)?;
if !path.join("dcr.toml").is_file() {
return Err(format!(
"workspace.{name}.path does not contain dcr.toml: {}",
path.display()
));
}
let deps = parse_deps(tbl.get("deps"))?;
members.push(WorkspaceMember {
name: name.to_string(),
path,
deps,
});
}
let mut ws = Workspace { members };
ws.members = topo_sort(&ws.members)?;
Ok(Some(ws))
}
fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
root.join(p)
};
Ok(full)
}
fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
let Some(value) = value else {
return Ok(Vec::new());
};
let arr = value
.as_array()
.ok_or_else(|| "workspace deps must be an array of strings".to_string())?;
let mut out = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| "workspace deps must be an array of strings".to_string())?;
out.push(s.to_string());
}
Ok(out)
}
fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String> {
let mut map = HashMap::new();
for m in members {
map.insert(m.name.clone(), m.clone());
}
let mut state: HashMap<String, u8> = HashMap::new();
let mut order: Vec<WorkspaceMember> = Vec::new();
for name in map.keys() {
if *state.get(name).unwrap_or(&0) == 0 {
visit(name, &map, &mut state, &mut order)?;
}
}
Ok(order)
}
fn visit(
name: &str,
map: &HashMap<String, WorkspaceMember>,
state: &mut HashMap<String, u8>,
order: &mut Vec<WorkspaceMember>,
) -> Result<(), String> {
match state.get(name).copied().unwrap_or(0) {
1 => return Err(format!("workspace dependency cycle at {name}")),
2 => return Ok(()),
_ => {}
}
state.insert(name.to_string(), 1);
let member = map
.get(name)
.ok_or_else(|| format!("unknown workspace member {name}"))?;
let mut seen = HashSet::new();
for dep in &member.deps {
if !seen.insert(dep) {
continue;
}
if !map.contains_key(dep) {
return Err(format!("workspace dependency '{dep}' not found"));
}
visit(dep, map, state, order)?;
}
state.insert(name.to_string(), 2);
order.push(member.clone());
Ok(())
}

View File

@@ -1,3 +1,4 @@
#![feature(let_chains)]
use crate::utils::log::error;
mod cli;
mod config;
@@ -18,9 +19,15 @@ fn main() {
let code = match cmd {
"new" => cli::r#new::new(rest),
"init" => cli::init::init(rest),
"setup" => cli::setup::setup(rest),
"add" => cli::add::add(rest),
"build" => cli::build::build(rest),
"run" => cli::run::run(rest),
"tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest),
"fmt" => cli::fmt::fmt(rest),
"clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),
"--version" => {
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET"));
0

View File

@@ -1,20 +1,110 @@
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}")
}
}
}
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/lib{name}.a")
}
}
}
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}")
}
}
}
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}.efi")
}
}
}
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/lib{name}.so")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bin_path_default() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/hello");
assert_eq!(bin_path("debug", "hello", None), expected);
}
#[test]
fn bin_path_release() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/release/hello");
assert_eq!(bin_path("release", "hello", None), expected);
}
#[test]
fn bin_path_custom_target() {
assert_eq!(bin_path("debug", "hello", Some("out")), "out/hello");
}
#[test]
fn bin_path_custom_target_trailing_slash() {
assert_eq!(bin_path("debug", "hello", Some("out/")), "out/hello");
}
#[test]
fn lib_path_default() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.a");
assert_eq!(lib_path("debug", "mylib", None), expected);
}
#[test]
fn lib_path_custom_target() {
assert_eq!(lib_path("debug", "mylib", Some("out")), "out/libmylib.a");
}
#[test]
fn shared_lib_path_default() {
let arch = std::env::consts::ARCH;
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.so");
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
}
#[test]
fn shared_lib_path_custom_target() {
assert_eq!(
shared_lib_path("release", "mylib", Some("dist")),
"dist/libmylib.so"
);
}
}

View File

@@ -12,6 +12,20 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
}
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
}
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
}
}
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.dylib", dir.trim_end_matches('/'), name),

View File

@@ -27,6 +27,50 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
}
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
linux::elf_path(profile, name, target_dir)
}
#[cfg(target_os = "macos")]
{
return macos::elf_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
return windows::elf_path(profile, name, target_dir);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
}
}
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
linux::efi_path(profile, name, target_dir)
}
#[cfg(target_os = "macos")]
{
return macos::efi_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
return windows::efi_path(profile, name, target_dir);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
}
}
}
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{

View File

@@ -12,6 +12,20 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
}
}
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.exe"),
}
}
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
}
}
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.dll", dir.trim_end_matches('/'), name),

376
src/utils/build.rs Normal file
View File

@@ -0,0 +1,376 @@
use crate::core::config::Config;
use crate::utils::log::warn;
pub struct VersionInfo {
pub full: String,
pub major: String,
pub minor: String,
pub patch: String,
pub suffix: String,
pub suffix_dash: String,
}
pub fn parse_version_info(version: &str) -> VersionInfo {
let mut full = version.trim().to_string();
if full.is_empty() {
full = "0.0.0".to_string();
}
let full_clone = full.clone();
let (base, suffix) = match full_clone.split_once('-') {
Some((head, tail)) => (head, tail),
None => (full_clone.as_str(), ""),
};
let mut parts = base.split('.');
let major = parts.next().unwrap_or("0").to_string();
let minor = parts.next().unwrap_or("0").to_string();
let patch = parts.next().unwrap_or("0").to_string();
let suffix_dash = if suffix.is_empty() {
"".to_string()
} else {
format!("-{suffix}")
};
VersionInfo {
full,
major,
minor,
patch,
suffix: suffix.to_string(),
suffix_dash,
}
}
pub fn normalize_target_os(target: &str) -> &str {
match target {
"linux" => "x86_64-unknown-linux-gnu",
"macos" => "x86_64-apple-darwin",
"windows" => "x86_64-pc-windows-msvc",
_ if target.contains('-') => target,
_ => {
warn(&format!(
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
target
));
target
}
}
}
pub fn get_config_str(config: &Config, key: &str) -> String {
config
.get(key)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> {
config
.get("build")
.and_then(|v| v.as_table())
.and_then(|b| b.get(profile))
.and_then(|v| v.as_table())
}
pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
let value = config.get(key)?.as_str()?;
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
let base = get_config_str(config, &format!("build.{field}"));
let Some(table) = profile_table(config, profile) else {
return base;
};
let value = table.get(field).and_then(|v| v.as_str()).unwrap_or("");
let trimmed = value.trim();
if trimmed.is_empty() {
base
} else {
trimmed.to_string()
}
}
pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
config
.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default()
}
pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec<String> {
let mut out = get_config_list(config, &format!("build.{field}"));
if let Some(table) = profile_table(config, profile)
&& let Some(extra) = table.get(field).and_then(|v| v.as_array())
{
out.extend(
extra
.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string()),
);
}
out
}
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
let base = config
.get(&format!("build.{field}"))
.and_then(|v| v.as_bool());
let profile_val =
profile_table(config, profile).and_then(|t| t.get(field).and_then(|v| v.as_bool()));
profile_val.or(base).unwrap_or(default)
}
pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<String, String> {
if let Some(table) = profile_table(config, profile)
&& let Some(value) = table.get("language")
{
return parse_language_value(value, "build.language");
}
match config.get("build.language") {
Some(v) => parse_language_value(v, "build.language"),
None => Err("build.language is missing".to_string()),
}
}
pub fn get_language_with_profile_or_default(config: &Config, profile: &str) -> String {
get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string())
}
pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, String> {
if let Some(s) = value.as_str() {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(format!("{key} is empty"));
}
return Ok(trimmed.to_string());
}
let arr = value
.as_array()
.ok_or_else(|| format!("{key} must be string or array of strings"))?;
let mut parts = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| format!("{key} must be string or array of strings"))?;
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(format!("{key} contains empty value"));
}
parts.push(trimmed.to_string());
}
if parts.is_empty() {
return Err(format!("{key} is empty"));
}
Ok(parts.join("+"))
}
pub fn resolve_compiler(
language: &str,
compiler: &str,
tc_cc: Option<&str>,
tc_cxx: Option<&str>,
tc_as: Option<&str>,
) -> String {
let lang = primary_language(language);
env_override_compiler(&lang)
.or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as))
.unwrap_or_else(|| compiler.to_string())
}
fn env_override_compiler(lang: &str) -> Option<String> {
if let Ok(value) = std::env::var("DCR_COMPILER") {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
if lang == "asm" {
if let Ok(value) = std::env::var("DCR_AS") {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
return None;
}
if (lang == "c++" || lang == "cpp" || lang == "cxx")
&& let Ok(value) = std::env::var("DCR_CXX")
{
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
if let Ok(value) = std::env::var("DCR_CC") {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
None
}
fn toolchain_override_compiler(
lang: &str,
tc_cc: Option<&str>,
tc_cxx: Option<&str>,
tc_as: Option<&str>,
) -> Option<String> {
if lang == "asm" {
return tc_as.map(|v| v.to_string());
}
if (lang == "c++" || lang == "cpp" || lang == "cxx")
&& let Some(v) = tc_cxx
{
return Some(v.to_string());
}
tc_cc.map(|v| v.to_string())
}
pub fn primary_language(language: &str) -> String {
let parts: Vec<String> = language
.split('+')
.map(|p| p.trim().to_lowercase())
.filter(|p| !p.is_empty())
.collect();
for p in &parts {
if p == "c++" || p == "cpp" || p == "cxx" {
return p.clone();
}
}
if parts.iter().any(|p| p == "c") {
return "c".to_string();
}
if parts.iter().any(|p| p == "asm") {
return "asm".to_string();
}
language.to_lowercase()
}
pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
if let Ok(value) = std::env::var(env_key) {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
fallback.map(|v| v.to_string())
}
pub fn resolve_pkg_config_flags(
pkgs: &[String],
base_cflags: &[String],
base_ldflags: &[String],
) -> Result<(Vec<String>, Vec<String>), String> {
let mut cflags = base_cflags.to_vec();
let mut ldflags = base_ldflags.to_vec();
for pkg in pkgs {
let c_out = run_pkg_config(pkg, "--cflags")?;
let l_out = run_pkg_config(pkg, "--libs")?;
cflags.extend(split_flags(&c_out));
ldflags.extend(split_flags(&l_out));
}
Ok((cflags, ldflags))
}
pub fn resolve_pkg_config_flags_lossy(
pkgs: &[String],
base_cflags: &[String],
base_ldflags: &[String],
) -> (Vec<String>, Vec<String>) {
match resolve_pkg_config_flags(pkgs, base_cflags, base_ldflags) {
Ok(flags) => flags,
Err(err) => {
eprintln!("Warning: {err}");
(base_cflags.to_vec(), base_ldflags.to_vec())
}
}
}
fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
let output = std::process::Command::new("pkg-config")
.arg(arg)
.arg(pkg)
.output()
.map_err(|err| format!("Failed to run pkg-config: {err}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("pkg-config failed for {pkg}: {stderr}"));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn split_flags(value: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
let mut chars = value.chars().peekable();
let mut quote: Option<char> = None;
while let Some(ch) = chars.next() {
if let Some(q) = quote {
if ch == q {
quote = None;
} else {
current.push(ch);
}
continue;
}
match ch {
'\'' | '"' => quote = Some(ch),
'\\' => {
if let Some(next) = chars.next() {
current.push(next);
}
}
c if c.is_whitespace() => {
if !current.is_empty() {
out.push(std::mem::take(&mut current));
}
}
c => current.push(c),
}
}
if !current.is_empty() {
out.push(current);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_target_short_names() {
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
assert_eq!(
normalize_target_os("x86_64-unknown-linux-gnu"),
"x86_64-unknown-linux-gnu"
);
assert_eq!(normalize_target_os("unknown"), "unknown");
}
#[test]
fn parse_version_parts() {
let info = parse_version_info("1.2.3-beta");
assert_eq!(info.full, "1.2.3-beta");
assert_eq!(info.major, "1");
assert_eq!(info.minor, "2");
assert_eq!(info.patch, "3");
assert_eq!(info.suffix, "beta");
assert_eq!(info.suffix_dash, "-beta");
}
}

View File

@@ -1,5 +1,5 @@
use std::io;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
#[allow(dead_code)]
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
@@ -16,3 +16,27 @@ pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
Ok(items)
}
pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
let mut current = start.to_path_buf();
loop {
if current.join("dcr.toml").is_file() {
return Ok(Some(current));
}
if !current.pop() {
break;
}
}
Ok(None)
}
pub 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

@@ -1,3 +1,4 @@
pub mod build;
pub mod fs;
pub mod log;
pub mod text;

View File

@@ -19,25 +19,32 @@ fn bin_path() -> PathBuf {
path
}
fn unique_temp_dir(prefix: &str) -> PathBuf {
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = std::env::temp_dir();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
Command::new(bin_path())
.args(args)
.current_dir(cwd)
.output()
.expect("failed to run dcr")
run_dcr_env(args, cwd, &[])
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
@@ -51,9 +58,23 @@ fn ensure_bin_built() {
});
}
fn available_compiler() -> Option<&'static str> {
for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if ok {
return Some(candidate);
}
}
None
}
#[test]
fn help_and_version_work() {
let dir = unique_temp_dir("help");
let dir = unique_sandbox_dir("help");
let out = run_dcr(&["--help"], &dir);
assert!(out.status.success(), "--help should succeed");
@@ -65,7 +86,7 @@ fn help_and_version_work() {
#[test]
fn new_creates_project_layout() {
let dir = unique_temp_dir("new");
let dir = unique_sandbox_dir("new");
let out = run_dcr(&["new", "hello"], &dir);
assert!(out.status.success(), "dcr new should succeed");
@@ -80,7 +101,7 @@ fn new_creates_project_layout() {
#[test]
fn init_and_clean_remove_target() {
let dir = unique_temp_dir("init");
let dir = unique_sandbox_dir("init");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
@@ -92,3 +113,312 @@ fn init_and_clean_remove_target() {
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn build_run_clean_flags_normal_project() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping build/run test");
return;
};
let dir = unique_sandbox_dir("normal");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "dcr build should succeed");
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!(
!dir.join(&target_dir).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join(&target_dir).join("debug").is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
);
let out = run_dcr_env(&["clean"], &dir, &envs);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn workspace_build_and_clean_all() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping workspace test");
return;
};
let root = unique_sandbox_dir("workspace");
let out = run_dcr(&["init"], &root);
assert!(out.status.success(), "root init should succeed");
let members = [
("userspace", &[][..]),
("core", &["userspace"][..]),
("kernel", &["core"][..]),
];
for (name, _) in &members {
let member_dir = root.join("src").join(name);
std::fs::create_dir_all(&member_dir).expect("failed to create member dir");
let out = run_dcr(&["init"], &member_dir);
assert!(out.status.success(), "member init should succeed");
}
let workspace_toml = "[package]\nname = \"ws-root\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[workspace]\nuserspace = { path = \"src/userspace\", deps = [] }\ncore = { path = \"src/core\", deps = [\"userspace\"] }\nkernel = { path = \"src/kernel\", deps = [\"core\"] }\n\n[dependencies]\n";
std::fs::write(root.join("dcr.toml"), workspace_toml).expect("failed to write root dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build", "--release"], &root, &envs);
assert!(
out.status.success(),
"workspace build --release should succeed"
);
let out = run_dcr_env(&["clean", "--release", "--all"], &root, &envs);
assert!(
out.status.success(),
"workspace clean --all --release should succeed"
);
for (name, _) in &members {
let member = root.join("src").join(name);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!member.join(target_dir).join("release").exists(),
"member target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
member.join(target_dir).join("debug").exists(),
"member target/x86_64-unknown-linux-gnu/debug should remain"
);
}
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!root.join(target_dir).join("release").exists(),
"root target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
root.join(target_dir).join("debug").exists(),
"root target/x86_64-unknown-linux-gnu/debug should remain"
);
}
#[test]
fn dcr_add_dependencies() {
let dir = unique_sandbox_dir("add_dep");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
// Test path: prefix
let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir);
assert!(out.status.success(), "dcr add path should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("mylib = { path = \"./libs/mylib\" }"),
"path dep not found in toml"
);
// Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"),
"github dep not found in toml"
);
// Test git: prefix (generic)
let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir);
assert!(out.status.success(), "dcr add custom git should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"),
"custom git dep not found in toml"
);
// Test git: prefix (github default)
let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir);
assert!(out.status.success(), "dcr add git short should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("git_short = { git = \"https://github.com/user/repo\" }"),
"git short dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
&dir,
);
assert!(out.status.success(), "dcr add with branch should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"),
"branch lib not found in toml"
);
// Test failure on no prefix
let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir);
assert!(!out.status.success(), "dcr add without prefix should fail");
}
#[test]
fn dcr_builds_lib_package() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping lib package test");
return;
};
let dir = unique_sandbox_dir("lib_package");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
let updated_toml = toml
.replace("kind = \"bin\"", "kind = \"staticlib\"")
.replace("type = \"none\"", "type = \"lib\"");
std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml");
std::fs::write(dir.join("src").join("my_lib.h"), "void hello();")
.expect("failed to write header");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(out.status.success(), "dcr build should succeed");
let target_dir = dir.join("target");
assert!(
target_dir.join("include").join("my_lib.h").is_file(),
"include/my_lib.h missing"
);
assert!(target_dir.join("lib").exists(), "lib directory missing");
}
#[test]
fn registry_dependency_is_built_from_cache() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping registry dependency build test");
return;
};
let root = unique_sandbox_dir("registry_dep");
let home = root.join("home");
let dcr_home = home.join(".dcr");
let dep = root.join("cache").join("mylib");
let app = root.join("app");
std::fs::create_dir_all(dcr_home.as_path()).expect("failed to create dcr home");
std::fs::create_dir_all(dep.join("src")).expect("failed to create dep src");
std::fs::create_dir_all(app.join("src")).expect("failed to create app src");
std::fs::write(
dcr_home.join("config.toml"),
"[registry.local]\nurl = \"file://local\"\npriority = 1\n",
)
.expect("failed to write registry config");
std::fs::write(
dcr_home.join("index.json"),
format!(
"{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}",
dep.display()
),
)
.expect("failed to write registry index");
std::fs::write(
dep.join("dcr.toml"),
"[package]\nname = \"mylib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\n",
)
.expect("failed to write dep dcr.toml");
std::fs::write(dep.join("src").join("mylib.h"), "int answer(void);\n")
.expect("failed to write header");
std::fs::write(
dep.join("src").join("mylib.c"),
"int answer(void) { return 42; }\n",
)
.expect("failed to write dep source");
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\ntype = \"none\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmylib = \"0.1.0\"\n",
)
.expect("failed to write app dcr.toml");
std::fs::write(
app.join("src").join("main.c"),
"#include \"mylib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
)
.expect("failed to write app source");
let index_path = dcr_home.join("index.json");
let envs = [
("DCR_COMPILER", compiler),
("HOME", home.to_str().unwrap()),
("DCR_INDEX_PATH", index_path.to_str().unwrap()),
];
let out = run_dcr_env(&["build"], &app, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"registry dependency build should succeed"
);
assert!(
dep.join("target").join("include").join("mylib.h").is_file(),
"registry dependency headers were not packaged"
);
assert!(
dep.join("target").join("lib").exists(),
"registry dependency library directory missing"
);
}
#[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
);
}

303
tests/cli_errors.rs Normal file
View File

@@ -0,0 +1,303 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
cmd.output().expect("failed to run dcr")
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
fn available_compiler() -> Option<&'static str> {
for candidate in ["clang", "gcc", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if ok {
return Some(candidate);
}
}
None
}
// --- Tests ---
#[test]
fn no_args_shows_help() {
let dir = unique_sandbox_dir("noargs");
let out = run_dcr(&[], &dir);
// Should succeed (help returns 0) and produce some output
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("DCR") || stdout.contains("dcr") || stdout.contains("USAGE"),
"no args should show help text"
);
}
#[test]
fn unknown_command_fails() {
let dir = unique_sandbox_dir("unknown_cmd");
let out = run_dcr(&["foobar"], &dir);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("error") || stdout.contains("Unknown"),
"unknown command should print error"
);
}
#[test]
fn build_without_toml_fails() {
let dir = unique_sandbox_dir("build_no_toml");
let out = run_dcr(&["build"], &dir);
assert!(!out.status.success(), "build without dcr.toml should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("dcr.toml") || stdout.contains("not found"),
"should mention missing dcr.toml"
);
}
#[test]
fn run_without_toml_fails() {
let dir = unique_sandbox_dir("run_no_toml");
let out = run_dcr(&["run"], &dir);
assert!(!out.status.success(), "run without dcr.toml should fail");
}
#[test]
fn clean_without_toml_fails() {
let dir = unique_sandbox_dir("clean_no_toml");
let out = run_dcr(&["clean"], &dir);
assert!(!out.status.success(), "clean without dcr.toml should fail");
}
#[test]
fn new_existing_dir_fails() {
let dir = unique_sandbox_dir("new_exist");
// Create the project directory first
std::fs::create_dir_all(dir.join("hello")).expect("failed to create dir");
let out = run_dcr(&["new", "hello"], &dir);
assert!(!out.status.success(), "dcr new on existing dir should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("already exists") || stdout.contains("error"),
"should report dir already exists"
);
}
#[test]
fn init_nonempty_dir_fails() {
let dir = unique_sandbox_dir("init_nonempty");
// Put a file in the dir so it's not empty
std::fs::write(dir.join("dummy.txt"), "x").expect("failed to write");
let out = run_dcr(&["init"], &dir);
assert!(
!out.status.success(),
"dcr init in non-empty dir should fail"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("not empty") || stdout.contains("error"),
"should report dir not empty"
);
}
#[test]
fn build_unknown_flag_fails() {
let dir = unique_sandbox_dir("build_badflag");
// Init a valid project first
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
let out = run_dcr(&["build", "--foobar"], &dir);
assert!(!out.status.success(), "build with unknown flag should fail");
}
#[test]
fn run_library_project_fails() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping run_library test");
return;
};
let dir = unique_sandbox_dir("run_lib");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
// Edit dcr.toml to set kind = "staticlib"
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let updated = toml.replace("kind = \"bin\"", "kind = \"staticlib\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["run"], &dir, &envs);
assert!(!out.status.success(), "dcr run on staticlib should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("library") || stdout.contains("Cannot run"),
"should report cannot run library"
);
}
#[test]
fn clean_specific_profile() {
let dir = unique_sandbox_dir("clean_profile");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
// Create target/x86_64-unknown-linux-gnu/debug and release dirs
let target_base = "target/x86_64-unknown-linux-gnu";
std::fs::create_dir_all(dir.join(target_base).join("debug")).expect("create debug");
std::fs::write(dir.join(target_base).join("debug").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
let out = run_dcr(&["clean", "--release"], &dir);
assert!(out.status.success(), "clean --release should succeed");
assert!(
!dir.join(target_base).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join("target/x86_64-unknown-linux-gnu")
.join("debug")
.is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
);
}
#[test]
fn new_no_name_fails() {
let dir = unique_sandbox_dir("new_noname");
let out = run_dcr(&["new"], &dir);
assert!(!out.status.success(), "dcr new without name should fail");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("not specified") || stdout.contains("error"),
"should report name not specified"
);
}
#[test]
fn staticlib_build() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping staticlib test");
return;
};
let dir = unique_sandbox_dir("staticlib");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("read dcr.toml");
let updated = toml.replace("kind = \"bin\"", "kind = \"staticlib\"");
std::fs::write(&toml_path, updated).expect("write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "staticlib build should succeed");
// Check that a .a file exists
let lib_path = dir
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug");
let has_lib = std::fs::read_dir(&lib_path)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.file_name().to_string_lossy().ends_with(".a"))
})
.unwrap_or(false);
assert!(has_lib, "staticlib should produce a .a file");
}
#[test]
fn build_release_profile() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping release build test");
return;
};
let dir = unique_sandbox_dir("release_build");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "release build should succeed");
assert!(
dir.join("target").join("release").is_dir(),
"target/release should exist"
);
}
#[test]
fn version_contains_version_string() {
let dir = unique_sandbox_dir("version");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
// Should contain version like "dcr 0.2.8 (target)"
assert!(
stdout.contains("dcr "),
"version output should start with 'dcr '"
);
assert!(
stdout.contains('.'),
"version output should contain a version number"
);
}