30 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
73 changed files with 4967 additions and 1819 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

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

View File

@@ -4,21 +4,76 @@ on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to build (e.g. v0.6.7)"
required: true
run_linux:
description: "Build Linux"
type: boolean
default: true
run_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:
contents: write
jobs:
# ── Resolve Tag ────────────────────────────────────────────────────────────
setup:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.value }}
version: ${{ steps.tag.outputs.version }}
prerelease: ${{ steps.tag.outputs.prerelease }}
steps:
- name: Resolve tag
id: tag
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi
VERSION="${TAG#v}"
[[ "$TAG" == *"-"* ]] && PRERELEASE="true" || PRERELEASE="false"
echo "value=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
# ── Release Notes ──────────────────────────────────────────────────────────
notes:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Prepare Release Notes
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
tag="${{ needs.setup.outputs.version }}"
notes_file=".github/release-notes.md"
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then
awk -v ver="$tag" '
@@ -37,88 +92,85 @@ jobs:
name: release-notes
path: .github/release-notes.md
build:
# ── Linux ──────────────────────────────────────────────────────────────────
build-linux:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }}
needs: [setup, notes]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# GNU/Linux
# x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
asset_name: dcr-x86_64-unknown-linux-gnu
# ARM64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
asset_name: dcr-aarch64-unknown-linux-gnu
# Musl/Linux
# x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
asset_name: dcr-x86_64-unknown-linux-musl
# ARM64
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
asset_name: dcr-aarch64-unknown-linux-musl
# MacOS
# x86_64 (Intel Mac, macOS 10.12+)
- os: macos-15-intel
target: x86_64-apple-darwin
asset_name: dcr-x86_64-apple-darwin
# ARM64 (Apple Silicon Mac, macOS 11+)
- os: macos-latest
target: aarch64-apple-darwin
asset_name: dcr-aarch64-apple-darwin
# Windows MSVC
# x86_64
- os: windows-latest
target: x86_64-pc-windows-msvc
asset_name: dcr-x86_64-pc-windows-msvc.exe
# ARM64
- os: windows-latest
target: aarch64-pc-windows-msvc
asset_name: dcr-aarch64-pc-windows-msvc.exe
# Windows GNU
# x86_64
- os: windows-latest
target: x86_64-pc-windows-gnu
asset_name: dcr-x86_64-pc-windows-gnu.exe
# Windows LLVM
# x86_64
- os: windows-latest
target: x86_64-pc-windows-gnullvm
asset_name: dcr-x86_64-pc-windows-gnullvm.exe
# ARM64
- os: windows-latest
target: aarch64-pc-windows-gnullvm
asset_name: dcr-aarch64-pc-windows-gnullvm.exe
runs-on: ${{ matrix.os }}
needs: notes
- target: x86_64-unknown-linux-gnu
- target: aarch64-unknown-linux-gnu
- target: x86_64-unknown-linux-musl
- target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Install Linux Cross Compilation Tools
if: runner.os == 'Linux'
- name: Install cargo-zigbuild
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu musl-tools
pip install cargo-zigbuild
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: |
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
- name: Prepare Artifact
run: |
ASSET="dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}"
cp target/${{ matrix.target }}/release/dcr "$ASSET"
echo "ASSET_NAME=${ASSET}" >> "$GITHUB_ENV"
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: ${{ env.ASSET_NAME }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── macOS ──────────────────────────────────────────────────────────────────
build-macos:
if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }}
needs: [setup, notes]
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
@@ -128,20 +180,207 @@ jobs:
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Prepare Artifact (Unix)
if: runner.os != 'Windows'
shell: bash
- name: Prepare Artifact
run: |
cp target/${{ matrix.target }}/release/dcr ${{ matrix.asset_name }}
- name: Prepare Artifact (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item "target/${{ matrix.target }}/release/dcr.exe" "${{ matrix.asset_name }}"
ASSET="dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}"
cp target/${{ matrix.target }}/release/dcr "$ASSET"
echo "ASSET_NAME=${ASSET}" >> "$GITHUB_ENV"
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.asset_name }}
tag_name: ${{ needs.setup.outputs.tag }}
files: ${{ env.ASSET_NAME }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── 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/**
.idea/
.vscode/
.env
.env
personal/

View File

@@ -1,77 +0,0 @@
# AGENT.md
## Project Overview
**DCR (Dexoron Cargo Realization)** — CLI tool for managing C/C++/ASM projects in a Cargo-style.
- **Implementation Language**: Rust (edition 2024)
- **License**: MIT
- **Repository**: [github.com/dexoron/dcr](https://github.com/dexoron/dcr)
## Architecture
### Source Tree (`src/`)
```
src/
├── main.rs # Entry point, CLI command routing
├── config.rs # Global constants
├── cli/ # CLI command implementation
│ ├── new.rs # dcr new <name>
│ ├── init.rs # dcr init
│ ├── setup.rs # dcr setup (registry initialization)
│ ├── add.rs # dcr add <name>
│ ├── build.rs # dcr build
│ ├── run.rs # dcr run
│ ├── clean.rs # dcr clean
│ ├── help.rs # dcr --help
│ └── flag_update.rs # dcr --update
├── core/ # Business logic
│ ├── config.rs # dcr.toml parsing
│ ├── registry.rs # Package index management (RegistryManager)
│ ├── deps/ # Dependency resolution (path, git)
│ ├── runner.rs # Binary execution
│ ├── workspace.rs # [workspace] support
│ └── builder/ # Compiler backends (gcc, clang, msvc, nasm, gas)
├── platform/ # Platform-specific logic
└── utils/ # Helper utilities
```
### Key Concepts
- **`~/.dcr/`**: Centralized storage for registries and configurations.
- **`config.toml`**: File in `~/.dcr/` defining available package indices and their priorities.
- **`dcr.toml`**: Local project configuration.
- **Registry System**: `RegistryManager` handles package discovery across multiple indices based on priority.
## Supported Platforms
| Platform | Target triple |
|---|---|
| Linux | `x86_64-unknown-linux-gnu` |
| macOS Intel | `x86_64-apple-darwin` |
| macOS ARM | `aarch64-apple-darwin` |
| Windows | `x86_64-pc-windows-msvc` |
## Dependencies (Cargo)
| Crate | Purpose |
|---|---|
| `reqwest` | HTTP requests |
| `serde` | Serialization |
| `toml` | Configuration parsing |
| `git2` | Git dependency handling |
## Development
### Build & Run
```bash
cargo build --release
./target/release/dcr setup
```
## Code Conventions
- Rust stable edition 2024.
- `cargo fmt` and `cargo clippy -- -D warnings`.
- CLI output in English.
- Use `utils::log` for messaging.

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,5 +1,137 @@
# Changelog
## [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:
- 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:
@@ -79,10 +211,10 @@ Added:
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)
- - `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

View File

@@ -31,7 +31,8 @@ DCR supports cross-compilation via `--target <triple>` or short names:
- `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"]
@@ -41,6 +42,7 @@ DCR supports cross-compilation via `--target <triple>` or short names:
[build.macos]
compiler = "clang"
```
- If no `--target`, builds for targets in `build.targets` or native if empty.
## Checks
@@ -70,10 +72,15 @@ cargo test
- In PRs, include a short description and rationale.
- Update documentation when behavior changes.
## License
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
- DS: dexoron

166
Cargo.lock generated
View File

@@ -35,6 +35,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
@@ -71,18 +80,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "console"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
dependencies = [
"encode_unicode",
"libc",
"unicode-width",
"windows-sys 0.61.2",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -103,13 +100,24 @@ dependencies = [
]
[[package]]
name = "dcr"
version = "0.6.5"
name = "ctrlc"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [
"console",
"dispatch2",
"nix",
"windows-sys 0.61.2",
]
[[package]]
name = "dcr"
version = "0.6.8"
dependencies = [
"ctrlc",
"git2",
"glob",
"indicatif",
"openssl",
"reqwest",
"self-replace",
"serde",
@@ -128,6 +136,18 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"block2",
"libc",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -139,12 +159,6 @@ dependencies = [
"syn",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -179,6 +193,21 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -549,19 +578,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "indicatif"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
dependencies = [
"console",
"portable-atomic",
"unicode-width",
"unit-prefix",
"web-time",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -697,18 +713,79 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "nix"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-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"
@@ -717,6 +794,7 @@ checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
@@ -745,12 +823,6 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -1373,24 +1445,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unit-prefix"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
[[package]]
name = "untrusted"
version = "0.9.0"

View File

@@ -1,8 +1,11 @@
[package]
name = "dcr"
version = "0.6.5"
version = "0.6.8"
edition = "2024"
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]
glob = "0.3"
@@ -11,7 +14,9 @@ serde = { version = "1", features = ["derive"] }
self-replace = "1"
sha2 = "0.10"
toml = "1.0.3"
indicatif = "0.18.4"
console = "0.16.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
## What is this project about?
`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.
The goal is to provide a simpler and easier-to-configure development workflow.
## How is the project developed?
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).
## Which tools are used for development?
Main development stack:
- `RustRover` 2025 - development IDE.
- `Codex` (`GPT-5.3-Codex (medium)`) - primary AI tool, integrated via JetBrains AI Chat.
- `Rust` - main project language.
- `Cargo` - tool for building, testing, and dependency management in Rust projects.
## Is the project open source?
Yes, the project is open source.
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?
Thank you for contributing.
You can:
- help with code;
- help with translations;
- help with documentation;
- suggest an idea or report a bug.
### If you want to submit changes (code, translation, documentation)
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`.
3. Push changes to your fork.
@@ -40,6 +48,7 @@ You can:
5. Wait for review.
### If you want to report a problem or suggest an idea
1. Open the relevant repository: `dexoron/dcr` or `dexoron/dcr-site`.
2. Go to the `Issues` tab.
3. Create an Issue and specify its type (bug, suggestion, question/help).
@@ -47,13 +56,17 @@ You can:
5. Wait for feedback.
## How do I install `dcr`?
Available installation methods may vary by project version.
Recommended path:
- check instructions in `README.md`;
- if there is no prebuilt package for your platform, build from source using `cargo build --release`.
## How can I get started quickly?
Minimal workflow:
1. Install `dcr`.
2. Create a project (or open an existing C/C++ project).
3. Initialize `dcr` configuration.
@@ -63,21 +76,25 @@ Minimal workflow:
For detailed commands and examples, see `README.md` and the project documentation.
## What is the project status?
`dcr` is under active development.
This means some interfaces and behaviors may change between versions.
Before upgrading, check the changelog and release notes.
## Which systems does `dcr` support?
`dcr` targets modern C/C++ development environments.
Actual compatibility depends on the `dcr` version, operating system, and toolchain.
Before starting, check documentation for:
- supported operating systems;
- supported compilers and linkers;
- version-specific limitations.
## How is `dcr` different from 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.
@@ -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.
## Where can I find documentation and examples?
Primary sources:
- `README.md` in the repository;
- project documentation (see the project website);
- `Issues` for known problems and discussions.
## How do I report a bug properly?
To make a bug report easy to reproduce, include:
1. `dcr` version.
2. OS and compiler/toolchain version.
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
of this software and associated documentation files (the "Software"), to deal
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:
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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
SOFTWARE.
## Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
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>.

354
README.md
View File

@@ -1,254 +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
- Unified project structure without manual setup
- Simple commands for common tasks
- Transparent compilation and predictable build profiles
[![CI](https://github.com/dexoron/dcr/actions/workflows/ci.yml/badge.svg)](https://github.com/dexoron/dcr/actions/workflows/ci.yml)
[![GitHub Release](https://img.shields.io/github/v/release/dexoron/dcr)](https://github.com/dexoron/dcr/releases/latest)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/dexoron/dcr/releases)
<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
- 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 IDE integration files (VS Code, CLion) and compilation databases
- Generate a minimal C project template
- Build static and shared libraries
- Dependency management:
- Git and Path dependencies with `dcr.lock`
- Registry-based dependency resolution
- Library packaging functionality (auto-generate `include/` and `lib/` for `type = "lib"` packages)
- ASM projects with NASM/GAS or via GCC/Clang
- Mixed language projects
- Cross-compilation with `--target` (full triple support and short names)
- Target-specific configurations and inheritance
- Update the binary via `dcr --update` (GitHub Releases)
</div>
## Supported Platforms
- Linux: x86_64/aarch64 (GNU and Musl)
- macOS: x86_64/aarch64
- Windows: x86_64/aarch64 (MSVC, GNU, LLVM)
---
## Why DCR?
- **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
### Package Manager
**Arch Linux**
```sh
yay -S dcr # or paru and other AUR package managers
```
### From Source
<table>
<tr>
<td><b>Arch Linux (AUR)</b></td>
<td>
```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
yay -S 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
curl -fsSL https://dcr.dexoron.su/install.sh | bash
```
### Via `install.ps1` (Windows)
</td>
</tr>
<tr>
<td><b>Windows (PowerShell)</b></td>
<td>
```powershell
irm https://dcr.dexoron.su/install.ps1 | iex
```
When executed, both scripts ask whether to:
- download a prebuilt binary from GitHub Releases
- or build the project from `git`
</td>
</tr>
<tr>
<td><b>From source</b></td>
<td>
Release assets:
- `dcr-x86_64-unknown-linux-gnu`
- `dcr-x86_64-apple-darwin`
- `dcr-aarch64-apple-darwin`
- `dcr-x86_64-pc-windows-msvc.exe`
```sh
git clone https://github.com/dexoron/dcr.git
cd dcr && cargo build --release
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
```
## 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
Create a new project:
`dcr new hello`
Or initialize the current directory (the directory must be empty):
`dcr init`
Project structure:
```txt
hello/
- src/
- - main.c
- dcr.toml
```
Build and run the project:
`dcr run` or `dcr run --release`
## Commands
### `dcr new <name>`
Creates a project with the specified name in the current directory.
### `dcr init`
Creates a project in the current directory. The project name is taken from the directory name. The directory must be empty.
### `dcr setup`
Initializes the registry system by reading `~/.dcr/config.toml` and verifying the availability of configured indices.
### `dcr add <name> <source>`
Adds a dependency to `dcr.toml`.
If a registry is configured, `dcr` automatically finds the package.
Git/Path syntax: `path:`, `github:`, `gitlab:`, `git:`, or a full URL.
Use `--branch`, `--tag`, or `--rev` for Git dependencies.
### `dcr build [profile]`
Builds the project. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
### `dcr run [profile]`
Builds the project and runs the binary. If no profile is specified, `--debug` is used.
Use `--force` to rebuild without cache, `--clean` to clean before build.
Run manually:
`./target/<profile>/<name>`
### `dcr clean`
Removes the `target` directory in the project root.
Use `dcr clean --all` in a workspace root to clean all member projects.
### `dcr gen`
Generates IDE integration files and build tools.
- `dcr gen vscode`: VS Code workspace config
- `dcr gen clion`: CLion project files
- `dcr gen compile-commands`: `compile_commands.json` for clang tools
### `dcr test`
Runs the test suite.
Use `dcr test --init` to create test files.
## Build Profiles
Two profiles are supported:
- `--debug` (default) - built-in flags per compiler
- `--release` - built-in flags per compiler
Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`.
Profile-specific flags can be added via `[build.debug]` and `[build.release]`.
You can also set `build.target` to override the output directory (profile-independent).
You can set `build.platform` to pass `-march=<platform>` (GCC/Clang) or `/arch:*` (MSVC) where supported.
Use `build.kind = "staticlib"` to build a static library instead of a binary.
Use `build.kind = "sharedlib"` to build a shared library (`.so`/`.dylib`/`.dll`).
`dcr run` is only for `build.kind = "bin"` and will fail for libraries.
Use `build.exclude`/`build.include` to control source/header collection.
Use `build.roots` and `build.src_disable` to replace the default `src/` root.
Use `build.steps` and `build.post_steps` to run custom commands.
Use `build.clean` for extra cleanup paths and `[run].cmd` to override run command.
## Cross-compilation
DCR supports cross-compilation via `--target <triple>` or short names:
```sh
dcr build --target linux --release # x86_64-unknown-linux-gnu
dcr build --target macos --release # x86_64-apple-darwin
dcr build --target windows --release # x86_64-pc-windows-msvc
dcr run --target aarch64-linux-gnu
dcr new hello
cd hello
dcr run
```
Configure multiple targets and target-specific settings in `dcr.toml`:
**Project structure:**
```toml
[build.targets]
targets = ["linux", "windows"]
[build.linux]
compiler = "gcc"
cflags = ["-O2"]
[build.windows]
compiler = "x86_64-w64-mingw32-gcc"
```
hello/
├── dcr.toml # project config
└── src/
└── main.c
```
See [Cross-compilation guide](docs/dependencies-and-build/cross-compilation.md) for details.
---
## Configuration
The main project file is `dcr.toml`.
For multi-registry management, use `~/.dcr/config.toml`.
Example `dcr.toml`:
## dcr.toml Example
```toml
[package]
name = "hello"
version = "0.1.0"
type = "app" # or "lib", "none"
[build]
language = "c"
standard = "c11"
compiler = "clang"
kind = "bin" # "bin", "staticlib", "sharedlib"
# Optional platform hint
# platform = "x86_64"
# Optional custom flags
cflags = ["-Wall", "-Wextra"]
ldflags = ["-lm"]
#
# Optional include/exclude:
# exclude = ["src/vendor", "src/legacy/**"]
# include = ["src/boot/arch/**"]
[toolchain]
# Optional tool overrides:
# cc = "clang"
# cxx = "clang++"
# as = "as"
# ar = "ar"
# ld = "ld"
# uic = "uic"
# moc = "moc"
# rcc = "rcc"
kind = "bin"
[dependencies]
my_lib = "path:./libs/my_lib"
cool_lib = { git = "https://github.com/user/cool_lib", tag = "v1.0" }
```
Workspace example:
...
[workspace]
kernel = { path = "src/kernel", deps = ["core", "userspace"] }
core = { path = "src/core", deps = ["userspace"] }
userspace = { path = "src/userspace" }
---
## Base Commands
| Command | Description |
|---|---|
| `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
```
Git and Path dependencies are supported. DCR will resolve them on build and generate `dcr.lock`.
---
Incremental build note: object files are rebuilt when source `.c/.cpp` or included header files change.
<div align="center">
GPL-3.0 — see [LICENSE](LICENSE)<br/>
Made with ❤️ by [Dexoron](https://github.com/dexoron) and contributors.
## Requirements
- Rust toolchain (`rustc`, `cargo`) - for building DCR from source
- C compiler (`clang`, `gcc`, or 'cl'(msvc))
## Releases
Releases are built automatically via GitHub Actions (`.github/workflows/release.yml`) when a tag matching `v*` is pushed.
## License
See `LICENSE`.
</div>

View File

@@ -57,9 +57,11 @@ Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers
---
**Currently in progress:**
- ...
**Completed in recent releases:**
- Workspaces
- Cross-compilation
- IDE config generation (VS Code, CLion, compile_commands.json)

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() {
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
println!("cargo:rustc-env=DCR_TARGET={target}");

View File

@@ -27,6 +27,7 @@
* [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

View File

@@ -5,11 +5,15 @@ Add a dependency to `dcr.toml`.
## Usage
```bash
dcr add <name> <source> [options]
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.
@@ -29,4 +33,5 @@ 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

@@ -25,6 +25,8 @@ dcr build --debug --clean
## Config values used
- `package.name`
- `build.filename`
- `build.extension`
- `build.compiler`
- `build.language`
- `build.standard`
@@ -33,6 +35,7 @@ dcr build --debug --clean
- `build.platform`
- `build.cflags`
- `build.ldflags`
- `build.ldscript`
- `build.debug` / `build.release`
- `build.exclude`
- `build.include`
@@ -56,11 +59,16 @@ dcr build --debug --clean
- 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`.

View File

@@ -15,7 +15,7 @@ dcr clean --release --all
## Behavior
- Without arguments: removes entire `target/`.
- With profile: removes only `target/<profile>/`.
- With profile: removes only the profile-specific directory. Respects `build.target` from config: if `build.target = "aarch64-unknown-linux-gnu"`, cleans `target/aarch64-unknown-linux-gnu/<profile>/`.
- With `--all`: also cleans all workspace members.
- Additionally removes paths from `build.clean` if configured.
- `build.clean` supports `{profile}` and version placeholders.

View File

@@ -23,7 +23,7 @@ dcr run --debug --clean
## Restrictions
- If `build.kind = "staticlib"` or `build.kind = "sharedlib"`, `run` exits with error.
- If `build.kind` (or `build.{profile}.kind`) is `"staticlib"` or `"sharedlib"`, `run` exits with error.
- The profile is selected from the first argument.
- `--target <triple>` runs the binary for specified target (short names: `linux`, `macos`, `windows`).
- `--force` skips build cache checks and recompiles.

View File

@@ -6,6 +6,7 @@ Runs the project's test suite.
```sh
dcr test
dcr test --debug
dcr test --release
dcr test --target linux
dcr test --init
@@ -39,6 +40,6 @@ DCR uses a simple test framework:
- Test binary is linked with the main project (for lib projects) or standalone.
- `--init` creates `tests/dcr_test.h` and `tests/test.c` if missing.
- Tests run in release mode by default for performance.
- 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

@@ -8,6 +8,8 @@ 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
@@ -29,15 +31,23 @@ kind = "bin"
- `language` (string or array, required): `c`, `c++`, `cpp`, `cxx`, or `asm`.
Mixed languages can be specified as an array, for example `["c", "c++", "asm"]`.
- `standard` (string, required): language standard passed to compiler.
- `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`, or `sharedlib`.
- `kind` (string, required): `bin`, `staticlib`, `sharedlib`, `efi`, or `elf`.
- `filename` (string, optional): custom output filename (without extension). Overrides `package.name` for the final artifact.
- `extension` (string, optional): custom file extension without the leading dot (e.g. `BIN`, `efi`, `EFI`). Combined with `filename`.
- `inherit` (bool, optional): inherit settings from base `[build]` (default `true`). If `false`, only use target/profile specific settings.
- `targets` (string array, optional): list of targets to build simultaneously. Supports short names (`linux`, `macos`, `windows`).
- `target` (string, optional): custom output directory for final artifact.
When set, DCR automatically injects `--target=<value>` into `cflags` (useful for clang cross-compilation).
- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`).
- `cflags` (string array, optional): extra compile flags.
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.
@@ -54,9 +64,30 @@ Notes for `exclude`/`include`:
- Use `include` to re-allow nested paths, for example exclude `src/boot` but allow `src/boot/arch/**`.
When using glob patterns in `exclude`, DCR converts them internally, so `exclude` globs still work as expected.
### Build steps
### Variable substitution
Build steps run shell commands before compilation. Post steps run after linking.
`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]]
@@ -145,4 +176,4 @@ rcc = "rcc"
- Compiler backend is selected by compiler string (`gcc`, `clang`, `cl`, `clang-cl`).
- Empty or unknown compiler value falls back to clang-like build path.
- `run` is not allowed for library kinds (`staticlib`, `sharedlib`).
- `run` is not allowed for library kinds (`staticlib`, `sharedlib`), `efi`, or `elf`.

View File

@@ -4,9 +4,12 @@
```toml
[package]
name = "my-app"
version = "0.1.0"
type = "app"
name = "my-app"
version = "0.1.0"
type = "app"
description = "My application"
author = "John Doe"
license = "MIT"
```
## Fields
@@ -14,6 +17,15 @@ type = "app"
- `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

View File

@@ -178,6 +178,8 @@ compiler = "aarch64-linux-gnu-gcc"' >> dcr.toml
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

View File

@@ -49,7 +49,7 @@ dcr clean --release --all
## Notes
- `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`.
- `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.

View File

@@ -7,9 +7,11 @@ dcr new <name>
dcr init
dcr setup
dcr add <name> <source>(optional)
dcr build [--debug|--release]
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
@@ -18,11 +20,13 @@ dcr --update
## Notes on argument parsing
- `build`/`run` parse flags in any order (`--debug|--release`, `--force`, `--clean`) and reject duplicates.
- `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

View File

@@ -4,85 +4,131 @@
```toml
[package]
name = "string" # required
version = "string" # required
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
compiler = "string" # required
kind = "bin|staticlib|sharedlib" # required
target = "string" # optional
platform = "string" # optional
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
exclude = ["string", "..."] # optional
include = ["string", "..."] # optional
roots = ["string", "..."] # optional
src_disable = false # optional
pkg_config = ["string", "..."] # optional
generated = ["string", "..."] # optional
expect = ["string", "..."] # optional
clean = ["string", "..."] # optional
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"
in = "glob"
out = "path or glob"
cmd = "string"
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"
in = "glob"
out = "path or glob"
cmd = "string"
name = "string" # required
in = "glob" # required (or "target/...")
out = "path" # required
cmd = "string" # required
# Inline array form (equivalent to [[build.steps]] / [[build.post_steps]])
steps = [
{ name = "string", in = "glob", out = "path or glob", cmd = "string" },
]
# Inline post_steps (equivalent to [[build.post_steps]])
post_steps = [
{ name = "string", in = "glob", out = "path or glob", cmd = "string" },
{ name = "string", in = "glob", out = "path", cmd = "string" },
]
# Profile overrides (inherit all from [build])
[build.debug]
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
[build.release]
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
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
cxx = "string" # optional
as = "string" # optional
ar = "string" # optional
ld = "string" # optional
uic = "string" # optional
moc = "string" # optional
rcc = "string" # optional
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" # optional
cmd = "string" # required for `dcr run`
# Debug run variant
[run.debug]
cmd = "string"
[workspace]
name = { path = "string", deps = ["string", "..."] }
[workspace.member_name]
path = "string" # required
deps = ["string", "..."] # optional
[dependencies]
name = {
path = "string", # required for each dependency
include = ["string", "..."], # optional
lib = ["string", "..."], # optional
libs = ["string", "..."], # optional
}
[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`, or `sharedlib`.
- `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.

View File

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

View File

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

View File

@@ -1,4 +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 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};
@@ -12,6 +30,7 @@ pub struct AddArgs {
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
pub version_from_registry: Option<String>,
}
pub fn add(args: &[String]) -> i32 {
@@ -68,7 +87,11 @@ pub fn add(args: &[String]) -> i32 {
Value::Table(table)
}
} else if let Some(path) = add_args.path {
Value::String(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;
@@ -122,8 +145,38 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
}
source_spec = s.clone();
} else {
error("Dependency source is required");
return Err(1);
// 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() {
@@ -195,5 +248,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
branch,
tag,
rev,
version_from_registry: 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/>.
use crate::cli::clean::clean;
use crate::cli::flags::parse_build_run_flags;
use crate::core::builder::common;
@@ -5,16 +22,28 @@ use crate::core::builder::{BuildContext, build as build_project, collect_sources
use crate::core::config::Config;
use crate::core::deps::{register, resolve_deps};
use crate::core::workspace::parse_workspace;
use crate::utils::fs::{check_dir, find_project_root};
use crate::utils::log::{error, warn};
use crate::utils::build::{
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile,
normalize_target_os, parse_version_info, profile_table, resolve_compiler,
resolve_pkg_config_flags, resolve_tool,
};
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use glob::glob;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Once;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;
static BUILD_INTERRUPTED: AtomicBool = AtomicBool::new(false);
static SIGNAL_HANDLER: Once = Once::new();
pub fn build(args: &[String]) -> i32 {
install_signal_handler();
BUILD_INTERRUPTED.store(false, Ordering::SeqCst);
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
@@ -38,7 +67,15 @@ pub fn build(args: &[String]) -> i32 {
Err(code) => return code,
};
// If no target specified, use default host target for target-specific config
if flags.target.is_none() {
let config_path = root.join("dcr.toml");
if let Ok(config) = Config::open(config_path.to_str().unwrap()) {
let bt = get_build_string_with_profile(&config, "target", "debug");
if !bt.is_empty() {
flags.target = Some(bt);
}
}
}
if flags.target.is_none() {
let default_target = if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu"
@@ -57,7 +94,13 @@ pub fn build(args: &[String]) -> i32 {
let _ = clean(&clean_args);
}
match with_dir(&root, || {
build_from_root(&root, &flags.profile, flags.target.as_deref(), flags.force)
build_from_root(
&root,
&flags.profile,
flags.target.as_deref(),
flags.force,
flags.verbose,
)
}) {
Ok(()) => 0,
Err(msg) => {
@@ -67,77 +110,20 @@ pub fn build(args: &[String]) -> i32 {
}
}
fn get_config_str(config: &Config, key: &str) -> String {
config
.get(key)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
fn install_signal_handler() {
SIGNAL_HANDLER.call_once(|| {
let _ = ctrlc::set_handler(|| {
BUILD_INTERRUPTED.store(true, Ordering::SeqCst);
});
});
}
fn get_config_language(config: &Config, key: &str) -> Result<String, String> {
let value = config.get(key).ok_or_else(|| format!("{key} is missing"))?;
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());
fn check_interrupted() -> Result<(), String> {
if BUILD_INTERRUPTED.load(Ordering::SeqCst) {
Err("Build interrupted".to_string())
} else {
Ok(())
}
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("+"))
}
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("+"))
}
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())
}
fn get_config_value_raw(
@@ -211,7 +197,7 @@ fn get_string_with_profile_and_target(
}
}
fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
get_string_with_profile_and_target(config, field, profile, None)
}
@@ -244,11 +230,18 @@ fn get_list_with_profile_and_target(
// Custom from target/profile
if let Some(val) = get_config_value_raw(config, "build", field, profile, target) {
if let Some(_arr) = val.as_array() {
let custom = parse_string_array(&val, &format!("build.{field}"))?;
if inherit {
out.extend(custom);
} else {
out = custom;
let is_base = inherit
&& config
.get(&format!("build.{field}"))
.map(|v| v == &val)
.unwrap_or(false);
if !is_base {
let custom = parse_string_array(&val, &format!("build.{field}"))?;
if inherit {
out.extend(custom);
} else {
out = custom;
}
}
}
} else if inherit {
@@ -275,22 +268,18 @@ fn get_list_with_profile_and_target(
#[cfg(test)]
mod tests {
use crate::utils::build::normalize_target_os;
#[test]
fn test_normalize_target_os() {
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!(
super::normalize_target_os("linux"),
normalize_target_os("x86_64-unknown-linux-gnu"),
"x86_64-unknown-linux-gnu"
);
assert_eq!(super::normalize_target_os("macos"), "x86_64-apple-darwin");
assert_eq!(
super::normalize_target_os("windows"),
"x86_64-pc-windows-msvc"
);
assert_eq!(
super::normalize_target_os("x86_64-unknown-linux-gnu"),
"x86_64-unknown-linux-gnu"
);
assert_eq!(super::normalize_target_os("unknown"), "unknown");
assert_eq!(normalize_target_os("unknown"), "unknown");
}
}
@@ -306,34 +295,6 @@ fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
get_list_with_profile(config, "targets", profile)
}
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(|table| table.get(field).and_then(|value| value.as_bool()));
profile_val.or(base).unwrap_or(default)
}
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, &format!("build.{profile}.language"));
}
get_config_language(config, "build.language")
}
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())
}
}
fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
let value = match config.get(key) {
Some(v) => v,
@@ -418,9 +379,15 @@ fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<String
if let Some(dir) = &target_dir {
let _ = fs::create_dir_all(dir);
} else {
let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}")
} else {
profile.to_string()
};
let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&profile.to_string()) {
let _ = fs::create_dir(format!("./target/{profile}"));
if !target_items.contains(&default_dir) {
let _ = fs::create_dir_all(format!("./target/{default_dir}"));
}
}
}
@@ -446,6 +413,7 @@ fn build_from_root(
profile: &str,
target: Option<&str>,
force: bool,
verbose: bool,
) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let project_name = get_config_str(&config, "package.name");
@@ -467,6 +435,7 @@ fn build_from_root(
let start_time = Instant::now();
for (i, build_target) in targets_to_build.iter().enumerate() {
check_interrupted()?;
if targets_to_build.len() > 1 {
println!(
" Building target {} of {}: {}",
@@ -486,12 +455,19 @@ fn build_from_root(
);
}
if let Some(workspace) = parse_workspace(&config, profile, build_target.as_deref(), root)? {
build_workspace(&workspace, profile, build_target.as_deref(), force)?;
build_workspace(&workspace, profile, build_target.as_deref(), force, verbose)?;
let excludes: Vec<std::path::PathBuf> =
workspace.members.iter().map(|m| m.path.clone()).collect();
build_project_at(root, profile, build_target.as_deref(), &excludes, force)?;
build_project_at(
root,
profile,
build_target.as_deref(),
&excludes,
force,
verbose,
)?;
} else {
build_project_at(root, profile, build_target.as_deref(), &[], force)?;
build_project_at(root, profile, build_target.as_deref(), &[], force, verbose)?;
}
}
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -508,9 +484,10 @@ fn build_workspace(
profile: &str,
target: Option<&str>,
force: bool,
verbose: bool,
) -> Result<(), String> {
for member in &workspace.members {
build_project_at(&member.path, profile, target, &[], force)?;
build_project_at(&member.path, profile, target, &[], force, verbose)?;
}
Ok(())
}
@@ -521,8 +498,10 @@ fn build_project_at(
target: Option<&str>,
exclude_dirs: &[std::path::PathBuf],
force: bool,
verbose: bool,
) -> Result<(), String> {
with_dir(project_root, || {
check_interrupted()?;
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());
@@ -530,7 +509,7 @@ fn build_project_at(
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let project_name = get_config_str(&config, "package.name");
let project_version = get_config_str(&config, "package.version");
let build_target_config = get_string_with_profile(&config, "target", profile);
let build_target_config = get_build_string_with_profile(&config, "target", profile);
let build_target = target.or(if build_target_config.is_empty() {
None
} else {
@@ -541,6 +520,8 @@ fn build_project_at(
let build_language = get_language_with_profile(&config, profile)?;
let build_standard =
get_string_with_profile_and_target(&config, "standard", profile, build_target);
let build_cxx_standard =
get_string_with_profile_and_target(&config, "cxx_standard", profile, build_target);
let build_kind = get_string_with_profile_and_target(&config, "kind", profile, build_target);
let build_platform =
get_string_with_profile_and_target(&config, "platform", profile, build_target);
@@ -565,10 +546,32 @@ fn build_project_at(
.or_else(|| get_config_opt(&config, "toolchain.moc"));
let tc_rcc = get_config_value(&config, "toolchain", "rcc", profile, build_target)
.or_else(|| get_config_opt(&config, "toolchain.rcc"));
let build_cflags =
let mut build_cflags =
get_list_with_profile_and_target(&config, "cflags", profile, build_target)?;
let build_ldflags =
if let Some(t) = build_target
&& !t.trim().is_empty()
{
let target_flag = format!("--target={}", t.trim());
if !build_cflags
.iter()
.any(|f| f == &target_flag || f.starts_with("--target="))
{
build_cflags.insert(0, target_flag);
}
}
let mut build_ldflags =
get_list_with_profile_and_target(&config, "ldflags", profile, build_target)?;
let build_ldscript =
get_string_with_profile_and_target(&config, "ldscript", profile, build_target);
if !build_ldscript.is_empty() {
build_ldflags.push(format!("-T{}", build_ldscript));
}
// Новые поля: filename и extension
let output_filename =
get_string_with_profile_and_target(&config, "filename", profile, build_target);
let output_extension =
get_string_with_profile_and_target(&config, "extension", profile, build_target);
let build_excludes =
get_list_with_profile_and_target(&config, "exclude", profile, build_target)?;
let build_includes =
@@ -599,41 +602,46 @@ fn build_project_at(
ensure_target_dirs(&items, profile, target_dir);
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let mut resolved = resolve_deps(&config, profile, build_target, project_root)?;
let resolved = resolve_deps(&config, profile, build_target, project_root)?;
// Process dependencies
// Registry dependencies are cached under the DCR registry root. Build
// them as normal DCR projects before the current project is linked.
if let Some(deps) = deps_table {
for (name, _) in deps {
if register::is_registry_dep(config.get(&format!("dependencies.{name}")).unwrap()) {
for (name, value) in deps {
if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let _version = pkg_info
let version = pkg_info
.get("latest_version")
.or_else(|| pkg_info.get("version"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let pkg_path = pkg_info.get("path").and_then(|v| v.as_str()).unwrap_or("");
let dep_root = register::get_index_path()
.parent()
.unwrap()
.join(Path::new(pkg_path).parent().unwrap());
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
let include_dir = dep_root.join("target").join("include");
let lib_dir = dep_root.join("target").join("lib");
if !include_dir.exists() {
if !include_dir.exists() || !lib_dir.exists() {
print!(
"\r{:100}\r {} {} v{}",
"",
colored("Building", BOLD_GREEN),
name,
_version
version
);
std::io::Write::flush(&mut std::io::stdout()).unwrap();
std::thread::sleep(std::time::Duration::from_millis(300));
if !dep_root.join("dcr.toml").is_file() {
return Err(format!(
"Registry dependency `{}` is missing dcr.toml at {}",
name,
dep_root.display()
));
}
build_project_at(&dep_root, profile, build_target, &[], force, verbose)?;
print!(
"\r{:100}\r {} {} v{}",
"",
colored("Ready", BOLD_GREEN),
name,
_version
version
);
println!();
} else {
@@ -641,21 +649,9 @@ fn build_project_at(
" {} {} v{}",
colored("Ready", BOLD_GREEN),
name,
_version
version
);
}
resolved
.include_dirs
.push(include_dir.to_string_lossy().to_string());
resolved.lib_dirs.push(
dep_root
.join("target")
.join("lib")
.to_string_lossy()
.to_string(),
);
resolved.libs.push(name.clone());
}
}
}
@@ -729,12 +725,29 @@ fn build_project_at(
}
let target_dir_binding = normalize_target(build_target.unwrap_or(""), profile);
let version_info = parse_version_info(&project_version);
let substitute = |s: &str| -> String {
s.replace("{profile}", profile)
.replace("{name}", &project_name)
.replace("{version}", &version_info.full)
.replace("{version_major}", &version_info.major)
.replace("{version_minor}", &version_info.minor)
.replace("{version_patch}", &version_info.patch)
.replace("{version_suffix}", &version_info.suffix)
.replace("{version_suffix_dash}", &version_info.suffix_dash)
};
let resolved_cflags: Vec<String> = resolved_cflags.iter().map(|f| substitute(f)).collect();
let modified_ldflags: Vec<String> =
resolved_ldflags.iter().map(|f| substitute(f)).collect();
let merged_include_dirs: Vec<String> =
merged_include_dirs.iter().map(|f| substitute(f)).collect();
let ctx = BuildContext {
profile,
project_name: &project_name,
compiler: &resolved_compiler,
language: &build_language,
standard: &build_standard,
cxx_standard: &build_cxx_standard,
target: build_target,
target_dir: target_dir_binding.as_deref(),
kind: normalize_kind(&build_kind),
@@ -753,7 +766,18 @@ fn build_project_at(
lib_dirs: &resolved.lib_dirs,
libs: &resolved.libs,
cflags: &resolved_cflags,
ldflags: &resolved_ldflags,
ldflags: &modified_ldflags,
output_filename: if output_filename.is_empty() {
None
} else {
Some(output_filename.as_str())
},
output_extension: if output_extension.is_empty() {
None
} else {
Some(output_extension.as_str())
},
verbose,
};
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] debug: compiler={}", ctx.compiler);
@@ -765,7 +789,6 @@ fn build_project_at(
let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs);
let step_flags =
build_step_flags(&resolved_cflags, &resolved.include_dirs, &resolved_compiler);
let version_info = parse_version_info(&project_version);
let step_vars = StepVars {
profile,
version: &version_info.full,
@@ -803,6 +826,7 @@ fn build_project_at(
);
if !skip {
run_build(&ctx)?;
check_interrupted()?;
write_build_fingerprint(&ctx, &fingerprint)?;
}
if ctx.package_type == Some("lib") {
@@ -870,33 +894,6 @@ fn package_library(ctx: &BuildContext, headers: &[PathBuf]) -> Result<(), String
Ok(())
}
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
}
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, // Assume valid triple
_ => {
warn(&format!(
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
target
));
target
}
}
}
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(target.trim());
if trimmed.is_empty() {
@@ -920,176 +917,6 @@ fn normalize_platform(platform: &str) -> Option<&str> {
}
}
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())
}
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()
}
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())
}
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();
if pkgs.is_empty() {
return Ok((cflags, ldflags));
}
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))
}
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;
continue;
}
if ch == '\\' {
if let Some(next) = chars.next() {
current.push(next);
}
continue;
}
current.push(ch);
continue;
}
if ch == '"' || ch == '\'' {
quote = Some(ch);
continue;
}
if ch.is_whitespace() {
if !current.is_empty() {
out.push(current.clone());
current.clear();
}
continue;
}
if ch == '\\' {
if let Some(next) = chars.next() {
current.push(next);
}
continue;
}
current.push(ch);
}
if !current.is_empty() {
out.push(current);
}
out
}
struct ToolchainExecs {
uic: String,
moc: String,
@@ -1449,44 +1276,6 @@ fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
.replace("{version_suffix_dash}", vars.version_suffix_dash)
}
struct VersionInfo {
full: String,
major: String,
minor: String,
patch: String,
suffix: String,
suffix_dash: String,
}
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,
}
}
struct StepVars<'a> {
profile: &'a str,
version: &'a str,
@@ -1576,18 +1365,35 @@ fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(),
}
fn build_cache_path(profile: &str, target_dir: Option<&str>) -> std::path::PathBuf {
let _ = target_dir;
Path::new("./target").join(profile).join(".dcr-build.hash")
match target_dir {
Some(dir) => Path::new(dir).join(".dcr-build.hash"),
None => Path::new("./target").join(profile).join(".dcr-build.hash"),
}
}
fn build_output_path(ctx: &BuildContext) -> String {
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)
};
if ctx.kind == "staticlib" {
return crate::platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "sharedlib" {
return crate::platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir);
}
crate::platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
if ctx.kind == "efi" {
return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir);
}
if ctx.kind == "elf" {
return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir);
}
crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
}
fn collect_header_files(

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::config::flags;
use crate::core::config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::fs::{check_dir, find_project_root};
use crate::utils::build::parse_version_info;
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored};
use glob::glob;
@@ -90,17 +108,27 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
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(|| {
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()
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(
@@ -182,17 +210,6 @@ fn clean_project_at(
})
}
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
}
fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
let patterns = match config.get("build.clean") {
Some(v) => v
@@ -237,41 +254,3 @@ fn substitute_version_vars(value: &str, config: &Config) -> String {
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}
struct VersionInfo {
full: String,
major: String,
minor: String,
patch: String,
suffix: String,
suffix_dash: String,
}
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,
}
}

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

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::{PROFILE, flags};
use crate::utils::log::warn;
@@ -6,6 +23,7 @@ pub struct BuildRunFlags {
pub target: Option<String>,
pub force: bool,
pub clean: bool,
pub verbose: bool,
}
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
@@ -13,6 +31,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
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() {
@@ -29,6 +48,10 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
clean = true;
continue;
}
if candidate == "verbose" {
verbose = true;
continue;
}
if candidate == "target" {
if let Some(t) = iter.next() {
target = Some(t.clone());
@@ -55,5 +78,6 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
target,
force,
clean,
verbose,
})
}

View File

@@ -1,183 +1,34 @@
// 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::collect_sources;
use crate::core::builder::common;
use crate::core::config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::{
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,
get_list_with_profile, get_string_with_profile, normalize_target_os, resolve_compiler,
resolve_pkg_config_flags_lossy,
};
use crate::utils::fs::find_project_root;
use crate::utils::log::{error, warn};
use crate::utils::log::error;
use std::path::{Path, PathBuf};
// ── helpers copied from cli::build (private there) ─────────────────────────
fn get_config_str(config: &Config, key: &str) -> String {
config
.get(key)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
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())
}
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()
}
}
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()
}
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
}
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)
}
fn get_language_with_profile(config: &Config, profile: &str) -> String {
if let Some(table) = profile_table(config, profile)
&& let Some(value) = table.get("language")
{
return parse_language_value(value);
}
let value = config.get("build.language");
match value {
Some(v) => parse_language_value(v),
None => "c".to_string(),
}
}
fn parse_language_value(value: &toml::Value) -> String {
if let Some(s) = value.as_str() {
return s.trim().to_string();
}
if let Some(arr) = value.as_array() {
let parts: Vec<&str> = arr.iter().filter_map(|v| v.as_str()).collect();
return parts.join("+");
}
"c".to_string()
}
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())
}
}
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);
if let Ok(v) = std::env::var("DCR_COMPILER") {
let t = v.trim().to_string();
if !t.is_empty() {
return t;
}
}
if lang == "asm" {
if let Ok(v) = std::env::var("DCR_AS") {
let t = v.trim().to_string();
if !t.is_empty() {
return t;
}
}
if let Some(v) = tc_as {
return v.to_string();
}
}
if lang == "c++" || lang == "cpp" || lang == "cxx" {
if let Ok(v) = std::env::var("DCR_CXX") {
let t = v.trim().to_string();
if !t.is_empty() {
return t;
}
}
if let Some(v) = tc_cxx {
return v.to_string();
}
}
if let Ok(v) = std::env::var("DCR_CC") {
let t = v.trim().to_string();
if !t.is_empty() {
return t;
}
}
if let Some(v) = tc_cc {
return v.to_string();
}
compiler.to_string()
}
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()
}
/// Like `deps::resolve_deps` but does NOT require lib directories to exist.
/// Used by `gen` commands where the project may not have been built yet.
struct GenDeps {
@@ -310,53 +161,6 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
}
}
fn resolve_pkg_config_flags(
pkgs: &[String],
base_cflags: &[String],
base_ldflags: &[String],
) -> (Vec<String>, Vec<String>) {
let mut cflags = base_cflags.to_vec();
let mut ldflags = base_ldflags.to_vec();
for pkg in pkgs {
match std::process::Command::new("pkg-config")
.arg("--cflags")
.arg(pkg)
.output()
{
Ok(out) if out.status.success() => {
let s = String::from_utf8_lossy(&out.stdout);
cflags.extend(s.split_whitespace().map(|v| v.to_string()));
}
Ok(out) => {
let err = String::from_utf8_lossy(&out.stderr);
eprintln!("Warning: pkg-config --cflags {pkg} failed: {err}");
}
Err(e) => {
eprintln!("Warning: pkg-config --cflags {pkg} error: {e}");
}
}
match std::process::Command::new("pkg-config")
.arg("--libs")
.arg(pkg)
.output()
{
Ok(out) if out.status.success() => {
let s = String::from_utf8_lossy(&out.stdout);
ldflags.extend(s.split_whitespace().map(|v| v.to_string()));
}
Ok(out) => {
let err = String::from_utf8_lossy(&out.stderr);
eprintln!("Warning: pkg-config --libs {pkg} failed: {err}");
}
Err(e) => {
eprintln!("Warning: pkg-config --libs {pkg} error: {e}");
}
}
}
(cflags, ldflags)
}
// ── public API ───────────────────────────────────────────────────────────────
/// Everything needed to generate output for one project member.
@@ -367,6 +171,7 @@ pub struct ProjectInfo {
pub profile: String,
pub language: String,
pub standard: String,
pub cxx_standard: String,
pub compiler: String,
pub kind: String,
pub sources: Vec<String>,
@@ -422,8 +227,9 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
let name = get_config_str(&config, "package.name");
let version = get_config_str(&config, "package.version");
let language = get_language_with_profile(&config, profile);
let language = get_language_with_profile_or_default(&config, profile);
let standard = get_string_with_profile(&config, "standard", profile);
let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile);
let compiler_s = get_string_with_profile(&config, "compiler", profile);
let kind = get_string_with_profile(&config, "kind", profile);
let build_target = get_string_with_profile(&config, "target", profile);
@@ -464,7 +270,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
let resolved = resolve_deps_for_gen(&config, profile, root);
let (resolved_cflags, resolved_ldflags) =
resolve_pkg_config_flags(&pkg_configs, &base_cflags, &base_ldflags);
resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags);
// Build exclude/include pattern lists (same logic as cli::build)
let mut combined_excludes: Vec<PathBuf> = Vec::new();
@@ -535,6 +341,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
compiler: &resolved_compiler,
language: &language,
standard: &standard,
cxx_standard: &cxx_standard,
target: Some(build_target.as_str()),
target_dir: target_dir_binding.as_deref(),
kind: normalize_kind(&kind),
@@ -550,6 +357,9 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
libs: &resolved.libs,
cflags: &resolved_cflags,
ldflags: &resolved_ldflags,
output_filename: None,
output_extension: None,
verbose: false,
};
let sources = collect_sources(&ctx).map_err(|e| format!("Failed to collect sources: {e}"))?;
@@ -574,6 +384,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
profile: profile.to_string(),
language,
standard,
cxx_standard,
compiler: resolved_compiler,
kind: normalize_kind(&kind).to_string(),
sources: abs_sources,
@@ -664,6 +475,10 @@ fn project_info_to_json(info: &ProjectInfo) -> String {
" \"standard\": {},\n",
json_str(&info.standard)
));
out.push_str(&format!(
" \"cxx_standard\": {},\n",
json_str(&info.cxx_standard)
));
out.push_str(&format!(
" \"compiler\": {},\n",
json_str(&info.compiler)
@@ -768,6 +583,13 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
};
cmd.push(compiler.to_string());
cmd.push("-c".to_string());
// ASM x flag — must be before source file
if let Some(flag) = asm_lang_flag(source) {
cmd.push("-x".to_string());
cmd.push(flag.to_string());
}
cmd.push(source.to_string());
// Object path (for -o, approximate — not critical for IntelliSense)
@@ -788,15 +610,18 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
cmd.push("-fPIC".to_string());
}
// ASM x flag
if let Some(flag) = asm_lang_flag(source) {
cmd.push("-x".to_string());
cmd.push(flag.to_string());
}
// -std=
if !info.standard.is_empty() && info.language.to_lowercase() != "asm" {
cmd.push(format!("-std={}", info.standard));
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 && !info.cxx_standard.is_empty() {
cmd.push(format!("-std={}", info.cxx_standard));
} else if !is_cpp {
cmd.push(format!("-std={}", info.standard));
}
}
// Default profile flags (mirrors unix_cc.rs defaults)
@@ -1395,22 +1220,6 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
Ok((root, profile))
}
fn normalize_target_os(s: &str) -> &str {
match s {
"linux" => "x86_64-unknown-linux-gnu",
"macos" => "x86_64-apple-darwin",
"windows" => "x86_64-pc-windows-msvc",
_ if s.contains('-') => s, // Assume valid triple
_ => {
warn(&format!(
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
s
));
s
}
}
}
fn normalize_target(s: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(s.trim());
if trimmed.is_empty() {

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};
pub fn help() -> i32 {
@@ -12,6 +29,7 @@ pub fn help() -> i32 {
println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)");
println!(" run [--profile] Build and run the project (default: --debug)");
println!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)");
println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");

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::core::config::Config;
use crate::utils::fs::check_dir;

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 mod add;
pub mod build;
pub mod clean;
@@ -10,3 +27,4 @@ pub mod new;
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::core::config::Config;
use crate::utils::fs::check_dir;

View File

@@ -1,11 +1,29 @@
// 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::flags::parse_build_run_flags;
use crate::core::config::Config;
use crate::core::runner::run_binary;
use crate::utils::build::{normalize_target_os, parse_version_info};
use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use std::path::Path;
use std::process::Command;
fn get_run_cmd(
@@ -36,15 +54,6 @@ fn get_run_cmd(
}
}
fn normalize_target_os(s: &str) -> &str {
match s {
"linux" => "x86_64-unknown-linux-gnu",
"macos" => "x86_64-apple-darwin",
"windows" => "x86_64-pc-windows-msvc",
_ => s,
}
}
pub fn run(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
@@ -77,15 +86,6 @@ pub fn run(args: &[String]) -> i32 {
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("");
let build_kind = config
.get("build.kind")
.and_then(|v| v.as_str())
.unwrap_or("");
let target_dir = config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim())
.filter(|s| !s.is_empty());
let mut flags = match parse_build_run_flags(args) {
Ok(v) => v,
@@ -106,6 +106,12 @@ pub fn run(args: &[String]) -> i32 {
flags.target = Some(default_target.to_string());
}
let build_kind = config
.get(&format!("build.{}.kind", flags.profile))
.and_then(|v| v.as_str())
.or_else(|| config.get("build.kind").and_then(|v| v.as_str()))
.unwrap_or("");
let normalized_target_dir = flags
.target
.as_ref()
@@ -118,7 +124,9 @@ pub fn run(args: &[String]) -> i32 {
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") {
if run_cmd.is_none()
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf")
{
error("Cannot run library build");
return 1;
}
@@ -136,13 +144,21 @@ pub fn run(args: &[String]) -> i32 {
}
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------");
return run_binary(project_name, &flags.profile, target_dir);
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, target_dir)
run_binary(
project_name,
&flags.profile,
normalized_target_dir.as_deref(),
)
};
if fallback_code != 1 {
return fallback_code;
@@ -152,17 +168,6 @@ pub fn run(args: &[String]) -> i32 {
1
}
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
}
fn run_shell(cmd: &str) -> i32 {
let status = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/C").arg(cmd).status()
@@ -186,41 +191,3 @@ fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}
struct VersionInfo {
full: String,
major: String,
minor: String,
patch: String,
suffix: String,
suffix_dash: String,
}
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,
}
}

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 fn setup(_args: &[String]) -> i32 {
println!("Setting up DCR registries...");
match crate::core::registry::RegistryManager::load() {

View File

@@ -1,22 +1,43 @@
// 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::fs::find_project_root;
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::env;
use std::fs;
use std::path::Path;
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]");
println!(" dcr test [--init] [--debug | --release]");
println!();
println!("ALIASES:");
println!(" dcr tests");
@@ -26,12 +47,18 @@ pub fn test(args: &[String]) -> i32 {
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;
}
@@ -65,7 +92,7 @@ pub fn test(args: &[String]) -> i32 {
}
}
match with_dir(&root, run_testsuite) {
match with_dir(&root, || run_testsuite(profile)) {
Ok(code) => code,
Err(msg) => {
error(&msg);
@@ -74,73 +101,84 @@ pub fn test(args: &[String]) -> i32 {
}
}
fn run_testsuite() -> Result<i32, String> {
if build::build(&[String::from("--release")]) != 0 {
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 = config
.get("package.name")
.and_then(|v| v.as_str())
.ok_or("package.name not found in dcr.toml")?;
let kind = config
.get("package.kind")
.and_then(|v| v.as_str())
.unwrap_or("bin");
let 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 compiler = env::var("DCR_CC").unwrap_or_else(|_| "cc".to_string());
let profile = "release";
let flags_vec = flags(profile).unwrap_or(&[]);
let flags_str = flags_vec.join(" ");
let test_c = "./tests/test.c";
if !Path::new(test_c).exists() {
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());
}
// Compile test.c to object
let obj_path = "./tests/test.o";
let compile_cmd = format!("{} -c {} -o {} {}", compiler, test_c, obj_path, flags_str);
let compile_status = Command::new("sh")
.arg("-c")
.arg(&compile_cmd)
.status()
.map_err(|_| format!("Failed to compile {}", test_c))?;
if !compile_status.success() {
return Err(format!("Compilation of {} failed", test_c));
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;
}
}
// Link
let bin_path = format!("./tests/test{}", std::env::consts::EXE_SUFFIX);
let link_cmd = if kind == "lib" {
format!(
"{} {} -o {} -Ltarget/release -l{}",
compiler, obj_path, bin_path, name
)
} else {
format!("{} {} -o {}", compiler, obj_path, bin_path)
};
let link_status = Command::new("sh")
.arg("-c")
.arg(&link_cmd)
.status()
.map_err(|_| format!("Failed to link {}", bin_path))?;
if !link_status.success() {
return Err(format!("Linking of {} failed", bin_path));
}
let out = Command::new(&bin_path)
.output()
.map_err(|_| format!("Failed to run `{bin_path}`"))?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.trim().is_empty() {
eprint!("{}", stderr);
}
let declared = extract_test_names("./tests/test.c");
let mut pass = 0;
let mut skip = 0;
let mut fail = 0;
@@ -189,7 +227,7 @@ fn run_testsuite() -> Result<i32, String> {
}
if !parsed_any {
if out.status.success() {
if suite_success {
for name in &declared {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
}
@@ -216,12 +254,104 @@ fn run_testsuite() -> Result<i32, String> {
print_field("FAIL", fail, BOLD_RED);
println!("{}", colored("=====================", BOLD_GREEN));
if fail > 0 || !out.status.success() {
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)
@@ -233,7 +363,7 @@ fn ensure_test_header() -> Result<(), String> {
Ok(())
}
fn extract_test_names(path: &str) -> Vec<String> {
fn extract_test_names_path(path: &Path) -> Vec<String> {
let Ok(content) = fs::read_to_string(path) else {
return Vec::new();
};
@@ -256,14 +386,3 @@ fn print_field(label: &str, value: i32, color: &str) {
println!("{}{}: {}{}", color, label, value, RESET);
}
}
fn with_dir<F, T>(dir: &Path, f: F) -> Result<T, String>
where
F: FnOnce() -> Result<T, String>,
{
let prev = std::env::current_dir().map_err(|_| "Failed to get current dir".to_string())?;
std::env::set_current_dir(dir).map_err(|_| "Failed to change directory".to_string())?;
let result = f();
let _ = std::env::set_current_dir(prev);
result
}

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 FILE_MAIN_C: &str = r#"#include <stdio.h>

View File

@@ -1,5 +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/>.
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],
@@ -153,6 +178,65 @@ 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();
@@ -167,12 +251,33 @@ pub fn normalize_source_path(path: &Path) -> String {
pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let stripped = src_path.strip_prefix("./").unwrap_or(src_path);
let rel = stripped
.components()
.skip(1)
.collect::<std::path::PathBuf>();
let rel = if rel.components().next().is_some() {
rel
} else {
stripped.to_path_buf()
};
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
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()
}
@@ -289,28 +394,28 @@ mod tests {
fn object_path_basic() {
let obj_dir = Path::new("target/debug/obj");
let result = object_path(obj_dir, "./src/main.c", "o");
assert_eq!(result, "target/debug/obj/main.o");
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.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.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.obj");
assert_eq!(result, "target/debug/obj/main.c.obj");
}
#[test]

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::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
@@ -8,7 +25,7 @@ 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") {
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() {
@@ -18,7 +35,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
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" {
@@ -37,7 +57,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
if std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -70,14 +90,24 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -107,25 +137,38 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(&obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -1,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 mod common;
pub mod gas;
pub mod msvc;
@@ -10,7 +27,7 @@ pub struct BuildContext<'a> {
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
#[allow(dead_code)]
pub cxx_standard: &'a str,
pub target: Option<&'a str>,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
@@ -26,10 +43,19 @@ pub struct BuildContext<'a> {
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);
}
@@ -45,6 +71,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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") {

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::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
@@ -18,7 +35,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
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" {
@@ -28,7 +48,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
if std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -92,12 +112,14 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
cmd.arg(format!("/Fe:{out_path}"));
if std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -122,8 +144,8 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split('+') {
let lang = part.trim().to_lowercase();
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"]),
@@ -192,96 +214,132 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler);
cmd.arg("/nologo");
match ctx.language.to_lowercase().as_str() {
"c" => {
cmd.arg("/TC");
}
"c++" | "cpp" | "cxx" => {
cmd.arg("/TP");
}
_ => {
return Err("Unsupported language".to_string());
}
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
if let Some(flag) = msvc_arch_flag(ctx.platform) {
cmd.arg(flag);
}
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");
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
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);
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
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');
}
}
if !output.status.success() {
eprint!("{}", clean_stdout);
eprint!("{}", stderr);
return Err("Build failed".to_string());
} else {
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));
}
std::fs::write(&d_path, d_content).map_err(|err| format!("d file error: {err}"))?;
}
objects.push(obj_path);
}
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(())
}

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::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
@@ -8,7 +25,7 @@ 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") {
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() {
@@ -18,7 +35,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
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" {
@@ -37,7 +57,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
if std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -70,14 +90,24 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
if std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -107,37 +137,53 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
let format = nasm_format(ctx.platform);
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(&obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], format, ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
format: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)
.arg(source)
.arg("-o")
.arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {
let p = p.to_lowercase().replace('-', "_");

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::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
@@ -20,7 +37,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
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" {
@@ -30,7 +50,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for obj in &objects {
cmd.arg(obj);
}
if std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -51,6 +71,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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);
}
@@ -63,14 +89,26 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "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 std::env::var("DCR_DEBUG").is_ok() {
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
@@ -95,8 +133,8 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split('+') {
let lang = part.trim().to_lowercase();
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"]),
@@ -125,6 +163,19 @@ fn default_flags(profile: &str) -> &'static [&'static str] {
}
}
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],
@@ -132,56 +183,89 @@ fn build_objects(
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = common::object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if common::needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
cmd.arg(format!("-march={}", platform));
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard));
}
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}"));
}
let d_path = Path::new(&obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn 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 {

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::path::{Path, PathBuf};
use toml::Value;
@@ -8,6 +27,7 @@ const DEFAULT_LANGUAGE: &str = "c";
const DEFAULT_STANDARD: &str = "c11";
const DEFAULT_COMPILER: &str = "clang";
const DEFAULT_KIND: &str = "bin";
const VALID_KINDS: &[&str] = &["bin", "staticlib", "sharedlib", "efi", "elf"];
#[derive(Debug)]
pub enum ConfigError {
@@ -51,6 +71,100 @@ impl From<toml::ser::Error> for ConfigError {
pub struct Config {
path: PathBuf,
data: Value,
typed: DcrConfig,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct DcrConfig {
pub package: PackageConfig,
pub build: BuildConfig,
#[serde(default)]
pub dependencies: BTreeMap<String, DependencyConfig>,
#[serde(default)]
pub toolchain: Option<ToolchainConfig>,
#[serde(default)]
pub workspace: BTreeMap<String, WorkspaceMemberConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub version: String,
#[serde(default, rename = "type")]
pub pkg_type: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct BuildConfig {
pub language: LanguageConfig,
pub standard: String,
#[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 {
@@ -63,7 +177,8 @@ impl Config {
write_toml(&path, &default_value)?;
default_value
};
let cfg = Self { path, data };
let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
cfg.validate()?;
Ok(cfg)
}
@@ -74,11 +189,27 @@ impl Config {
return Err(ConfigError::Invalid("dcr.toml not found".into()));
}
let data = read_toml(&path)?;
let cfg = Self { path, data };
let typed = parse_typed_config(&data)?;
let cfg = Self { path, data, typed };
cfg.validate()?;
Ok(cfg)
}
#[allow(dead_code)]
pub fn typed(&self) -> &DcrConfig {
&self.typed
}
#[allow(dead_code)]
pub fn package(&self) -> &PackageConfig {
&self.typed.package
}
#[allow(dead_code)]
pub fn build_config(&self) -> &BuildConfig {
&self.typed.build
}
pub fn get(&self, key: &str) -> Option<&Value> {
let parts: Vec<&str> = key.split('.').collect();
get_path(&self.data, &parts)
@@ -98,19 +229,15 @@ impl Config {
}
pub fn validate(&self) -> Result<(), ConfigError> {
let package = self
.get("package")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
for key in ["name", "version"] {
let value = package.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("package.{key} is empty")));
}
if self.typed.package.name.trim().is_empty() {
return Err(ConfigError::Invalid("package.name is empty".into()));
}
if self.typed.package.version.trim().is_empty() {
return Err(ConfigError::Invalid("package.version is empty".into()));
}
validate_package_name(&self.typed.package.name)?;
if let Some(pkg_type) = package.get("type").and_then(|v| v.as_str()) {
if let Some(pkg_type) = &self.typed.package.pkg_type {
let pkg_type = pkg_type.trim();
if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none"
{
@@ -120,106 +247,41 @@ impl Config {
}
}
let build = self
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
if let Some(language) = build.get("language") {
self.validate_language(language)?;
} else {
return Err(ConfigError::Invalid("build.language is empty".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()));
}
for key in ["standard", "compiler"] {
let value = build.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("build.{key} is empty")));
}
if self.typed.build.compiler.trim().is_empty() {
return Err(ConfigError::Invalid("build.compiler is empty".into()));
}
if let Some(platform) = build.get("platform")
&& !platform
.as_str()
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
if let Some(platform) = &self.typed.build.platform
&& platform.trim().is_empty()
{
return Err(ConfigError::Invalid("build.platform is empty".into()));
}
if let Some(toolchain) = self.get("toolchain").and_then(|v| v.as_table()) {
for key in ["cc", "cxx", "as", "ar", "ld"] {
if let Some(v) = toolchain.get(key)
&& !v.as_str().map(|s| !s.trim().is_empty()).unwrap_or(false)
{
return Err(ConfigError::Invalid(format!("toolchain.{key} is invalid")));
}
}
}
if let Some(kind) = build.get("kind").and_then(|v| v.as_str()) {
validate_toolchain(self.typed.toolchain.as_ref())?;
if let Some(kind) = &self.typed.build.kind {
let kind = kind.trim();
if !kind.is_empty() && kind != "bin" && kind != "staticlib" && kind != "sharedlib" {
if !kind.is_empty() && !VALID_KINDS.contains(&kind) {
return Err(ConfigError::Invalid("build.kind is invalid".into()));
}
}
if let Some(exclude) = build.get("exclude") {
let arr = exclude.as_array().ok_or_else(|| {
ConfigError::Invalid("build.exclude must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.exclude contains empty value".into(),
));
}
}
}
if let Some(include) = build.get("include") {
let arr = include.as_array().ok_or_else(|| {
ConfigError::Invalid("build.include must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.include contains empty value".into(),
));
}
}
}
if let Some(roots) = build.get("roots") {
let arr = roots.as_array().ok_or_else(|| {
ConfigError::Invalid("build.roots must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.roots contains empty value".into(),
));
}
}
}
if let Some(src_disable) = build.get("src_disable")
&& !src_disable.is_bool()
{
return Err(ConfigError::Invalid(
"build.src_disable must be a boolean".into(),
));
}
if let Some(clean) = build.get("clean") {
let arr = clean.as_array().ok_or_else(|| {
ConfigError::Invalid("build.clean must be an array of strings".into())
})?;
for item in arr {
let s = item.as_str().unwrap_or("");
if s.trim().is_empty() {
return Err(ConfigError::Invalid(
"build.clean contains empty value".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) = build.get(profile) {
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"))
})?;
@@ -236,12 +298,93 @@ impl Config {
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
let parts: Vec<&str> = key.split('.').collect();
let previous = self.data.clone();
set_path(&mut self.data, &parts, value)?;
self.typed = match parse_typed_config(&self.data) {
Ok(typed) => typed,
Err(err) => {
self.data = previous;
return Err(err);
}
};
if let Err(err) = self.validate() {
self.data = previous;
self.typed = parse_typed_config(&self.data)?;
return Err(err);
}
self.save()?;
Ok(())
}
}
fn parse_typed_config(value: &Value) -> Result<DcrConfig, ConfigError> {
value.clone().try_into().map_err(ConfigError::TomlDe)
}
fn validate_package_name(name: &str) -> Result<(), ConfigError> {
let trimmed = name.trim();
if trimmed != name {
return Err(ConfigError::Invalid(
"package.name must not contain leading or trailing whitespace".into(),
));
}
if trimmed == "." || trimmed == ".." || trimmed.contains("..") {
return Err(ConfigError::Invalid("package.name is invalid".into()));
}
if !trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err(ConfigError::Invalid(
"package.name must contain only ASCII letters, digits, '_' or '-'".into(),
));
}
Ok(())
}
fn validate_language_config(language: &LanguageConfig, key: &str) -> Result<(), ConfigError> {
let values = language.values();
if values.is_empty() {
return Err(ConfigError::Invalid(format!("{key} is empty")));
}
for value in values {
validate_non_empty_string(value, key)?;
}
Ok(())
}
fn validate_toolchain(toolchain: Option<&ToolchainConfig>) -> Result<(), ConfigError> {
let Some(toolchain) = toolchain else {
return Ok(());
};
for (key, value) in [
("cc", toolchain.cc.as_deref()),
("cxx", toolchain.cxx.as_deref()),
("as", toolchain.assembler.as_deref()),
("ar", toolchain.ar.as_deref()),
("ld", toolchain.ld.as_deref()),
] {
if let Some(value) = value {
validate_non_empty_string(value, &format!("toolchain.{key}"))?;
}
}
Ok(())
}
fn validate_string_list(values: &[String], key: &str) -> Result<(), ConfigError> {
for value in values {
validate_non_empty_string(value, key)?;
}
Ok(())
}
fn validate_non_empty_string(value: &str, key: &str) -> Result<(), ConfigError> {
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("{key} contains empty value")));
}
Ok(())
}
impl Config {
fn validate_language(&self, value: &Value) -> Result<(), ConfigError> {
if let Some(s) = value.as_str() {
@@ -287,7 +430,7 @@ impl Config {
}
if let Some(kind) = table.get("kind").and_then(|v| v.as_str()) {
let kind = kind.trim();
if !kind.is_empty() && kind != "bin" && kind != "staticlib" && kind != "sharedlib" {
if !kind.is_empty() && !VALID_KINDS.contains(&kind) {
return Err(ConfigError::Invalid(format!(
"build.{profile}.kind is invalid"
)));
@@ -411,7 +554,10 @@ fn default_toml() -> Result<Value, ConfigError> {
package.insert("type".to_string(), Value::String("none".to_string()));
package.insert("description".to_string(), Value::String("".to_string()));
package.insert("author".to_string(), Value::String("".to_string()));
package.insert("license".to_string(), Value::String("MIT".to_string()));
package.insert(
"license".to_string(),
Value::String("GPL-3.0-or-later".to_string()),
);
let mut build = Map::new();
build.insert(
@@ -670,6 +816,21 @@ mod tests {
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");
@@ -706,6 +867,35 @@ mod tests {
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");
@@ -752,14 +942,16 @@ mod tests {
#[test]
fn validate_valid_kinds() {
for kind in ["bin", "staticlib", "sharedlib"] {
let dir = temp_dir(&format!("kind_{kind}"));
let content = format!(
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, &content);
let result = Config::open(&path.to_string_lossy());
assert!(result.is_ok(), "kind '{kind}' should be valid");
let path = write_toml_file(&dir, &toml);
assert!(
Config::open(&path.to_string_lossy()).is_ok(),
"kind '{kind}' should be valid"
);
}
}

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)]
use std::fs;
use std::path::{Path, PathBuf};

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 std::path::Path;
#[allow(dead_code)]

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 sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;

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 mod common;
pub mod git;
pub mod lock;
@@ -5,8 +22,20 @@ 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,
@@ -26,63 +55,102 @@ pub fn resolve_deps(
.to_string();
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let lock_packages = Vec::new();
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)?;
// Используем путь относительно project_root или системный путь, если нужно
let dep_root = project_root.join("dcr-index");
let pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
resolved.include_dirs.push(
dep_root
.join("target")
.join("include")
register::registry_include_dir(&dep_root)
.to_string_lossy()
.to_string(),
);
resolved.lib_dirs.push(
dep_root
.join("target")
.join("lib")
register::registry_lib_dir(&dep_root)
.to_string_lossy()
.to_string(),
);
resolved.libs.push(name.clone());
} else if let Some(table) = value.as_table()
&& let Some(path) = table.get("path").and_then(|v| v.as_str())
{
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(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());
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"));
}
}
let lib_path = dep_root.join("target").join("lib");
if lib_path.exists() {
resolved
.lib_dirs
.push(lib_path.to_string_lossy().to_string());
}
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());
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),
});
}
}
}
crate::core::deps::lock::write_lock(
write_lock(
project_root,
&project_name,
&project_version,
@@ -91,3 +159,86 @@ pub fn resolve_deps(
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);
}
}

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 serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::fs;
@@ -15,9 +32,23 @@ 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 = std::env::var("HOME").ok()?;
let config_path = Path::new(&home).join(".dcr/config.toml");
let home = home_dir()?;
let config_path = home.join(".dcr/config.toml");
if !config_path.exists() {
return None;
}
@@ -30,11 +61,61 @@ pub fn get_index_path() -> PathBuf {
std::env::var("DCR_INDEX_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
Path::new(&home).join(".dcr").join("index.json")
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();
@@ -63,14 +144,97 @@ pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
}
pub fn is_registry_dep(value: &TomlValue) -> bool {
if value.is_str() {
return true;
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() {
return !table.contains_key("git")
&& !table.contains_key("path")
&& !table.contains_key("url")
&& !table.contains_key("version");
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")
);
}
false
}

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 mod builder;
pub mod config;
pub mod deps;

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 serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
@@ -20,10 +37,21 @@ pub struct RegistryManager {
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 home = std::env::var("HOME")?;
let dcr_dir = PathBuf::from(home).join(".dcr");
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() {

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::platform;
use std::path::Path;
use std::process::Command;

View File

@@ -1,4 +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 crate::core::config::Config;
use crate::utils::build::normalize_target_os;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use toml::Value;
@@ -24,7 +42,7 @@ pub fn parse_workspace(
let mut table = None;
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
let combinations = if let Some(t) = target {
let normalized_t = crate::cli::build::normalize_target_os(t);
let normalized_t = normalize_target_os(t);
vec![
format!("workspace.{}.{}", normalized_t, profile),
format!("workspace.{}.{}", profile, normalized_t),

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::log::error;
mod cli;
mod config;
@@ -22,6 +39,7 @@ fn main() {
"add" => cli::add::add(rest),
"build" => cli::build::build(rest),
"run" => cli::run::run(rest),
"tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest),
"clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),

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 fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
@@ -12,14 +29,44 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/lib{name}.a")
}
}
}
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}")
}
}
}
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/{name}.efi")
}
}
}
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
None => {
let arch = std::env::consts::ARCH;
let target = format!("{arch}-unknown-linux-gnu");
format!("./target/{target}/{profile}/lib{name}.so")
}
}
}
@@ -53,10 +100,9 @@ mod tests {
#[test]
fn lib_path_default() {
assert_eq!(
lib_path("debug", "mylib", None),
"./target/debug/libmylib.a"
);
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]
@@ -66,10 +112,9 @@ mod tests {
#[test]
fn shared_lib_path_default() {
assert_eq!(
shared_lib_path("debug", "mylib", None),
"./target/debug/libmylib.so"
);
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]

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

View File

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

View File

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

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,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 std::io;
use std::path::{Path, PathBuf};
@@ -29,3 +46,14 @@ pub fn find_project_root(start: &Path) -> io::Result<Option<PathBuf>> {
}
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};
#[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 log;
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)]
pub const RESET: &str = "\x1b[0m";

View File

@@ -232,7 +232,7 @@ fn dcr_add_dependencies() {
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 = \"./libs/mylib\""),
toml.contains("mylib = { path = \"./libs/mylib\" }"),
"path dep not found in toml"
);
@@ -316,6 +316,85 @@ fn dcr_builds_lib_package() {
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 {