32 Commits

Author SHA1 Message Date
c22d2dbddc New docs 2026-02-26 18:09:07 +08:00
83061cc460 GITBOOK-6: No subject 2026-02-26 09:39:05 +00:00
467e64aa4f rename docs dir 2026-02-26 17:17:57 +08:00
7817834790 Added:
- Incremental builds (object caching by mtime)
- `build.kind` with `staticlib` support
- Custom output directory via `build.target`
2026-02-26 17:03:35 +08:00
9fe2604154 Custom output directory via build.target 2026-02-23 14:02:32 +08:00
cb8a3b2001 Custom output directory via build.target 2026-02-23 13:51:53 +08:00
e0fd729635 translated CONTRIBUTING.md to eng 2026-02-22 15:54:55 +08:00
e70edf8ae4 Update project TODO 2026-02-22 15:43:10 +08:00
210e82f026 Added:
- Custom build flags: `build.cflags` and `build.ldflags`
- Recursive source discovery inside `src/`
- Path dependencies with auto include/lib resolution and `dcr.lock` generation (experimental)
- Add docs/index.md and docs/dcr-toml.md
2026-02-22 15:04:02 +08:00
ff12ca2758 Update release.yml 2026-02-21 16:19:44 +08:00
468491b5b1 Added:
- Modular builders for `gcc`, `clang`, `msvc`
- Platform-specific binary path generation (Linux/macOS/Windows)

Changed:
- Build configuration moved to `[build]` (`language`, `standard`, `compiler`)
- `dcr.toml` formatting now includes the `[build]` section
- Build uses built-in `debug/release` flags per compiler
- Build compiles all `*.c/*.cpp` files in `src/` into a single binary (no incremental build)
- Updated `dcr.toml` examples in documentation
2026-02-21 16:12:32 +08:00
0f48151b68 Added:
- Modular builders for `gcc`, `clang`, `msvc`
- Platform-specific binary path generation (Linux/macOS/Windows)

Changed:
- Build configuration moved to `[build]` (`language`, `standard`, `compiler`)
- `dcr.toml` formatting now includes the `[build]` section
- Build uses built-in `debug/release` flags per compiler
- Build compiles all `*.c/*.cpp` files in `src/` into a single binary (no incremental build)
- Updated `dcr.toml` examples in documentation
2026-02-21 16:12:20 +08:00
e87e0cbc47 update aur publish 2026-02-20 18:31:30 +08:00
fff8baa3c9 update aur publish 2026-02-20 18:22:05 +08:00
ad0d4cdcb7 update aur publish 2026-02-20 18:20:50 +08:00
45806f193e update aur publish 2026-02-20 18:17:36 +08:00
9ae4bb5ab1 update release.yml and CHANGELOG.md 2026-02-20 18:09:16 +08:00
5a2025fb80 Changed:
- Reworked `dcr.toml` handling: added read, validation, and edit through `core::config`
- `new` and `init` use the new config creation logic
- `build` and `run` now require `dcr.toml` and read `compiler` and `name` from it
2026-02-20 17:54:01 +08:00
06e7af6ab6 update README.md, --update and aur 2026-02-19 14:50:16 +08:00
8495441b1a Update pkgrel 1 to 2 2026-02-19 13:14:03 +08:00
b56f502a30 fix aur 2026-02-19 13:02:01 +08:00
115692253e add AUR publish support 2026-02-18 23:16:02 +08:00
7a0ebb43a5 remove one project deps 2026-02-18 21:08:56 +08:00
4c628ff336 update CHANGELOG.md 2026-02-18 21:05:36 +08:00
95324ce199 chore: translate CLI and installer messages to English 2026-02-18 21:04:28 +08:00
d1e23a3179 Update project docs lang to Eng
Add FAQ.md and docs/
2026-02-18 19:53:39 +08:00
ae49ebe44a Fix project version on Cargo.toml 2026-02-17 18:50:34 +08:00
40a6bf1427 Fix single match in updater 2026-02-17 18:48:37 +08:00
Dexoron
4c06307921 Merge pull request #2 from dexoron/rust-rewrite
Rewrite project from Python to Rust and add Windows/MacOS supports
2026-02-17 18:31:34 +08:00
211438a4e8 Rewrite project from Python to Rust and add Windows/MacOS supports 2026-02-17 18:15:08 +08:00
ff69e50127 добавлены правила контрибьюции 2026-02-13 00:49:18 +08:00
2f28cc4e2c Удалены не используемые импорты и переменые, исправлены маленькие проблемы, добавлен файл check.sh, для проверки форматирования и типизации 2026-02-13 00:21:36 +08:00
87 changed files with 5866 additions and 748 deletions

136
.github/workflows/aur.yml vendored Normal file
View File

@@ -0,0 +1,136 @@
name: AUR Publish
on:
workflow_dispatch:
inputs:
version:
description: "Tag version without v prefix (e.g. 0.2.2)"
required: true
push:
tags:
- "v*"
permissions:
contents: read
jobs:
publish-aur:
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive version and checksum
id: meta
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
PKGVER="${{ inputs.version }}"
else
PKGVER="${GITHUB_REF_NAME#v}"
fi
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/v${PKGVER}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
echo "pkgver=$PKGVER" >> "$GITHUB_OUTPUT"
echo "source_url=$SRC_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Render AUR metadata
shell: bash
run: |
set -euo pipefail
PKGVER='${{ steps.meta.outputs.pkgver }}'
SRC_URL='${{ steps.meta.outputs.source_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
cat > packaging/aur/dcr/PKGBUILD <<PKG
pkgname=dcr
pkgver=${PKGVER}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64' 'aarch64')
url="https://github.com/dexoron/dcr"
license=('MIT')
depends=('gcc-libs')
makedepends=('cargo')
options=('!lto')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source=("\$pkgname-\$pkgver.tar.gz::\$url/archive/refs/tags/v\$pkgver.tar.gz")
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/\$pkgname-\$pkgver"
export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="\${RUSTFLAGS:-}"
export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
cargo build --release --locked
}
package() {
cd "\$srcdir/\$pkgname-\$pkgver"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
}
PKG
cat > packaging/aur/dcr/.SRCINFO <<SRC
pkgbase = dcr
pkgdesc = Cargo-like utility to manage C/C++ projects
pkgver = ${PKGVER}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
license = MIT
makedepends = cargo
depends = gcc-libs
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
options = !lto
source = dcr-${PKGVER}.tar.gz::${SRC_URL}
sha256sums = ${SHA256}
pkgname = dcr
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr
shell: bash
run: |
set -euo pipefail
if [[ -z "${AUR_SSH_PRIVATE_KEY:-}" ]]; then
echo "AUR_SSH_PRIVATE_KEY secret is missing"
exit 1
fi
install -d -m 700 ~/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
export GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes'
git clone "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git" aur-repo
cp packaging/aur/dcr/PKGBUILD aur-repo/PKGBUILD
cp packaging/aur/dcr/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr ${{ steps.meta.outputs.pkgver }}"
git push

28
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: CI
on:
push:
branches:
- master
- main
pull_request:
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Format
run: cargo fmt --all --check
- name: Lint
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Check
run: cargo check --all-targets --all-features

92
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
notes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Prepare Release Notes
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
notes_file=".github/release-notes.md"
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then
awk -v ver="$tag" '
$0 ~ "^## \\[" ver "\\]" {found=1; next}
found && $0 ~ "^## \\[" {exit}
found {print}
' CHANGELOG.md | sed -e '${/^$/d;}' > "$notes_file"
else
echo "No CHANGELOG section for $tag." > "$notes_file"
git log -1 --pretty=format:"%s%n%n%b" >> "$notes_file"
fi
- name: Upload Notes Artifact
uses: actions/upload-artifact@v4
with:
name: release-notes
path: .github/release-notes.md
build:
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
steps:
- uses: actions/checkout@v4
- 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 (Unix)
if: runner.os != 'Windows'
shell: bash
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 }}"
- name: Upload Release Asset
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.asset_name }}
body_path: .github/release-notes.md

12
.gitignore vendored
View File

@@ -1,9 +1,5 @@
__pycache__/ target/
build/ sandbox/**
dist/ .idea/
wheels/
*.egg-info
.venv
sandbox/*
.ruff_cache/
.vscode/ .vscode/
.env

View File

@@ -1 +0,0 @@
3.14.2

View File

@@ -1,24 +1,78 @@
# Changelog # Changelog
## [0.1.2] - 2026-02-12 ## [0.2.5] - 2026-02-26
Добавлено: Added:
- Команда обновления - Incremental builds (object caching by mtime)
- Скрипт установки `install.sh` и инструкции по установке в README - `build.kind` with `staticlib` support
- Флаг `--version` для вывода версии - Custom output directory via `build.target`
Изменено: ## [0.2.4] - 2026-02-22
- Улучшен консольный вывод команд, добавлены цвета и обновлена справка `--help` Added:
- Обновлен запуск проекта и обработка сборки в `run.py`/`build.py` - Custom build flags: `build.cflags` and `build.ldflags`
- Recursive source discovery inside `src/`
- Path dependencies with auto include/lib resolution and `dcr.lock` generation (experimental)
- Add docs/index.md and docs/dcr-toml.md
## [0.2.3] - 2026-02-21
Added:
- Modular builders for `gcc`, `clang`, `msvc`
- Platform-specific binary path generation (Linux/macOS/Windows)
Changed:
- Build configuration moved to `[build]` (`language`, `standard`, `compiler`)
- `dcr.toml` formatting now includes the `[build]` section
- Build uses built-in `debug/release` flags per compiler
- Build compiles all `*.c/*.cpp` files in `src/` into a single binary (no incremental build)
- Updated `dcr.toml` examples in documentation
## [0.2.2] - 2026-02-20
Changed:
- Reworked `dcr.toml` handling: added read, validation, and edit through `core::config`
- `new` and `init` use the new config creation logic
- `build` and `run` now require `dcr.toml` and read `compiler` and `name` from it
## [0.2.1] - 2026-02-18
Changed:
- Translated user-facing CLI messages to English
- Unified error and warning output via `utils::log::{error, warn}`
- Updated `--help` output: translated headers and examples to English, used `printc`, and applied `BOLD_*` styles
- Translated installer script messages in `install.sh` and `install.ps1` to English
## [0.2.0] - 2026-02-17
Changed:
- Project migrated from Python to Rust
- CLI and commands (`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`) ported to Rust
- Updated `--update` flag: added support for GNU/Linux, Windows, macOS
- Updated `README.md`, `CONTRIBUTING.md`, and `install.sh` for the Rust implementation
- Added `install.ps1` for Windows
- Updated `install.sh` for GNU/Linux and macOS
Added:
- Added `install.ps1` for Windows
- Support for GNU/Linux, Windows, macOS (x86_64/arm)
Important:
- Code was ported with neural networks; future versions will include bug fixes and logic changes
## [0.1.2] - 2026-02-12
Added:
- Update command
- `install.sh` install script and README instructions
- `--version` flag
Changed:
- Improved CLI output, added colors, and updated `--help`
- Updated project run/build handling in `run.py`/`build.py`
## [0.1.1] - 2026-02-11 ## [0.1.1] - 2026-02-11
Изменено: Changed:
- Обновлена справка `--help` - Updated `--help`
- `main.py` теперь корректно запускается при прямом выполнении - `main.py` now runs correctly when executed directly
## [0.1.0] - 2026-02-11 ## [0.1.0] - 2026-02-11
Первый публичный релиз. First public release.
Добавлено: Added:
- Базовые команды `new`, `init`, `build`, `run`, `clean` - Base commands `new`, `init`, `build`, `run`, `clean`
- Профили сборки `debug` и `release` - Build profiles `debug` and `release`
- Шаблоны `dcr.toml` и `src/main.c` - `dcr.toml` and `src/main.c` templates

57
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,57 @@
# Contributing
Thanks for your interest in the project.
## Requirements
- Use Rust stable.
- Before opening a PR, the code must pass formatting, linting, and build.
## Quick Start
```bash
git clone https://github.com/dexoron/dcr.git
cd dcr
cargo check
```
## Code Style
- Format code with `cargo fmt`.
- Check warnings with `cargo clippy`.
- Keep functions small and focused.
- Prefer clear names over clever ones.
## Checks
Before PR, run:
```bash
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo check
```
## CI/CD and Releases
- CI: `.github/workflows/ci.yml`
- Release: `.github/workflows/release.yml`
- Releases are triggered by tags `v*` and publish binaries for:
- `x86_64-unknown-linux-gnu`
- `x86_64-apple-darwin`
- `aarch64-apple-darwin`
- `x86_64-pc-windows-msvc`
## Commits and PRs
- Write clear commit messages.
- In PRs, include a short description and rationale.
- Update documentation when behavior changes.
## Questions
If anything is unclear, open an issue or ask in the PR.
You can also reach out directly:
- TG: @dexoron
- DS: dexoron

1797
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "dcr"
version = "0.2.4"
edition = "2024"
build = "build.rs"
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
self-replace = "1"
sha2 = "0.10"
toml = "1.0.3"

102
FAQ.md Normal file
View File

@@ -0,0 +1,102 @@
# 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`).
## 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.
4. Open a Pull Request.
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).
4. Describe the problem or idea clearly and concisely.
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.
4. Add project dependencies.
5. Build using `dcr`.
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.
If you need low-level, full control over build steps, classic tools may be a better fit.
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.
4. Expected behavior.
5. Actual behavior and error output.
6. Minimal project example (if possible).
Submit bugs and issues through `Issues` in the relevant repository.

176
README.md
View File

@@ -1,124 +1,158 @@
# DCR (Dexoron Cargo Realization) # DCR (Dexoron Cargo Realization)
DCR — это утилита для управления проектами на C. Она задает единый формат проекта, дает команды для создания, сборки и запуска, и использует один конфигурационный файл `dcr.toml`. DCR is a utility for managing C/C++ projects in a Cargo-like style.
Проект находится на стадии MVP. Текущая реализация написана на Python и в будущем будет переписана. The current implementation is written in Rust.
## Зачем DCR ## Why DCR
- Единая структура проекта без ручной настройки - Unified project structure without manual setup
- Простые команды для типовых задач - Simple commands for common tasks
- Прозрачная компиляция и предсказуемые профили сборки - Transparent compilation and predictable build profiles
## Возможности ## Features
- Создание нового проекта или инициализация текущей директории - Create a new project or initialize the current directory
- Сборка проекта в профилях `debug` и `release` - Build a project with `debug` and `release` profiles
- Запуск собранного бинарника - Run the compiled binary
- Очистка результатов сборки - Clean build artifacts
- Генерация минимального шаблона C-проекта - Generate a minimal C project template
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
## Установка ## Supported Platforms
- Linux: `x86_64-unknown-linux-gnu`
- macOS Intel: `x86_64-apple-darwin`
- macOS Apple Silicon: `aarch64-apple-darwin`
- Windows: `x86_64-pc-windows-msvc`
### Автоматическая установка(Рекомендуется) ## Installation
**GNU/Linux** ### Package Manager
**Arch Linux**
```sh ```sh
curl -L dcr.zov.tatar | bash yay -S dcr # or paru and other AUR package managers
``` ```
### Самостоятельная установка ### From Source
```sh ```sh
git clone https://github.com/dexoron/dcr.git "~/.local/share/dcr" git clone https://github.com/dexoron/dcr.git
chmod +x "~/.local/share/dcr/dcr" cd dcr
ln -sf ~/.local/share/dcr/dcr ~/.local/bin cargo build --release
mkdir -p ~/.local/bin
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
``` ```
## Быстрый старт ### Via `install.sh` (Linux/macOS)
Создать новый проект:
```sh
curl -fsSL https://dcr.dexoron.su/install.sh | bash
```
### Via `install.ps1` (Windows)
```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`
Release assets:
- `dcr-x86_64-unknown-linux-gnu`
- `dcr-x86_64-apple-darwin`
- `dcr-aarch64-apple-darwin`
- `dcr-x86_64-pc-windows-msvc.exe`
## Update
- 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` `dcr new hello`
Или инициализировать текущую директорию (директория должна быть пустой): Or initialize the current directory (the directory must be empty):
`dcr init` `dcr init`
Структура проекта: Project structure:
```
```txt
hello/ hello/
- src/ - src/
- - main.c - - main.c
- dcr.toml - dcr.toml
``` ```
Сборка и запуск проекта: Build and run the project:
`dcr run` или `dcr run --release` - для сбрки с оптимизацией `dcr run` or `dcr run --release`
## Commands
## Команды
### `dcr new <name>` ### `dcr new <name>`
Создает проект в текущей директории с указанным именем. Creates a project with the specified name in the current directory.
### `dcr init` ### `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]` ### `dcr build [profile]`
Собирает проект. Если профиль не указан, используется `--debug`. Builds the project. If no profile is specified, `--debug` is used.
### `dcr run [profile]` ### `dcr run [profile]`
Собирает проект и запускает бинарник. Если профиль не указан, используется `--debug`. Builds the project and runs the binary. If no profile is specified, `--debug` is used.
Запуск вручную: Run manually:
`./target/<profile>/main` `./target/<profile>/<name>`
### `dcr clean` ### `dcr clean`
Удаляет папку `target` в корне проекта. Removes the `target` directory in the project root.
## Профили сборки ## Build Profiles
Поддерживаются два профиля: Two profiles are supported:
- `--debug` (по умолчанию) — флаги: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG` - `--debug` (default) - built-in flags per compiler
- `--release` — флаги: `-O3 -DNDEBUG -march=native` - `--release` - built-in flags per compiler
## Конфигурация Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`.
Основной файл проекта — `dcr.toml`. Сейчас он создается автоматически и служит точкой входа для будущих настроек. You can also set `build.target` to override the output directory (profile-independent).
Use `build.kind = "staticlib"` to build a static library instead of a binary.
`dcr run` is only for `build.kind = "bin"` and will fail for `staticlib`.
Пример `dcr.toml`: ## Configuration
``` The main project file is `dcr.toml`.
Example `dcr.toml`:
```toml
[package] [package]
name = "hello" name = "hello"
version = "0.1.0" version = "0.1.0"
[build]
language = "c" language = "c"
standard = "c11"
compiler = "clang" compiler = "clang"
# Optional custom flags
cflags = ["-Wall", "-Wextra"]
ldflags = ["-lm"]
[dependencies] [dependencies]
``` ```
## Установка в PATH Path dependencies are supported. DCR will resolve them on build and generate `dcr.lock`.
Чтобы вызывать `dcr` из любой директории, можно сделать symlink в `~/.local/bin`:
```sh
ln -s "$PWD/dcr" ~/.local/bin/dcr
```
Убедитесь, что `~/.local/bin` находится в `PATH`.
## Пример проекта Incremental build note: object files are rebuilt when source `.c/.cpp` files change. Header dependency tracking is not implemented yet.
`src/main.c` содержит минимальный `Hello, World!`:
```c
#include <stdio.h>
int main(void) { ## Requirements
puts("Hello, World!"); - Rust toolchain (`rustc`, `cargo`) - for building DCR from source
return 0; - C compiler (`clang`, `gcc`, or 'cl'(msvc))
}
```
## Требования ## Releases
- C-компилятор (например, `gcc` или `clang`) Releases are built automatically via GitHub Actions (`.github/workflows/release.yml`) when a tag matching `v*` is pushed.
- Python 3.14.2 (для текущей версии DCR)
- uv (менеджер проекта для Python)
## Статус и планы ## License
Проект в стадии MVP. Планируется переписывание на более подходящий язык и расширение функциональности: See `LICENSE`.
- более гибкая конфигурация через `dcr.toml`
- расширяемые профили сборки
- менеджер пакетов
## Лицензия
См. файл `LICENSE`.

6
TODO.md Normal file
View File

@@ -0,0 +1,6 @@
# TODO
- Add incremental builds
- Add header dependency tracking for incremental builds (GCC/Clang: -MMD, MSVC: /showIncludes)
- Add external package managers (Conan, vcpkg)
- Support building static libraries (ar .a)

4
build.rs Normal file
View File

@@ -0,0 +1,4 @@
fn main() {
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
println!("cargo:rustc-env=DCR_TARGET={target}");
}

10
dcr
View File

@@ -1,10 +0,0 @@
#!/usr/bin/env bash
BASE_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
if command -v python3 >/dev/null 2>&1; then
PYTHON_BIN="python3"
else
PYTHON_BIN="$BASE_DIR/Python/bin/python3"
fi
PYTHONPATH="$BASE_DIR/src" exec "$PYTHON_BIN" -m dcr.main "$@"

35
docs/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Introduction
DCR (Dexoron Cargo Realization) is a Cargo-style CLI for C/C++ projects.
Current DCR focuses on a simple, predictable workflow:
- create or initialize a project (`dcr new`, `dcr init`)
- build with `debug` or `release` profiles (`dcr build`)
- run binaries (`dcr run`)
- clean artifacts (`dcr clean`)
- update the installed binary (`dcr --update`)
## What is implemented in this version
- Project configuration in `dcr.toml` with `[package]`, `[build]`, `[dependencies]`.
- Recursive source discovery in `src/`:
- C: `*.c`
- C++: `*.cpp`, `*.cxx`, `*.cc`
- Compiler selection from `dcr.toml` (`clang`, `gcc`, `cl`, `clang-cl`, etc.).
- Two build kinds:
- `bin` (default)
- `staticlib`
- Optional custom output directory via `build.target`.
- Path dependencies with include/lib resolution and `dcr.lock` generation.
## Important behavior notes
- `dcr.toml` is required for `build`, `run`, and `clean`.
- `run` works only with `build.kind = "bin"`.
- Incremental rebuild currently tracks source file timestamps (not header dependency graphs).
- Object files are always created under `target/<profile>/obj/`.
## Repository
- <https://github.com/dexoron/dcr>

41
docs/SUMMARY.md Normal file
View File

@@ -0,0 +1,41 @@
# Table of contents
* [Introduction](README.md)
## Getting started
* [Installation](getting-started/installation.md)
* [Create your first project](getting-started/create-your-first-project.md)
* [Project structure](getting-started/project-structure.md)
* [Build and run](getting-started/build-and-run.md)
## Configuration
* [dcr.toml overview](configuration/dcr.toml-overview.md)
* [Package section](configuration/package-section.md)
* [Build section](configuration/build-section.md)
* [Dependencies section](configuration/dependencies-section.md)
* [Build profiles](configuration/build-profiles.md)
## Commands
* [Global options](commands/global-options.md)
* [dcr init/new](commands/dcr-init-or-new.md)
* [dcr build](commands/dcr-build.md)
* [dcr run](commands/dcr-run.md)
* [dcr clean](commands/dcr-clean.md)
## Dependencies & Build
* [Dependency format](dependencies-and-build/dependency-format.md)
* [Local dependencies](dependencies-and-build/local-dependencies.md)
* [Versioning & lock file](dependencies-and-build/versioning-and-lock-file.md)
* [Target directory](dependencies-and-build/target-directory.md)
* [Compilers & linking](dependencies-and-build/compilers-and-linking.md)
## Reference & Help
* [CLI reference](reference-and-help/cli-reference.md)
* [Config reference](reference-and-help/config-reference.md)
* [FAQ](reference-and-help/faq.md)
* [Contributing & Bug reports](reference-and-help/contributing-and-bug-reports.md)

View File

@@ -0,0 +1,42 @@
# dcr build
Builds the project in `debug` or `release` profile.
## Usage
```sh
dcr build
dcr build --debug
dcr build --release
```
## What `build` does
1. Checks that `dcr.toml` exists and is valid.
2. Reads build settings from `[build]`.
3. Resolves path dependencies from `[dependencies]`.
4. Creates required output directories.
5. Recursively compiles sources from `src/`.
6. Links final artifact (binary or static library).
## Config values used
- `package.name`
- `build.compiler`
- `build.language`
- `build.standard`
- `build.kind`
- `build.target`
- `build.cflags`
- `build.ldflags`
## Source selection
- `language = "c"` -> `*.c`
- `language = "c++" | "cpp" | "cxx"` -> `*.cpp`, `*.cxx`, `*.cc`
## Notes
- The profile is selected from the first argument only.
- Unknown profile flags return an error.
- Incremental rebuild is based on source/object mtime comparison.

View File

@@ -0,0 +1,23 @@
# dcr clean
Removes build artifacts in `target/`.
## Usage
```sh
dcr clean
dcr clean --debug
dcr clean --release
```
## Behavior
- Without arguments: removes entire `target/`.
- With profile: removes only `target/<profile>/`.
## Validation
- `dcr.toml` must exist.
- More than one argument is treated as error.
- Unknown profile is treated as error.
- If `target` or selected profile directory does not exist, command prints a warning.

View File

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

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

@@ -0,0 +1,27 @@
# dcr run
Builds the project and runs the resulting binary.
## Usage
```sh
dcr run
dcr run --debug
dcr run --release
```
## Behavior
1. Validates `dcr.toml`.
2. Reads `package.name`, `build.kind`, and optional `build.target`.
3. Runs the same build flow as `dcr build`.
4. Executes the built artifact path for the selected profile.
## Restrictions
- If `build.kind = "staticlib"`, `run` exits with error.
- The profile is selected from the first argument.
## On build failure
`dcr run` attempts to launch an existing binary from the same profile if present; otherwise it reports that code errors must be fixed.

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
# Build section
## Schema
```toml
[build]
language = "c"
standard = "c11"
compiler = "clang"
kind = "bin"
# optional:
# target = "./dist"
# cflags = ["-Wall"]
# ldflags = ["-lm"]
```
## Fields
- `language` (string, required): `c`, `c++`, `cpp`, or `cxx`.
- `standard` (string, required): language standard passed to compiler.
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
- `kind` (string, required): `bin` or `staticlib`.
- `target` (string, optional): custom output directory for final artifact.
- `cflags` (string array, optional): extra compile flags.
- `ldflags` (string array, optional): extra link flags.
## Behavior notes
- Compiler backend is selected by compiler string (`gcc`, `clang`, `cl`, `clang-cl`).
- Empty or unknown compiler value falls back to clang-like build path.
- `run` is not allowed for `kind = "staticlib"`.

View File

@@ -0,0 +1,45 @@
# dcr.toml overview
`dcr.toml` is the main project file used by DCR.
## Minimal generated template
```toml
[package]
name = "hello"
version = "0.1.0"
[build]
language = "c"
standard = "c11"
compiler = "clang"
kind = "bin"
[dependencies]
```
`dcr new` and `dcr init` generate this structure automatically.
## Required sections
- `[package]`
- `[build]`
- `[dependencies]`
## Required keys
- `package.name`
- `package.version`
- `build.language`
- `build.standard`
- `build.compiler`
- `build.kind`
`build.kind` must be either `bin` or `staticlib`.
## Optional keys
- `build.target`
- `build.cflags`
- `build.ldflags`
- dependency fields (`include`, `lib`, `libs`, `system`)

View File

@@ -0,0 +1,30 @@
# Dependencies section
DCR currently supports path dependencies.
## Example
```toml
[dependencies]
frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"] }
```
## Supported fields per dependency
- `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
- If `include` is omitted, DCR tries `<dep>/include`.
- If `lib` is omitted, DCR tries `<dep>/lib`, then `<dep>/lib64`.
- If `libs` is omitted, dependency key name is used.
## Validation
- Dependency value must be a TOML table.
- `path` is mandatory.
- `include`/`lib`/`libs` must be arrays of strings if provided.

View File

@@ -0,0 +1,20 @@
# Package section
## Schema
```toml
[package]
name = "my-app"
version = "0.1.0"
```
## Fields
- `name` (string, required): used as output artifact base name.
- `version` (string, required): used in `dcr.lock` project entry.
## 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.

View File

@@ -0,0 +1,30 @@
# Compilers & linking
## Compiler backend selection
DCR picks a backend from `build.compiler`:
- contains `clang-cl` -> MSVC backend
- contains `gcc` or `g++` -> GCC backend
- contains `clang` or `clang++` -> Clang backend
- equals `cl` or contains `msvc` -> MSVC backend
- otherwise -> Clang backend fallback
## Compilation model
- Source files are found recursively in `src/` based on `build.language`.
- Each source is compiled to `target/<profile>/obj/<relative-path>.o` (`.obj` on MSVC).
- Recompile happens when source mtime is newer than object mtime.
## Linking
For `kind = "bin"`:
- Object files are linked into executable artifact.
- Dependency lib dirs and library names are passed to linker.
- `build.ldflags` are appended to link command.
For `kind = "staticlib"`:
- Linux/macOS (GCC/Clang): `ar rcs` creates `.a` archive.
- Windows (MSVC): `lib` creates `.lib` archive.

View File

@@ -0,0 +1,26 @@
# Dependency format
Dependencies are defined in `dcr.toml` under `[dependencies]`.
## Full example
```toml
[dependencies]
fmt = { path = "./third_party/fmt", include = ["include"], lib = ["lib"], libs = ["fmt"] }
```
## Field reference
- `path`: required dependency root path.
- `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
```toml
[dependencies]
mylib = { path = "./libs/mylib" }
```
In this form DCR will try defaults for include/lib dirs and use `mylib` as the linker library name.

View File

@@ -0,0 +1,19 @@
# Local dependencies
## Resolution flow during build
For each dependency in `[dependencies]`, DCR:
1. Resolves `path` (absolute or project-relative).
2. Resolves include dirs (`include` or default `include`).
3. Resolves lib dirs (`lib` or defaults `lib`, `lib64`).
4. Resolves link libs (`libs` or dependency name).
5. Synchronizes dependency files to `target/<profile>/deps/<dep-name>/`.
6. Adds include/lib/link options to compile/link commands.
## Failure cases
- Dependency path is missing or not a directory.
- No valid include/lib directories were found.
- Any configured include/lib path does not exist.
- Invalid field types in dependency config.

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
# Build and run
## Build
Debug (default):
```sh
dcr build
# or
dcr build --debug
```
Release:
```sh
dcr build --release
```
## Run
```sh
dcr run
# or
dcr run --release
```
`dcr run` first builds, then launches the produced binary.
## Clean artifacts
```sh
dcr clean
```
Profile-only cleanup:
```sh
dcr clean --debug
dcr clean --release
```
## Notes
- `run` is unavailable for `build.kind = "staticlib"`.
- Build profile flag is parsed from the first command argument.
- Unknown profile flags produce an error.

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
# Project structure
## Minimal project
```text
project/
- dcr.toml
- src/
- - main.c
```
## Typical generated/optional files
```text
project/
- dcr.toml
- dcr.lock
- src/
- - ... source files ...
- target/
- - debug/
- - - obj/
- - - deps/
- - release/
- - - obj/
- - - deps/
```
## Meaning
- `dcr.toml`: main project configuration.
- `dcr.lock`: generated lock file for resolved path dependencies.
- `src/`: recursively scanned source directory.
- `target/<profile>/obj/`: cached object files for incremental rebuild.
- `target/<profile>/deps/`: synchronized copies of path dependencies.
- `target/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`.
- `target/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`.
If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<profile>/obj`.

View File

@@ -0,0 +1,26 @@
# CLI reference
## Command list
```text
dcr new <name>
dcr init
dcr build [--debug|--release]
dcr run [--debug|--release]
dcr clean [--debug|--release]
dcr --help
dcr --version
dcr --update
```
## Notes on argument parsing
- Most commands parse profile from the first argument.
- `new` requires exactly one argument.
- `init` and `--update` do not accept extra arguments.
- `clean` accepts at most one argument.
## Exit behavior overview
- Successful command execution returns `0`.
- Validation/build/runtime errors return non-zero status.

View File

@@ -0,0 +1,41 @@
# Config reference
## Full schema
```toml
[package]
name = "string" # required
version = "string" # required
[build]
language = "c|c++|cpp|cxx" # required
standard = "string" # required
compiler = "string" # required
kind = "bin|staticlib" # required
target = "string" # optional
cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional
[dependencies]
name = {
path = "string", # required for each dependency
include = ["string", "..."], # optional
lib = ["string", "..."], # optional
libs = ["string", "..."], # optional
}
```
## Validation rules
- `[package]`, `[build]`, `[dependencies]` must exist.
- Required string fields must be non-empty.
- `build.kind` must be `bin` or `staticlib`.
- Dependency fields `include/lib/libs` must be string arrays when provided.
## Generated defaults (`dcr new` / `dcr init`)
- `package.version = "0.1.0"`
- `build.language = "c"`
- `build.standard = "c11"`
- `build.compiler = "clang"`
- `build.kind = "bin"`

View File

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

View File

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

113
install.ps1 Normal file
View File

@@ -0,0 +1,113 @@
$ErrorActionPreference = 'Stop'
$RepoUrl = 'https://github.com/dexoron/dcr'
$ApiUrl = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
$InstallPath = Join-Path $env:LOCALAPPDATA 'dcr'
$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 Require-Cmd($name) {
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
Fail "Not found: $name"
}
}
function Select-Mode {
Write-Host "Choose installation mode:"
Write-Host " 1) Download prebuilt binary from GitHub Release (recommended)"
Write-Host " 2) Build from git"
$choice = Read-Host "Enter 1 or 2 [1]"
if ([string]::IsNullOrWhiteSpace($choice)) { $choice = '1' }
switch ($choice) {
'1' { return 'release' }
'2' { return 'build' }
default { Fail 'Unknown option' }
}
}
function Get-Target {
if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') {
Fail "Only x86_64 Windows is supported, current architecture: $env:PROCESSOR_ARCHITECTURE"
}
return 'x86_64-pc-windows-msvc'
}
function Install-FromRelease {
param([string]$target)
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
$assetName = "dcr-$target.exe"
Info "Fetching latest release..."
$release = Invoke-RestMethod -Method Get -Uri $ApiUrl
$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)"
}
Info "Downloading $assetName..."
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $BinaryPath
Ok "Binary downloaded: $assetName"
}
function Install-FromSource {
param([string]$target)
Require-Cmd 'git'
Require-Cmd 'cargo'
$tmp = Join-Path $env:TEMP 'dcr-install'
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
Info 'Cloning repository...'
git clone --depth 1 $RepoUrl $tmp | Out-Null
Info 'Building release binary...'
Push-Location $tmp
cargo build --release --target $target
Pop-Location
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
Copy-Item (Join-Path $tmp "target\$target\release\dcr.exe") $BinaryPath -Force
Ok 'Binary installed from source'
Remove-Item -Recurse -Force $tmp
}
function Setup-Path {
New-Item -ItemType Directory -Path $BinPath -Force | Out-Null
$linkPath = Join-Path $BinPath 'dcr.exe'
Copy-Item $BinaryPath $linkPath -Force
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not $userPath) { $userPath = '' }
if (-not ($userPath -split ';' | Where-Object { $_ -eq $BinPath })) {
$newPath = if ($userPath) { "$userPath;$BinPath" } else { $BinPath }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
$env:Path = "$env:Path;$BinPath"
Warn "Added path to PATH: $BinPath"
}
Ok "dcr command is available via $linkPath"
}
Info 'Starting DCR installation'
$mode = Select-Mode
$target = Get-Target
if ($mode -eq 'build') {
Install-FromSource -target $target
} else {
Install-FromRelease -target $target
}
Setup-Path
Ok 'Installation completed successfully'

View File

@@ -1,14 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
# ===== CONFIG ===== TMPDIR="/tmp/dcr-install"
TMPDIR="/tmp/dcr-temp"
INSTALL_PATH="$HOME/.local/share/dcr" INSTALL_PATH="$HOME/.local/share/dcr"
PYTHON_DIR="$INSTALL_PATH/Python"
BINPATH="$HOME/.local/bin" BINPATH="$HOME/.local/bin"
LOGFILE="$HOME/.cache/dcr-install.log" LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr"
PYTHON_DOWNLOAD_URL="https://github.com/astral-sh/python-build-standalone/releases/download/20260203/cpython-3.10.19+20260203-x86_64-unknown-linux-gnu-install_only.tar.gz"
GITHUB_API="https://api.github.com/repos/dexoron/dcr/releases/latest" GITHUB_API="https://api.github.com/repos/dexoron/dcr/releases/latest"
RED='\033[0;31m' RED='\033[0;31m'
@@ -25,127 +22,98 @@ success() { echo -e "${GREEN}✔ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; } warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
error() { echo -e "${RED}✖ $1${NC}"; } error() { echo -e "${RED}✖ $1${NC}"; }
trap 'error "Ошибка на строке $LINENO"; exit 1' ERR trap 'error "Error on line $LINENO"; exit 1' ERR
check_dependencies() {
log "Проверка зависимостей..."
command -v curl >/dev/null 2>&1 || { error "curl не установлен"; exit 1; }
command -v tar >/dev/null 2>&1 || { error "tar не установлен"; exit 1; }
success "Зависимости в порядке"
}
check_os() { check_os() {
if [[ "$(uname -s)" != "Linux" ]]; then case "$(uname -s)" in
error "Поддерживается только Linux" Linux|Darwin) ;;
exit 1 *) error "Only Linux and macOS are supported"; exit 1 ;;
fi esac
} }
get_latest_version() { detect_target() {
curl -fsSL "$GITHUB_API" | sed -n 's/.*"tag_name": "\(.*\)".*/\1/p' local os arch
os="$(uname -s)"
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" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;;
*) error "Unsupported platform: $os/$arch"; exit 1 ;;
esac
ASSET_NAME="dcr-${TARGET_TRIPLE}"
} }
get_installed_version() { check_common_dependencies() {
if [[ -f "$INSTALL_PATH/VERSION" ]]; then command -v curl >/dev/null 2>&1 || { error "curl is not installed"; exit 1; }
cat "$INSTALL_PATH/VERSION"
else
echo ""
fi
} }
install_python() { check_build_dependencies() {
log "Python не найден, установка встроенного Python..." 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; }
mkdir -p "$PYTHON_DIR"
mkdir -p "$TMPDIR"
curl -fsSL "$PYTHON_DOWNLOAD_URL" -o "$TMPDIR/python.tar.gz"
tar -xf "$TMPDIR/python.tar.gz" -C "$PYTHON_DIR" --strip-components=1
rm -f "$TMPDIR/python.tar.gz"
success "Встроенный Python установлен"
} }
install_app() { prepare_sources() {
log "Получение последнего релиза..." log "Fetching sources..."
rm -rf "$TMPDIR"
git clone --depth 1 "$REPO_URL" "$TMPDIR"
success "Sources fetched"
}
TAG=$(get_latest_version) build_binary() {
log "Building release binary..."
(cd "$TMPDIR" && cargo build --release)
success "Build completed"
}
if [[ -z "$TAG" ]]; then fetch_latest_release_json() {
error "Не удалось определить версию релиза" 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 exit 1
fi fi
INSTALLED_VERSION=$(get_installed_version) download_url="$(printf '%s\n' "$release_json" | sed -n "s#.*\"browser_download_url\": \"\([^\"]*${ASSET_NAME}\)\".*#\1#p" | head -n1)"
if [[ -z "$download_url" ]]; then
if [[ -n "$INSTALLED_VERSION" ]]; then error "Asset ${ASSET_NAME} not found in release ${tag}"
log "Установленная версия: $INSTALLED_VERSION" exit 1
log "Последняя версия: $TAG"
if [[ "$INSTALLED_VERSION" == "$TAG" ]]; then
success "Установлена последняя версия ($TAG), обновление не требуется"
exit 0
else
warn "Найдена устаревшая версия, выполняется обновление"
fi
else
log "DCR не установлен, выполняется установка"
fi fi
mkdir -p "$TMPDIR"
curl -fsSL -o "$TMPDIR/sources.tar.gz" \
"https://github.com/dexoron/dcr/archive/refs/tags/$TAG.tar.gz"
rm -rf "$INSTALL_PATH"
mkdir -p "$INSTALL_PATH" mkdir -p "$INSTALL_PATH"
curl -fL "$download_url" -o "$INSTALL_PATH/dcr"
tar -xf "$TMPDIR/sources.tar.gz" -C "$INSTALL_PATH" --strip-components=1
rm -f "$TMPDIR/sources.tar.gz"
echo "$TAG" > "$INSTALL_PATH/VERSION"
success "Приложение установлено (версия $TAG)"
}
install_wrapper() {
log "Создание launcher..."
mkdir -p "$INSTALL_PATH"
cat > "$INSTALL_PATH/dcr" <<'EOF'
#!/usr/bin/env bash
BASE_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
if command -v python3 >/dev/null 2>&1; then
PYTHON_BIN="python3"
else
PYTHON_BIN="$BASE_DIR/Python/bin/python3"
fi
PYTHONPATH="$BASE_DIR/src" exec "$PYTHON_BIN" -m dcr.main "$@"
EOF
chmod +x "$INSTALL_PATH/dcr" chmod +x "$INSTALL_PATH/dcr"
success "Binary ${ASSET_NAME} downloaded (${tag})"
}
success "Launcher создан" install_built_binary() {
mkdir -p "$INSTALL_PATH"
cp "$TMPDIR/target/release/dcr" "$INSTALL_PATH/dcr"
chmod +x "$INSTALL_PATH/dcr"
success "Binary installed from source"
} }
install_link() { install_link() {
log "Создание симлинка..." log "Creating symlink..."
mkdir -p "$BINPATH" mkdir -p "$BINPATH"
ln -sf "$INSTALL_PATH/dcr" "$BINPATH/dcr" ln -sf "$INSTALL_PATH/dcr" "$BINPATH/dcr"
success "Command 'dcr' added to $BINPATH"
success "Команда 'dcr' добавлена в $BINPATH"
} }
check_path() { check_path() {
if ! echo "$PATH" | grep -q "$BINPATH"; then if ! echo "$PATH" | grep -q "$BINPATH"; then
warn "Каталог $BINPATH не найден в PATH" warn "Directory $BINPATH not found in PATH"
echo "Добавь в ~/.bashrc или ~/.zshrc:" echo "Add this to ~/.bashrc or ~/.zshrc:"
echo "export PATH=\"$BINPATH:\$PATH\"" echo "export PATH=\"$BINPATH:\$PATH\""
fi fi
} }
@@ -154,29 +122,43 @@ cleanup() {
rm -rf "$TMPDIR" 2>/dev/null || true rm -rf "$TMPDIR" 2>/dev/null || true
} }
select_install_mode() {
echo "Choose installation mode:"
echo " 1) Download prebuilt binary from GitHub Release (recommended)"
echo " 2) Build from git"
read -r -p "Enter 1 or 2 [1]: " choice
case "${choice:-1}" in
1) INSTALL_MODE="release" ;;
2) INSTALL_MODE="build" ;;
*) error "Unknown option"; exit 1 ;;
esac
}
main() { main() {
log "Запуск установки DCR" log "Starting DCR installation"
check_os check_os
check_dependencies detect_target
check_common_dependencies
select_install_mode
cleanup cleanup
install_app if [[ "$INSTALL_MODE" == "build" ]]; then
check_build_dependencies
if ! command -v python3 >/dev/null 2>&1; then prepare_sources
install_python build_binary
install_built_binary
else else
success "Найден системный Python" download_binary
fi fi
install_wrapper
install_link install_link
check_path check_path
cleanup cleanup
success "Установка завершена успешно" success "Installation completed successfully"
log "Лог сохранён в $LOGFILE" log "Log saved to $LOGFILE"
} }
main "$@" main "$@"

21
packaging/aur/README.md Normal file
View File

@@ -0,0 +1,21 @@
# AUR publishing
This repository includes an automated AUR publish workflow:
- package files: `packaging/aur/dcr/PKGBUILD`, `packaging/aur/dcr/.SRCINFO`
- workflow: `.github/workflows/aur.yml`
## One-time setup
1. Create the `dcr` package on AUR and initialize its git repository.
2. Create an SSH key pair for CI.
3. Add the public key to your AUR account.
4. Add the private key to GitHub Actions secret `AUR_SSH_PRIVATE_KEY`.
## Release flow
1. Bump project version in `Cargo.toml`.
2. Push tag `v<version>` (example: `v0.2.2`).
3. GitHub Actions will:
- compute source tarball SHA256
- render `PKGBUILD` and `.SRCINFO`
- push updates to `ssh://aur@aur.archlinux.org/dcr.git`

View File

@@ -0,0 +1,17 @@
pkgbase = dcr
pkgdesc = Cargo-like utility to manage C/C++ projects
pkgver = 0.2.1
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
license = MIT
makedepends = cargo
depends = gcc-libs
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
options = !lto
source = dcr-0.2.1.tar.gz::https://github.com/dexoron/dcr/archive/refs/tags/v0.2.1.tar.gz
sha256sums = 30e334fa98cd51073c0fa84c80a982584b00156b57084311ad53419441ce6235
pkgname = dcr

View File

@@ -0,0 +1,30 @@
pkgname=dcr
pkgver=0.2.1
pkgrel=3
pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64' 'aarch64')
url="https://github.com/dexoron/dcr"
license=('MIT')
depends=('gcc-libs')
makedepends=('cargo')
options=('!lto')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('30e334fa98cd51073c0fa84c80a982584b00156b57084311ad53419441ce6235')
build() {
cd "$srcdir/$pkgname-$pkgver"
export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="${RUSTFLAGS:-}"
export RUSTFLAGS="${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
cargo build --release --locked
}
package() {
cd "$srcdir/$pkgname-$pkgver"
install -Dm755 "target/release/dcr" "$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}

View File

@@ -1,11 +0,0 @@
[project]
name = "dcr"
version = "0.1.0"
description = "Утилита для управления проектами для C/C++"
readme = "README.md"
license = "LICENSE"
authors = [
{ name = "Dexoron" },
{ name = "F1F1FISHKA" }
]
requires-python = ">=3.10"

View File

186
src/cli/build.rs Normal file
View File

@@ -0,0 +1,186 @@
use crate::config::{PROFILE, flags};
use crate::core::builder::{BuildContext, build as build_project};
use crate::core::config::Config;
use crate::core::deps::resolve_deps;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored};
use std::fs;
use std::time::Instant;
pub fn build(args: &[String]) -> i32 {
let items = check_dir(None).unwrap_or_default();
if !items.contains(&"dcr.toml".to_string()) {
error("dcr.toml file not found");
return 1;
}
let config = match Config::open("./dcr.toml") {
Ok(cfg) => cfg,
Err(_) => {
error("dcr.toml file not found");
return 1;
}
};
let active_profile = match parse_profile(args) {
Ok(profile) => profile,
Err(code) => return code,
};
let project_name = get_config_str(&config, "package.name");
let project_compiler = get_config_str(&config, "build.compiler");
let build_language = get_config_str(&config, "build.language");
let build_standard = get_config_str(&config, "build.standard");
let build_target = get_config_str(&config, "build.target");
let build_kind = get_config_str(&config, "build.kind");
let build_cflags = match get_config_list(&config, "build.cflags") {
Ok(v) => v,
Err(msg) => {
error(&msg);
return 1;
}
};
let build_ldflags = match get_config_list(&config, "build.ldflags") {
Ok(v) => v,
Err(msg) => {
error(&msg);
return 1;
}
};
println!(
" Building project `{}`\n Profile: {}\n Compiler: {}\n",
colored(&project_name, BOLD_GREEN),
colored(&active_profile, BOLD_GREEN),
colored(&project_compiler, BOLD_GREEN)
);
ensure_target_dirs(&items, &active_profile, normalize_target(&build_target));
let project_root = match std::env::current_dir() {
Ok(p) => p,
Err(_) => {
error("Failed to determine project root");
return 1;
}
};
let resolved = match resolve_deps(&config, &active_profile, &project_root) {
Ok(r) => r,
Err(msg) => {
error(&msg);
return 1;
}
};
let ctx = BuildContext {
profile: &active_profile,
project_name: &project_name,
compiler: &project_compiler,
language: &build_language,
standard: &build_standard,
target_dir: normalize_target(&build_target),
kind: normalize_kind(&build_kind),
include_dirs: &resolved.include_dirs,
lib_dirs: &resolved.lib_dirs,
libs: &resolved.libs,
cflags: &build_cflags,
ldflags: &build_ldflags,
};
match run_build(&ctx) {
Ok(times) => {
println!(
" {} Build completed successfully in {} seconds",
colored("✔", BOLD_GREEN),
colored(&times.to_string(), BOLD_GREEN)
);
0
}
Err(msg) => {
error(&msg);
1
}
}
}
fn parse_profile(args: &[String]) -> Result<String, i32> {
if let Some(first_arg) = args.first() {
if first_arg.starts_with("--") {
let candidate = first_arg.trim_start_matches("--");
if flags(candidate).is_some() {
return Ok(candidate.to_string());
}
warn("Unknown build flag");
return Err(1);
}
warn("Unknown argument");
return Err(1);
}
Ok(PROFILE.to_string())
}
fn get_config_str(config: &Config, key: &str) -> String {
config
.get(key)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
let value = match config.get(key) {
Some(v) => v,
None => return Ok(Vec::new()),
};
let arr = value
.as_array()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
let mut out = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| format!("{key} must be an array of strings"))?;
out.push(s.to_string());
}
Ok(out)
}
fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<&str>) {
if let Some(dir) = target_dir {
let _ = fs::create_dir_all(dir);
return;
}
if !items.contains(&"target".to_string()) {
let _ = fs::create_dir("./target");
}
let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&profile.to_string()) {
let _ = fs::create_dir(format!("./target/{profile}"));
}
}
fn run_build(ctx: &BuildContext) -> Result<f64, String> {
let start_time = Instant::now();
match build_project(ctx) {
Ok(times) => {
let times = if times == 0.0 {
((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
} else {
times
};
Ok(times)
}
Err(_) => Err("Build failed".to_string()),
}
}
fn normalize_target(target: &str) -> Option<&str> {
let trimmed = target.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
fn normalize_kind(kind: &str) -> &str {
let trimmed = kind.trim();
if trimmed.is_empty() { "bin" } else { trimmed }
}

63
src/cli/clean.rs Normal file
View File

@@ -0,0 +1,63 @@
use crate::config::flags;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored};
use std::fs;
pub fn clean(args: &[String]) -> i32 {
let project_name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
.unwrap_or_else(|| "project".to_string());
let items = check_dir(None).unwrap_or_default();
if args.len() > 1 {
error("Unknown argument");
return 1;
}
if !items.contains(&"dcr.toml".to_string()) {
error("dcr.toml file not found");
return 1;
}
println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
if !items.contains(&"target".to_string()) {
warn("Directory target not found");
return 1;
}
if let Some(arg) = args.first() {
let mut profile = arg.clone();
if profile.starts_with("--") {
profile = profile.trim_start_matches("--").to_string();
}
if flags(&profile).is_none() {
error("Unknown profile");
return 1;
}
let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&profile) {
warn(&format!("Directory target/{profile} not found"));
return 1;
}
println!(" Profile: {}", colored(&profile, BOLD_GREEN));
let _ = fs::remove_dir_all(format!("target/{profile}"));
println!(
"{} Removed directory target/{profile}",
colored("\n ✔", BOLD_GREEN)
);
return 0;
}
let _ = fs::remove_dir_all("target");
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
0
}

189
src/cli/flag_update.rs Normal file
View File

@@ -0,0 +1,189 @@
use crate::utils::log::{error, warn};
use reqwest::blocking::Client;
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest";
#[derive(Debug, Deserialize)]
struct Release {
tag_name: String,
assets: Vec<ReleaseAsset>,
}
#[derive(Debug, Deserialize)]
struct ReleaseAsset {
name: String,
browser_download_url: String,
}
pub fn flag_update(args: &[String]) -> i32 {
if !args.is_empty() {
warn("Command does not support additional arguments");
return 1;
}
let current_exe = match std::env::current_exe() {
Ok(path) => path,
Err(_) => {
error("Failed to resolve current binary path");
return 1;
}
};
if let Some(package_name) = pacman_owned_package(&current_exe) {
warn("This dcr binary is managed by pacman/AUR");
println!(
"Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
);
return 0;
}
let current_version = env!("CARGO_PKG_VERSION");
let target = env!("DCR_TARGET");
let client = match Client::builder().user_agent("dcr-updater").build() {
Ok(client) => client,
Err(_) => {
error("Failed to initialize HTTP client");
return 1;
}
};
let release = match fetch_latest_release(&client) {
Ok(release) => release,
Err(err) => {
error(&format!("Failed to check for updates: {err}"));
return 1;
}
};
let latest_version = release.tag_name.trim_start_matches('v');
if latest_version == current_version {
println!("Latest version is already installed: {current_version}");
return 0;
}
let candidate_names = asset_candidates(target);
let Some(asset) = release
.assets
.iter()
.find(|asset| candidate_names.iter().any(|name| name == &asset.name))
else {
error(&format!("Binary for target {target} not found"));
return 1;
};
let bytes = match download_asset(&client, &asset.browser_download_url) {
Ok(bytes) => bytes,
Err(err) => {
error(&format!("Failed to download update: {err}"));
return 1;
}
};
let temp_path = temp_binary_path(&current_exe);
if fs::write(&temp_path, &bytes).is_err() {
error("Failed to write temporary binary");
return 1;
}
set_executable_permissions(&temp_path);
if self_replace::self_replace(&temp_path).is_err() {
let _ = fs::remove_file(&temp_path);
error("Failed to replace current binary");
return 1;
}
let _ = fs::remove_file(&temp_path);
println!("Update completed: {current_version} -> {latest_version}");
0
}
fn fetch_latest_release(client: &Client) -> Result<Release, String> {
let response = client
.get(LATEST_RELEASE_URL)
.send()
.map_err(|_| "GitHub API request failed".to_string())?;
if !response.status().is_success() {
return Err(format!("GitHub API returned status {}", response.status()));
}
response
.json::<Release>()
.map_err(|_| "GitHub API response has an unexpected format".to_string())
}
fn download_asset(client: &Client, url: &str) -> Result<Vec<u8>, String> {
let response = client
.get(url)
.send()
.map_err(|_| "Download request failed".to_string())?;
if !response.status().is_success() {
return Err(format!("Download returned status {}", response.status()));
}
response
.bytes()
.map(|bytes| bytes.to_vec())
.map_err(|_| "Failed to read downloaded data".to_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());
}
names
}
fn temp_binary_path(current_exe: &Path) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|value| value.as_millis())
.unwrap_or(0);
let mut extension = format!("new-{stamp}");
if cfg!(windows) {
extension.push_str(".exe");
}
current_exe.with_extension(extension)
}
fn set_executable_permissions(path: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = fs::metadata(path) {
let mut perms = meta.permissions();
perms.set_mode(0o755);
let _ = fs::set_permissions(path, perms);
}
}
}
#[cfg(target_os = "linux")]
fn pacman_owned_package(path: &Path) -> Option<String> {
let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?;
if !output.status.success() {
return None;
}
let package_name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if package_name.is_empty() {
return None;
}
Some(package_name)
}
#[cfg(not(target_os = "linux"))]
fn pacman_owned_package(_path: &Path) -> Option<String> {
None
}

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

@@ -0,0 +1,33 @@
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
pub fn help() -> i32 {
println!("DCR (Dexoron Cargo Realization)");
println!("C project manager inspired by Cargo.");
println!();
printc("USAGE:", BOLD_GREEN);
printc(" dcr <command> [options]", BOLD_CYAN);
println!();
printc("COMMANDS:", BOLD_GREEN);
println!(" new <name> Create a new project");
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!(" clean Remove the target directory");
printc("FLAGS:", BOLD_GREEN);
println!(" --help Show command help");
println!(" --update Update dcr to the latest version");
println!(" --version Show dcr version");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Build with debug profile");
println!(" --release Build with release profile");
println!();
printc("EXAMPLES:", BOLD_GREEN);
printc(" dcr new hello", BOLD_CYAN);
printc(" dcr build --release", BOLD_CYAN);
printc(" dcr run --debug", BOLD_CYAN);
println!();
printc("TIP:", BOLD_GREEN);
println!(" Run 'dcr <command> --help' for command-specific help.");
0
}

80
src/cli/init.rs Normal file
View File

@@ -0,0 +1,80 @@
use crate::config::FILE_MAIN_C;
use crate::core::config::Config;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs;
use std::io::Write;
use toml::Value;
pub fn init(args: &[String]) -> i32 {
if !args.is_empty() {
warn("Command does not support additional arguments");
return 1;
}
let items = check_dir(None).unwrap_or_default();
let project_name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
.unwrap_or_else(|| "project".to_string());
if !items.is_empty() {
error("Directory not empty");
return 1;
}
let cwd = std::env::current_dir()
.map(|v| v.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string());
println!("Initializing the project in {cwd}");
let mut config = match Config::new("./dcr.toml") {
Ok(cfg) => cfg,
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.clone()))
.is_err()
{
error("Failed to write dcr.toml");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
if fs::create_dir("src").is_err() {
error("Failed to create src/");
return 1;
}
let mut main_c = match fs::File::create("./src/main.c") {
Ok(file) => file,
Err(_) => {
error("Failed to create src/main.c");
return 1;
}
};
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
error("Failed to write src/main.c");
return 1;
}
println!(
" {} Created file {}\n",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
println!(
"Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(" dcr run", BOLD_CYAN);
0
}

7
src/cli/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod build;
pub mod clean;
pub mod flag_update;
pub mod help;
pub mod init;
pub mod new;
pub mod run;

101
src/cli/new.rs Normal file
View File

@@ -0,0 +1,101 @@
use crate::config::FILE_MAIN_C;
use crate::core::config::Config;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs;
use std::io::Write;
use toml::Value;
pub fn new(args: &[String]) -> i32 {
let items = check_dir(None).unwrap_or_default();
if args.is_empty() {
error("Project name not specified");
return 1;
}
if args.len() > 1 {
warn("Command does not support additional arguments");
return 1;
}
let project_name = &args[0];
println!(
"Creating a Project `{}`...",
colored(project_name, BOLD_CYAN)
);
if items.contains(project_name) {
error(&format!(
"Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN)
));
printc("Подсказка:", BOLD_CYAN);
println!(
" Use `{}` to initialize an existing project\n or specify a different project name",
colored("dcr init", BOLD_CYAN)
);
return 1;
}
if fs::create_dir(project_name).is_err() {
error("Failed to create directory");
return 1;
}
println!(
" {} Directory created {}",
colored("✔", BOLD_GREEN),
project_name
);
let toml_path = format!("./{project_name}/dcr.toml");
let mut config = match Config::new(&toml_path) {
Ok(cfg) => cfg,
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.to_string()))
.is_err()
{
error("Failed to write dcr.toml");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
error("Failed to create src/");
return 1;
}
let main_c_path = format!("./{project_name}/src/main.c");
let mut main_c = match fs::File::create(&main_c_path) {
Ok(file) => file,
Err(_) => {
error("Failed to create src/main.c");
return 1;
}
};
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
error("Failed to write src/main.c");
return 1;
}
println!(
" {} Created file {}\n",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
println!(
"Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(&format!(" cd {}\n dcr run", project_name), BOLD_CYAN);
0
}

73
src/cli/run.rs Normal file
View File

@@ -0,0 +1,73 @@
use crate::cli::build::build;
use crate::config::{PROFILE, flags};
use crate::core::config::Config;
use crate::core::runner::run_binary;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored};
pub fn run(args: &[String]) -> i32 {
let mut active_profile = PROFILE.to_string();
let items = check_dir(None).unwrap_or_default();
if !items.contains(&"dcr.toml".to_string()) {
error("dcr.toml file not found");
return 1;
}
let config = match Config::open("./dcr.toml") {
Ok(cfg) => cfg,
Err(_) => {
error("dcr.toml file not found");
return 1;
}
};
let project_name: &str = config
.get("package.name")
.and_then(|v| v.as_str())
.unwrap_or("");
let build_kind = config
.get("build.kind")
.and_then(|v| v.as_str())
.unwrap_or("");
let target_dir = config
.get("build.target")
.and_then(|v| v.as_str())
.map(|s| s.trim())
.filter(|s| !s.is_empty());
if let Some(first_arg) = args.first() {
if first_arg.starts_with("--") {
let candidate = first_arg.trim_start_matches("--");
if flags(candidate).is_some() {
active_profile = candidate.to_string();
} else {
warn("Unknown build flag");
return 1;
}
} else {
warn("Unknown argument");
return 1;
}
}
if build_kind.trim() == "staticlib" {
error("Cannot run static library");
return 1;
}
let build_status = build(args);
let bin_path = crate::platform::bin_path(&active_profile, project_name, target_dir);
if build_status == 0 {
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------");
return run_binary(project_name, &active_profile, target_dir);
}
let fallback_code = run_binary(project_name, &active_profile, target_dir);
if fallback_code != 1 {
return fallback_code;
}
error("Fix errors in the code to run the project");
1
}

23
src/config.rs Normal file
View File

@@ -0,0 +1,23 @@
pub const PROFILE: &str = "debug";
pub const FILE_MAIN_C: &str = r#"#include <stdio.h>
int main(void) {
printf("Hello World!\n");
return 0;
}
"#;
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
match profile {
"release" => Some(&["-O3", "-DNDEBUG", "-march=native"]),
"debug" => Some(&[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDEBUG",
]),
_ => None,
}
}

183
src/core/builder/clang.rs Normal file
View File

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

183
src/core/builder/gcc.rs Normal file
View File

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

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

@@ -0,0 +1,35 @@
pub mod clang;
pub mod gcc;
pub mod msvc;
pub struct BuildContext<'a> {
pub profile: &'a str,
pub project_name: &'a str,
pub compiler: &'a str,
pub language: &'a str,
pub standard: &'a str,
pub target_dir: Option<&'a str>,
pub kind: &'a str,
pub include_dirs: &'a [String],
pub lib_dirs: &'a [String],
pub libs: &'a [String],
pub cflags: &'a [String],
pub ldflags: &'a [String],
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = ctx.compiler.to_lowercase();
if compiler.contains("clang-cl") {
return msvc::build(ctx);
}
if compiler.contains("gcc") || compiler.contains("g++") {
return gcc::build(ctx);
}
if compiler.contains("clang") || compiler.contains("clang++") {
return clang::build(ctx);
}
if compiler == "cl" || compiler.contains("msvc") {
return msvc::build(ctx);
}
clang::build(ctx)
}

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

@@ -0,0 +1,236 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let compiler = if ctx.compiler.is_empty() {
"cl"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
let mut cmd = Command::new("lib");
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
for obj in &objects {
cmd.arg(obj);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(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() {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
for obj in &objects {
cmd.arg(obj);
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("/LIBPATH:{dir}"));
}
for lib in ctx.libs {
if lib.to_lowercase().ends_with(".lib") {
cmd.arg(lib);
} else {
cmd.arg(format!("{lib}.lib"));
}
}
if !ctx.ldflags.is_empty() {
cmd.arg("/link");
for flag in ctx.ldflags {
cmd.arg(flag);
}
}
cmd.arg(format!(
"/Fe:{}",
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
));
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or("")
.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc"));
if allowed {
out.push(file);
}
}
Ok(())
}
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
let lang = language.to_lowercase();
let std = standard.to_lowercase();
if lang == "c" {
return match std.as_str() {
"c11" => Ok("/std:c11".to_string()),
"c17" => Ok("/std:c17".to_string()),
_ => Err("Unsupported C standard for MSVC".to_string()),
};
}
if lang == "c++" || lang == "cpp" || lang == "cxx" {
return match std.as_str() {
"c++11" => Ok("/std:c++11".to_string()),
"c++14" => Ok("/std:c++14".to_string()),
"c++17" => Ok("/std:c++17".to_string()),
"c++20" => Ok("/std:c++20".to_string()),
"c++23" => Ok("/std:c++latest".to_string()),
_ => Err("Unsupported C++ standard for MSVC".to_string()),
};
}
Err("Unsupported language".to_string())
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["/O2", "/DNDEBUG"],
"debug" => &["/Od", "/Zi", "/W4", "/DDEBUG", "/Oy-"],
_ => &[],
}
}
fn build_objects(
compiler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler);
cmd.arg("/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() {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag);
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
for flag in ctx.cflags {
cmd.arg(flag);
}
for dir in ctx.include_dirs {
cmd.arg(format!("/I{dir}"));
}
cmd.arg("/c").arg(source).arg(format!("/Fo:{}", obj_path));
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

325
src/core/config.rs Normal file
View File

@@ -0,0 +1,325 @@
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
use toml::map::Map;
const DEFAULT_VERSION: &str = "0.1.0";
const DEFAULT_LANGUAGE: &str = "c";
const DEFAULT_STANDARD: &str = "c11";
const DEFAULT_COMPILER: &str = "clang";
const DEFAULT_KIND: &str = "bin";
#[derive(Debug)]
pub enum ConfigError {
Io(std::io::Error),
TomlDe(toml::de::Error),
TomlSer(toml::ser::Error),
Invalid(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::Io(err) => write!(f, "I/O error: {err}"),
ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"),
ConfigError::TomlSer(err) => write!(f, "TOML serialize error: {err}"),
ConfigError::Invalid(msg) => write!(f, "Invalid config: {msg}"),
}
}
}
impl std::error::Error for ConfigError {}
impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
ConfigError::Io(err)
}
}
impl From<toml::de::Error> for ConfigError {
fn from(err: toml::de::Error) -> Self {
ConfigError::TomlDe(err)
}
}
impl From<toml::ser::Error> for ConfigError {
fn from(err: toml::ser::Error) -> Self {
ConfigError::TomlSer(err)
}
}
pub struct Config {
path: PathBuf,
data: Value,
}
impl Config {
pub fn new(path: &str) -> Result<Self, ConfigError> {
let path = PathBuf::from(path);
let data = if path.exists() {
read_toml(&path)?
} else {
let default_value = default_toml()?;
write_toml(&path, &default_value)?;
default_value
};
let cfg = Self { path, data };
cfg.validate()?;
Ok(cfg)
}
pub fn open(path: &str) -> Result<Self, ConfigError> {
let path = PathBuf::from(path);
if !path.exists() {
return Err(ConfigError::Invalid("dcr.toml not found".into()));
}
let data = read_toml(&path)?;
let cfg = Self { path, data };
cfg.validate()?;
Ok(cfg)
}
pub fn get(&self, key: &str) -> Option<&Value> {
let parts: Vec<&str> = key.split('.').collect();
get_path(&self.data, &parts)
}
#[allow(dead_code)]
pub fn add(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
self.set(key, value)
}
pub fn edit(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
self.set(key, value)
}
#[allow(dead_code)]
pub fn check(&self) -> bool {
self.validate().is_ok()
}
pub fn validate(&self) -> Result<(), ConfigError> {
let package = self
.get("package")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
for key in ["name", "version"] {
let value = package.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("package.{key} is empty")));
}
}
let build = self
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
for key in ["language", "standard", "compiler", "kind"] {
let value = build.get(key).and_then(|v| v.as_str()).unwrap_or("");
if value.trim().is_empty() {
return Err(ConfigError::Invalid(format!("build.{key} is empty")));
}
}
if let Some(kind) = build.get("kind").and_then(|v| v.as_str()) {
let kind = kind.trim();
if kind != "bin" && kind != "staticlib" {
return Err(ConfigError::Invalid("build.kind is invalid".into()));
}
}
Ok(())
}
pub fn save(&self) -> Result<(), ConfigError> {
write_toml(&self.path, &self.data)
}
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
let parts: Vec<&str> = key.split('.').collect();
set_path(&mut self.data, &parts, value)?;
self.save()?;
Ok(())
}
}
fn read_toml(path: &Path) -> Result<Value, ConfigError> {
let content = fs::read_to_string(path)?;
Ok(toml::from_str(&content)?)
}
fn write_toml(path: &Path, value: &Value) -> Result<(), ConfigError> {
let content = format_toml(value)?;
fs::write(path, content)?;
Ok(())
}
fn default_toml() -> Result<Value, ConfigError> {
let name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
.unwrap_or_else(|| "project".to_string());
let mut package = Map::new();
package.insert("name".to_string(), Value::String(name));
package.insert(
"version".to_string(),
Value::String(DEFAULT_VERSION.to_string()),
);
let mut build = Map::new();
build.insert(
"language".to_string(),
Value::String(DEFAULT_LANGUAGE.to_string()),
);
build.insert(
"standard".to_string(),
Value::String(DEFAULT_STANDARD.to_string()),
);
build.insert(
"compiler".to_string(),
Value::String(DEFAULT_COMPILER.to_string()),
);
build.insert("kind".to_string(), Value::String(DEFAULT_KIND.to_string()));
let mut root = Map::new();
root.insert("package".to_string(), Value::Table(package));
root.insert("build".to_string(), Value::Table(build));
root.insert("dependencies".to_string(), Value::Table(Map::new()));
Ok(Value::Table(root))
}
fn format_toml(value: &Value) -> Result<String, ConfigError> {
let root = value
.as_table()
.ok_or_else(|| ConfigError::Invalid("root is not a table".into()))?;
let package = root
.get("package")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
let build = root
.get("build")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
let deps = root
.get("dependencies")
.and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [dependencies]".into()))?;
let name = package.get("name").and_then(|v| v.as_str()).unwrap_or("");
let version = package
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("");
let language = build.get("language").and_then(|v| v.as_str()).unwrap_or("");
let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or("");
let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
let mut out = String::new();
out.push_str("[package]\n");
out.push_str(&format!("name = \"{name}\"\n"));
out.push_str(&format!("version = \"{version}\"\n\n"));
out.push_str("[build]\n");
out.push_str(&format!("language = \"{language}\"\n"));
out.push_str(&format!("standard = \"{standard}\"\n"));
out.push_str(&format!("compiler = \"{compiler}\"\n"));
let kind = build
.get("kind")
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_KIND);
out.push_str(&format!("kind = \"{kind}\"\n"));
if let Some(target) = build.get("target").and_then(|v| v.as_str())
&& !target.trim().is_empty()
{
out.push_str(&format!("target = \"{target}\"\n"));
}
if let Some(cflags) = build.get("cflags") {
out.push_str(&format!("cflags = {}\n", format_string_array(cflags)));
}
if let Some(ldflags) = build.get("ldflags") {
out.push_str(&format!("ldflags = {}\n", format_string_array(ldflags)));
}
out.push('\n');
out.push_str("[dependencies]\n");
if !deps.is_empty() {
let mut keys: Vec<&String> = deps.keys().collect();
keys.sort();
for key in keys {
if let Some(val) = deps.get(key) {
out.push_str(&format!("{key} = {}\n", format_dep_value(val)));
}
}
}
Ok(out)
}
fn format_dep_value(value: &Value) -> String {
match value {
Value::String(s) => format!("\"{s}\""),
Value::Table(tbl) => {
let mut parts = Vec::new();
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
parts.push(format!("path = \"{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(),
}
}
fn format_string_array(value: &Value) -> String {
if let Some(arr) = value.as_array() {
let items: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(|s| format!("\"{s}\"")))
.collect();
return format!("[{}]", items.join(", "));
}
"[]".to_string()
}
fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
let mut current = value;
for key in path {
current = current.as_table()?.get(*key)?;
}
Some(current)
}
fn set_path(value: &mut Value, path: &[&str], new_value: Value) -> Result<(), ConfigError> {
let mut current = value
.as_table_mut()
.ok_or_else(|| ConfigError::Invalid("root is not a table".into()))?;
for key in &path[..path.len().saturating_sub(1)] {
if !current.contains_key(*key) {
current.insert((*key).to_string(), Value::Table(Map::new()));
}
current = current
.get_mut(*key)
.and_then(|v| v.as_table_mut())
.ok_or_else(|| ConfigError::Invalid(format!("'{key}' is not a table")))?;
}
if let Some(last) = path.last() {
current.insert((*last).to_string(), new_value);
Ok(())
} else {
Err(ConfigError::Invalid("empty key".into()))
}
}

353
src/core/deps.rs Normal file
View File

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

4
src/core/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod builder;
pub mod config;
pub mod deps;
pub mod runner;

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

@@ -0,0 +1,37 @@
use crate::platform;
use crate::utils::fs::check_dir;
use crate::utils::log::warn;
use std::path::Path;
use std::process::Command;
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
let bin_path = platform::bin_path(profile, project_name, target_dir);
if Path::new(&bin_path).exists() {
return Command::new(bin_path)
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
if target_dir.is_none()
&& check_dir(Some(&format!("target/{profile}")))
.unwrap_or_default()
.contains(&profile.to_string())
{
warn("Launch of the latest release");
return Command::new(platform::bin_path(profile, project_name, target_dir))
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
if target_dir.is_some() && Path::new(&bin_path).exists() {
warn("Launch of the latest release");
return Command::new(bin_path)
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
}
1
}

View File

View File

@@ -1,18 +0,0 @@
from .help import help
from .init import init
from .new import new
from .run import run
from .build import build
from .clean import clean
from .flag_update import flag_update
__all__ = [
"help",
"init",
"new",
"run",
"build",
"clean",
"flag_update",
]

View File

@@ -1,70 +0,0 @@
import time
import os
import subprocess
from ..config import flags, profile, c_comp
from ..utils.fs import check_dir
from ..utils.text import colored, BRIGHT_GREEN, BOLD, BRIGHT_RED
def build(args: list[str] | None = None) -> int:
active_profile: str = profile
project_name = os.path.basename(os.getcwd())
if "dcr.toml" not in check_dir():
print(colored("error", BRIGHT_RED + BOLD) + ": не найден файл dcr.toml")
return 1
if args:
if len(args) >= 1 and args[0].startswith("--"):
candidate: str = args[0][2:]
if candidate in flags:
active_profile: str = candidate
else:
print(colored("error", BRIGHT_RED + BOLD) + ": неизвестный флаг сборки")
return 1
else:
print(colored("error", BRIGHT_RED + BOLD) + ": неизвестный аргумент")
return 1
print(
" Сборка проекта `"
+ colored(project_name, BRIGHT_GREEN + BOLD)
+ "`\n Профиль: "
+ colored(active_profile, BRIGHT_GREEN + BOLD)
+ "\n Компилятор: "
+ colored(c_comp, BRIGHT_GREEN + BOLD)
+ "\n"
)
if "target" not in check_dir():
os.mkdir("./target")
if active_profile not in check_dir("target"):
os.mkdir(f"./target/{active_profile}")
if "main.c" in check_dir("src"):
compile_flags = flags[active_profile]
start_time: float = time.time()
try:
subprocess.run(
[
c_comp,
"./src/main.c",
*compile_flags,
"-o",
f"./target/{active_profile}/main",
],
check=True,
)
end_time: float = time.time()
times: float = end_time - start_time
times: float = int(times * 100) / 100
print(
" "
+ colored("✔", BRIGHT_GREEN + BOLD)
+ " Сборка завершена успешно, за "
+ colored(str(times), BRIGHT_GREEN + BOLD)
+ " секунд"
)
return 0
except:
print(colored("error", BRIGHT_RED + BOLD) + ": сборка не удалась")
return 1
return 0

View File

@@ -1,53 +0,0 @@
import os
import shutil
from ..utils.fs import check_dir
from ..utils.text import colored, BRIGHT_RED, BOLD, BRIGHT_GREEN, BRIGHT_YELLOW
from ..config import flags
def clean(args: list[str] | None = None) -> int:
project_name = os.path.basename(os.getcwd())
items: list[str] = check_dir()
if args and len(args) > 1:
print(colored("error", BRIGHT_RED + BOLD) + ": неизвестный аргумент")
return 1
if "dcr.toml" not in items:
print(colored("error", BRIGHT_RED + BOLD) + ": не найден файл dcr.toml")
return 1
print(" Очистка проекта `" + colored(project_name, BRIGHT_GREEN + BOLD) + "`")
if "target" not in items:
print(colored("warn", BRIGHT_YELLOW + BOLD) + ": директория target не найдена")
return 1
if args:
profile = args[0]
if profile.startswith("--"):
profile = profile[2:]
if profile not in flags:
print(colored("error", BRIGHT_RED + BOLD) + ": неизвестный профиль")
return 1
if profile not in check_dir("target"):
print(
colored("warn", BRIGHT_YELLOW + BOLD)
+ f": директория target/{profile} не найдена"
)
return 1
print(" Профиль: " + colored(profile, BRIGHT_GREEN + BOLD))
shutil.rmtree(os.path.join("target", profile))
print(
colored("\n ✔", BRIGHT_GREEN + BOLD)
+ f" Удалена директория target/{profile}"
)
return 0
shutil.rmtree("target")
print(colored("\n ✔", BRIGHT_GREEN + BOLD) + " Удалена директория target")
return 0

View File

@@ -1,23 +0,0 @@
import platform
import shutil
import subprocess
def flag_update(args: list[str] | None = None) -> int:
if args and len(args) > 0:
print("Ошибка: команда не поддежривает аргументы")
return 1
if platform.system() != "Linux":
print("Ошибка: обновление поддерживается только на Linux")
return 1
if shutil.which("curl") is None:
print("Ошибка: не найден curl")
return 1
try:
subprocess.run(
["bash", "-c", "curl -fsSL dcr.zov.tatar | bash"], check=True
)
return 0
except subprocess.CalledProcessError:
print("Ошибка: не удалось выполнить обновление")
return 1

View File

@@ -1,39 +0,0 @@
from ..utils.text import colored, BRIGHT_GREEN, BRIGHT_CYAN, BOLD
def help() -> int:
print(
"\n".join(
[
"DCR (Dexoron Cargo Realization)",
"Менеджер C-проектов, вдохновленный Cargo.",
"",
colored("ИСПОЛЬЗОВАНИЕ:", BRIGHT_GREEN + BOLD),
colored(" dcr <команда> [опции]", BRIGHT_CYAN + BOLD),
"",
colored("КОМАНДЫ:", BRIGHT_GREEN + BOLD),
" new <name> Создать новый проект",
" init Инициализировать текущую директорию как проект",
" build [--profile] Собрать проект (по умолчанию: --debug)",
" run [--profile] Собрать и запустить (по умолчанию: --debug)",
" clean Удалить директорию target",
colored("ФЛАГИ:", BRIGHT_GREEN + BOLD),
" --help Показать справку по команде",
" --update Обновить dcr до актуальной версии",
" --version Показать версию dcr",
"",
colored("ОПЦИИ:", BRIGHT_GREEN + BOLD),
" --debug Сборка с профилем debug",
" --release Сборка с профилем release",
"",
colored("ПРИМЕРЫ:", BRIGHT_GREEN + BOLD),
colored(" dcr new hello", BRIGHT_CYAN + BOLD),
colored(" dcr build --release", BRIGHT_CYAN + BOLD),
colored(" dcr run --debug", BRIGHT_CYAN + BOLD),
"",
colored("ПОДСКАЗКА:", BRIGHT_GREEN + BOLD),
" Запусти 'dcr <команда> --help' для справки по команде.",
]
)
)
return 0

View File

@@ -1,61 +0,0 @@
from ..utils.text import colored, BRIGHT_RED, BOLD, printc, BRIGHT_GREEN, BRIGHT_CYAN
from ..config import (
file_dcr_toml,
file_main_c,
project_version,
project_language,
project_compiler,
)
import os
from ..utils.fs import check_dir
def init(args: list[str] | None = None) -> int:
if args:
print(
colored("error", BRIGHT_RED + BOLD)
+ ": команда не поддерживает доп. аргументы"
)
return 1
items: list[str] = check_dir()
project_name = os.path.basename(os.getcwd())
if items:
print(colored("error", BRIGHT_RED + BOLD) + ": директория не пуста")
return 1
print("Инициализация проекта в " + os.getcwd())
with open(f"./dcr.toml", "w") as file:
file.write(
file_dcr_toml.format(
project_name=project_name,
project_version=project_version,
project_language=project_language,
project_compiler=project_compiler,
)
)
print(
" "
+ colored("✔", BRIGHT_GREEN + BOLD)
+ " Создан файл "
+ colored("dcr.toml", BRIGHT_CYAN + BOLD)
)
os.mkdir("src")
with open("./src/main.c", "w") as file:
file.write(file_main_c)
print(
" "
+ colored("✔", BRIGHT_GREEN + BOLD)
+ " Создан файл "
+ colored("src/main.c\n", BRIGHT_CYAN + BOLD)
)
print(
"Проект `" + colored(project_name, BRIGHT_GREEN + BOLD) + "` успешно создан\n"
)
printc("Следуюший шаг:", BRIGHT_GREEN + BOLD)
printc(" dcr run", BRIGHT_CYAN + BOLD)
return 0

View File

@@ -1,89 +0,0 @@
from ..utils.text import (
colored,
printc,
BRIGHT_GREEN,
BOLD,
BRIGHT_CYAN,
BRIGHT_RED,
)
from ..config import (
file_dcr_toml,
file_main_c,
project_version,
project_language,
project_compiler,
)
import os
from ..utils.fs import check_dir
def new(args: list[str]) -> int:
items: list[str] = check_dir()
if len(args) == 0:
print(colored("error", BRIGHT_RED + BOLD) + ": не указанно название проекта")
return 1
if len(args) > 1:
print(
colored("error", BRIGHT_RED + BOLD)
+ ": команда не поддерживает доп. аргументы"
)
return 1
project_name: str = args[0]
print("Создание проекта `" + colored(project_name, BRIGHT_GREEN + BOLD) + "`...")
if project_name in items:
print(
colored("error", BRIGHT_RED + BOLD)
+ ": директория `"
+ colored(project_name, BRIGHT_GREEN + BOLD)
+ "` уже существует\n"
)
printc("Подсказка:", BRIGHT_GREEN + BOLD)
print(
" Используй `"
+ colored("dcr init", BRIGHT_CYAN + BOLD)
+ "` для инициализации существующего проекта\n или задай другое имя проекта"
)
return 1
os.mkdir(project_name)
print(
" "
+ colored("✔", BRIGHT_GREEN + BOLD)
+ " Создана директория "
+ project_name
)
with open(f"./{project_name}/dcr.toml", "w") as file:
file.write(
file_dcr_toml.format(
project_name=project_name,
project_version=project_version,
project_language=project_language,
project_compiler=project_compiler,
)
)
print(
" "
+ colored("✔", BRIGHT_GREEN + BOLD)
+ " Создан файл "
+ colored("dcr.toml", BRIGHT_CYAN + BOLD)
)
os.makedirs(f"./{project_name}/src")
with open(f"./{project_name}/src/main.c", "w") as file:
file.write(file_main_c)
print(
" "
+ colored("✔", BRIGHT_GREEN + BOLD)
+ " Создан файл "
+ colored("src/main.c\n", BRIGHT_CYAN + BOLD)
)
print(
"Проект `" + colored(project_name, BRIGHT_GREEN + BOLD) + "` успешно создан\n"
)
printc("Следуюший шаг:", BRIGHT_GREEN + BOLD)
printc(" cd " + project_name + "\n dcr run", BRIGHT_CYAN + BOLD)
return 0

View File

@@ -1,39 +0,0 @@
import subprocess
import os
from ..utils.fs import check_dir
from ..utils.text import colored, BRIGHT_RED, BOLD
from ..config import flags, profile
from .build import build
def run(args: list[str] | None = None) -> int:
project_name = os.path.basename(os.getcwd())
active_profile: str = profile
if "dcr.toml" not in check_dir():
print(colored("error", BRIGHT_RED + BOLD) + ": не найден файл dcr.toml")
return 1
if args:
if len(args) >= 1 and args[0].startswith("--"):
candidate: str = args[0][2:]
if candidate in flags:
active_profile: str = candidate
else:
print(colored("error", BRIGHT_RED + BOLD) + ": неизвестный флаг сборки")
return 1
else:
print(colored("error", BRIGHT_RED + BOLD) + ": неизвестный аргумент")
return 1
build_status: int = build(args)
if build_status == 0:
print("\n Запуск " + "target/" + active_profile + "/main")
print("--------------------------------")
return subprocess.run([f"./target/{active_profile}/main"]).returncode
else:
if active_profile in check_dir("target/" + active_profile):
print("Запуск последнего релиза")
return subprocess.run([f"./target/{active_profile}/main"]).returncode
else:
print("Исправьте ошибки в коде для запуска проекта")
return 0

View File

@@ -1,44 +0,0 @@
file_dcr_toml: str = """[package]
name = "{project_name}"
version = "{project_version}"
language = "{project_language}"
compiler = "{project_compiler}"
[dependencies]
"""
file_main_c: str = """#include <stdio.h>
int main(void) {
printf("Hello World!\\n");
return 0;
}
"""
file_dcr_lock: str = """# This file is automatically @generated by dcr.
# It is not intended for manual editing.
version = 1
[[package]]
name = "{project_name}"
version = "{project_version}"
"""
"""[[package]]
name = "{package_name}"
version = "{package_version}"
source = "{{package_source}"
checksum = "{package_checksum}"
dependencies = [{package_dependencies}]"""
project_version: str = "0.1.0"
project_language: str = "c"
project_compiler: str = "clang"
profile: str = "debug"
c_comp: str = "clang"
flags: dict[str, list[str]] = {
"release": ["-O3", "-DNDEBUG", "-march=native"],
"debug": ["-O0", "-g", "-Wall", "-Wextra", "-fno-omit-frame-pointer", "-DDEBUG"],
}
VERSION = "v0.1.1"

View File

@@ -1,38 +0,0 @@
from .config import VERSION
import dcr.cli as cli
import sys
def main() -> int:
if len(sys.argv) < 2:
cli.help()
return 0
cmd: str = sys.argv[1]
args: str | list[str] = sys.argv[2:]
match cmd:
case "new":
cli.new(args)
case "init":
cli.init(args)
case "build":
cli.build(args)
case "run":
cli.run(args)
case "clean":
cli.clean(args)
case "--version":
print(f"dcr {VERSION} (GNU/Linux)")
case "--help":
cli.help()
case "--update":
cli.flag_update(args)
case _:
print("Неизвестная команда или аргумент")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,8 +0,0 @@
import os
def check_dir(dir: str | None = None) -> list[str]:
if dir in (None, ".", "./"):
return os.listdir(os.getcwd())
else:
return os.listdir(os.getcwd() + "/" + dir)

View File

@@ -1,40 +0,0 @@
# RESET
RESET = "\033[0m"
# TEXT STYLES
BOLD = "\033[1m"
DIM = "\033[2m"
ITALIC = "\033[3m"
UNDERLINE = "\033[4m"
BLINK = "\033[5m"
REVERSE = "\033[7m"
HIDDEN = "\033[8m"
STRIKETHROUGH = "\033[9m"
# STANDARD COLORS (Foreground)
BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
# BRIGHT COLORS
BRIGHT_BLACK = "\033[90m"
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
def colored(msg: str, style: str) -> str:
return style + msg + RESET
def printc(msg: str, style: str) -> None:
print(style + msg + RESET)

37
src/main.rs Normal file
View File

@@ -0,0 +1,37 @@
use crate::utils::log::error;
mod cli;
mod config;
mod core;
mod platform;
mod utils;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
std::process::exit(cli::help::help());
}
let cmd = args[1].as_str();
let rest = &args[2..];
let code = match cmd {
"new" => cli::r#new::new(rest),
"init" => cli::init::init(rest),
"build" => cli::build::build(rest),
"run" => cli::run::run(rest),
"clean" => cli::clean::clean(rest),
"--version" => {
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET"));
0
}
"--help" => cli::help::help(),
"--update" => cli::flag_update::flag_update(rest),
_ => {
error("Unknown command or argument");
0
}
};
std::process::exit(code);
}

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

@@ -0,0 +1,13 @@
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
}
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
}
}

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

@@ -0,0 +1,13 @@
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
}
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
}
}

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

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

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

@@ -0,0 +1,13 @@
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.exe"),
}
}
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.lib", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.lib"),
}
}

View File

18
src/utils/fs.rs Normal file
View File

@@ -0,0 +1,18 @@
use std::io;
use std::path::PathBuf;
#[allow(dead_code)]
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
let path: PathBuf = match dir {
None | Some(".") | Some("./") => std::env::current_dir()?,
Some(value) => std::env::current_dir()?.join(value),
};
let mut items = Vec::new();
for entry in std::fs::read_dir(path)? {
let entry = entry?;
items.push(entry.file_name().to_string_lossy().to_string());
}
Ok(items)
}

24
src/utils/log.rs Normal file
View File

@@ -0,0 +1,24 @@
use crate::utils::text::{BOLD_CYAN, colored};
#[allow(dead_code)]
pub fn error(msg: &str) {
println!("{}: {msg}", colored("error", BOLD_CYAN),);
}
#[allow(dead_code)]
pub fn warn(msg: &str) {
println!("{}: {msg}", colored("warn", BOLD_CYAN),);
}
// #[allow(dead_code)]
// pub fn info(msg: &str) {
// println!("{}: {msg}", colored("info", BOLD));
// }
//
// #[allow(dead_code)]
// pub fn ok(msg: &str) {
// println!(
// "{}: {msg}",
// colored("ok", &(BRIGHT_GREEN.to_owned() + BOLD))
// );
// }

3
src/utils/mod.rs Normal file
View File

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

32
src/utils/text.rs Normal file
View File

@@ -0,0 +1,32 @@
#[allow(dead_code)]
pub const RESET: &str = "\x1b[0m";
#[allow(dead_code)]
pub const BOLD: &str = "\x1b[1m";
#[allow(dead_code)]
pub const BRIGHT_RED: &str = "\x1b[91m";
#[allow(dead_code)]
pub const BRIGHT_GREEN: &str = "\x1b[92m";
#[allow(dead_code)]
pub const BRIGHT_YELLOW: &str = "\x1b[93m";
#[allow(dead_code)]
pub const BRIGHT_CYAN: &str = "\x1b[96m";
#[allow(dead_code)]
pub const BOLD_RED: &str = "\x1b[1m\x1b[91m";
#[allow(dead_code)]
pub const BOLD_GREEN: &str = "\x1b[1m\x1b[92m";
#[allow(dead_code)]
pub const BOLD_YELLOW: &str = "\x1b[1m\x1b[93m";
#[allow(dead_code)]
pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m";
#[allow(dead_code)]
pub fn colored(msg: &str, style: &str) -> String {
format!("{style}{msg}{RESET}")
}
#[allow(dead_code)]
pub fn printc(msg: &str, style: &str) {
println!("{style}{msg}{RESET}");
}