70 Commits

Author SHA1 Message Date
a55d64c5ff feat: bump version to 0.6.8 in Cargo files 2026-05-27 20:18:49 +08:00
acdd1cc175 feat: enhance build configuration with C++ standard support and variable substitution
- Added `cxx_standard` field to the build configuration for specifying C++ standards.
- Updated documentation to reflect new `cxx_standard` and variable substitution capabilities in `cflags`, `ldflags`, and `include`.
- Implemented logic to handle C++ standard flags in the build process.
- Modified object file naming to include source file extensions.
- Updated various builder modules to support new language parsing logic.
- Introduced issue and feature request templates for better community engagement.
2026-05-27 20:04:01 +08:00
b5bbdb929b Add GPL-3.0-or-later license headers to source files
- Updated all relevant source files to include the new license header.
- Changed the default license in the configuration from MIT to GPL-3.0-or-later.
2026-05-27 17:34:56 +08:00
29120e0e29 feat: add Homebrew tap update workflow with asset download and formula generation 2026-05-26 20:20:39 +08: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
617ba85442 Added:
- Toolchain overrides via `[toolchain]` and env (`DCR_*`)
- ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`)
2026-03-03 22:25:00 +08:00
9af91a0d9f Added:
- `sharedlib` build kind (shared library output)
- ASM projects (`build.language = "asm"`)
- NASM and GAS backends
- `build.platform` architecture hint for `-march` / `/arch`
- Basic CLI integration tests
2026-03-01 20:04:08 +08:00
a247e33555 Added:
- `sharedlib` build kind (shared library output)
- ASM projects (`build.language = "asm"`)
- NASM and GAS backends
- `build.platform` architecture hint for `-march` / `/arch`
- Basic CLI integration tests
2026-03-01 19:58:43 +08:00
d1e3b0c384 Added:
- `sharedlib` build kind (shared library output)
- ASM projects (`build.language = "asm"`)
- NASM and GAS backends
- `build.platform` architecture hint for `-march` / `/arch`
- Basic CLI integration tests
2026-03-01 19:47:38 +08:00
98531c7335 Update TODO.md 2026-02-27 22:07:04 +08:00
75859a6a53 fix Cargo.toml - version 2026-02-27 21:36:13 +08:00
c22d2dbddc New docs 2026-02-26 18:09:07 +08:00
83061cc460 GITBOOK-6: No subject 2026-02-26 09:39:05 +00:00
467e64aa4f rename docs dir 2026-02-26 17:17:57 +08:00
7817834790 Added:
- Incremental builds (object caching by mtime)
- `build.kind` with `staticlib` support
- Custom output directory via `build.target`
2026-02-26 17:03:35 +08:00
9fe2604154 Custom output directory via build.target 2026-02-23 14:02:32 +08:00
cb8a3b2001 Custom output directory via build.target 2026-02-23 13:51:53 +08:00
e0fd729635 translated CONTRIBUTING.md to eng 2026-02-22 15:54:55 +08:00
e70edf8ae4 Update project TODO 2026-02-22 15:43:10 +08:00
210e82f026 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
2026-02-22 15:04:02 +08:00
ff12ca2758 Update release.yml 2026-02-21 16:19:44 +08:00
468491b5b1 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
- Build compiles all `*.c/*.cpp` files in `src/` into a single binary (no incremental build)
- Updated `dcr.toml` examples in documentation
2026-02-21 16:12:32 +08:00
0f48151b68 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
- Build compiles all `*.c/*.cpp` files in `src/` into a single binary (no incremental build)
- Updated `dcr.toml` examples in documentation
2026-02-21 16:12:20 +08:00
e87e0cbc47 update aur publish 2026-02-20 18:31:30 +08:00
fff8baa3c9 update aur publish 2026-02-20 18:22:05 +08:00
ad0d4cdcb7 update aur publish 2026-02-20 18:20:50 +08:00
45806f193e update aur publish 2026-02-20 18:17:36 +08:00
97 changed files with 14024 additions and 761 deletions

51
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,51 @@
---
name: Bug Report
about: Something is broken or behaves unexpectedly
title: "[bug] "
labels: bug
assignees: ''
---
## Description
<!-- A clear and concise description of the bug -->
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
<!-- What should have happened -->
## Actual Behavior
<!-- What actually happened, include full error output -->
```
paste error output here
```
## Environment
- **DCR version** (`dcr --version`):
- **OS**:
- **Compiler / toolchain** (`gcc --version` or `clang --version`):
## Minimal Reproducible Project
<!-- If possible, share a minimal dcr.toml + source that reproduces the issue -->
```toml
# dcr.toml
```
```c
// src/main.c
```
## Additional Context
<!-- Anything else that might help -->

View File

@@ -0,0 +1,35 @@
---
name: Feature Request
about: Suggest an idea or improvement
title: "[feat] "
labels: enhancement
assignees: ''
---
## Problem / Motivation
<!-- What problem does this solve? What are you trying to do that's currently hard or impossible? -->
## Proposed Solution
<!-- Describe what you want. Be as specific as possible — CLI flags, config syntax, behavior -->
## Example
<!-- Show how it would look in practice (command, dcr.toml snippet, output) -->
```sh
# example command
```
```toml
# example config
```
## Alternatives Considered
<!-- Have you tried workarounds? Are there other ways to solve this? -->
## Additional Context
<!-- Links, prior art, related issues, etc. -->

View File

@@ -1,6 +1,11 @@
name: AUR Publish name: AUR Publish
on: on:
workflow_dispatch:
inputs:
version:
description: "Tag version without v prefix (e.g. 0.2.2)"
required: true
push: push:
tags: tags:
- "v*" - "v*"
@@ -10,7 +15,10 @@ permissions:
jobs: jobs:
publish-aur: publish-aur:
# Не публикуем dev/pre-release теги в AUR
if: "!contains(github.ref_name, '-')"
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: aur
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -20,7 +28,11 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
PKGVER="${GITHUB_REF_NAME#v}" if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
PKGVER="${{ inputs.version }}"
else
PKGVER="${GITHUB_REF_NAME#v}"
fi
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/v${PKGVER}.tar.gz" SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/v${PKGVER}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')" SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
@@ -57,7 +69,8 @@ jobs:
build() { build() {
cd "\$srcdir/\$pkgname-\$pkgver" cd "\$srcdir/\$pkgname-\$pkgver"
export CARGO_PROFILE_RELEASE_LTO=false export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}" export RUSTFLAGS="\${RUSTFLAGS:-}"
export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
cargo build --release --locked cargo build --release --locked
} }
@@ -122,4 +135,4 @@ jobs:
git add PKGBUILD .SRCINFO git add PKGBUILD .SRCINFO
git commit -m "dcr ${{ steps.meta.outputs.pkgver }}" git commit -m "dcr ${{ steps.meta.outputs.pkgver }}"
git push git push

View File

@@ -4,32 +4,173 @@ on:
push: push:
tags: tags:
- "v*" - "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_bsd:
description: "Build BSD"
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
run_brew:
description: "Update Homebrew tap"
type: boolean
default: false
permissions: permissions:
contents: write contents: write
jobs: jobs:
build: # ── 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="${{ needs.setup.outputs.version }}"
notes_file=".github/release-notes.md"
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then
awk -v ver="$tag" '
$0 ~ "^## \\[" ver "\\]" {found=1; next}
found && $0 ~ "^## \\[" {exit}
found {print}
' CHANGELOG.md | sed -e '${/^$/d;}' > "$notes_file"
else
echo "No CHANGELOG section for $tag." > "$notes_file"
git log -1 --pretty=format:"%s%n%n%b" >> "$notes_file"
fi
- name: Upload Notes Artifact
uses: actions/upload-artifact@v4
with:
name: release-notes
path: .github/release-notes.md
# ── 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:
- 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: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
asset_name: dcr-x86_64-unknown-linux-gnu
- os: macos-15-intel - os: macos-15-intel
target: x86_64-apple-darwin target: x86_64-apple-darwin
asset_name: dcr-x86_64-apple-darwin - os: macos-latest
- os: macos-15
target: aarch64-apple-darwin 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 }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - 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 - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -39,19 +180,207 @@ jobs:
- name: Build - name: Build
run: cargo build --release --target ${{ matrix.target }} run: cargo build --release --target ${{ matrix.target }}
- name: Prepare Artifact (Unix) - name: Prepare Artifact
if: runner.os != 'Windows'
shell: bash
run: | run: |
cp target/${{ matrix.target }}/release/dcr ${{ matrix.asset_name }} ASSET="dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}"
cp target/${{ matrix.target }}/release/dcr "$ASSET"
- name: Prepare Artifact (Windows) echo "ASSET_NAME=${ASSET}" >> "$GITHUB_ENV"
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item "target/${{ matrix.target }}/release/dcr.exe" "${{ matrix.asset_name }}"
- name: Upload Release Asset - name: Upload Release Asset
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: 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' }}
# ── BSD ────────────────────────────────────────────────────────────────────
build-bsd:
if: ${{ github.event_name == 'push' || github.event.inputs.run_bsd == 'true' }}
needs: [setup, notes]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-freebsd
# При необходимости сюда можно докинуть x86_64-unknown-netbsd
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: cargo zigbuild --release --target ${{ matrix.target }}
- 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' }}
# ── 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, build-bsd]
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 }}
# ── Homebrew tap ───────────────────────────────────────────────────────────
publish-brew:
if: |
(github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_brew == 'true')
needs: [setup, build-linux, build-macos]
runs-on: ubuntu-latest
steps:
- name: Download release assets and compute sha256
id: sha
run: |
VERSION="${{ needs.setup.outputs.version }}"
BASE="https://github.com/dexoron/dcr/releases/download/v${VERSION}"
download_sha() {
curl -fsSL "${BASE}/$1" | sha256sum | awk '{print $1}'
}
echo "linux_x86=$(download_sha "dcr-x86_64-unknown-linux-gnu-${VERSION}")" >> "$GITHUB_OUTPUT"
echo "linux_arm=$(download_sha "dcr-aarch64-unknown-linux-gnu-${VERSION}")" >> "$GITHUB_OUTPUT"
echo "macos_x86=$(download_sha "dcr-x86_64-apple-darwin-${VERSION}")" >> "$GITHUB_OUTPUT"
echo "macos_arm=$(download_sha "dcr-aarch64-apple-darwin-${VERSION}")" >> "$GITHUB_OUTPUT"
- name: Checkout homebrew-dexoron
uses: actions/checkout@v4
with:
repository: dexoron/homebrew-dexoron
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
- name: Update Formula/dcr.rb
run: |
VERSION="${{ needs.setup.outputs.version }}"
cat > Formula/dcr.rb <<FORMULA
class Dcr < Formula
desc "Cargo-like utility to manage C/C++ projects"
homepage "https://dcr.dexoron.su"
version "${VERSION}"
on_macos do
on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-apple-darwin-#{version}"
sha256 "${{ steps.sha.outputs.macos_x86 }}"
end
on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-apple-darwin-#{version}"
sha256 "${{ steps.sha.outputs.macos_arm }}"
end
end
on_linux do
on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-unknown-linux-gnu-#{version}"
sha256 "${{ steps.sha.outputs.linux_x86 }}"
end
on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-unknown-linux-gnu-#{version}"
sha256 "${{ steps.sha.outputs.linux_arm }}"
end
end
def install
bin.install Dir["dcr*"].first => "dcr"
end
test do
system "#{bin}/dcr", "--version"
end
end
FORMULA
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/dcr.rb
git diff --staged --quiet && echo "No changes" && exit 0
git commit -m "dcr ${{ needs.setup.outputs.version }}"
git push

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@ target/
sandbox/** sandbox/**
.idea/ .idea/
.vscode/ .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,53 +1,371 @@
# Changelog # Changelog
## [0.2.2] - 2026-02-20 ## [0.6.8] - 2026-05-27
Added:
- `build.cxx_standard` — separate C++ language standard for `.cpp`/`.cxx`/`.cc` files.
When set, overrides `build.standard` for C++ sources. Example:
```toml
[build]
language = ["c", "c++"]
standard = "gnu11"
cxx_standard = "gnu++17"
```
- Variable substitution for `cflags`, `ldflags`, and `include` directory entries.
All `{version}`, `{version_major}`, `{name}`, `{profile}` and other step variables
are now available. Example:
```toml
[build]
cflags = ['-DPROJECT_VERSION="{version}"']
```
- FreeBSD build support in CI — new `run_bsd` workflow input and `build-bsd` job
that cross-compiles `x86_64-unknown-freebsd` via `cargo-zigbuild`.
Changed: Changed:
- License changed from MIT to **GPL-3.0-or-later**. All source files now carry
the GPL-3.0 header. Default license in `dcr new` / `dcr init` changed to
`"GPL-3.0-or-later"`. README, FAQ, and CONTRIBUTING.md updated accordingly.
- `get_list_with_profile_and_target` no longer duplicates list values when
`inherit = true` and the profile/target value is identical to the base value.
Fixed:
- Multi-language `language` array (e.g. `["c", "c++", "asm"]`) no longer drops `c++`
during internal parsing — `.cxx`/`.cpp`/`.cc` source files are correctly discovered
and compiled.
- `-std=` flag is no longer passed to C++ files when only `standard` (C standard) is
configured — uses `cxx_standard` when available, otherwise skips `-std=` for C++
sources, avoiding clang error `invalid argument '-std=c11' not allowed with 'C++'`.
- Object file collision: source files with the same stem but different extensions
(e.g. `gdt.cxx` and `gdt.S`) no longer overwrite each other. Object filenames
now include the original source extension (e.g. `gdt.cxx.o`, `gdt.S.o`).
- MSVC backend compile step now detects language per source file extension instead
of using the global `language` field, fixing mixed C/C++ compilation with MSVC.
- Assembler `-x` flag in `unix_cc.rs` and `gen.rs` is now placed **before** the
source file, as required by clang/gcc.
- `{version}` variables in `build.clean` are now correctly substituted.
## [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
- `build.platform` architecture hint for `-march` / `/arch`
- 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
- Build compiles all `*.c/*.cpp` files in `src/` into a single binary (no incremental build)
- 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` - Reworked `dcr.toml` handling: added read, validation, and edit through `core::config`
- `new` and `init` use the new config creation logic - `new` and `init` use the new config creation logic
- `build` and `run` now require `dcr.toml` and read `compiler` and `name` from it - `build` and `run` now require `dcr.toml` and read `compiler` and `name` from it
## [0.2.1] - 2026-02-18 ## [0.2.1] - 2026-02-18
Changed: Changed:
- Translated user-facing CLI messages to English - Translated user-facing CLI messages to English
- Unified error and warning output via `utils::log::{error, warn}` - Unified error and warning output via `utils::log::{error, warn}`
- Updated `--help` output: translated headers and examples to English, used `printc`, and applied `BOLD_*` styles - Updated `--help` output: translated headers and examples to English, used `printc`, and applied `BOLD_*` styles
- Translated installer script messages in `install.sh` and `install.ps1` to English - Translated installer script messages in `install.sh` and `install.ps1` to English
## [0.2.0] - 2026-02-17 ## [0.2.0] - 2026-02-17
Изменено:
- Проект переведен с Python на Rust
- CLI и команды (`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`) перенесены на Rust
- Обновлен флаг `--update` - добавлена поддержка GNU\Linux, Windows, MacOS
- Обновлены `README.md`, `CONTRIBUTING.md`, `install.sh` под Rust-реализацию
- Добавлен `install.ps1` для Windows
- Обновлен `install.sh` для GNU/Linux и MacOS
Добавлено: Changed:
- Добавлен `install.ps1` для Windows
- Поддержка GNU\Linux, Windows, MacOS(x86_64/arm)
ВАЖНО: - 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
- Updated `README.md`, `CONTRIBUTING.md`, and `install.sh` for the Rust implementation
- Added `install.ps1` for Windows
- 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 ## [0.1.2] - 2026-02-12
Добавлено:
- Команда обновления
- Скрипт установки `install.sh` и инструкции по установке в README
- Флаг `--version` для вывода версии
Изменено: Added:
- Улучшен консольный вывод команд, добавлены цвета и обновлена справка `--help`
- Обновлен запуск проекта и обработка сборки в `run.py`/`build.py` - 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 ## [0.1.1] - 2026-02-11
Изменено:
- Обновлена справка `--help` Changed:
- `main.py` теперь корректно запускается при прямом выполнении
- Updated `--help`
- `main.py` now runs correctly when executed directly
## [0.1.0] - 2026-02-11 ## [0.1.0] - 2026-02-11
Первый публичный релиз.
Добавлено: First public release.
- Базовые команды `new`, `init`, `build`, `run`, `clean`
- Профили сборки `debug` и `release` Added:
- Шаблоны `dcr.toml` и `src/main.c`
- Base commands `new`, `init`, `build`, `run`, `clean`
- Build profiles `debug` and `release`
- `dcr.toml` and `src/main.c` templates

View File

@@ -1,13 +1,13 @@
# Вклад в проект # Contributing
Спасибо за интерес к проекту. Thanks for your interest in the project.
## Требования ## Requirements
- Использовать Rust stable. - Use Rust stable.
- Перед PR код должен проходить форматирование, линт и сборку. - Before opening a PR, the code must pass formatting, linting, and build.
## Быстрый старт ## Quick Start
```bash ```bash
git clone https://github.com/dexoron/dcr.git git clone https://github.com/dexoron/dcr.git
@@ -15,43 +15,72 @@ cd dcr
cargo check cargo check
``` ```
## Стиль кода ## Code Style
- Форматируйте код через `cargo fmt`. - Format code with `cargo fmt`.
- Проверяйте предупреждения через `cargo clippy`. - Check warnings with `cargo clippy`.
- Держите функции небольшими и сфокусированными. - Keep functions small and focused.
- Предпочитайте понятные имена вместо "хитрых". - Prefer clear names over clever ones.
## Проверки ## Target System
Перед PR запускайте: 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`:
```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:
```bash ```bash
cargo fmt --all cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
cargo check cargo check
cargo test
``` ```
## CI/CD и релизы ## CI/CD and Releases
- CI: `.github/workflows/ci.yml` - CI: `.github/workflows/ci.yml`
- Release: `.github/workflows/release.yml` - Release: `.github/workflows/release.yml`
- Релиз запускается по тегу `v*` и публикует бинарники для: - Releases are triggered by tags `v*` and publish binaries for:
- `x86_64-unknown-linux-gnu` - `x86_64-unknown-linux-gnu`
- `x86_64-apple-darwin` - `x86_64-apple-darwin`
- `aarch64-apple-darwin` - `aarch64-apple-darwin`
- `x86_64-pc-windows-msvc` - `x86_64-pc-windows-msvc`
## Коммиты и PR ## Commits and PRs
- Делайте коммиты понятными. - Write clear commit messages.
- В PR указывайте краткое описание и мотивацию изменений. - In PRs, include a short description and rationale.
- Обновляйте документацию при изменении поведения. - Update documentation when behavior changes.
## Вопросы ## License
Если что-то непонятно, открой issue или задайте вопрос в PR. This project is licensed under the **GNU General Public License v3.0**. By contributing, you agree that your contributions will be licensed under the same license.
## Questions
If anything is unclear, open an issue or ask in the PR.
You can also reach out directly:
Также можно задать вопрос напрямую:
- TG: @dexoron - TG: @dexoron
- DS: dexoron - DS: dexoron

293
Cargo.lock generated
View File

@@ -26,6 +26,24 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.1" version = "3.19.1"
@@ -45,6 +63,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver",
"libc",
"shlex", "shlex",
] ]
@@ -61,15 +81,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "dcr" name = "cpufeatures"
version = "0.2.2" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [ dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
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.8"
dependencies = [
"ctrlc",
"git2",
"glob",
"openssl",
"reqwest", "reqwest",
"self-replace", "self-replace",
"serde", "serde",
"serde_json",
"sha2",
"toml", "toml",
] ]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"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]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -115,6 +193,21 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 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]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@@ -173,6 +266,16 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.17" version = "0.2.17"
@@ -213,6 +316,27 @@ dependencies = [
"wasip3", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.5" version = "0.15.5"
@@ -476,6 +600,16 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 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]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.85" version = "0.3.85"
@@ -498,6 +632,46 @@ version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" 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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.11.0"
@@ -539,12 +713,92 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 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-src"
version = "300.6.0+3.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -563,6 +817,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.4" version = "0.1.4"
@@ -900,6 +1160,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -1162,6 +1433,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@@ -1198,6 +1475,18 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"

View File

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

23
FAQ.md
View File

@@ -1,38 +1,46 @@
# FAQ # FAQ
## What is this project about? ## What is this project about?
`dcr` is a tool for managing C/C++ projects and their dependencies. `dcr` is a tool for managing C/C++ projects and their dependencies.
It is inspired by Cargo, but it is not a direct equivalent of Makefile, CMake, or other classic build systems. It is inspired by Cargo, but it is not a direct equivalent of Makefile, CMake, or other classic build systems.
The goal is to provide a simpler and easier-to-configure development workflow. The goal is to provide a simpler and easier-to-configure development workflow.
## How is the project developed? ## How is the project developed?
The project is written in Rust and is primarily developed by one maintainer, Dexoron. The project is written in Rust and is primarily developed by one maintainer, Dexoron.
AI tools are used during development, but they do not write the project end-to-end. Their role is limited to supporting specific tasks (for example, suggestions, wording checks, and translations). AI tools are used during development, but they do not write the project end-to-end. Their role is limited to supporting specific tasks (for example, suggestions, wording checks, and translations).
## Which tools are used for development? ## Which tools are used for development?
Main development stack: Main development stack:
- `RustRover` 2025 - development IDE. - `RustRover` 2025 - development IDE.
- `Codex` (`GPT-5.3-Codex (medium)`) - primary AI tool, integrated via JetBrains AI Chat. - `Codex` (`GPT-5.3-Codex (medium)`) - primary AI tool, integrated via JetBrains AI Chat.
- `Rust` - main project language. - `Rust` - main project language.
- `Cargo` - tool for building, testing, and dependency management in Rust projects. - `Cargo` - tool for building, testing, and dependency management in Rust projects.
## Is the project open source? ## Is the project open source?
Yes, the project is open source. Yes, the project is open source.
You can propose ideas, report issues, and contribute code changes. You can propose ideas, report issues, and contribute code changes.
The project is distributed under the MIT license (see `LICENSE`). The project is distributed under the GPL-3.0 license (see `LICENSE`).
## How can I contribute? ## How can I contribute?
Thank you for contributing. Thank you for contributing.
You can: You can:
- help with code; - help with code;
- help with translations; - help with translations;
- help with documentation; - help with documentation;
- suggest an idea or report a bug. - suggest an idea or report a bug.
### If you want to submit changes (code, translation, documentation) ### If you want to submit changes (code, translation, documentation)
1. Fork the required repository: `dexoron/dcr` (main project) or `dexoron/dcr-site` (project website). 1. Fork the required repository: `dexoron/dcr` (main project) or `dexoron/dcr-site` (project website).
2. Make your changes following the rules in `CONTRIBUTING.md`. 2. Make your changes following the rules in `CONTRIBUTING.md`.
3. Push changes to your fork. 3. Push changes to your fork.
@@ -40,6 +48,7 @@ You can:
5. Wait for review. 5. Wait for review.
### If you want to report a problem or suggest an idea ### If you want to report a problem or suggest an idea
1. Open the relevant repository: `dexoron/dcr` or `dexoron/dcr-site`. 1. Open the relevant repository: `dexoron/dcr` or `dexoron/dcr-site`.
2. Go to the `Issues` tab. 2. Go to the `Issues` tab.
3. Create an Issue and specify its type (bug, suggestion, question/help). 3. Create an Issue and specify its type (bug, suggestion, question/help).
@@ -47,13 +56,17 @@ You can:
5. Wait for feedback. 5. Wait for feedback.
## How do I install `dcr`? ## How do I install `dcr`?
Available installation methods may vary by project version. Available installation methods may vary by project version.
Recommended path: Recommended path:
- check instructions in `README.md`; - check instructions in `README.md`;
- if there is no prebuilt package for your platform, build from source using `cargo build --release`. - if there is no prebuilt package for your platform, build from source using `cargo build --release`.
## How can I get started quickly? ## How can I get started quickly?
Minimal workflow: Minimal workflow:
1. Install `dcr`. 1. Install `dcr`.
2. Create a project (or open an existing C/C++ project). 2. Create a project (or open an existing C/C++ project).
3. Initialize `dcr` configuration. 3. Initialize `dcr` configuration.
@@ -63,21 +76,25 @@ Minimal workflow:
For detailed commands and examples, see `README.md` and the project documentation. For detailed commands and examples, see `README.md` and the project documentation.
## What is the project status? ## What is the project status?
`dcr` is under active development. `dcr` is under active development.
This means some interfaces and behaviors may change between versions. This means some interfaces and behaviors may change between versions.
Before upgrading, check the changelog and release notes. Before upgrading, check the changelog and release notes.
## Which systems does `dcr` support? ## Which systems does `dcr` support?
`dcr` targets modern C/C++ development environments. `dcr` targets modern C/C++ development environments.
Actual compatibility depends on the `dcr` version, operating system, and toolchain. Actual compatibility depends on the `dcr` version, operating system, and toolchain.
Before starting, check documentation for: Before starting, check documentation for:
- supported operating systems; - supported operating systems;
- supported compilers and linkers; - supported compilers and linkers;
- version-specific limitations. - version-specific limitations.
## How is `dcr` different from Make/CMake? ## How is `dcr` different from Make/CMake?
`dcr` does not aim to be a direct replacement for Make/CMake. `dcr` does not aim to be a direct replacement for Make/CMake.
Its main focus is simplifying project and dependency management with a Cargo-like experience. Its main focus is simplifying project and dependency management with a Cargo-like experience.
@@ -85,13 +102,17 @@ If you need low-level, full control over build steps, classic tools may be a bet
If you want a simpler and more uniform configuration model, `dcr` may be more convenient. If you want a simpler and more uniform configuration model, `dcr` may be more convenient.
## Where can I find documentation and examples? ## Where can I find documentation and examples?
Primary sources: Primary sources:
- `README.md` in the repository; - `README.md` in the repository;
- project documentation (see the project website); - project documentation (see the project website);
- `Issues` for known problems and discussions. - `Issues` for known problems and discussions.
## How do I report a bug properly? ## How do I report a bug properly?
To make a bug report easy to reproduce, include: To make a bug report easy to reproduce, include:
1. `dcr` version. 1. `dcr` version.
2. OS and compiler/toolchain version. 2. OS and compiler/toolchain version.
3. Steps to reproduce. 3. Steps to reproduce.

688
LICENSE
View File

@@ -1,21 +1,675 @@
MIT License # GNU GENERAL PUBLIC LICENSE
Copyright (c) 2026 Dexoron (Bezotechestvo Vladimir) [main@dexoron.su] Version 3, 29 June 2007
Permission is hereby granted, free of charge, to any person obtaining a copy Copyright (C) 2007 Free Software Foundation, Inc.
of this software and associated documentation files (the "Software"), to deal <https://fsf.org/>
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all Everyone is permitted to copy and distribute verbatim copies of this
copies or substantial portions of the Software. license document, but changing it is not allowed.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ## Preamble
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE The GNU General Public License is a free, copyleft license for
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER software and other kinds of works.
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE The licenses for most software and other practical works are designed
SOFTWARE. to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom
to share and change all versions of a program--to make sure it remains
free software for all its users. We, the Free Software Foundation, use
the GNU General Public License for most of our software; it applies
also to any other work released this way by its authors. You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you
have certain responsibilities if you distribute copies of the
software, or if you modify it: responsibilities to respect the freedom
of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the
manufacturer can do so. This is fundamentally incompatible with the
aim of protecting users' freedom to change the software. The
systematic pattern of such abuse occurs in the area of products for
individuals to use, which is precisely where it is most unacceptable.
Therefore, we have designed this version of the GPL to prohibit the
practice for those products. If such problems arise substantially in
other domains, we stand ready to extend this provision to those
domains in future versions of the GPL, as needed to protect the
freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish
to avoid the special danger that patents applied to a free program
could make it effectively proprietary. To prevent this, the GPL
assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
## TERMS AND CONDITIONS
### 0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
### 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies that a certain numbered version of the GNU General Public
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that numbered version or
of any later version published by the Free Software Foundation. If the
Program does not specify a version number of the GNU General Public
License, you may choose any version ever published by the Free
Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU General Public License can be used, that proxy's public
statement of acceptance of a version permanently authorizes you to
choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
## How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively state
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper
mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands \`show w' and \`show c' should show the
appropriate parts of the General Public License. Of course, your
program's commands might be different; for a GUI interface, you would
use an "about box".
You should also get your employer (if you work as a programmer) or
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. For more information on this, and how to apply and follow
the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your
program into proprietary programs. If your program is a subroutine
library, you may consider it more useful to permit linking proprietary
applications with the library. If this is what you want to do, use the
GNU Lesser General Public License instead of this License. But first,
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.

259
README.md
View File

@@ -1,143 +1,208 @@
# DCR (Dexoron Cargo Realization) <div align="center">
DCR is a utility for managing C/C++ projects in a Cargo-like style. # DCR — Dexoron Cargo Realization
The current implementation is written in Rust. **A Cargo-style build tool for C/C++ projects**
## Why DCR [![CI](https://github.com/dexoron/dcr/actions/workflows/ci.yml/badge.svg)](https://github.com/dexoron/dcr/actions/workflows/ci.yml)
- Unified project structure without manual setup [![GitHub Release](https://img.shields.io/github/v/release/dexoron/dcr)](https://github.com/dexoron/dcr/releases/latest)
- Simple commands for common tasks [![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/dexoron/dcr/releases)
- Transparent compilation and predictable build profiles <br/>
[![AUR](https://img.shields.io/aur/version/dcr)](https://aur.archlinux.org/packages/dcr)
[![Crates.io](https://img.shields.io/crates/v/dcr)](https://crates.io/crates/dcr)
[![Homebrew](https://img.shields.io/badge/homebrew-dexoron%2Fdexoron-orange)](https://github.com/dexoron/homebrew-dexoron)
<br/>
[![GitHub Stars](https://img.shields.io/github/stars/dexoron/dcr?style=flat)](https://github.com/dexoron/dcr/stargazers)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](LICENSE)
## Features </div>
- Create a new project or initialize the current directory
- Build a project with `debug` and `release` profiles
- Run the compiled binary
- Clean build artifacts
- Generate a minimal C project template
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
## Supported Platforms ---
- Linux: `x86_64-unknown-linux-gnu`
- macOS Intel: `x86_64-apple-darwin` ## Why DCR?
- macOS Apple Silicon: `aarch64-apple-darwin`
- Windows: `x86_64-pc-windows-msvc` - **No boilerplate** — one config file, predictable structure
- **Cargo-like workflow** — `build`, `run`, `clean`, `test`, `add`
- **Cross-compilation** — full target triple support with short names
- **IDE integration** — VS Code, CLion, `compile_commands.json` out of the box
- **Dependencies** — path, git, and registry-based with lock file
---
## Installation ## Installation
### Package Manager <table>
<tr>
**Arch Linux** <td><b>Arch Linux (AUR)</b></td>
```sh <td>
yay -S dcr # or paru and other AUR package managers
```
### From Source
```sh ```sh
git clone https://github.com/dexoron/dcr.git yay -S dcr
cd dcr
cargo build --release
mkdir -p ~/.local/bin
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
``` ```
### Via `install.sh` (Linux/macOS) </td>
</tr>
<tr>
<td><b>macOS/Linux(GNU) (Homebrew)</b></td>
<td>
```sh
brew tap dexoron/dexoron
brew install dcr
```
</td>
</tr>
<tr>
<td><b>Cargo (crates.io)</b></td>
<td>
```sh
cargo install dcr
```
</td>
</tr>
<tr>
<td><b>Linux / macOS (script)</b></td>
<td>
```sh ```sh
curl -fsSL https://dcr.dexoron.su/install.sh | bash curl -fsSL https://dcr.dexoron.su/install.sh | bash
``` ```
### Via `install.ps1` (Windows) </td>
</tr>
<tr>
<td><b>Windows (PowerShell)</b></td>
<td>
```powershell ```powershell
irm https://dcr.dexoron.su/install.ps1 | iex irm https://dcr.dexoron.su/install.ps1 | iex
``` ```
When executed, both scripts ask whether to: </td>
- download a prebuilt binary from GitHub Releases </tr>
- or build the project from `git` <tr>
<td><b>From source</b></td>
<td>
Release assets: ```sh
- `dcr-x86_64-unknown-linux-gnu` git clone https://github.com/dexoron/dcr.git
- `dcr-x86_64-apple-darwin` cd dcr && cargo build --release
- `dcr-aarch64-apple-darwin` ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
- `dcr-x86_64-pc-windows-msvc.exe` ```
## Update </td>
</tr>
</table>
- If DCR was installed from GitHub release assets, `install.sh`, `install.ps1`, or built manually: ---
- use `dcr --update`
- If DCR is installed via `pacman/AUR`:
- update with your package manager: `paru/yay -Syu dcr` or `sudo pacman -Syu dcr`
- `dcr --update` detects package-managed installs and asks you to update via package manager
## Quick Start ## Quick Start
Create a new project: ```sh
`dcr new hello` dcr new hello
cd hello
Or initialize the current directory (the directory must be empty): dcr run
`dcr init`
Project structure:
```txt
hello/
- src/
- - main.c
- dcr.toml
``` ```
Build and run the project: **Project structure:**
`dcr run` or `dcr run --release`
## Commands ```
hello/
├── dcr.toml # project config
└── src/
└── main.c
```
### `dcr new <name>` ---
Creates a project with the specified name in the current directory.
### `dcr init` ## dcr.toml Example
Creates a project in the current directory. The project name is taken from the directory name. The directory must be empty.
### `dcr build [profile]`
Builds the project. If no profile is specified, `--debug` is used.
### `dcr run [profile]`
Builds the project and runs the binary. If no profile is specified, `--debug` is used.
Run manually:
`./target/<profile>/main`
### `dcr clean`
Removes the `target` directory in the project root.
## Build Profiles
Two profiles are supported:
- `--debug` (default) - flags: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`
- `--release` - flags: `-O3 -DNDEBUG -march=native`
## Configuration
The main project file is `dcr.toml`.
Example `dcr.toml`:
```toml ```toml
[package] [package]
name = "hello" name = "hello"
version = "0.1.0" version = "0.1.0"
[build]
language = "c" language = "c"
standard = "c11"
compiler = "clang" compiler = "clang"
kind = "bin"
[dependencies] [dependencies]
``` ```
## Requirements ---
- Rust toolchain (`rustc`, `cargo`) - for building DCR from source
- C compiler (`clang`, `gcc`, or others)
## Releases ## Base Commands
Releases are built automatically via GitHub Actions (`.github/workflows/release.yml`) when a tag matching `v*` is pushed.
## License | Command | Description |
See `LICENSE`. |---|---|
| `dcr new <name>` | Create a new project |
| `dcr init` | Initialize current directory |
| `dcr build [--release]` | Build the project |
| `dcr run [--release]` | Build and run |
| `dcr clean` | Remove build artifacts |
other in [docs/](docs/) and `dcr --help`
---
## Platforms
<div align="center">
| OS | x86_64 | aarch64 | armv7 | i686 | riscv64 | GNU | Musl | MSVC | MinGW |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Linux | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
| macOS | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Windows | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| FreeBSD | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ⚠️ | ❌ | ❌ | ❌ |
| OpenBSD | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ⚠️ | ❌ | ❌ | ❌ |
| NetBSD | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ⚠️ | ❌ | ❌ | ❌ |
✅ officially supported · ⚠️ community / best-effort · ❌ not supported
</div>
---
## Documentation
Full docs at **[dcr.dexoron.su](https://dcr.dexoron.su)** or in the [`docs/`](docs/) directory.
---
## Contributors
<div align="center">
| | Name | Role | GitHub |
|:---:|---|---|---|
| 👤 | Dexoron (Bezotechestvo Vladimir) | Maintainer, Creator | [@dexoron](https://github.com/dexoron) |
| 👤 | Kai | Maintainer | [@peoplemiau1](https://github.com/peoplemiau1) |
</div>
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). Before a PR:
```sh
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test
```
---
<div align="center">
GPL-3.0 — see [LICENSE](LICENSE)<br/>
Made with ❤️ by [Dexoron](https://github.com/dexoron) and contributors.
</div>

69
TODO.md Normal file
View File

@@ -0,0 +1,69 @@
# DCR TODO.md
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

@@ -1,3 +1,20 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
fn main() { fn main() {
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string()); let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
println!("cargo:rustc-env=DCR_TARGET={target}"); println!("cargo:rustc-env=DCR_TARGET={target}");

View File

@@ -1,57 +0,0 @@
# Вклад в проект
Спасибо за интерес к проекту.
## Требования
- Использовать Rust stable.
- Перед PR код должен проходить форматирование, линт и сборку.
## Быстрый старт
```bash
git clone https://github.com/dexoron/dcr.git
cd dcr
cargo check
```
## Стиль кода
- Форматируйте код через `cargo fmt`.
- Проверяйте предупреждения через `cargo clippy`.
- Держите функции небольшими и сфокусированными.
- Предпочитайте понятные имена вместо "хитрых".
## Проверки
Перед PR запускайте:
```bash
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check
```
## CI/CD и релизы
- CI: `.github/workflows/ci.yml`
- Release: `.github/workflows/release.yml`
- Релиз запускается по тегу `v*` и публикует бинарники для:
- `x86_64-unknown-linux-gnu`
- `x86_64-apple-darwin`
- `aarch64-apple-darwin`
- `x86_64-pc-windows-msvc`
## Коммиты и PR
- Делайте коммиты понятными.
- В PR указывайте краткое описание и мотивацию изменений.
- Обновляйте документацию при изменении поведения.
## Вопросы
Если что-то непонятно, открой issue или задайте вопрос в PR.
Также можно задать вопрос напрямую:
- TG: @dexoron
- DS: dexoron

View File

@@ -1,102 +0,0 @@
# FAQ
## В чем суть проекта?
`dcr` - это инструмент для управления C/C++ проектами и их зависимостями.
Он вдохновлен Cargo, но не является аналогом Makefile, CMake или других классических систем сборки.
Цель проекта - дать более простую и удобную в настройке среду для разработки.
## Как разрабатывается проект?
Проект написан на Rust и в основном развивается одним разработчиком - Dexoron.
В работе используются ИИ-инструменты, но они не пишут проект целиком: их роль - помощь с отдельными задачами (например, советы, проверка формулировок, переводы и т.д.).
## Какие инструменты используются для разработки?
Основной стек разработки:
- `RustRover` 2025 - IDE для разработки.
- `Codex` (`GPT-5.3-Codex (medium)`) - основной ИИ-инструмент, интегрированный через JetBrains AI Chat.
- `Rust` - основной язык проекта.
- `Cargo` - инструмент сборки, тестов и управления зависимостями Rust-проектов.
## Проект открытый?
Да, проект открыт (Open Source).
Вы можете предлагать идеи, находить ошибки и вносить изменения в код.
Проект распространяется по лицензии MIT (см. `LICENSE`).
## Как внести вклад?
Спасибо за желание помочь проекту.
Вы можете:
- помочь с кодом;
- помочь с переводом;
- помочь с документацией;
- предложить идею или сообщить о баге.
### Если вы хотите внести изменения (код, перевод, документация)
1. Сделайте форк нужного репозитория: `dexoron/dcr` (основной проект) или `dexoron/dcr-site` (сайт проекта).
2. Внесите изменения с учетом правил из `CONTRIBUTING.md`.
3. Отправьте изменения в свой форк.
4. Создайте Pull Request.
5. Дождитесь ревью.
### Если вы хотите сообщить о проблеме или предложить идею
1. Откройте нужный репозиторий: `dexoron/dcr` или `dexoron/dcr-site`.
2. Перейдите во вкладку `Issues`.
3. Создайте Issue и укажите тип (баг, предложение, вопрос/помощь).
4. Опишите проблему или идею кратко и понятно.
5. Дождитесь обратной связи.
## Как установить `dcr`?
Актуальные способы установки зависят от текущей версии проекта.
Рекомендуемый путь:
- проверьте инструкции в `README.md`;
- если готового пакета для вашей платформы нет, соберите проект из исходников через `cargo build --release`.
## Как быстро начать работу?
Минимальный сценарий:
1. Установите `dcr`.
2. Создайте проект (или перейдите в существующий C/C++ проект).
3. Инициализируйте конфигурацию `dcr`.
4. Добавьте зависимости проекта.
5. Запустите сборку через `dcr`.
Подробные команды и примеры смотрите в `README.md` и документации проекта.
## На какой стадии проект?
`dcr` активно развивается.
Это означает, что интерфейсы и поведение некоторых функций могут меняться между версиями.
Перед обновлением рекомендуется читать changelog и проверять заметки к релизу.
## С какими системами работает `dcr`?
`dcr` ориентирован на современные среды разработки C/C++.
Фактическая совместимость зависит от версии `dcr`, операционной системы и используемого toolchain.
Перед началом работы проверьте в документации:
- поддерживаемые ОС;
- поддерживаемые компиляторы и линкеры;
- ограничения конкретной версии.
## Чем `dcr` отличается от Make/CMake?
`dcr` не пытается быть прямой заменой Make/CMake.
Главный фокус `dcr` - упростить управление проектом и зависимостями в стиле, близком к опыту Cargo.
Если вам нужен низкоуровневый полный контроль над процессом сборки, классические инструменты могут быть привычнее.
Если нужна более простая и единообразная модель конфигурации - `dcr` может быть удобнее.
## Где смотреть документацию и примеры?
Основные источники:
- `README.md` в репозитории;
- документация проекта (смотреть сайт проекта);
- `Issues` для известных проблем и обсуждений;
## Как правильно сообщить о баге?
Чтобы баг-репорт можно было быстро воспроизвести, укажите:
1. Версию `dcr`.
2. ОС и версию компилятора/toolchain.
3. Шаги воспроизведения.
4. Ожидаемое поведение.
5. Фактическое поведение и текст ошибки.
6. Минимальный пример проекта (по возможности).
Баги и проблемы отправляйте через `Issues` в соответствующем репозитории.

46
docs/README.md Normal file
View File

@@ -0,0 +1,46 @@
# Introduction
DCR (Dexoron Cargo Realization) is a Cargo-style CLI for C/C++ projects.
Current DCR focuses on a simple, predictable workflow:
- create or initialize a project (`dcr new`, `dcr init`)
- build with `debug` or `release` profiles (`dcr build`)
- run binaries (`dcr run`)
- clean artifacts (`dcr clean`)
- update the installed binary (`dcr --update`)
## What is implemented in this version
- Project configuration in `dcr.toml` with `[package]`, `[build]`, `[dependencies]`.
- Recursive source discovery in `src/`:
- C: `*.c`
- 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 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
- <https://github.com/dexoron/dcr>

View File

@@ -1,143 +0,0 @@
# DCR (Dexoron Cargo Realization)
DCR — это утилита для управления проектами на C/C++ в стиле Cargo.
Текущая реализация написана на Rust.
## Зачем DCR
- Единая структура проекта без ручной настройки
- Простые команды для типовых задач
- Прозрачная компиляция и предсказуемые профили сборки
## Возможности
- Создание нового проекта или инициализация текущей директории
- Сборка проекта в профилях `debug` и `release`
- Запуск собранного бинарника
- Очистка результатов сборки
- Генерация минимального шаблона C-проекта
- Обновление бинарника через `dcr --update` (GitHub Releases, не для pacman-установок)
## Поддерживаемые платформы
- Linux: `x86_64-unknown-linux-gnu`
- macOS Intel: `x86_64-apple-darwin`
- macOS Apple Silicon: `aarch64-apple-darwin`
- Windows: `x86_64-pc-windows-msvc`
## Установка
### Пакетный менеджер
**ArchLinux**
```sh
yay -S dcr # или paru и остальные менеджеры пакетов AUR
```
### Из исходников
```sh
git clone https://github.com/dexoron/dcr.git
cd dcr
cargo build --release
mkdir -p ~/.local/bin
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
```
### Через install.sh (Linux/macOS)
```sh
curl -fsSL https://dcr.dexoron.su/install.sh | bash
```
### Через install.ps1 (Windows)
```powershell
irm https://dcr.dexoron.su/install.ps1 | iex
```
Оба скрипта при запуске спрашивают:
- скачать готовый бинарник из GitHub Release
- или собрать проект из `git`
Релизные ассеты:
- `dcr-x86_64-unknown-linux-gnu`
- `dcr-x86_64-apple-darwin`
- `dcr-aarch64-apple-darwin`
- `dcr-x86_64-pc-windows-msvc.exe`
## Обновление
- Если DCR установлен из GitHub Releases, через `install.sh`, `install.ps1` или собран вручную:
- используйте `dcr --update`
- Если DCR установлен через `pacman/AUR`:
- обновляйте через пакетный менеджер: `paru/yay -Syu dcr` или `sudo pacman -Syu dcr`
- `dcr --update` определяет пакетную установку и подсказывает обновляться через пакетный менеджер
## Быстрый старт
Создать новый проект:
`dcr new hello`
Или инициализировать текущую директорию (директория должна быть пустой):
`dcr init`
Структура проекта:
```txt
hello/
- src/
- - main.c
- dcr.toml
```
Сборка и запуск проекта:
`dcr run` или `dcr run --release`
## Команды
### `dcr new <name>`
Создает проект в текущей директории с указанным именем.
### `dcr init`
Создает проект в текущей директории. Имя проекта будет равно имени директории. Директория должна быть пустой.
### `dcr build [profile]`
Собирает проект. Если профиль не указан, используется `--debug`.
### `dcr run [profile]`
Собирает проект и запускает бинарник. Если профиль не указан, используется `--debug`.
Запуск вручную:
`./target/<profile>/main`
### `dcr clean`
Удаляет папку `target` в корне проекта.
## Профили сборки
Поддерживаются два профиля:
- `--debug` (по умолчанию) — флаги: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`
- `--release` — флаги: `-O3 -DNDEBUG -march=native`
## Конфигурация
Основной файл проекта — `dcr.toml`.
Пример `dcr.toml`:
```toml
[package]
name = "hello"
version = "0.1.0"
language = "c"
compiler = "clang"
[dependencies]
```
## Требования
- Rust toolchain (`rustc`, `cargo`) - Для ручной сборки DCR
- C-компилятор (`clang`, `gcc` или другие)
## Релизы
Релизы собираются автоматически через GitHub Actions (`.github/workflows/release.yml`) при пуше тега формата `v*`.
## Лицензия
См. файл `LICENSE`.

49
docs/SUMMARY.md Normal file
View File

@@ -0,0 +1,49 @@
# Table of contents
* [Introduction](README.md)
## Getting started
* [Installation](getting-started/installation.md)
* [Create your first project](getting-started/create-your-first-project.md)
* [Project structure](getting-started/project-structure.md)
* [Build and run](getting-started/build-and-run.md)
## Configuration
* [dcr.toml overview](configuration/dcr.toml-overview.md)
* [Package section](configuration/package-section.md)
* [Build section](configuration/build-section.md)
* [Dependencies section](configuration/dependencies-section.md)
* [Build profiles](configuration/build-profiles.md)
## Commands
* [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
* [Dependency format](dependencies-and-build/dependency-format.md)
* [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

@@ -0,0 +1,76 @@
# dcr build
Builds the project in `debug` or `release` profile.
## Usage
```sh
dcr build
dcr build --debug
dcr build --release
dcr build --target linux --release
dcr build --release --force
dcr build --debug --clean
```
## What `build` does
1. Checks that `dcr.toml` exists and is valid.
2. Reads build settings from `[build]`.
3. Resolves path dependencies from `[dependencies]`.
4. Creates required output directories.
5. Recursively compiles sources from `src/`.
6. Links final artifact (binary or library).
## Config values used
- `package.name`
- `build.filename`
- `build.extension`
- `build.compiler`
- `build.language`
- `build.standard`
- `build.kind`
- `build.target`
- `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
- Profile flag (`--debug` / `--release`) can appear in any argument position (duplicates are rejected).
- Unknown profile flags return an error.
- `--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

@@ -0,0 +1,27 @@
# dcr clean
Removes build artifacts in `target/`.
## Usage
```sh
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 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.
- 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

@@ -0,0 +1,43 @@
# dcr init/new
## `dcr new <name>`
Creates a new project directory and initializes it.
```sh
dcr new hello
```
Behavior:
1. Validates arguments (`<name>` is required, only one name is allowed).
2. Creates `<name>/`.
3. Creates `<name>/dcr.toml`.
4. Sets `package.name = "<name>"`.
5. Creates `<name>/src/main.c` with a Hello World template.
Errors:
- Missing project name.
- Extra arguments.
- Target directory already exists.
## `dcr init`
Initializes DCR in the current directory.
```sh
dcr init
```
Behavior:
1. Requires current directory to be empty.
2. Creates `dcr.toml`.
3. Sets `package.name` from the current directory name.
4. Creates `src/main.c`.
Errors:
- Any arguments are treated as invalid.
- Non-empty directory is rejected.

40
docs/commands/dcr-run.md Normal file
View File

@@ -0,0 +1,40 @@
# dcr run
Builds the project and runs the resulting binary.
## Usage
```sh
dcr run
dcr run --debug
dcr run --release
dcr run --target linux --release
dcr run --release --force
dcr run --debug --clean
```
## Behavior
1. Validates `dcr.toml`.
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` (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
`dcr run` attempts to launch an existing binary from the same profile if present; otherwise it reports that code errors must be fixed.

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

@@ -0,0 +1,44 @@
# Global options
## `dcr --help`
```sh
dcr --help
```
Prints top-level CLI usage, commands, flags, and examples.
## `dcr --version`
```sh
dcr --version
```
Prints:
```text
dcr <version> (<target>)
```
## `dcr --update`
```sh
dcr --update
```
Update flow:
1. Requests latest release from GitHub API.
2. Finds asset matching current `DCR_TARGET`.
3. Downloads replacement binary.
4. Replaces current executable via `self-replace`.
Limitations and behavior:
- No extra arguments are accepted.
- Requires network access to GitHub API/assets.
- On Linux, if binary is owned by pacman, DCR prints package-manager update instructions instead of self-updating.
## Running without command
Running plain `dcr` (without arguments) shows help output.

View File

@@ -0,0 +1,74 @@
# Build profiles
DCR supports two profiles:
- `debug` (default)
- `release`
## How to select
```sh
dcr build --debug
dcr build --release
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 -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG`
### MSVC toolchain
- `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
- `release`: `/O2 /DNDEBUG`
## Artifacts
By default:
- `target/debug/<name>` (or `.exe` on Windows)
- `target/release/<name>` (or `.exe` on Windows)
Default compiler flags:
GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG`
MSVC:
- `debug`: `/Od /Zi /W4 /DDCR_DEBUG /Oy-`
- `release`: `/O2 /DNDEBUG`
Static library mode (`kind = "staticlib"`):
- Linux/macOS: `lib<name>.a`
- Windows: `<name>.lib`
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

@@ -0,0 +1,179 @@
# Build section
## Schema
```toml
[build]
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 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): C language standard passed to compiler (e.g. `c11`, `c17`, `gnu11`).
- `cxx_standard` (string, optional): C++ language standard passed to compiler for `.cpp`/`.cxx`/`.cc` files
(e.g. `c++17`, `gnu++17`, `c++20`). When set, overrides `standard` for C++ sources.
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
- `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.
Supports variable substitution — see [Variable substitution](#variable-substitution) below.
- `ldflags` (string array, optional): extra link flags.
Supports variable substitution — see [Variable substitution](#variable-substitution) below.
- `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.
### Variable substitution
`cflags`, `ldflags`, and `include` directory entries support the same `{placeholder}` variables
as build/post steps:
| Variable | Description |
|---|---|
| `{profile}` | Build profile (`debug` / `release`) |
| `{name}` | Package name |
| `{version}` | Full package version (e.g. `0.2.1`) |
| `{version_major}` | Major version number |
| `{version_minor}` | Minor version number |
| `{version_patch}` | Patch version number |
| `{version_suffix}` | Version suffix (e.g. `-rc1`) |
| `{version_suffix_dash}` | Suffix with leading dash (or empty) |
Example:
```toml
[build]
cflags = ['-DPROJECT_VERSION="{version}"', '-DVERSION_MAJOR={version_major}']
```
## Build steps
```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
```toml
[toolchain]
cc = "clang"
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`), `efi`, or `elf`.

View File

@@ -0,0 +1,71 @@
# dcr.toml overview
`dcr.toml` is the main project file used by DCR.
## Minimal generated template
```toml
[package]
name = "hello"
version = "0.1.0"
[build]
language = "c"
standard = "c11"
compiler = "clang"
kind = "bin"
[dependencies]
```
`dcr new` and `dcr init` generate this structure automatically.
## Required sections
- `[package]`
- `[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`
- `package.version`
- `build.language`
- `build.standard`
- `build.compiler`
- `build.kind`
`build.kind` must be either `bin`, `staticlib`, or `sharedlib`.
## 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

@@ -0,0 +1,44 @@
# Dependencies section
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 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.
## Defaults for path dependencies
- If `include` is omitted, DCR tries `<dep>/include`.
- If `lib` is omitted, DCR tries `<dep>/lib`, then `<dep>/lib64`.
- If `libs` is omitted, dependency key name is used.
## Validation
- 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

@@ -0,0 +1,35 @@
# Package section
## Schema
```toml
[package]
name = "my-app"
version = "0.1.0"
type = "app"
description = "My application"
author = "John Doe"
license = "MIT"
```
## 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`).
- `description` (string, optional): human-readable project description.
- `author` (string, optional): single author name.
- `authors` (array of strings, optional): list of authors.
- `license` (string, optional): project license identifier (e.g. `MIT`, `GPL-3.0-or-later`).
- `homepage` (string, optional): project homepage URL.
- `repository` (string, optional): source repository URL.
- `readme` (string, optional): path to README file.
- `keywords` (array of strings, optional): project keywords.
- `categories` (array of strings, optional): project categories.
## 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

@@ -0,0 +1,78 @@
# Compilers & linking
## Compiler backend selection
DCR picks a backend from `build.compiler`:
- contains `clang-cl` -> MSVC backend
- equals `as` or contains `gas` -> GAS backend (standalone assembler)
- contains `gcc` or `g++` -> GCC backend
- contains `clang` or `clang++` -> Clang backend
- equals `cl` or contains `msvc` -> MSVC backend
- contains `nasm` -> NASM backend
- otherwise -> Clang backend fallback
## Compilation model
- Source files are found recursively in `src/` based on `build.language`.
- `build.roots` and `build.src_disable` can replace the default `src/` root.
- Mixed language builds use arrays, for example `build.language = ["c", "asm"]`.
- Each source is compiled to `target/<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
These flags are applied automatically based on profile and compiler backend.
GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`
- `release`: `-O3 -DNDEBUG`
MSVC:
- `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:
- GCC/Clang: `-march=<platform>`
- MSVC: maps to `/arch:*` for known values (`x86`, `i386`, `i486`, `i586`, `i686`, `sse2`, `avx`, `avx2`)
## Toolchain overrides
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
For `kind = "bin"`:
- Object files are linked into executable artifact.
- Dependency lib dirs and library names are passed to linker.
- `build.ldflags` are appended to link command.
For `kind = "staticlib"`:
- Linux/macOS (GCC/Clang): `ar rcs` creates `.a` archive.
- Windows (MSVC): `lib` creates `.lib` archive.
For `kind = "sharedlib"`:
- Linux (GCC/Clang): `-shared` with output `.so`.
- macOS (Clang): `-dynamiclib` with output `.dylib`.
- Windows (MSVC): `/LD` with output `.dll`.

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

@@ -0,0 +1,40 @@
# Dependency format
Dependencies are defined in `dcr.toml` under `[dependencies]`.
## 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`: 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 forms
```toml
[dependencies]
mylib = { path = "./libs/mylib" }
gitlib = { git = "https://github.com/user/repo" }
# Shortest form for path
simple = "./libs/simple"
```
In these forms DCR will try defaults for include/lib dirs and use the dependency name as the linker library name.

View File

@@ -0,0 +1,24 @@
# Local dependencies
## Resolution flow during build
For each dependency in `[dependencies]`, DCR:
1. Resolves `path` (absolute or project-relative).
2. Resolves include dirs (`include` or default `include`).
3. Resolves lib dirs (`lib` or defaults `lib`, `lib64`).
4. Resolves link libs (`libs` or dependency name).
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.
- No valid include/lib directories were found.
- Any configured include/lib path does not exist.
- Invalid field types in dependency config.

View File

@@ -0,0 +1,23 @@
# Target directory
By default, DCR writes final artifacts to `target/<target>/<profile>/` (where `<target>` is `native` or target triple).
You can override this with `build.target`:
```toml
[build]
target = "./dist"
```
## Behavior
- Binary mode:
- Linux/macOS: `<target>/<name>`
- Windows: `<target>/<name>.exe`
- Static library mode:
- Linux/macOS: `<target>/lib<name>.a`
- Windows: `<target>/<name>.lib`
## Important detail
`build.target` changes only the final artifact location. Object cache and dependency sync directories still use `target/<target>/<profile>/...`.

View File

@@ -0,0 +1,32 @@
# Versioning & lock file
DCR generates `dcr.lock` when dependency resolution succeeds and at least one dependency exists.
## What is stored
- Project entry (`name`, `version`, dependency names).
- One entry per resolved dependency:
- `name`
- `version` (from dependency `dcr.toml`, fallback `0.0.0`)
- `source` (`path+...`)
- `checksum` (SHA-256 over dependency files, excluding nested `target/`).
## Example shape
```toml
[[package]]
name = "my-app"
version = "0.1.0"
dependencies = ["fmt"]
[[package]]
name = "fmt"
version = "1.2.3"
source = "path+./third_party/fmt"
checksum = "..."
```
## Notes
- `dcr.lock` is rewritten on successful dependency resolution.
- If `[dependencies]` is empty, lock generation is skipped.

View File

@@ -0,0 +1,57 @@
# Build and run
## Build
Debug (default):
```sh
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
```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.
## Clean artifacts
```sh
dcr clean
```
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"`, `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

@@ -0,0 +1,41 @@
# Create your first project
## Create in a new folder
```sh
dcr new hello
cd hello
```
This creates:
```text
hello/
- dcr.toml
- src/
- - main.c
```
## Or initialize current folder
```sh
mkdir hello
cd hello
dcr init
```
`dcr init` requires the current directory to be empty.
## Next steps
Build and run:
```sh
dcr run
```
Release build/run:
```sh
dcr run --release
```

View File

@@ -0,0 +1,56 @@
# Installation
## Option 1: Install via script
### Linux/macOS
```sh
curl -fsSL https://dcr.dexoron.su/install.sh | bash
```
Alternative with `wget`:
```sh
wget -qO- https://dcr.dexoron.su/install.sh | bash
```
### Windows (PowerShell)
```powershell
powershell -ExecutionPolicy ByPass -c "irm https://dcr.dexoron.su/install.ps1 | iex"
```
## Option 2: Build from source
```sh
git clone https://github.com/dexoron/dcr.git
cd dcr
cargo build --release
mkdir -p ~/.local/bin
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
```
## Option 3: Arch Linux (AUR)
```sh
yay -S dcr
```
(You can also use `paru` or other AUR helpers.)
## Verify installation
```sh
dcr --version
```
Expected format:
```text
dcr <version> (<target>)
```
## Update policy
- If DCR was installed from GitHub releases, scripts, or manual build: use `dcr --update`.
- If DCR is managed by `pacman/AUR`: update via package manager. On Linux, `dcr --update` detects pacman-owned binaries and prints a package-manager hint.

View File

@@ -0,0 +1,40 @@
# Project structure
## Minimal project
```text
project/
- dcr.toml
- src/
- - main.c
```
## Typical generated/optional files
```text
project/
- dcr.toml
- dcr.lock
- src/
- - ... source files ...
- target/
- - debug/
- - - obj/
- - - deps/
- - release/
- - - obj/
- - - deps/
```
## Meaning
- `dcr.toml`: main project configuration.
- `dcr.lock`: generated lock file for resolved path dependencies.
- `src/`: recursively scanned source directory.
- `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/<target>/<profile>/obj`.

View File

@@ -0,0 +1,34 @@
# CLI reference
## Command list
```text
dcr new <name>
dcr init
dcr setup
dcr add <name> <source>(optional)
dcr build [--debug|--release] [--target <triple>]
dcr run [--debug|--release]
dcr clean [--debug|--release] [--all]
dcr tree
dcr test
dcr gen <subcommand>
dcr --help
dcr --version
dcr --update
```
## Notes on argument parsing
- `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 `--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 failures return non-zero status.

View File

@@ -0,0 +1,149 @@
# Config reference
## Full schema
```toml
[package]
name = "string" # required
version = "string" # required
type = "string" # optional: "app", "lib", "none"
description = "string" # optional
author = "string" # optional
authors = ["string", "..."] # optional
license = "string" # optional
homepage = "string" # optional
repository = "string" # optional
readme = "string" # optional
keywords = ["string", "..."] # optional
categories = ["string", "..."] # optional
[build]
language = "c|c++|cpp|cxx|asm" # required (string or array)
standard = "string" # required (e.g. "c11", "gnu11")
cxx_standard = "string" # optional (e.g. "c++17", "gnu++17")
compiler = "string" # required (e.g. "clang", "gcc")
kind = "bin|staticlib|sharedlib|efi|elf" # required
target = "string" # optional (cross-compilation target triple)
platform = "string" # optional (architecture hint, -march)
type = "string" # optional
cflags = ["string", "..."] # optional (supports {version} etc.)
ldflags = ["string", "..."] # optional (supports {version} etc.)
ldscript = "string" # optional (linker script path)
exclude = ["string", "..."] # optional (glob patterns)
include = ["string", "..."] # optional (allowlist + -I dirs)
roots = ["string", "..."] # optional (source roots)
src_disable = false # optional (disable default src/)
pkg_config = ["string", "..."] # optional (pkg-config packages)
generated = ["string", "..."] # optional (generated file patterns)
expect = ["string", "..."] # optional (expected artifact patterns)
clean = ["string", "..."] # optional (extra clean paths)
targets = ["string", "..."] # optional (build targets)
filename = "string" # optional (custom output name)
extension = "string" # optional (custom extension)
inherit = true # optional (inherit base settings)
[[build.steps]]
name = "string" # required
in = "glob" # required
out = "path" # required
cmd = "string" # required
# Inline steps (equivalent to [[build.steps]])
steps = [
{ name = "string", in = "glob", out = "path", cmd = "string" },
]
[[build.post_steps]]
name = "string" # required
in = "glob" # required (or "target/...")
out = "path" # required
cmd = "string" # required
# Inline post_steps (equivalent to [[build.post_steps]])
post_steps = [
{ name = "string", in = "glob", out = "path", cmd = "string" },
]
# Profile overrides (inherit all from [build])
[build.debug]
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
[build.release]
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
# Target-specific overrides
[build.linux]
compiler = "string"
cflags = ["string", "..."]
[build.windows]
compiler = "string"
# Target + profile override
[build.linux.release]
cflags = ["string", "..."]
[toolchain]
cc = "string" # optional (C compiler override)
cxx = "string" # optional (C++ compiler override)
as = "string" # optional (assembler override)
ar = "string" # optional (archiver override)
ld = "string" # optional (linker override)
uic = "string" # optional (Qt uic)
moc = "string" # optional (Qt moc)
rcc = "string" # optional (Qt rcc)
[run]
cmd = "string" # required for `dcr run`
# Debug run variant
[run.debug]
cmd = "string"
[workspace]
[workspace.member_name]
path = "string" # required
deps = ["string", "..."] # optional
[dependencies]
[dependencies.dep_name]
path = "string" # local path
# or
version = "string" # registry version
# or
git = "string" # git URL
branch = "string" # optional (git branch)
tag = "string" # optional (git tag)
rev = "string" # optional (git commit)
# common fields:
include = ["string", "..."] # optional (include dirs)
lib = ["string", "..."] # optional (lib dirs)
libs = ["string", "..."] # optional (link libs)
features = ["string", "..."] # optional (registry features)
```
## Validation rules
- `[package]`, `[build]`, `[dependencies]` must exist.
- Required string fields must be non-empty.
- `build.kind` must be `bin`, `staticlib`, `sharedlib`, `efi`, or `elf`.
- 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`)
- `package.version = "0.1.0"`
- `build.language = "c"`
- `build.standard = "c11"`
- `build.compiler = "clang"`
- `build.kind = "bin"`

View File

@@ -0,0 +1,34 @@
# Contributing & Bug reports
## Contribution workflow
1. Fork `dexoron/dcr` (and `dexoron/dcr-site` if docs website changes are needed).
2. Make focused changes.
3. Run checks locally:
```bash
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check
```
4. Open a PR with short rationale and behavior impact.
5. Update docs when CLI behavior changes.
## Bug report checklist
Include:
1. DCR version (`dcr --version`).
2. OS and compiler/toolchain version.
3. Reproduction steps.
4. Expected result.
5. Actual result and full error output.
6. Minimal reproducible project.
## Where to report
Use GitHub Issues in the relevant repository:
- <https://github.com/dexoron/dcr>
- <https://github.com/dexoron/dcr-site>

View File

@@ -0,0 +1,35 @@
# FAQ
## What commands are available?
`new`, `init`, `build`, `run`, `clean`, `gen`, `--help`, `--version`, `--update`.
## Is `dcr.toml` mandatory?
Yes, for `build`, `run`, and `clean`.
## What source files are compiled?
DCR scans `src/` recursively:
- for C: `*.c`
- for C++: `*.cpp`, `*.cxx`, `*.cc`
## Can I switch compiler?
Yes. Set `build.compiler` in `dcr.toml` (for example `gcc`, `clang`, `cl`, `clang-cl`).
## How does incremental rebuild work?
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?
It tries to run an existing binary from the selected profile; if none exists, it reports an error.
## How do I update DCR?
Use `dcr --update` for non-package-managed installs.
If installed through pacman/AUR, update via package manager.

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' $ErrorActionPreference = 'Stop'
$RepoUrl = 'https://github.com/dexoron/dcr' $RepoUrl = 'https://github.com/dexoron/dcr'
$ApiUrl = 'https://api.github.com/repos/dexoron/dcr/releases/latest' $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' $InstallPath = Join-Path $env:LOCALAPPDATA 'dcr'
$BinPath = Join-Path $env:USERPROFILE '.local\bin' $BinPath = Join-Path $env:USERPROFILE '.local\bin'
$BinaryPath = Join-Path $InstallPath 'dcr.exe' $BinaryPath = Join-Path $InstallPath 'dcr.exe'
function Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan } function Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green } function Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow } function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Fail($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 } function Fail($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Require-Cmd($name) { function Require-Cmd($name) {
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) { 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 { function Select-Mode {
Write-Host "Choose installation mode:" Write-Host "Choose installation mode:"
Write-Host " 1) Download prebuilt binary from GitHub Release (recommended)" Write-Host " 1) Download prebuilt binary from GitHub Release (recommended)"
@@ -26,7 +41,7 @@ function Select-Mode {
switch ($choice) { switch ($choice) {
'1' { return 'release' } '1' { return 'release' }
'2' { return 'build' } '2' { return 'build' }
default { Fail 'Unknown option' } default { Fail 'Unknown option' }
} }
} }
@@ -38,18 +53,35 @@ function Get-Target {
return 'x86_64-pc-windows-msvc' 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 { function Install-FromRelease {
param([string]$target) param([string]$target, [string]$channel)
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
$assetName = "dcr-$target.exe" $release = Fetch-ReleaseJson $channel
Info "Fetching latest release..." $tag = $release.tag_name
$release = Invoke-RestMethod -Method Get -Uri $ApiUrl $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 $asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
if (-not $asset) { if (-not $asset) {
Fail "Asset $assetName not found in release $($release.tag_name)" Fail "Asset $assetName not found in release $tag"
} }
Info "Downloading $assetName..." Info "Downloading $assetName..."
@@ -58,7 +90,7 @@ function Install-FromRelease {
} }
function Install-FromSource { function Install-FromSource {
param([string]$target) param([string]$target, [string]$channel)
Require-Cmd 'git' Require-Cmd 'git'
Require-Cmd 'cargo' Require-Cmd 'cargo'
@@ -67,7 +99,15 @@ function Install-FromSource {
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp } if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
Info 'Cloning repository...' 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...' Info 'Building release binary...'
Push-Location $tmp Push-Location $tmp
@@ -100,14 +140,15 @@ function Setup-Path {
} }
Info 'Starting DCR installation' Info 'Starting DCR installation'
$mode = Select-Mode $channel = Select-Channel
$target = Get-Target $mode = Select-Mode
$target = Get-Target
if ($mode -eq 'build') { if ($mode -eq 'build') {
Install-FromSource -target $target Install-FromSource -target $target -channel $channel
} else { } else {
Install-FromRelease -target $target Install-FromRelease -target $target -channel $channel
} }
Setup-Path 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" BINPATH="$HOME/.local/bin"
LOGFILE="$HOME/.cache/dcr-install.log" LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr" 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' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -17,13 +18,16 @@ NC='\033[0m'
mkdir -p "$(dirname "$LOGFILE")" mkdir -p "$(dirname "$LOGFILE")"
exec > >(tee -a "$LOGFILE") 2>&1 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}"; } success() { echo -e "${GREEN}✔ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; } warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
error() { echo -e "${RED}✖ $1${NC}"; } error() { echo -e "${RED}✖ $1${NC}"; }
trap 'error "Error on line $LINENO"; exit 1' ERR trap 'error "Error on line $LINENO"; exit 1' ERR
INSTALL_MODE=""
CHANNEL=""
check_os() { check_os() {
case "$(uname -s)" in case "$(uname -s)" in
Linux|Darwin) ;; Linux|Darwin) ;;
@@ -37,13 +41,11 @@ detect_target() {
arch="$(uname -m)" arch="$(uname -m)"
case "$os:$arch" in case "$os:$arch" in
Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;; Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;;
Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;; Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;; Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;;
*) error "Unsupported platform: $os/$arch"; exit 1 ;; *) error "Unsupported platform: $os/$arch"; exit 1 ;;
esac esac
ASSET_NAME="dcr-${TARGET_TRIPLE}"
} }
check_common_dependencies() { check_common_dependencies() {
@@ -51,14 +53,106 @@ check_common_dependencies() {
} }
check_build_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; } 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() { prepare_sources() {
log "Fetching sources..." log "Fetching sources..."
rm -rf "$TMPDIR" rm -rf "$TMPDIR"
git clone --depth 1 "$REPO_URL" "$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" success "Sources fetched"
} }
@@ -68,34 +162,6 @@ build_binary() {
success "Build completed" 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() { install_built_binary() {
mkdir -p "$INSTALL_PATH" mkdir -p "$INSTALL_PATH"
cp "$TMPDIR/target/release/dcr" "$INSTALL_PATH/dcr" cp "$TMPDIR/target/release/dcr" "$INSTALL_PATH/dcr"
@@ -122,25 +188,13 @@ cleanup() {
rm -rf "$TMPDIR" 2>/dev/null || true 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() { main() {
log "Starting DCR installation" log "Starting DCR installation"
check_os check_os
detect_target detect_target
check_common_dependencies check_common_dependencies
select_channel
select_install_mode select_install_mode
cleanup cleanup
@@ -161,4 +215,4 @@ main() {
log "Log saved to $LOGFILE" log "Log saved to $LOGFILE"
} }
main "$@" main "$@"

View File

@@ -18,6 +18,7 @@ sha256sums=('30e334fa98cd51073c0fa84c80a982584b00156b57084311ad53419441ce6235')
build() { build() {
cd "$srcdir/$pkgname-$pkgver" cd "$srcdir/$pkgname-$pkgver"
export CARGO_PROFILE_RELEASE_LTO=false export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="${RUSTFLAGS:-}"
export RUSTFLAGS="${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}" export RUSTFLAGS="${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
cargo build --release --locked cargo build --release --locked
} }

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

@@ -0,0 +1,253 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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,256 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::flags; 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::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored}; use crate::utils::text::{BOLD_GREEN, colored};
use glob::glob;
use std::fs; use std::fs;
use std::path::Path;
pub fn clean(args: &[String]) -> i32 { pub fn clean(args: &[String]) -> i32 {
let project_name = std::env::current_dir() let start_dir = match std::env::current_dir() {
.ok() Ok(dir) => dir,
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string())) Err(_) => {
.unwrap_or_else(|| "project".to_string()); error("Failed to determine current directory");
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");
return 1; return 1;
} }
};
let target_items = check_dir(Some("target")).unwrap_or_default(); let root = match find_project_root(&start_dir) {
if !target_items.contains(&profile) { Ok(Some(dir)) => dir,
warn(&format!("Directory target/{profile} not found")); Ok(None) => {
error("dcr.toml file not found");
return 1; 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)); match with_dir(&root, || clean_from_root(&root, &flags)) {
let _ = fs::remove_dir_all(format!("target/{profile}")); Ok(()) => 0,
println!( Err(msg) => {
"{} Removed directory target/{profile}", error(&msg);
colored("\n ✔", BOLD_GREEN) 1
); }
return 0;
} }
}
let _ = fs::remove_dir_all("target");
println!( struct CleanFlags {
"{} Removed directory target", profile: Option<String>,
colored("\n ✔", BOLD_GREEN) target: Option<String>,
); all: bool,
0 }
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

@@ -1,8 +1,26 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde::Deserialize; use serde::Deserialize;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use std::process::Command; use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@@ -137,8 +155,8 @@ fn download_asset(client: &Client, url: &str) -> Result<Vec<u8>, String> {
fn asset_candidates(target: &str) -> Vec<String> { fn asset_candidates(target: &str) -> Vec<String> {
let mut names = vec![format!("dcr-{target}")]; let mut names = vec![format!("dcr-{target}")];
if target == "x86_64-pc-windows-msvc" { if target.contains("-windows-") || target.ends_with("-windows") {
names.push("dcr-x86_64-pc-windows-msvc.exe".to_string()); names.push(format!("dcr-{target}.exe"));
} }
names names
@@ -156,14 +174,14 @@ fn temp_binary_path(current_exe: &Path) -> PathBuf {
current_exe.with_extension(extension) current_exe.with_extension(extension)
} }
fn set_executable_permissions(path: &Path) { fn set_executable_permissions(_path: &Path) {
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::fs::PermissionsExt; 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(); let mut perms = meta.permissions();
perms.set_mode(0o755); perms.set_mode(0o755);
let _ = fs::set_permissions(path, perms); let _ = fs::set_permissions(_path, perms);
} }
} }
} }

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

@@ -0,0 +1,83 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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,
})
}

1268
src/cli/gen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,20 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
pub fn help() -> i32 { pub fn help() -> i32 {
@@ -12,15 +29,21 @@ pub fn help() -> i32 {
println!(" init Initialize the current directory as a project"); println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)"); println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run the project (default: --debug)"); println!(" run [--profile] Build and run the project (default: --debug)");
println!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)");
println!(" clean Remove the target directory"); println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN); printc("FLAGS:", BOLD_GREEN);
println!(" --help Show command help"); println!(" --help Show command help");
println!(" --update Update dcr to the latest version"); println!(" --update Update dcr to the latest version");
println!(" --version Show dcr version"); println!(" --version Show dcr version");
println!(); println!();
printc("OPTIONS:", BOLD_GREEN); printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Build with debug profile"); println!(" --debug Use debug profile");
println!(" --release Build with release profile"); println!(" --release Use release profile");
println!(" --force Force rebuild (build/run)");
println!(" --clean Clean before build (build/run)");
println!(" --all Clean all workspace members (clean)");
println!(); println!();
printc("EXAMPLES:", BOLD_GREEN); printc("EXAMPLES:", BOLD_GREEN);
printc(" dcr new hello", BOLD_CYAN); printc(" dcr new hello", BOLD_CYAN);

View File

@@ -1,3 +1,20 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C; use crate::config::FILE_MAIN_C;
use crate::core::config::Config; use crate::core::config::Config;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
@@ -65,7 +82,7 @@ pub fn init(args: &[String]) -> i32 {
return 1; return 1;
} }
println!( println!(
" {} Created file {}\n", " {} Created file {}",
colored("✔", BOLD_GREEN), colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );

View File

@@ -1,7 +1,30 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod add;
pub mod build; pub mod build;
pub mod clean; pub mod clean;
pub mod flag_update; pub mod flag_update;
pub mod flags;
pub mod r#gen;
pub mod help; pub mod help;
pub mod init; pub mod init;
pub mod new; pub mod new;
pub mod run; pub mod run;
pub mod setup;
pub mod test;
pub mod tree;

View File

@@ -1,3 +1,20 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C; use crate::config::FILE_MAIN_C;
use crate::core::config::Config; use crate::core::config::Config;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
@@ -86,7 +103,7 @@ pub fn new(args: &[String]) -> i32 {
return 1; return 1;
} }
println!( println!(
" {} Created file {}\n", " {} Created file {}",
colored("✔", BOLD_GREEN), colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );

View File

@@ -1,71 +1,193 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::cli::build::build; use crate::cli::build::build;
use crate::config::{PROFILE, flags}; use crate::cli::flags::parse_build_run_flags;
use crate::core::config::Config; use crate::core::config::Config;
use crate::utils::fs::check_dir; use crate::core::runner::run_binary;
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 crate::utils::text::{BOLD_GREEN, colored};
use std::process::Command; use std::process::Command;
pub fn run(args: &[String]) -> i32 { fn get_run_cmd(
let mut active_profile = PROFILE.to_string(); config: &Config,
profile: &str,
let items = check_dir(None).unwrap_or_default(); target: Option<&str>,
if !items.contains(&"dcr.toml".to_string()) { version: &str,
error("dcr.toml file not found"); ) -> Option<String> {
return 1; 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))
} }
}
let config = match Config::open("./dcr.toml") { pub fn run(args: &[String]) -> i32 {
Ok(cfg) => cfg, let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => { 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"); error("dcr.toml file not found");
return 1; 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 let project_name: &str = config
.get("package.name") .get("package.name")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
if let Some(first_arg) = args.first() { let mut flags = match parse_build_run_flags(args) {
if first_arg.starts_with("--") { Ok(v) => v,
let candidate = first_arg.trim_start_matches("--"); Err(_) => return 1,
if flags(candidate).is_some() { };
active_profile = candidate.to_string();
} else { // If no target specified, use default host target for target-specific config
warn("Unknown build flag"); if flags.target.is_none() {
return 1; 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 { } else {
warn("Unknown argument"); "unknown"
return 1; };
} flags.target = Some(default_target.to_string());
} }
let build_status = build(args); let build_kind = config
if build_status == 0 { .get(&format!("build.{}.kind", flags.profile))
println!( .and_then(|v| v.as_str())
"\n {} target/{active_profile}/{project_name}", .or_else(|| config.get("build.kind").and_then(|v| v.as_str()))
colored("Running", BOLD_GREEN) .unwrap_or("");
);
println!("--------------------------------");
return Command::new(format!("./target/{active_profile}/{project_name}"))
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
if check_dir(Some(&format!("target/{active_profile}"))) let normalized_target_dir = flags
.unwrap_or_default() .target
.contains(&active_profile) .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 run_cmd.is_none()
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf")
{ {
warn("Launch of the latest release"); error("Cannot run library build");
return Command::new(format!("./target/{active_profile}/{project_name}")) return 1;
.status() }
.map(|status| status.code().unwrap_or(1)) let build_status = build(args);
.unwrap_or(1); 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,
&flags.profile,
normalized_target_dir.as_deref(),
);
}
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;
} }
error("Fix errors in the code to run the project"); error("Fix errors in the code to run the project");
0 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)
} }

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

@@ -0,0 +1,33 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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
}
}
}

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

@@ -0,0 +1,388 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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);
}
}

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

@@ -0,0 +1,135 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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

@@ -1,3 +1,20 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub const PROFILE: &str = "debug"; pub const PROFILE: &str = "debug";
pub const FILE_MAIN_C: &str = r#"#include <stdio.h> pub const FILE_MAIN_C: &str = r#"#include <stdio.h>
@@ -7,16 +24,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]> { pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
match profile { match profile {
"release" => Some(&["-O3", "-DNDEBUG", "-march=native"]), "release" => Some(&["-O3", "-DNDEBUG"]),
"debug" => Some(&[ "debug" => Some(&[
"-O0", "-O0",
"-g", "-g",
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-fno-omit-frame-pointer", "-fno-omit-frame-pointer",
"-DDEBUG", "-DDCR_DEBUG",
]), ]),
_ => None, _ => None,
} }

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

@@ -0,0 +1,728 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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);
let ext = out
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_string();
let obj_ext = obj_ext.trim_start_matches('.');
if !ext.is_empty() {
let stem = out
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
out.set_file_name(format!("{stem}.{ext}.{obj_ext}"));
} else {
out.set_extension(obj_ext);
}
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.c.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.c.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.c.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.c.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"
);
}
}

174
src/core/builder/gas.rs Normal file
View File

@@ -0,0 +1,174 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 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() {
"as"
} else {
ctx.compiler
};
let start_time = Instant::now();
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" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
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("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
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 == "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> {
// GAS handles only lowercase .s files
common::collect_sources(
ctx.source_roots,
&["s"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_objects(
assembler: &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(assembler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
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)
}

94
src/core/builder/mod.rs Normal file
View File

@@ -0,0 +1,94 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod common;
pub mod gas;
pub mod msvc;
pub mod nasm;
pub mod unix_cc;
pub struct BuildContext<'a> {
pub profile: &'a str,
pub project_name: &'a str,
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
pub cxx_standard: &'a str,
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);
}
if compiler == "as" || compiler.contains("gas") {
return gas::build(ctx);
}
if compiler.contains("nasm") {
return nasm::build(ctx);
}
if compiler == "cl" || compiler.contains("msvc") {
return msvc::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)
}

345
src/core/builder/msvc.rs Normal file
View File

@@ -0,0 +1,345 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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() {
"cl"
} else {
ctx.compiler
};
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)?;
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" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
cmd.arg("/nologo").arg(format!("/OUT:{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(compiler);
cmd.arg("/nologo");
if ctx.kind == "sharedlib" {
cmd.arg("/LD");
}
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() {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
for obj in &objects {
cmd.arg(obj);
}
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.lib_dirs {
cmd.arg(format!("/LIBPATH:{dir}"));
}
for lib in ctx.libs {
if lib.to_lowercase().ends_with(".lib") {
cmd.arg(lib);
} else {
cmd.arg(format!("{lib}.lib"));
}
}
if !ctx.ldflags.is_empty() {
cmd.arg("/link");
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 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;
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(',').map(|s| s.trim()) {
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => {}
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
}
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
let lang = language.to_lowercase();
let std = standard.to_lowercase();
if lang == "c" {
return match std.as_str() {
"c11" => Ok("/std:c11".to_string()),
"c17" => Ok("/std:c17".to_string()),
_ => Err("Unsupported C standard for MSVC".to_string()),
};
}
if lang == "c++" || lang == "cpp" || lang == "cxx" {
return match std.as_str() {
"c++11" => Ok("/std:c++11".to_string()),
"c++14" => Ok("/std:c++14".to_string()),
"c++17" => Ok("/std:c++17".to_string()),
"c++20" => Ok("/std:c++20".to_string()),
"c++23" => Ok("/std:c++latest".to_string()),
_ => Err("Unsupported C++ standard for MSVC".to_string()),
};
}
Err("Unsupported language".to_string())
}
fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
let raw = platform?.trim();
if raw.is_empty() {
return None;
}
let p = raw.to_lowercase().replace('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
return Some("/arch:IA32");
}
match p.as_str() {
"sse2" => Some("/arch:SSE2"),
"avx" => Some("/arch:AVX"),
"avx2" => Some("/arch:AVX2"),
_ => None,
}
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["/O2", "/DNDEBUG"],
"debug" => &["/Od", "/Zi", "/W4", "/DDCR_DEBUG", "/Oy-"],
_ => &[],
}
}
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("/nologo");
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp {
cmd.arg("/TP");
} else {
cmd.arg("/TC");
}
let std_val = if is_cpp && !ctx.cxx_standard.is_empty() {
ctx.cxx_standard
} else if !is_cpp && !ctx.standard.is_empty() {
ctx.standard
} else {
""
};
if !std_val.is_empty() {
let std_flag = msvc_standard_flag(if is_cpp { "c++" } else { "c" }, std_val)?;
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(())
}

221
src/core/builder/nasm.rs Normal file
View File

@@ -0,0 +1,221 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 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() {
"nasm"
} else {
ctx.compiler
};
let start_time = Instant::now();
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" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
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("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
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 == "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> {
// 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(
assembler: &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();
let format = nasm_format(ctx.platform);
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('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
#[cfg(target_os = "macos")]
{
return "macho32";
}
#[cfg(target_os = "windows")]
{
return "win32";
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return "elf32";
}
}
}
#[cfg(target_os = "linux")]
{
"elf64"
}
#[cfg(target_os = "macos")]
{
"macho64"
}
#[cfg(target_os = "windows")]
{
"win64"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"elf64"
}
}

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

@@ -0,0 +1,276 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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(',').map(|s| s.trim()) {
let lang = part.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");
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
cmd.arg(source).arg("-o").arg(obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
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" {
let ext = Path::new(source)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let is_cpp = matches!(ext, "cpp" | "cxx" | "cc");
if is_cpp && !ctx.cxx_standard.is_empty() {
cmd.arg(format!("-std={}", ctx.cxx_standard));
} else if !is_cpp {
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,22 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use toml::Value; use toml::Value;
@@ -5,7 +24,10 @@ use toml::map::Map;
const DEFAULT_VERSION: &str = "0.1.0"; const DEFAULT_VERSION: &str = "0.1.0";
const DEFAULT_LANGUAGE: &str = "c"; const DEFAULT_LANGUAGE: &str = "c";
const DEFAULT_STANDARD: &str = "c11";
const DEFAULT_COMPILER: &str = "clang"; const DEFAULT_COMPILER: &str = "clang";
const DEFAULT_KIND: &str = "bin";
const VALID_KINDS: &[&str] = &["bin", "staticlib", "sharedlib", "efi", "elf"];
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum ConfigError {
@@ -49,6 +71,100 @@ impl From<toml::ser::Error> for ConfigError {
pub struct Config { pub struct Config {
path: PathBuf, path: PathBuf,
data: Value, 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,
#[serde(default)]
pub cxx_standard: Option<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 { impl Config {
@@ -61,7 +177,8 @@ impl Config {
write_toml(&path, &default_value)?; write_toml(&path, &default_value)?;
default_value default_value
}; };
let cfg = Self { path, data }; let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
cfg.validate()?; cfg.validate()?;
Ok(cfg) Ok(cfg)
} }
@@ -72,11 +189,27 @@ impl Config {
return Err(ConfigError::Invalid("dcr.toml not found".into())); return Err(ConfigError::Invalid("dcr.toml not found".into()));
} }
let data = read_toml(&path)?; let data = read_toml(&path)?;
let cfg = Self { path, data }; let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
cfg.validate()?; cfg.validate()?;
Ok(cfg) 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> { pub fn get(&self, key: &str) -> Option<&Value> {
let parts: Vec<&str> = key.split('.').collect(); let parts: Vec<&str> = key.split('.').collect();
get_path(&self.data, &parts) get_path(&self.data, &parts)
@@ -96,17 +229,66 @@ impl Config {
} }
pub fn validate(&self) -> Result<(), ConfigError> { pub fn validate(&self) -> Result<(), ConfigError> {
let package = self if self.typed.package.name.trim().is_empty() {
.get("package") return Err(ConfigError::Invalid("package.name is empty".into()));
.and_then(|v| v.as_table()) }
.ok_or_else(|| ConfigError::Invalid("missing [package]".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", "language", "compiler"] { if let Some(pkg_type) = &self.typed.package.pkg_type {
let value = package.get(key).and_then(|v| v.as_str()).unwrap_or(""); let pkg_type = pkg_type.trim();
if value.trim().is_empty() { if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none"
return Err(ConfigError::Invalid(format!("package.{key} is empty"))); {
return Err(ConfigError::Invalid(
"package.type must be 'lib', 'app', or 'none'".into(),
));
} }
} }
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 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()));
}
validate_toolchain(self.typed.toolchain.as_ref())?;
if let Some(kind) = &self.typed.build.kind {
let kind = kind.trim();
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(()) Ok(())
} }
@@ -116,19 +298,243 @@ impl Config {
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> { fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
let parts: Vec<&str> = key.split('.').collect(); let parts: Vec<&str> = key.split('.').collect();
let previous = self.data.clone();
set_path(&mut self.data, &parts, value)?; 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()?; self.save()?;
Ok(()) 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> { fn read_toml(path: &Path) -> Result<Value, ConfigError> {
let content = fs::read_to_string(path)?; let content = fs::read_to_string(path)?;
Ok(toml::from_str(&content)?) Ok(toml::from_str(&content)?)
} }
fn write_toml(path: &Path, value: &Value) -> Result<(), ConfigError> { fn write_toml(path: &Path, value: &Value) -> Result<(), ConfigError> {
let content = toml::to_string(value)?; let content = format_toml(value)?;
fs::write(path, content)?; fs::write(path, content)?;
Ok(()) Ok(())
} }
@@ -145,22 +551,207 @@ fn default_toml() -> Result<Value, ConfigError> {
"version".to_string(), "version".to_string(),
Value::String(DEFAULT_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( package.insert(
"license".to_string(),
Value::String("GPL-3.0-or-later".to_string()),
);
let mut build = Map::new();
build.insert(
"language".to_string(), "language".to_string(),
Value::String(DEFAULT_LANGUAGE.to_string()), Value::String(DEFAULT_LANGUAGE.to_string()),
); );
package.insert( build.insert(
"standard".to_string(),
Value::String(DEFAULT_STANDARD.to_string()),
);
build.insert(
"compiler".to_string(), "compiler".to_string(),
Value::String(DEFAULT_COMPILER.to_string()), Value::String(DEFAULT_COMPILER.to_string()),
); );
build.insert("kind".to_string(), Value::String(DEFAULT_KIND.to_string()));
let mut root = Map::new(); let mut root = Map::new();
root.insert("package".to_string(), Value::Table(package)); root.insert("package".to_string(), Value::Table(package));
root.insert("build".to_string(), Value::Table(build));
root.insert("dependencies".to_string(), Value::Table(Map::new())); root.insert("dependencies".to_string(), Value::Table(Map::new()));
Ok(Value::Table(root)) Ok(Value::Table(root))
} }
fn format_toml(value: &Value) -> Result<String, ConfigError> {
let root = value
.as_table()
.ok_or_else(|| ConfigError::Invalid("root is not a table".into()))?;
let package = root
.get("package")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
let build = root
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
let deps = root
.get("dependencies")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [dependencies]".into()))?;
let toolchain = root.get("toolchain").and_then(|v| v.as_table());
let name = package.get("name").and_then(|v| v.as_str()).unwrap_or("");
let version = package
.get("version")
.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"));
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");
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
.get("kind")
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_KIND);
out.push_str(&format!("kind = \"{kind}\"\n"));
if let Some(target) = build.get("target").and_then(|v| v.as_str())
&& !target.trim().is_empty()
{
out.push_str(&format!("target = \"{target}\"\n"));
}
if let Some(cflags) = build.get("cflags") {
out.push_str(&format!("cflags = {}\n", format_string_array(cflags)));
}
if let Some(ldflags) = build.get("ldflags") {
out.push_str(&format!("ldflags = {}\n", format_string_array(ldflags)));
}
out.push('\n');
if let Some(toolchain) = toolchain {
let mut lines = Vec::new();
for key in ["cc", "cxx", "as", "ar", "ld"] {
if let Some(value) = toolchain.get(key).and_then(|v| v.as_str())
&& !value.trim().is_empty()
{
lines.push(format!("{key} = \"{value}\""));
}
}
if !lines.is_empty() {
out.push_str("[toolchain]\n");
for line in lines {
out.push_str(&format!("{line}\n"));
}
out.push('\n');
}
}
out.push_str("[dependencies]\n");
if !deps.is_empty() {
let mut keys: Vec<&String> = deps.keys().collect();
keys.sort();
for key in keys {
if let Some(val) = deps.get(key) {
out.push_str(&format!("{key} = {}\n", format_dep_value(val)));
}
}
}
Ok(out)
}
fn format_dep_value(value: &Value) -> String {
match value {
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" }));
}
format!("{{ {} }}", parts.join(", "))
}
_ => "\"\"".to_string(),
}
}
fn format_string_array(value: &Value) -> String {
if let Some(arr) = value.as_array() {
let items: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(|s| format!("\"{s}\"")))
.collect();
return format!("[{}]", items.join(", "));
}
"[]".to_string()
}
fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
let mut current = value; let mut current = value;
for key in path { for key in path {
@@ -191,3 +782,266 @@ fn set_path(value: &mut Value, path: &[&str], new_value: Value) -> Result<(), Co
Err(ConfigError::Invalid("empty key".into())) 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");
}
}

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

@@ -0,0 +1,128 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[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(())
}

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

@@ -0,0 +1,71 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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(())
}

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

@@ -0,0 +1,137 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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
}

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

@@ -0,0 +1,244 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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);
}
}

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

@@ -0,0 +1,240 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 +1,23 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod builder;
pub mod config; pub mod config;
pub mod deps;
pub mod registry;
pub mod runner;
pub mod workspace;

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

@@ -0,0 +1,69 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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,
})
}
}

38
src/core/runner.rs Normal file
View File

@@ -0,0 +1,38 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::platform;
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() {
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;
}
}
}
1
}

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

@@ -0,0 +1,167 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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,7 +1,25 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::error; use crate::utils::log::error;
mod cli; mod cli;
mod config; mod config;
mod core; mod core;
mod platform;
mod utils; mod utils;
fn main() { fn main() {
@@ -17,9 +35,14 @@ fn main() {
let code = match cmd { let code = match cmd {
"new" => cli::r#new::new(rest), "new" => cli::r#new::new(rest),
"init" => cli::init::init(rest), "init" => cli::init::init(rest),
"setup" => cli::setup::setup(rest),
"add" => cli::add::add(rest),
"build" => cli::build::build(rest), "build" => cli::build::build(rest),
"run" => cli::run::run(rest), "run" => cli::run::run(rest),
"tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest),
"clean" => cli::clean::clean(rest), "clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),
"--version" => { "--version" => {
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET")); println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET"));
0 0

127
src/platform/linux.rs Normal file
View File

@@ -0,0 +1,127 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 => {
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 => {
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 => {
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"
);
}
}

51
src/platform/macos.rs Normal file
View File

@@ -0,0 +1,51 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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}"),
}
}
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"),
}
}
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),
None => format!("./target/{profile}/lib{name}.dylib"),
}
}

133
src/platform/mod.rs Normal file
View File

@@ -0,0 +1,133 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "windows")]
pub mod windows;
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
linux::bin_path(profile, name, target_dir)
}
#[cfg(target_os = "macos")]
{
return macos::bin_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
return windows::bin_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 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")]
{
linux::lib_path(profile, name, target_dir)
}
#[cfg(target_os = "macos")]
{
return macos::lib_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
return windows::lib_path(profile, name, target_dir);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
}
}
}
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
linux::shared_lib_path(profile, name, target_dir)
}
#[cfg(target_os = "macos")]
{
return macos::shared_lib_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
return windows::shared_lib_path(profile, name, target_dir);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
}
}
}

51
src/platform/windows.rs Normal file
View File

@@ -0,0 +1,51 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub fn bin_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 lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.lib", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.lib"),
}
}
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),
None => format!("./target/{profile}/{name}.dll"),
}
}

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

@@ -0,0 +1,393 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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,22 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::io; use std::io;
use std::path::PathBuf; use std::path::{Path, PathBuf};
#[allow(dead_code)] #[allow(dead_code)]
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> { pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
@@ -16,3 +33,27 @@ pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
Ok(items) 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,20 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::text::{BOLD_CYAN, colored}; use crate::utils::text::{BOLD_CYAN, colored};
#[allow(dead_code)] #[allow(dead_code)]

View File

@@ -1,3 +1,21 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod build;
pub mod fs; pub mod fs;
pub mod log; pub mod log;
pub mod text; pub mod text;

View File

@@ -1,3 +1,20 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[allow(dead_code)] #[allow(dead_code)]
pub const RESET: &str = "\x1b[0m"; pub const RESET: &str = "\x1b[0m";

424
tests/cli_basic.rs Normal file
View File

@@ -0,0 +1,424 @@
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 {
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() {
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 ["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_sandbox_dir("help");
let out = run_dcr(&["--help"], &dir);
assert!(out.status.success(), "--help should succeed");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success(), "--version should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("dcr"), "version output should mention dcr");
}
#[test]
fn new_creates_project_layout() {
let dir = unique_sandbox_dir("new");
let out = run_dcr(&["new", "hello"], &dir);
assert!(out.status.success(), "dcr new should succeed");
let project_dir = dir.join("hello");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
assert!(
project_dir.join("src").join("main.c").is_file(),
"src/main.c missing"
);
}
#[test]
fn init_and_clean_remove_target() {
let dir = unique_sandbox_dir("init");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let target_debug = dir.join("target").join("debug");
std::fs::create_dir_all(&target_debug).expect("failed to create target/debug");
std::fs::write(target_debug.join("dummy.o"), "x").expect("failed to write dummy file");
let out = run_dcr(&["clean"], &dir);
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"
);
}