36 Commits

Author SHA1 Message Date
a55d64c5ff feat: bump version to 0.6.8 in Cargo files 2026-05-27 20:18:49 +08:00
acdd1cc175 feat: enhance build configuration with C++ standard support and variable substitution
- Added `cxx_standard` field to the build configuration for specifying C++ standards.
- Updated documentation to reflect new `cxx_standard` and variable substitution capabilities in `cflags`, `ldflags`, and `include`.
- Implemented logic to handle C++ standard flags in the build process.
- Modified object file naming to include source file extensions.
- Updated various builder modules to support new language parsing logic.
- Introduced issue and feature request templates for better community engagement.
2026-05-27 20:04:01 +08:00
b5bbdb929b Add GPL-3.0-or-later license headers to source files
- Updated all relevant source files to include the new license header.
- Changed the default license in the configuration from MIT to GPL-3.0-or-later.
2026-05-27 17:34:56 +08:00
29120e0e29 feat: add Homebrew tap update workflow with asset download and formula generation 2026-05-26 20:20:39 +08:00
b66c846956 feat: improve release workflow with enhanced tag handling and clearer input descriptions 2026-05-26 19:39:35 +08:00
2a9c918711 fix: add missing package metadata including description, license, and repository URL 2026-05-26 19:36:39 +08:00
2af3baf4bb feat: enhance release workflow with additional input options and build configurations for Linux, macOS, and Windows 2026-05-26 19:16:09 +08:00
4e3601b1df Changed: openssl dependency is now conditional (cfg(not(windows))) — no longer pulled in on Windows, fixing Windows CI builds that lack Perl Locale::Maketext::Simple. On Linux (including musl cross-compilation) the vendored OpenSSL build is unchanged. 2026-05-26 17:57:37 +08:00
7969209eae Added:
- `build.kind = "efi"` — UEFI PE32+ executable support. Links with `-shared -nostdlib -Wl,-dll -Wl,--subsystem,10`, output via `efi_path()` with `.efi` extension, rejected by `dcr run`.
- `build.kind = "elf"` — bare-metal ELF executable support. Uses `elf_path()` output path, rejected by `dcr run`, no extra linker flags.

Fixed:

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

Added:

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

Removed:

- Removed unused dependencies `indicatif` and `console` from `Cargo.toml`
2026-05-18 16:13:33 +08:00
kie
9ab7ce15db Merge pull request #6 from dexoron/feat-tree
feat: add dcr tree command
2026-05-15 12:44:56 +03:00
peoplemiau1
954f2870ba style: final formatting and clippy fixes for tree.rs 2026-05-15 12:39:28 +03:00
peoplemiau1
9a24a80d59 fix: address clippy warnings in tree.rs 2026-05-15 12:22:05 +03:00
peoplemiau1
a123f647c8 style: fix formatting in tree.rs 2026-05-15 12:10:09 +03:00
peoplemiau1
8f04a37eb0 feat: add dcr tree command to visualize dependencies 2026-05-15 12:06:41 +03:00
441c9f4df3 update: add dev release support and update cross build 2026-05-15 11:40:17 +08:00
60b9c4f983 Merge pull request #5 from peoplemiau1/master
refactor: optimize parallel build and fix architectural issues
2026-05-14 21:35:36 +08:00
peoplemiau1
f5fd273e29 refactor: optimize parallel build and fix architectural issues 2026-05-13 19:35:43 +03:00
d6840fc7a2 Merge pull request #4 from peoplemiau1/master
feat: parallel compilation for all builders
2026-05-14 00:02:02 +08:00
peoplemiau1
f507148849 style: format code according to project standards 2026-05-13 18:59:59 +03:00
peoplemiau1
cfc6be1e95 feat: parallel compilation for all builders 2026-05-13 18:50:42 +03:00
2de836a421 feat: Add installation of Linux cross compilation tools for builds 2026-05-13 20:39:03 +08:00
ed85f801e9 refactor: Improve registry sorting logic for package resolution 2026-05-13 19:43:25 +08:00
9d8dcda366 style: Update macOS target descriptions for clarity 2026-05-13 19:41:26 +08:00
0193834bfd feat: Enhance dependency management and project setup
- Added support for registry-based dependencies in the dependencies section.
- Updated the package section to include a project type field.
- Introduced a new CLI command `dcr setup` to load and display registry configurations.
- Implemented a new module for handling registry operations, including loading and resolving packages from registries.
- Enhanced the build process to handle library packaging, including copying headers and libraries to the appropriate target directories.
- Updated the CLI to support building library packages and added tests for library package builds.
- Improved error handling and output messages during dependency resolution and package fetching.
2026-05-13 19:24:53 +08:00
debc5d2044 Added:
- Full Git dependency support in `dcr.toml`
  - Support for `git`, `branch`, `tag`, and `rev` fields
  - Automatic cloning and checkout via `git2`
  - Dependencies are stored in `target/<profile>/deps/git/`
- New `dcr add` command for easy dependency management
  - Short syntax: `github:user/repo`, `gitlab:user/repo`, `path:/to/lib`
  - Automatic GitHub resolution for `git:user/repo`
- Visual feedback for dependency fetching with in-place status updates ("Fetching" → "Fetched")
- Modular dependency management architecture in `src/core/deps/`
2026-05-12 01:29:26 +08:00
00ed418291 New TODO.md 2026-05-11 23:22:12 +08:00
76 changed files with 6180 additions and 2114 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,28 +92,80 @@ jobs:
name: release-notes
path: .github/release-notes.md
build:
# ── Linux ──────────────────────────────────────────────────────────────────
build-linux:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }}
needs: [setup, notes]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
asset_name: dcr-x86_64-unknown-linux-gnu
- os: macos-15-intel
target: x86_64-apple-darwin
asset_name: dcr-x86_64-apple-darwin
- os: macos-15
target: aarch64-apple-darwin
asset_name: dcr-aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
asset_name: dcr-x86_64-pc-windows-msvc.exe
runs-on: ${{ matrix.os }}
needs: notes
- target: x86_64-unknown-linux-gnu
- target: aarch64-unknown-linux-gnu
- target: x86_64-unknown-linux-musl
- target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Install cargo-zigbuild
run: |
pip install cargo-zigbuild
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: |
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
- name: Prepare Artifact
run: |
ASSET="dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}"
cp target/${{ matrix.target }}/release/dcr "$ASSET"
echo "ASSET_NAME=${ASSET}" >> "$GITHUB_ENV"
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: ${{ env.ASSET_NAME }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── macOS ──────────────────────────────────────────────────────────────────
build-macos:
if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }}
needs: [setup, notes]
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
@@ -73,20 +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/

126
AGENT.md
View File

@@ -1,126 +0,0 @@
# AGENT.md
## Project Overview
**DCR (Dexoron Cargo Realization)** — CLI-утилита для управления C/C++/ASM проектами в стиле Cargo.
- **Язык реализации**: Rust (edition 2024)
- **Лицензия**: MIT
- **Репозиторий**: [github.com/dexoron/dcr](https://github.com/dexoron/dcr)
## Architecture
### Source Tree (`src/`)
```
src/
├── main.rs # Точка входа, маршрутизация CLI-команд
├── config.rs # Глобальные константы: профили сборки, шаблон main.c
├── cli/ # Реализация CLI-команд
│ ├── new.rs # dcr new <name>
│ ├── init.rs # dcr init
│ ├── build.rs # dcr build [--debug|--release]
│ ├── run.rs # dcr run [--debug|--release]
│ ├── clean.rs # dcr clean [--all]
│ ├── help.rs # dcr --help
│ └── flag_update.rs # dcr --update (self-update через GitHub Releases)
├── core/ # Бизнес-логика
│ ├── config.rs # Парсинг/валидация/форматирование dcr.toml (struct Config)
│ ├── deps.rs # Разрешение path-зависимостей, генерация dcr.lock
│ ├── runner.rs # Запуск скомпилированного бинарника
│ ├── workspace.rs # Поддержка [workspace] и упорядочивание сборки
│ └── builder/ # Бэкенды компиляторов/ассемблеров
│ ├── mod.rs # Общий интерфейс сборки
│ ├── gcc.rs # GCC backend
│ ├── clang.rs # Clang backend
│ ├── msvc.rs # MSVC backend
│ ├── nasm.rs # NASM backend (ASM)
│ └── gas.rs # GAS backend (ASM)
├── platform/ # Платформенно-зависимая логика
│ ├── mod.rs # Общий интерфейс платформ
│ ├── linux.rs
│ ├── macos.rs
│ └── windows.rs
└── utils/ # Вспомогательные утилиты
├── fs.rs # Работа с файловой системой
├── log.rs # Логирование (error, warn)
└── text.rs # Форматирование текста / цвета
```
### Key Concepts
- **`dcr.toml`** — конфигурационный файл проекта (аналог `Cargo.toml`). Секции: `[package]`, `[build]`, `[toolchain]`, `[dependencies]`, `[workspace]`.
- **Профили сборки**: `debug` (по умолчанию) и `release`. Встроенные флаги зависят от компилятора. Пользовательские флаги через `build.cflags` / `build.ldflags`.
- **Инкрементальная сборка**: объектные файлы кешируются по mtime. Отслеживание заголовков пока не реализовано (см. `TODO.md`).
- **Workspace**: корневой `dcr.toml` с секцией `[workspace]` описывает member-проекты и их зависимости; сборка происходит в топологическом порядке.
- **Path dependencies**: зависимости по путям автоматически добавляют include/lib-пути; генерируется `dcr.lock`.
- **Build kinds**: `bin` (бинарник), `staticlib` (`.a`), `sharedlib` (`.so`/`.dylib`/`.dll`).
## 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-запросы для `--update` (blocking, rustls-tls) |
| `serde` | Сериализация/десериализация |
| `toml` | Парсинг `dcr.toml` |
| `sha2` | Хеширование при обновлении |
| `self-replace` | Замена бинарника при `--update` |
## Development
### Build & Run
```bash
cargo build # debug build
cargo build --release # release build
cargo run -- new hello # создать проект "hello"
```
### Code Quality
```bash
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check
cargo test
```
### CI/CD
- **CI**: `.github/workflows/ci.yml` — проверки при PR/push
- **Release**: `.github/workflows/release.yml` — сборка бинарников при пуше тега `v*`
### Tests
- Интеграционные CLI-тесты: `tests/cli_basic.rs`
## Code Conventions
- Rust stable edition 2024
- Форматирование: `cargo fmt`
- Линтинг: `cargo clippy` без warnings
- Функции — маленькие и сфокусированные
- Имена — понятные и описательные
- Пользовательские сообщения CLI — на английском языке
- Вывод ошибок/предупреждений через `utils::log::{error, warn}`
## TODO
- В файле `TODO.md`
## Useful Links
- Документация проекта: `docs/`
- Changelog: `CHANGELOG.md`
- Contributing: `CONTRIBUTING.md`
- FAQ: `FAQ.md`
- Установка: `install.sh` (Linux/macOS), `install.ps1` (Windows)

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,168 @@
# 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:
- New dependency registry system
- Package type support (`lib`, `app`, `none`) in `dcr.toml`
- Library packaging functionality: automatically generates `include` and `lib` directories in `target/` for `lib` type projects
- Expanded CI build targets:
- Linux: `aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`
- Windows: `aarch64-pc-windows-msvc`, `x86_64-pc-windows-gnu`, `aarch64-pc-windows-gnullvm`
Changed:
- `src/core/deps/` modular architecture overhaul
- Native platform pathing defaults for Linux (now dynamic architecture detection)
- Improved `run` command output handling
## [0.6.0] - 2026-05-12
Added:
- Full Git dependency support in `dcr.toml`
- Support for `git`, `branch`, `tag`, and `rev` fields
- Automatic cloning and checkout via `git2`
- Dependencies are stored in `target/<profile>/deps/git/`
- New `dcr add` command for easy dependency management
- Short syntax: `github:user/repo`, `gitlab:user/repo`, `path:/to/lib`
- Automatic GitHub resolution for `git:user/repo`
- Visual feedback for dependency fetching with in-place status updates ("Fetching" → "Fetched")
- Modular dependency management architecture in `src/core/deps/`
## [0.5.1] - 2026-04-12
Fixed:
@@ -48,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

214
Cargo.lock generated
View File

@@ -35,6 +35,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
@@ -54,6 +63,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@@ -89,13 +100,28 @@ dependencies = [
]
[[package]]
name = "dcr"
version = "0.5.1"
name = "ctrlc"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [
"dispatch2",
"nix",
"windows-sys 0.61.2",
]
[[package]]
name = "dcr"
version = "0.6.8"
dependencies = [
"ctrlc",
"git2",
"glob",
"openssl",
"reqwest",
"self-replace",
"serde",
"serde_json",
"sha2",
"toml",
]
@@ -110,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"
@@ -155,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"
@@ -263,6 +316,21 @@ dependencies = [
"wasip3",
]
[[package]]
name = "git2"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"openssl-probe",
"openssl-sys",
"url",
]
[[package]]
name = "glob"
version = "0.3.3"
@@ -532,6 +600,16 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.85"
@@ -554,6 +632,46 @@ version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libgit2-sys"
version = "0.18.4+1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7"
dependencies = [
"cc",
"libc",
"libssh2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
]
[[package]]
name = "libssh2-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -595,12 +713,92 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -619,6 +817,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -1271,6 +1475,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"

View File

@@ -1,8 +1,11 @@
[package]
name = "dcr"
version = "0.5.1"
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,3 +14,9 @@ serde = { version = "1", features = ["derive"] }
self-replace = "1"
sha2 = "0.10"
toml = "1.0.3"
git2 = "0.20.4"
serde_json = "1"
ctrlc = "3"
[target.'cfg(not(windows))'.dependencies]
openssl = { version = "0.10", 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>.

335
README.md
View File

@@ -1,184 +1,124 @@
# 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
- ASM projects with NASM/GAS or via GCC/Clang
- Mixed language projects (`language = ["c", "asm"]` and similar)
- Cross-compilation with `--target` (short names: `linux`, `macos`, `windows`)
- Target-specific configurations and inheritance
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
</div>
## Supported Platforms
- Linux: `x86_64-unknown-linux-gnu`
- macOS Intel: `x86_64-apple-darwin`
- macOS Apple Silicon: `aarch64-apple-darwin`
- Windows: `x86_64-pc-windows-msvc`
---
## 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 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`.
Example `dcr.toml`:
## dcr.toml Example
```toml
[package]
@@ -189,49 +129,80 @@ version = "0.1.0"
language = "c"
standard = "c11"
compiler = "clang"
# 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]
```
Workspace example:
---
```toml
[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
```
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` files change. Header dependency tracking is not implemented yet.
<div align="center">
## Requirements
- Rust toolchain (`rustc`, `cargo`) - for building DCR from source
- C compiler (`clang`, `gcc`, or 'cl'(msvc))
GPL-3.0 — see [LICENSE](LICENSE)<br/>
Made with ❤️ by [Dexoron](https://github.com/dexoron) and contributors.
## Releases
Releases are built automatically via GitHub Actions (`.github/workflows/release.yml`) when a tag matching `v*` is pushed.
## License
See `LICENSE`.
</div>

76
TODO.md
View File

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

View File

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

View File

@@ -21,11 +21,13 @@
* [Global options](commands/global-options.md)
* [dcr init/new](commands/dcr-init-or-new.md)
* [dcr add](commands/dcr-add.md)
* [dcr build](commands/dcr-build.md)
* [dcr run](commands/dcr-run.md)
* [dcr clean](commands/dcr-clean.md)
* [dcr gen](commands/dcr-gen.md)
* [dcr test](commands/dcr-test.md)
* [dcr tree](commands/dcr-tree.md)
## Dependencies & Build

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

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

View File

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

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

View File

@@ -4,17 +4,32 @@
```toml
[package]
name = "my-app"
version = "0.1.0"
name = "my-app"
version = "0.1.0"
type = "app"
description = "My application"
author = "John Doe"
license = "MIT"
```
## Fields
- `name` (string, required): used as output artifact base name.
- `version` (string, required): used in `dcr.lock` project entry.
- `type` (string, optional): project type (`app`, `lib`, `none`).
- `description` (string, optional): human-readable project description.
- `author` (string, optional): single author name.
- `authors` (array of strings, optional): list of authors.
- `license` (string, optional): project license identifier (e.g. `MIT`, `GPL-3.0-or-later`).
- `homepage` (string, optional): project homepage URL.
- `repository` (string, optional): source repository URL.
- `readme` (string, optional): path to README file.
- `keywords` (array of strings, optional): project keywords.
- `categories` (array of strings, optional): project categories.
## Notes
- Empty `name` or `version` makes config invalid.
- `dcr new` sets `name` from the passed project name.
- `dcr init` sets `name` from current directory name.
- If `type = "lib"`, DCR generates `include/` and `lib/` directories in the target path, copying public headers and built libraries.

View File

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

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

View File

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

@@ -5,9 +5,13 @@
```text
dcr new <name>
dcr init
dcr build [--debug|--release]
dcr setup
dcr add <name> <source>(optional)
dcr build [--debug|--release] [--target <triple>]
dcr run [--debug|--release]
dcr clean [--debug|--release] [--all]
dcr tree
dcr test
dcr gen <subcommand>
dcr --help
dcr --version
@@ -16,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 "$@"

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

@@ -0,0 +1,253 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::deps::register;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use toml::Value;
use toml::map::Map;
pub struct AddArgs {
pub name: String,
pub path: Option<String>,
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
pub version_from_registry: Option<String>,
}
pub fn add(args: &[String]) -> i32 {
let add_args = match parse_add_args(args) {
Ok(a) => a,
Err(code) => return code,
};
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
return 1;
}
};
let mut config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(cfg) => cfg,
Err(err) => {
error(&err.to_string());
return 1;
}
};
let dep_value = if let Some(git_url) = add_args.git {
if add_args.branch.is_none() && add_args.tag.is_none() && add_args.rev.is_none() {
let mut table = Map::new();
table.insert("git".to_string(), Value::String(git_url));
Value::Table(table)
} else {
let mut table = Map::new();
table.insert("git".to_string(), Value::String(git_url));
if let Some(b) = add_args.branch {
table.insert("branch".to_string(), Value::String(b));
}
if let Some(t) = add_args.tag {
table.insert("tag".to_string(), Value::String(t));
}
if let Some(r) = add_args.rev {
table.insert("rev".to_string(), Value::String(r));
}
Value::Table(table)
}
} else if let Some(path) = add_args.path {
let mut table = Map::new();
table.insert("path".to_string(), Value::String(path));
Value::Table(table)
} else if let Some(version) = add_args.version_from_registry {
Value::String(version)
} else {
error("Dependency source (path or git) must be provided");
return 1;
};
let key = format!("dependencies.{}", add_args.name);
if let Err(err) = config.edit(&key, dep_value) {
error(&format!("Failed to update dcr.toml: {}", err));
return 1;
}
println!(
" {} dependency `{}` to dcr.toml",
colored("Added", BOLD_GREEN),
add_args.name
);
0
}
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:");
error(" user/repo -> github.com/user/repo");
error(" github:user/repo -> github.com/user/repo");
error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo");
error(" path:./path/to/lib -> local path");
return Err(1);
}
let mut name = String::new();
let source_spec: String;
let mut branch = None;
let mut tag = None;
let mut rev = None;
let mut iter = args.iter();
if let Some(n) = iter.next() {
if n.starts_with("--") {
error("First argument must be the dependency name");
return Err(1);
}
name = n.clone();
}
if let Some(s) = iter.next() {
if s.starts_with("--") {
error("Second argument must be the dependency source");
return Err(1);
}
source_spec = s.clone();
} else {
// No source provided — try registry auto-lookup
match register::resolve_package_from_registry(&name) {
Ok(info) => {
let version = info
.get("latest_version")
.or_else(|| info.get("version"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
(
"Registry entry for `".to_string() + &name + "` has no version field",
1,
)
})
.map_err(|(msg, code)| {
error(&msg);
code
})?;
return Ok(AddArgs {
name,
path: None,
git: None,
branch: None,
tag: None,
rev: None,
version_from_registry: Some(version.to_string()),
});
}
Err(e) => {
error(&format!("Cannot add `{}` without source: {}", name, e));
return Err(1);
}
}
}
while let Some(arg) = iter.next() {
match arg.as_str() {
"--branch" => {
branch = iter.next().cloned();
if branch.is_none() {
error("--branch requires a value");
return Err(1);
}
}
"--tag" => {
tag = iter.next().cloned();
if tag.is_none() {
error("--tag requires a value");
return Err(1);
}
}
"--rev" => {
rev = iter.next().cloned();
if rev.is_none() {
error("--rev requires a value");
return Err(1);
}
}
_ => {
error(&format!("Unknown argument: {}", arg));
return Err(1);
}
}
}
let mut path = None;
let mut git = None;
if let Some(p) = source_spec.strip_prefix("path:") {
path = Some(p.to_string());
} else if let Some(g) = source_spec.strip_prefix("github:") {
git = Some(format!("https://github.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("gitlab:") {
git = Some(format!("https://gitlab.com/{}", g));
} else if let Some(g) = source_spec.strip_prefix("git:") {
if g.contains('/') && !g.contains('.') {
// git:user/repo -> default to github
git = Some(format!("https://github.com/{}", g));
} else {
// git:host.com/user/repo
let url = if g.starts_with("http") || g.starts_with("git@") {
g.to_string()
} else {
format!("https://{}", g)
};
git = Some(url);
}
} else if source_spec.starts_with("http://")
|| source_spec.starts_with("https://")
|| source_spec.starts_with("git@")
{
git = Some(source_spec);
} else {
error("Source must have a prefix (path:, git:, github:, gitlab:) or be a full URL");
return Err(1);
}
Ok(AddArgs {
name,
path,
git,
branch,
tag,
rev,
version_from_registry: None,
})
}

View File

@@ -1,20 +1,49 @@
// 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;
use crate::core::builder::{BuildContext, build as build_project, collect_sources};
use crate::core::config::Config;
use crate::core::deps::resolve_deps;
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 {}: {}",
@@ -476,7 +445,7 @@ fn build_from_root(
);
} else {
println!(
" Building project `{}`\n Profile: {}\n Target: {}",
" Building project `{}`\n Profile: {}\n Target: {}",
colored(&project_name, BOLD_GREEN),
colored(profile, BOLD_GREEN),
colored(
@@ -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,9 +520,16 @@ 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);
let mut build_type =
get_string_with_profile_and_target(&config, "type", profile, build_target);
if build_type.is_empty() {
build_type = get_config_str(&config, "package.type");
}
let tc_cc = get_config_value(&config, "toolchain", "cc", profile, build_target)
.or_else(|| get_config_opt(&config, "toolchain.cc"));
let tc_cxx = get_config_value(&config, "toolchain", "cxx", profile, build_target)
@@ -560,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 =
@@ -593,7 +601,61 @@ fn build_project_at(
let target_dir = build_target.map(|t| format!("target/{t}/{profile}"));
ensure_target_dirs(&items, profile, target_dir);
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let resolved = resolve_deps(&config, profile, build_target, project_root)?;
// 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, value) in deps {
if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let version = pkg_info
.get("latest_version")
.or_else(|| pkg_info.get("version"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
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() || !lib_dir.exists() {
print!(
"\r{:100}\r {} {} v{}",
"",
colored("Building", BOLD_GREEN),
name,
version
);
std::io::Write::flush(&mut std::io::stdout()).unwrap();
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
);
println!();
} else {
println!(
" {} {} v{}",
colored("Ready", BOLD_GREEN),
name,
version
);
}
}
}
}
let (resolved_cflags, resolved_ldflags) =
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
let mut combined_excludes = Vec::new();
@@ -663,18 +725,40 @@ 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),
platform: normalize_platform(&build_platform),
linker: resolved_linker.as_deref(),
archiver: resolved_archiver.as_deref(),
package_type: if build_type.is_empty() {
None
} else {
Some(build_type.as_str())
},
source_roots: &source_roots,
exclude_dirs: &combined_excludes,
include_paths: &combined_includes,
@@ -682,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);
@@ -694,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,
@@ -732,8 +826,12 @@ fn build_project_at(
);
if !skip {
run_build(&ctx)?;
check_interrupted()?;
write_build_fingerprint(&ctx, &fingerprint)?;
}
if ctx.package_type == Some("lib") {
package_library(&ctx, &headers)?;
}
let post_steps_dirty = build_steps_need_run(&build_post_steps, &step_vars)?;
if post_steps_dirty {
run_build_steps(&build_post_steps, &tool_execs, &step_flags, &step_vars)?;
@@ -743,31 +841,57 @@ fn build_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 package_library(ctx: &BuildContext, headers: &[PathBuf]) -> Result<(), String> {
let target_root = if let Some(dir) = ctx.target_dir {
Path::new(dir)
.parent()
.and_then(|p| p.parent())
.unwrap_or_else(|| Path::new("target"))
} else {
Path::new("target")
};
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
let include_dir = target_root.join("include");
let lib_dir = target_root.join("lib");
fs::create_dir_all(&include_dir).map_err(|e| e.to_string())?;
fs::create_dir_all(&lib_dir).map_err(|e| e.to_string())?;
for header in headers {
if let Some(name) = header.file_name() {
let dest = include_dir.join(name);
fs::copy(header, dest)
.map_err(|e| format!("Failed to copy header {:?}: {}", header, e))?;
}
}
let mut outputs = Vec::new();
if ctx.kind == "staticlib" || ctx.kind == "any" {
outputs.push(crate::platform::lib_path(
ctx.profile,
ctx.project_name,
ctx.target_dir,
));
}
if ctx.kind == "sharedlib" || ctx.kind == "any" {
outputs.push(crate::platform::shared_lib_path(
ctx.profile,
ctx.project_name,
ctx.target_dir,
));
}
for output in outputs {
let path = Path::new(&output);
if path.exists()
&& let Some(name) = path.file_name()
{
let dest = lib_dir.join(name);
fs::copy(path, dest).map_err(|e| format!("Failed to copy lib {:?}: {}", path, e))?;
}
}
Ok(())
}
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
@@ -793,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,
@@ -1322,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,
@@ -1449,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,12 +341,14 @@ 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),
platform: normalize_platform(&platform),
linker: resolved_linker.as_deref(),
archiver: resolved_archiver.as_deref(),
package_type: None,
source_roots: &source_roots,
exclude_dirs: &combined_excludes,
include_paths: &combined_includes,
@@ -549,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}"))?;
@@ -573,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,
@@ -663,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)
@@ -767,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)
@@ -787,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)
@@ -1394,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,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 add;
pub mod build;
pub mod clean;
pub mod flag_update;
@@ -7,4 +25,6 @@ pub mod help;
pub mod init;
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,
}
}

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

@@ -0,0 +1,33 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub fn setup(_args: &[String]) -> i32 {
println!("Setting up DCR registries...");
match crate::core::registry::RegistryManager::load() {
Ok(manager) => {
println!("Loaded {} registries.", manager.config.registry.len());
for (name, reg) in manager.config.registry {
println!("- {}: {} (priority {})", name, reg.url, reg.priority);
}
0
}
Err(e) => {
println!("Error: {}", e);
1
}
}
}

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,13 +27,14 @@ 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,
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub package_type: Option<&'a str>,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
@@ -25,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);
}
@@ -44,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,118 +229,59 @@ impl Config {
}
pub fn validate(&self) -> Result<(), ConfigError> {
let package = self
.get("package")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
if self.typed.package.name.trim().is_empty() {
return Err(ConfigError::Invalid("package.name is empty".into()));
}
if self.typed.package.version.trim().is_empty() {
return Err(ConfigError::Invalid("package.version is empty".into()));
}
validate_package_name(&self.typed.package.name)?;
for key in ["name", "version"] {
let value = package.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("package.{key} is empty")));
if let Some(pkg_type) = &self.typed.package.pkg_type {
let pkg_type = pkg_type.trim();
if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none"
{
return Err(ConfigError::Invalid(
"package.type must be 'lib', 'app', or 'none'".into(),
));
}
}
let build = self
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
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"))
})?;
@@ -226,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() {
@@ -277,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"
)));
@@ -398,6 +551,14 @@ fn default_toml() -> Result<Value, ConfigError> {
"version".to_string(),
Value::String(DEFAULT_VERSION.to_string()),
);
package.insert("type".to_string(), Value::String("none".to_string()));
package.insert("description".to_string(), Value::String("".to_string()));
package.insert("author".to_string(), Value::String("".to_string()));
package.insert(
"license".to_string(),
Value::String("GPL-3.0-or-later".to_string()),
);
let mut build = Map::new();
build.insert(
"language".to_string(),
@@ -458,7 +619,29 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
let mut out = String::new();
out.push_str("[package]\n");
out.push_str(&format!("name = \"{name}\"\n"));
out.push_str(&format!("version = \"{version}\"\n\n"));
out.push_str(&format!("version = \"{version}\"\n"));
for key in [
"type",
"description",
"author",
"authors",
"homepage",
"license",
"repository",
"readme",
"keywords",
"categories",
] {
if let Some(val) = package.get(key) {
match val {
Value::String(s) => out.push_str(&format!("{key} = \"{s}\"\n")),
Value::Array(_) => out.push_str(&format!("{key} = {}\n", format_string_array(val))),
_ => {}
}
}
}
out.push('\n');
out.push_str("[build]\n");
if language.starts_with('[') {
@@ -522,21 +705,36 @@ fn format_dep_value(value: &Value) -> String {
Value::String(s) => format!("\"{s}\""),
Value::Table(tbl) => {
let mut parts = Vec::new();
if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) {
parts.push(format!("version = \"{v}\""));
}
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
parts.push(format!("path = \"{v}\""));
}
if let Some(v) = tbl.get("git").and_then(|v| v.as_str()) {
parts.push(format!("git = \"{v}\""));
}
if let Some(v) = tbl.get("branch").and_then(|v| v.as_str()) {
parts.push(format!("branch = \"{v}\""));
}
if let Some(v) = tbl.get("tag").and_then(|v| v.as_str()) {
parts.push(format!("tag = \"{v}\""));
}
if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) {
parts.push(format!("rev = \"{v}\""));
}
if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) {
parts.push(format!(
"default-features = {}",
if v { "true" } else { "false" }
));
}
if let Some(v) = tbl.get("features") {
parts.push(format!("features = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) {
parts.push(format!("system = {}", if v { "true" } else { "false" }));
}
if let Some(v) = tbl.get("include") {
parts.push(format!("include = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("lib") {
parts.push(format!("lib = {}", format_string_array(v)));
}
if let Some(v) = tbl.get("libs") {
parts.push(format!("libs = {}", format_string_array(v)));
}
format!("{{ {} }}", parts.join(", "))
}
_ => "\"\"".to_string(),
@@ -618,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");
@@ -654,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");
@@ -700,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,390 +0,0 @@
use crate::core::config::Config;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub path_raw: String,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
pub fn resolve_deps(
config: &Config,
profile: &str,
target: Option<&str>,
project_root: &Path,
) -> Result<ResolvedDeps, String> {
let project_name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let project_version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let deps = parse_dependencies(config, profile, target)?;
if deps.is_empty() {
return Ok(ResolvedDeps {
include_dirs: Vec::new(),
lib_dirs: Vec::new(),
libs: Vec::new(),
});
}
let mut include_dirs = Vec::new();
let mut lib_dirs = Vec::new();
let mut libs = Vec::new();
let mut lock_packages = Vec::new();
for dep in deps {
let dep_path_raw = expand_profile(&dep.path_raw, profile);
let dep_path = resolve_path(project_root, &dep_path_raw)?;
if !dep_path.is_dir() {
return Err(format!(
"Dependency '{}' path is not a directory: {}",
dep.name,
dep_path.display()
));
}
let include = resolve_paths(&dep_path, dep.include_raw.as_deref(), &["include"], profile)?;
let lib = resolve_paths(
&dep_path,
dep.lib_raw.as_deref(),
&["lib", "lib64"],
profile,
)?;
let libs_list = dep
.libs_raw
.clone()
.unwrap_or_else(|| vec![dep.name.clone()]);
if include.is_empty() || lib.is_empty() {
return Err(format!(
"Dependency '{}' missing include/lib dirs. Add include/lib in dcr.toml.",
dep.name
));
}
include_dirs.extend(include.iter().map(|p| p.to_string_lossy().to_string()));
lib_dirs.extend(lib.iter().map(|p| p.to_string_lossy().to_string()));
libs.extend(libs_list.iter().cloned());
let dep_cache = project_root
.join("target")
.join(profile)
.join("deps")
.join(&dep.name);
sync_dep_dir(&dep_path, &dep_cache)
.map_err(|err| format!("Failed to sync dep {}: {err}", dep.name))?;
let dep_version = read_dep_version(&dep_path).unwrap_or_else(|| "0.0.0".to_string());
let dep_checksum = compute_checksum(&dep_path)
.map_err(|err| format!("Failed to hash dep {}: {err}", dep.name))?;
lock_packages.push(DepLock {
name: dep.name.clone(),
version: dep_version,
checksum: dep_checksum,
source: format!("path+{}", dep.path_raw),
});
}
write_lock(
project_root,
&project_name,
&project_version,
&lock_packages,
)?;
Ok(ResolvedDeps {
include_dirs,
lib_dirs,
libs,
})
}
#[derive(Debug, Clone)]
struct DepLock {
name: String,
version: String,
checksum: String,
source: String,
}
fn parse_dependencies(
config: &Config,
profile: &str,
target: Option<&str>,
) -> Result<Vec<DepSpec>, String> {
let mut deps_table = None;
// Order: dependencies.target.profile, dependencies.profile.target, dependencies.target, dependencies.profile, dependencies
let combinations = if let Some(t) = target {
let normalized_t = crate::cli::build::normalize_target_os(t);
vec![
format!("dependencies.{}.{}", normalized_t, profile),
format!("dependencies.{}.{}", profile, normalized_t),
format!("dependencies.{}", normalized_t),
format!("dependencies.{}", profile),
"dependencies".to_string(),
]
} else {
vec![
format!("dependencies.{}", profile),
"dependencies".to_string(),
]
};
for key in combinations {
if let Some(val) = config.get(&key).and_then(|v| v.as_table()) {
deps_table = Some(val);
break;
}
}
let deps_table = match deps_table {
Some(t) => t,
None => return Ok(Vec::new()),
};
let mut deps = Vec::new();
for (name, value) in deps_table {
match value {
Value::Table(tbl) => {
if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) {
return Err(format!(
"Dependency '{}' uses system=true, which is not supported yet",
name
));
}
let path = tbl
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("Dependency '{}' missing path", name))?
.to_string();
let include = parse_string_list(tbl.get("include"), name, "include")?;
let lib = parse_string_list(tbl.get("lib"), name, "lib")?;
let libs = parse_string_list(tbl.get("libs"), name, "libs")?;
deps.push(DepSpec {
name: name.to_string(),
path_raw: path,
include_raw: include,
lib_raw: lib,
libs_raw: libs,
});
}
_ => {
return Err(format!(
"Dependency '{}' must be a table with path/include/lib",
name
));
}
}
}
deps.sort_by(|a, b| a.name.cmp(&b.name));
Ok(deps)
}
fn parse_string_list(
value: Option<&Value>,
dep_name: &str,
key: &str,
) -> Result<Option<Vec<String>>, String> {
let Some(value) = value else { return Ok(None) };
let list = value.as_array().ok_or_else(|| {
format!(
"Dependency '{}' field '{}' must be an array of strings",
dep_name, key
)
})?;
let mut out = Vec::new();
for item in list {
let s = item
.as_str()
.ok_or_else(|| format!("Dependency '{}' field '{}' must be strings", dep_name, key))?;
out.push(s.to_string());
}
Ok(Some(out))
}
fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}
fn write_lock(
project_root: &Path,
project_name: &str,
project_version: &str,
packages: &[DepLock],
) -> Result<(), String> {
let mut out = String::new();
if !packages.is_empty() {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(project_name)));
out.push_str(&format!(
"version = \"{}\"\n",
escape_value(project_version)
));
out.push_str(&format!(
"dependencies = [{}]\n\n",
quote_list(&packages.iter().map(|p| p.name.clone()).collect::<Vec<_>>())
));
}
for pkg in packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
out.push_str(&format!("version = \"{}\"\n", escape_value(&pkg.version)));
out.push_str(&format!("source = \"{}\"\n", escape_value(&pkg.source)));
out.push_str(&format!("checksum = \"{}\"\n", escape_value(&pkg.checksum)));
out.push('\n');
}
fs::write(project_root.join("dcr.lock"), out)
.map_err(|err| format!("Failed to write dcr.lock: {err}"))?;
Ok(())
}
fn quote_list(items: &[String]) -> String {
items
.iter()
.map(|s| format!("\"{}\"", escape_value(s)))
.collect::<Vec<_>>()
.join(", ")
}
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

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

@@ -0,0 +1,128 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[allow(dead_code)]
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum DepSource {
Path(String),
Git {
url: String,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
},
Registry {
version: String,
features: Option<Vec<String>>,
},
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub source: DepSource,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
#[allow(dead_code)]
pub fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
project_root.join(p)
};
Ok(full)
}
#[allow(dead_code)]
pub fn resolve_paths(
base: &Path,
raw: Option<&[String]>,
defaults: &[&str],
profile: &str,
) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
if let Some(raw) = raw {
for r in raw {
let expanded = expand_profile(r, profile);
let p = Path::new(&expanded);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
};
if !full.exists() {
return Err(format!("Path does not exist: {}", full.display()));
}
out.push(full);
}
return Ok(out);
}
for d in defaults {
let candidate = base.join(d);
if candidate.exists() {
out.push(candidate);
}
}
Ok(out)
}
#[allow(dead_code)]
pub fn expand_profile(raw: &str, profile: &str) -> String {
raw.replace("{profile}", profile)
}
#[allow(dead_code)]
pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
copy_dir_all(src, dst)
}
#[allow(dead_code)]
pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&from, &to)?;
} else {
fs::copy(&from, &to)?;
}
}
Ok(())
}

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

@@ -0,0 +1,71 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::Path;
#[allow(dead_code)]
pub fn fetch_git_dep(
url: &str,
target_dir: &Path,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<(), String> {
if target_dir.exists() {
return Ok(());
}
let mut builder = git2::build::RepoBuilder::new();
if let Some(b) = branch {
builder.branch(b);
}
let repo = builder
.clone(url, target_dir)
.map_err(|e| format!("failed to clone {}: {}", url, e))?;
if let Some(t) = tag {
checkout_ref(&repo, &format!("refs/tags/{}", t))?;
} else if let Some(r) = rev {
checkout_ref(&repo, r)?;
}
Ok(())
}
#[allow(dead_code)]
fn checkout_ref(repo: &git2::Repository, reference_str: &str) -> Result<(), String> {
let (object, reference) = repo
.revparse_ext(reference_str)
.map_err(|e| format!("failed to find ref {}: {}", reference_str, e))?;
repo.checkout_tree(&object, None).map_err(|e| {
let ref_name = reference.as_ref().and_then(|r| r.name()).unwrap_or("HEAD");
format!("failed to checkout {}: {}", ref_name, e)
})?;
match reference {
Some(gref) => {
let ref_name = gref.name().unwrap_or("HEAD");
repo.set_head(ref_name)
}
None => repo.set_head_detached(object.id()),
}
.map_err(|e| format!("failed to set HEAD: {}", e))?;
Ok(())
}

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

@@ -0,0 +1,137 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepLock {
pub name: String,
pub version: String,
pub checksum: String,
pub source: String,
}
pub fn write_lock(
project_root: &Path,
project_name: &str,
project_version: &str,
packages: &[DepLock],
) -> Result<(), String> {
let mut out = String::new();
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(project_name)));
out.push_str(&format!(
"version = \"{}\"\n",
escape_value(project_version)
));
if !packages.is_empty() {
out.push_str(&format!(
"dependencies = [{}]\n",
quote_list(&packages.iter().map(|p| p.name.clone()).collect::<Vec<_>>())
));
}
out.push('\n');
for pkg in packages {
out.push_str("[[package]]\n");
out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name)));
out.push_str(&format!("version = \"{}\"\n", escape_value(&pkg.version)));
out.push_str(&format!("source = \"{}\"\n", escape_value(&pkg.source)));
out.push_str(&format!("checksum = \"{}\"\n", escape_value(&pkg.checksum)));
out.push('\n');
}
fs::write(project_root.join("dcr.lock"), out)
.map_err(|err| format!("Failed to write dcr.lock: {err}"))?;
Ok(())
}
fn quote_list(items: &[String]) -> String {
items
.iter()
.map(|s| format!("\"{}\"", escape_value(s)))
.collect::<Vec<_>>()
.join(", ")
}
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
#[allow(dead_code)]
pub fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[allow(dead_code)]
pub fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
#[allow(dead_code)]
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
#[allow(dead_code)]
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

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

@@ -0,0 +1,244 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod common;
pub mod git;
pub mod lock;
pub mod register;
use crate::core::config::Config;
use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock};
use std::path::Path;
fn dep_version(path: &Path) -> String {
Config::open(&path.join("dcr.toml").to_string_lossy())
.ok()
.and_then(|c| {
c.get("package.version")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.unwrap_or_default()
}
pub fn resolve_deps(
config: &Config,
_profile: &str,
_target: Option<&str>,
project_root: &Path,
) -> Result<ResolvedDeps, String> {
let mut resolved = ResolvedDeps::default();
let project_name = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let project_version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let mut lock_packages: Vec<DepLock> = Vec::new();
if let Some(deps) = deps_table {
for (name, value) in deps {
if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
resolved.include_dirs.push(
register::registry_include_dir(&dep_root)
.to_string_lossy()
.to_string(),
);
resolved.lib_dirs.push(
register::registry_lib_dir(&dep_root)
.to_string_lossy()
.to_string(),
);
resolved.libs.push(name.clone());
lock_packages.push(DepLock {
name: name.clone(),
version: pkg_info
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
checksum: String::new(),
source: format!(
"registry+{}",
pkg_info
.get("registry_url")
.and_then(|v| v.as_str())
.unwrap_or("https://dcr-registry.pages.dev")
),
});
} else if let Some(path) = path_dep_path(value) {
let dep_root = project_root.join(path);
if let Some(table) = value.as_table() {
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
for inc in includes {
if let Some(inc_str) = inc.as_str() {
resolved
.include_dirs
.push(dep_root.join(inc_str).to_string_lossy().to_string());
}
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
}
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
for lib_dir in lib_dirs {
if let Some(lib_dir_str) = lib_dir.as_str() {
resolved
.lib_dirs
.push(dep_root.join(lib_dir_str).to_string_lossy().to_string());
}
}
} else {
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
}
if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) {
for lib in libs {
if let Some(lib_str) = lib.as_str() {
resolved.libs.push(lib_str.to_string());
}
}
} else {
resolved.libs.push(name.clone());
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
resolved.libs.push(name.clone());
}
lock_packages.push(DepLock {
name: name.clone(),
version: dep_version(&dep_root),
checksum: String::new(),
source: format!("path+{}", dep_root.display()),
});
} else if let Some(git_info) = git_dep(value) {
lock_packages.push(DepLock {
name: name.clone(),
version: git_info.version.unwrap_or_default(),
checksum: String::new(),
source: format!("git+{}", git_info.url),
});
}
}
}
write_lock(
project_root,
&project_name,
&project_version,
&lock_packages,
)?;
Ok(resolved)
}
struct GitDep<'a> {
url: &'a str,
version: Option<String>,
}
fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
if let Some(table) = value.as_table()
&& let Some(url) = table.get("git").and_then(|v| v.as_str())
{
let version = table
.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
return Some(GitDep { url, version });
}
None
}
fn path_dep_path(value: &toml::Value) -> Option<&str> {
if let Some(table) = value.as_table() {
return table.get("path").and_then(|v| v.as_str());
}
register::path_from_string_dep(value)
}
fn push_if_exists(paths: &mut Vec<String>, path: &Path) {
if path.exists() {
paths.push(path.to_string_lossy().to_string());
}
}
fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
for dir in ["lib", "lib64"] {
push_if_exists(paths, &dep_root.join(dir));
}
push_if_exists(paths, &dep_root.join("target").join("lib"));
}
#[cfg(test)]
mod tests {
use super::*;
use toml::Value;
#[test]
fn path_dep_path_supports_table_and_legacy_strings() {
let table = Value::Table(
[(
"path".to_string(),
Value::String("./libs/mylib".to_string()),
)]
.into_iter()
.collect(),
);
assert_eq!(path_dep_path(&table), Some("./libs/mylib"));
assert_eq!(
path_dep_path(&Value::String("path:./libs/mylib".to_string())),
Some("./libs/mylib")
);
assert_eq!(
path_dep_path(&Value::String("./libs/mylib".to_string())),
Some("./libs/mylib")
);
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
}
#[test]
fn default_lib_dirs_include_packaged_library_output() {
let root = std::env::temp_dir().join(format!(
"dcr_default_lib_dirs_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(root.join("target/lib")).unwrap();
let mut paths = Vec::new();
push_default_lib_dirs(&mut paths, &root);
assert!(paths.iter().any(|p| p.ends_with("target/lib")));
let _ = std::fs::remove_dir_all(root);
}
}

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

@@ -0,0 +1,240 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value as TomlValue;
#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DcrConfig {
pub registry: std::collections::HashMap<String, Registry>,
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
}
fn dcr_config_dir() -> Option<PathBuf> {
home_dir().map(|h| h.join(".dcr"))
}
pub fn get_registry_config() -> Option<DcrConfig> {
let home = home_dir()?;
let config_path = home.join(".dcr/config.toml");
if !config_path.exists() {
return None;
}
let content = fs::read_to_string(config_path).ok()?;
toml::from_str(&content).ok()
}
pub fn get_index_path() -> PathBuf {
std::env::var("DCR_INDEX_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dcr_config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("index.json")
})
}
pub fn get_registry_cache_root() -> PathBuf {
let index_path = get_index_path();
if let Some(parent) = index_path.parent() {
if parent.exists() {
return parent.to_path_buf();
}
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("Warning: failed to create registry cache directory: {e}");
}
parent.to_path_buf()
} else {
PathBuf::from(".")
}
}
pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf, String> {
let raw_path = pkg_info
.get("path")
.and_then(|v| v.as_str())
.ok_or("Registry package is missing path")?;
if raw_path.trim().is_empty() {
return Err("Registry package path is empty".to_string());
}
let path = Path::new(raw_path);
let full_path = if path.is_absolute() {
path.to_path_buf()
} else {
get_registry_cache_root().join(path)
};
if full_path.file_name().and_then(|v| v.to_str()) == Some("dcr.toml") {
return full_path
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| format!("Invalid registry package path: {}", full_path.display()));
}
Ok(full_path)
}
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("include")
}
pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("lib")
}
pub fn resolve_package_from_registry(name: &str) -> Result<JsonValue, String> {
let config = get_registry_config().ok_or("No registry config found")?;
let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect();
registries.sort_by_key(|b| std::cmp::Reverse(b.1.priority));
for (_name_reg, _reg) in registries {
let index_path = get_index_path();
if index_path.exists() {
let index_content = fs::read_to_string(&index_path).map_err(|e| e.to_string())?;
let index: JsonValue =
serde_json::from_str(&index_content).map_err(|e| e.to_string())?;
if let Some(pkgs) = index.get("packages").and_then(|v| v.as_array()) {
for pkg in pkgs {
if pkg.get("name").and_then(|v| v.as_str()) == Some(name) {
return Ok(pkg.clone());
}
}
}
}
}
Err(format!(
"Package {} not found in registry (checked: {:?})",
name,
get_index_path()
))
}
pub fn is_registry_dep(value: &TomlValue) -> bool {
if let Some(raw) = value.as_str() {
let raw = raw.trim();
return !is_path_like_string(raw) && !is_git_like_string(raw);
}
if let Some(table) = value.as_table() {
if table.contains_key("git") || table.contains_key("path") || table.contains_key("url") {
return false;
}
table.contains_key("version")
|| table.contains_key("features")
|| table.contains_key("optional")
|| table.contains_key("registry")
} else {
false
}
}
pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> {
let raw = value.as_str()?.trim();
if let Some(path) = raw.strip_prefix("path:") {
return Some(path);
}
if is_path_like_string(raw) {
return Some(raw);
}
None
}
fn is_path_like_string(raw: &str) -> bool {
raw.starts_with("path:")
|| raw.starts_with("./")
|| raw.starts_with("../")
|| raw.starts_with('/')
|| raw.starts_with("~/")
|| raw.contains('\\')
}
fn is_git_like_string(raw: &str) -> bool {
raw.starts_with("git:")
|| raw.starts_with("github:")
|| raw.starts_with("gitlab:")
|| raw.starts_with("http://")
|| raw.starts_with("https://")
|| raw.starts_with("git@")
}
#[cfg(test)]
mod tests {
use super::*;
use toml::map::Map;
#[test]
fn registry_dep_detection_excludes_local_and_git_strings() {
assert!(is_registry_dep(&TomlValue::String("1.2.3".to_string())));
assert!(!is_registry_dep(&TomlValue::String(
"path:./libs/mylib".to_string()
)));
assert!(!is_registry_dep(&TomlValue::String(
"./libs/mylib".to_string()
)));
assert!(!is_registry_dep(&TomlValue::String(
"git:https://example.com/repo.git".to_string()
)));
}
#[test]
fn registry_dep_detection_excludes_source_tables() {
let mut path_table = Map::new();
path_table.insert(
"path".to_string(),
TomlValue::String("./libs/mylib".to_string()),
);
assert!(!is_registry_dep(&TomlValue::Table(path_table)));
let mut registry_table = Map::new();
registry_table.insert("features".to_string(), TomlValue::Array(Vec::new()));
assert!(is_registry_dep(&TomlValue::Table(registry_table)));
}
#[test]
fn registry_package_root_accepts_package_dir_or_manifest_path() {
let pkg = serde_json::json!({ "path": "/tmp/pkg" });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
);
let pkg = serde_json::json!({ "path": "/tmp/pkg/dcr.toml" });
assert_eq!(
package_root_from_registry_info(&pkg).unwrap(),
PathBuf::from("/tmp/pkg")
);
}
}

View File

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

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

@@ -0,0 +1,69 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
pub url: String,
pub priority: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub registry: HashMap<String, Registry>,
}
pub struct RegistryManager {
pub config: Config,
#[allow(dead_code)]
pub path: PathBuf,
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
}
impl RegistryManager {
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let dcr_dir = home_dir()
.ok_or("Cannot determine home directory")?
.join(".dcr");
let config_path = dcr_dir.join("config.toml");
if !config_path.exists() {
return Err("Config file ~/.dcr/config.toml not found".into());
}
let content = fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&content)?;
Ok(Self {
config,
path: dcr_dir,
})
}
}

View File

@@ -1,44 +1,38 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::platform;
use crate::utils::fs::check_dir;
use crate::utils::log::warn;
use std::path::Path;
use std::process::Command;
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
let bin_path = platform::bin_path(profile, project_name, target_dir);
if Path::new(&bin_path).exists() {
let output = Command::new(&bin_path)
.output()
.unwrap_or_else(|_| panic!("Failed to run {}", bin_path));
print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
return output.status.code().unwrap_or(1);
let output = Command::new(&bin_path).output();
match output {
Ok(o) => {
print!("{}", String::from_utf8_lossy(&o.stdout));
eprint!("{}", String::from_utf8_lossy(&o.stderr));
return o.status.code().unwrap_or(0);
}
Err(_) => {
return 1;
}
}
}
if target_dir.is_none()
&& check_dir(Some(&format!("target/{profile}")))
.unwrap_or_default()
.contains(&profile.to_string())
{
warn("Launch of the latest release");
let bin_path = platform::bin_path(profile, project_name, target_dir);
let output = Command::new(&bin_path)
.output()
.unwrap_or_else(|_| panic!("Failed to run {}", &bin_path));
print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
return output.status.code().unwrap_or(1);
}
if target_dir.is_some() && Path::new(&bin_path).exists() {
warn("Launch of the latest release");
let output = Command::new(&bin_path)
.output()
.unwrap_or_else(|_| panic!("Failed to run {}", &bin_path));
print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
return output.status.code().unwrap_or(1);
}
1
}

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;
@@ -18,8 +35,11 @@ fn main() {
let code = match cmd {
"new" => cli::r#new::new(rest),
"init" => cli::init::init(rest),
"setup" => cli::setup::setup(rest),
"add" => cli::add::add(rest),
"build" => cli::build::build(rest),
"run" => cli::run::run(rest),
"tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest),
"clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),

View File

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

@@ -221,6 +221,180 @@ fn workspace_build_and_clean_all() {
);
}
#[test]
fn dcr_add_dependencies() {
let dir = unique_sandbox_dir("add_dep");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
// Test path: prefix
let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir);
assert!(out.status.success(), "dcr add path should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("mylib = { path = \"./libs/mylib\" }"),
"path dep not found in toml"
);
// Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"),
"github dep not found in toml"
);
// Test git: prefix (generic)
let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir);
assert!(out.status.success(), "dcr add custom git should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"),
"custom git dep not found in toml"
);
// Test git: prefix (github default)
let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir);
assert!(out.status.success(), "dcr add git short should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("git_short = { git = \"https://github.com/user/repo\" }"),
"git short dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
&dir,
);
assert!(out.status.success(), "dcr add with branch should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"),
"branch lib not found in toml"
);
// Test failure on no prefix
let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir);
assert!(!out.status.success(), "dcr add without prefix should fail");
}
#[test]
fn dcr_builds_lib_package() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping lib package test");
return;
};
let dir = unique_sandbox_dir("lib_package");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
let updated_toml = toml
.replace("kind = \"bin\"", "kind = \"staticlib\"")
.replace("type = \"none\"", "type = \"lib\"");
std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml");
std::fs::write(dir.join("src").join("my_lib.h"), "void hello();")
.expect("failed to write header");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(out.status.success(), "dcr build should succeed");
let target_dir = dir.join("target");
assert!(
target_dir.join("include").join("my_lib.h").is_file(),
"include/my_lib.h missing"
);
assert!(target_dir.join("lib").exists(), "lib directory missing");
}
#[test]
fn registry_dependency_is_built_from_cache() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping registry dependency build test");
return;
};
let root = unique_sandbox_dir("registry_dep");
let home = root.join("home");
let dcr_home = home.join(".dcr");
let dep = root.join("cache").join("mylib");
let app = root.join("app");
std::fs::create_dir_all(dcr_home.as_path()).expect("failed to create dcr home");
std::fs::create_dir_all(dep.join("src")).expect("failed to create dep src");
std::fs::create_dir_all(app.join("src")).expect("failed to create app src");
std::fs::write(
dcr_home.join("config.toml"),
"[registry.local]\nurl = \"file://local\"\npriority = 1\n",
)
.expect("failed to write registry config");
std::fs::write(
dcr_home.join("index.json"),
format!(
"{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}",
dep.display()
),
)
.expect("failed to write registry index");
std::fs::write(
dep.join("dcr.toml"),
"[package]\nname = \"mylib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\n",
)
.expect("failed to write dep dcr.toml");
std::fs::write(dep.join("src").join("mylib.h"), "int answer(void);\n")
.expect("failed to write header");
std::fs::write(
dep.join("src").join("mylib.c"),
"int answer(void) { return 42; }\n",
)
.expect("failed to write dep source");
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\ntype = \"none\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmylib = \"0.1.0\"\n",
)
.expect("failed to write app dcr.toml");
std::fs::write(
app.join("src").join("main.c"),
"#include \"mylib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
)
.expect("failed to write app source");
let index_path = dcr_home.join("index.json");
let envs = [
("DCR_COMPILER", compiler),
("HOME", home.to_str().unwrap()),
("DCR_INDEX_PATH", index_path.to_str().unwrap()),
];
let out = run_dcr_env(&["build"], &app, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"registry dependency build should succeed"
);
assert!(
dep.join("target").join("include").join("mylib.h").is_file(),
"registry dependency headers were not packaged"
);
assert!(
dep.join("target").join("lib").exists(),
"registry dependency library directory missing"
);
}
#[test]
fn dcr_test_runs_without_sandbox_dependency() {
let Some(compiler) = available_compiler() else {