13 Commits

Author SHA1 Message Date
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
27 changed files with 1153 additions and 141 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

View File

@@ -9,6 +9,34 @@ permissions:
contents: write contents: write
jobs: 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: build:
strategy: strategy:
fail-fast: false fail-fast: false
@@ -17,10 +45,10 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
asset_name: dcr-x86_64-unknown-linux-gnu asset_name: dcr-x86_64-unknown-linux-gnu
- os: macos-13 - os: macos-15-intel
target: x86_64-apple-darwin target: x86_64-apple-darwin
asset_name: dcr-x86_64-apple-darwin asset_name: dcr-x86_64-apple-darwin
- os: macos-14 - os: macos-15
target: aarch64-apple-darwin target: aarch64-apple-darwin
asset_name: dcr-aarch64-apple-darwin asset_name: dcr-aarch64-apple-darwin
- os: windows-latest - os: windows-latest
@@ -28,9 +56,14 @@ jobs:
asset_name: dcr-x86_64-pc-windows-msvc.exe asset_name: dcr-x86_64-pc-windows-msvc.exe
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: notes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -56,3 +89,4 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ${{ matrix.asset_name }} files: ${{ matrix.asset_name }}
body_path: .github/release-notes.md

View File

@@ -1,5 +1,23 @@
# Changelog # Changelog
## [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 ## [0.2.1] - 2026-02-18
Changed: Changed:
- Translated user-facing CLI messages to English - Translated user-facing CLI messages to English
@@ -8,40 +26,40 @@ Changed:
- Translated installer script messages in `install.sh` and `install.ps1` to English - Translated installer script messages in `install.sh` and `install.ps1` to English
## [0.2.0] - 2026-02-17 ## [0.2.0] - 2026-02-17
Изменено: Changed:
- Проект переведен с Python на Rust - Project migrated from Python to Rust
- CLI и команды (`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`) перенесены на Rust - CLI and commands (`new`, `init`, `build`, `run`, `clean`, `--help`, `--version`, `--update`) ported to Rust
- Обновлен флаг `--update` - добавлена поддержка GNU\Linux, Windows, MacOS - Updated `--update` flag: added support for GNU/Linux, Windows, macOS
- Обновлены `README.md`, `CONTRIBUTING.md`, `install.sh` под Rust-реализацию - Updated `README.md`, `CONTRIBUTING.md`, and `install.sh` for the Rust implementation
- Добавлен `install.ps1` для Windows - Added `install.ps1` for Windows
- Обновлен `install.sh` для GNU/Linux и MacOS - Updated `install.sh` for GNU/Linux and macOS
Добавлено: Added:
- Добавлен `install.ps1` для Windows - Added `install.ps1` for Windows
- Поддержка GNU\Linux, Windows, MacOS(x86_64/arm) - Support for GNU/Linux, Windows, macOS (x86_64/arm)
ВАЖНО: Important:
- Код перенесен с помощью нейронных сетей, в будущих версиях будет баг-фикс и изменение логики - Code was ported with neural networks; future versions will include bug fixes and logic changes
## [0.1.2] - 2026-02-12 ## [0.1.2] - 2026-02-12
Добавлено: Added:
- Команда обновления - Update command
- Скрипт установки `install.sh` и инструкции по установке в README - `install.sh` install script and README instructions
- Флаг `--version` для вывода версии - `--version` flag
Изменено: Changed:
- Улучшен консольный вывод команд, добавлены цвета и обновлена справка `--help` - Improved CLI output, added colors, and updated `--help`
- Обновлен запуск проекта и обработка сборки в `run.py`/`build.py` - 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
Cargo.lock generated
View File

@@ -62,11 +62,12 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.2.1" version = "0.2.3"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"self-replace", "self-replace",
"serde", "serde",
"toml",
] ]
[[package]] [[package]]
@@ -878,6 +879,15 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_spanned"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@@ -1043,6 +1053,45 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "1.0.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.3" version = "0.5.3"
@@ -1466,6 +1515,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.2.1" version = "0.2.3"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
@@ -8,3 +8,4 @@ build = "build.rs"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
self-replace = "1" self-replace = "1"
toml = "1.0.3"

View File

@@ -15,7 +15,7 @@ The current implementation is written in Rust.
- Run the compiled binary - Run the compiled binary
- Clean build artifacts - Clean build artifacts
- Generate a minimal C project template - Generate a minimal C project template
- Update the binary via `dcr --update` (GitHub Releases) - Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
## Supported Platforms ## Supported Platforms
- Linux: `x86_64-unknown-linux-gnu` - Linux: `x86_64-unknown-linux-gnu`
@@ -25,6 +25,13 @@ The current implementation is written in Rust.
## Installation ## Installation
### Package Manager
**Arch Linux**
```sh
yay -S dcr # or paru and other AUR package managers
```
### From Source ### From Source
```sh ```sh
@@ -57,6 +64,14 @@ Release assets:
- `dcr-aarch64-apple-darwin` - `dcr-aarch64-apple-darwin`
- `dcr-x86_64-pc-windows-msvc.exe` - `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 ## Quick Start
Create a new project: Create a new project:
@@ -92,15 +107,15 @@ Builds the project. If no profile is specified, `--debug` is used.
Builds the project and runs the binary. If no profile is specified, `--debug` is used. Builds the project and runs the binary. If no profile is specified, `--debug` is used.
Run manually: Run manually:
`./target/<profile>/main` `./target/<profile>/<name>`
### `dcr clean` ### `dcr clean`
Removes the `target` directory in the project root. Removes the `target` directory in the project root.
## Build Profiles ## Build Profiles
Two profiles are supported: Two profiles are supported:
- `--debug` (default) - flags: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG` - `--debug` (default) - flags: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`(Clang)
- `--release` - flags: `-O3 -DNDEBUG -march=native` - `--release` - flags: `-O3 -DNDEBUG -march=native`(Clang)
## Configuration ## Configuration
The main project file is `dcr.toml`. The main project file is `dcr.toml`.
@@ -111,7 +126,10 @@ Example `dcr.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"
[dependencies] [dependencies]
@@ -119,7 +137,7 @@ compiler = "clang"
## Requirements ## Requirements
- Rust toolchain (`rustc`, `cargo`) - for building DCR from source - Rust toolchain (`rustc`, `cargo`) - for building DCR from source
- C compiler (`clang`, `gcc`, or others) - C compiler (`clang`, `gcc`, or 'cl'(msvc))
## Releases ## Releases
Releases are built automatically via GitHub Actions (`.github/workflows/release.yml`) when a tag matching `v*` is pushed. Releases are built automatically via GitHub Actions (`.github/workflows/release.yml`) when a tag matching `v*` is pushed.

View File

@@ -15,7 +15,7 @@ DCR — это утилита для управления проектами н
- Запуск собранного бинарника - Запуск собранного бинарника
- Очистка результатов сборки - Очистка результатов сборки
- Генерация минимального шаблона C-проекта - Генерация минимального шаблона C-проекта
- Обновление бинарника через `dcr --update` (GitHub Releases) - Обновление бинарника через `dcr --update` (GitHub Releases, не для pacman-установок)
## Поддерживаемые платформы ## Поддерживаемые платформы
- Linux: `x86_64-unknown-linux-gnu` - Linux: `x86_64-unknown-linux-gnu`
@@ -25,6 +25,13 @@ DCR — это утилита для управления проектами н
## Установка ## Установка
### Пакетный менеджер
**ArchLinux**
```sh
yay -S dcr # или paru и остальные менеджеры пакетов AUR
```
### Из исходников ### Из исходников
```sh ```sh
@@ -57,6 +64,14 @@ irm https://dcr.dexoron.su/install.ps1 | iex
- `dcr-aarch64-apple-darwin` - `dcr-aarch64-apple-darwin`
- `dcr-x86_64-pc-windows-msvc.exe` - `dcr-x86_64-pc-windows-msvc.exe`
## Обновление
- Если DCR установлен из GitHub Releases, через `install.sh`, `install.ps1` или собран вручную:
- используйте `dcr --update`
- Если DCR установлен через `pacman/AUR`:
- обновляйте через пакетный менеджер: `paru/yay -Syu dcr` или `sudo pacman -Syu dcr`
- `dcr --update` определяет пакетную установку и подсказывает обновляться через пакетный менеджер
## Быстрый старт ## Быстрый старт
Создать новый проект: Создать новый проект:
@@ -92,15 +107,15 @@ hello/
Собирает проект и запускает бинарник. Если профиль не указан, используется `--debug`. Собирает проект и запускает бинарник. Если профиль не указан, используется `--debug`.
Запуск вручную: Запуск вручную:
`./target/<profile>/main` `./target/<profile>/<name>`
### `dcr clean` ### `dcr clean`
Удаляет папку `target` в корне проекта. Удаляет папку `target` в корне проекта.
## Профили сборки ## Профили сборки
Поддерживаются два профиля: Поддерживаются два профиля:
- `--debug` (по умолчанию) — флаги: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG` - `--debug` (по умолчанию) — флаги: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`(Clang)
- `--release` — флаги: `-O3 -DNDEBUG -march=native` - `--release` — флаги: `-O3 -DNDEBUG -march=native`(Clang)
## Конфигурация ## Конфигурация
Основной файл проекта — `dcr.toml`. Основной файл проекта — `dcr.toml`.
@@ -111,7 +126,10 @@ hello/
[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"
[dependencies] [dependencies]
@@ -119,7 +137,7 @@ compiler = "clang"
## Требования ## Требования
- Rust toolchain (`rustc`, `cargo`) - Для ручной сборки DCR - Rust toolchain (`rustc`, `cargo`) - Для ручной сборки DCR
- C-компилятор (`clang`, `gcc` или другие) - C-компилятор (`clang`, `gcc` или 'cl'(msvc))
## Релизы ## Релизы
Релизы собираются автоматически через GitHub Actions (`.github/workflows/release.yml`) при пуше тега формата `v*`. Релизы собираются автоматически через GitHub Actions (`.github/workflows/release.yml`) при пуше тега формата `v*`.

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,81 +1,112 @@
use crate::config::{PROFILE, PROJECT_COMPILER, flags}; use crate::config::{PROFILE, flags};
use crate::core::builder::{BuildContext, build as build_project};
use crate::core::config::Config;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored}; use crate::utils::text::{BOLD_GREEN, colored};
use std::fs; use std::fs;
use std::process::Command;
use std::time::Instant; use std::time::Instant;
pub fn build(args: &[String]) -> i32 { pub fn build(args: &[String]) -> i32 {
let mut active_profile = PROFILE.to_string();
let project_name = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
.unwrap_or_else(|| "project".to_string());
let items = check_dir(None).unwrap_or_default(); let items = check_dir(None).unwrap_or_default();
if !items.contains(&"dcr.toml".to_string()) { if !items.contains(&"dcr.toml".to_string()) {
error("dcr.toml file not found"); error("dcr.toml file not found");
return 1; return 1;
} }
if let Some(first_arg) = args.first() { let config = match Config::open("./dcr.toml") {
if first_arg.starts_with("--") { Ok(cfg) => cfg,
let candidate = first_arg.trim_start_matches("--"); Err(_) => {
if flags(candidate).is_some() { error("dcr.toml file not found");
active_profile = candidate.to_string();
} else {
warn("Unknown build flag");
return 1; return 1;
} }
} else { };
warn("Unknown argument"); let active_profile = match parse_profile(args) {
return 1; 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");
println!( println!(
" Building project `{}`\n Profile: {}\n Compiler: {}\n", " Building project `{}`\n Profile: {}\n Compiler: {}\n",
colored(&project_name, BOLD_GREEN), colored(&project_name, BOLD_GREEN),
colored(&active_profile, BOLD_GREEN), colored(&active_profile, BOLD_GREEN),
colored(PROJECT_COMPILER, BOLD_GREEN) colored(&project_compiler, BOLD_GREEN)
); );
if !items.contains(&"target".to_string()) { ensure_target_dirs(&items, &active_profile);
let _ = fs::create_dir("./target");
}
let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&active_profile) {
let _ = fs::create_dir(format!("./target/{active_profile}"));
}
let src_items = check_dir(Some("src")).unwrap_or_default(); let ctx = BuildContext {
if src_items.contains(&"main.c".to_string()) { profile: &active_profile,
let compile_flags = flags(&active_profile).unwrap_or(&[]); project_name: &project_name,
let start_time = Instant::now(); compiler: &project_compiler,
let mut cmd = Command::new(PROJECT_COMPILER); language: &build_language,
cmd.arg("./src/main.c"); standard: &build_standard,
for flag in compile_flags { };
cmd.arg(flag); match run_build(&ctx) {
} Ok(times) => {
cmd.arg("-o").arg(format!("./target/{active_profile}/main"));
match cmd.status() {
Ok(status) if status.success() => {
let times = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
println!( println!(
" {} Build completed successfully in {} seconds", " {} Build completed successfully in {} seconds",
colored("✔", BOLD_GREEN), colored("✔", BOLD_GREEN),
colored(&times.to_string(), BOLD_GREEN) colored(&times.to_string(), BOLD_GREEN)
); );
return 0; 0
} }
_ => { Err(msg) => {
error("Build failed"); error(&msg);
return 1; 1
} }
} }
} }
0 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 ensure_target_dirs(items: &[String], profile: &str) {
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()),
}
} }

View File

@@ -3,6 +3,7 @@ use reqwest::blocking::Client;
use serde::Deserialize; use serde::Deserialize;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest"; const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest";
@@ -25,6 +26,22 @@ pub fn flag_update(args: &[String]) -> i32 {
return 1; 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 current_version = env!("CARGO_PKG_VERSION");
let target = env!("DCR_TARGET"); let target = env!("DCR_TARGET");
let client = match Client::builder().user_agent("dcr-updater").build() { let client = match Client::builder().user_agent("dcr-updater").build() {
@@ -67,13 +84,6 @@ pub fn flag_update(args: &[String]) -> i32 {
} }
}; };
let current_exe = match std::env::current_exe() {
Ok(path) => path,
Err(_) => {
error("Failed to resolve current binary path");
return 1;
}
};
let temp_path = temp_binary_path(&current_exe); let temp_path = temp_binary_path(&current_exe);
if fs::write(&temp_path, &bytes).is_err() { if fs::write(&temp_path, &bytes).is_err() {
@@ -157,3 +167,23 @@ fn set_executable_permissions(path: &Path) {
} }
} }
} }
#[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
}

View File

@@ -1,9 +1,11 @@
use crate::config::{FILE_MAIN_C, file_dcr_toml}; use crate::config::FILE_MAIN_C;
use crate::core::config::Config;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use toml::Value;
pub fn init(args: &[String]) -> i32 { pub fn init(args: &[String]) -> i32 {
if !args.is_empty() { if !args.is_empty() {
@@ -27,15 +29,15 @@ pub fn init(args: &[String]) -> i32 {
.unwrap_or_else(|_| ".".to_string()); .unwrap_or_else(|_| ".".to_string());
println!("Initializing the project in {cwd}"); println!("Initializing the project in {cwd}");
let mut dcr_toml = match fs::File::create("./dcr.toml") { let mut config = match Config::new("./dcr.toml") {
Ok(file) => file, Ok(cfg) => cfg,
Err(_) => { Err(_) => {
error("Failed to create dcr.toml"); error("Failed to create dcr.toml");
return 1; return 1;
} }
}; };
if dcr_toml if config
.write_all(file_dcr_toml(&project_name).as_bytes()) .edit("package.name", Value::String(project_name.clone()))
.is_err() .is_err()
{ {
error("Failed to write dcr.toml"); error("Failed to write dcr.toml");
@@ -43,7 +45,7 @@ pub fn init(args: &[String]) -> i32 {
} }
println!( println!(
" {} Created file {}", " {} Created file {}",
colored("✔", BOLD_CYAN), colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN) colored("dcr.toml", BOLD_CYAN)
); );
@@ -73,6 +75,6 @@ pub fn init(args: &[String]) -> i32 {
colored(project_name.as_str(), BOLD_GREEN) colored(project_name.as_str(), BOLD_GREEN)
); );
printc("Next step:", BOLD_GREEN); printc("Next step:", BOLD_GREEN);
printc(" cd {}\n dcr run", project_name.as_str()); printc(" dcr run", BOLD_CYAN);
0 0
} }

View File

@@ -1,9 +1,11 @@
use crate::config::{FILE_MAIN_C, file_dcr_toml}; use crate::config::FILE_MAIN_C;
use crate::core::config::Config;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use toml::Value;
pub fn new(args: &[String]) -> i32 { pub fn new(args: &[String]) -> i32 {
let items = check_dir(None).unwrap_or_default(); let items = check_dir(None).unwrap_or_default();
@@ -42,20 +44,20 @@ pub fn new(args: &[String]) -> i32 {
} }
println!( println!(
" {} Directory created {}", " {} Directory created {}",
colored("✔", BOLD_CYAN), colored("✔", BOLD_GREEN),
project_name project_name
); );
let toml_path = format!("./{project_name}/dcr.toml"); let toml_path = format!("./{project_name}/dcr.toml");
let mut dcr_toml = match fs::File::create(&toml_path) { let mut config = match Config::new(&toml_path) {
Ok(file) => file, Ok(cfg) => cfg,
Err(_) => { Err(_) => {
error("Failed to create dcr.toml"); error("Failed to create dcr.toml");
return 1; return 1;
} }
}; };
if dcr_toml if config
.write_all(file_dcr_toml(project_name).as_bytes()) .edit("package.name", Value::String(project_name.to_string()))
.is_err() .is_err()
{ {
error("Failed to write dcr.toml"); error("Failed to write dcr.toml");
@@ -63,7 +65,7 @@ pub fn new(args: &[String]) -> i32 {
} }
println!( println!(
" {} Created file {}", " {} Created file {}",
colored("✔", BOLD_CYAN), colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN) colored("dcr.toml", BOLD_CYAN)
); );
@@ -94,6 +96,6 @@ pub fn new(args: &[String]) -> i32 {
colored(project_name, BOLD_GREEN) colored(project_name, BOLD_GREEN)
); );
printc("Next step:", BOLD_GREEN); printc("Next step:", BOLD_GREEN);
printc(" cd {}\n dcr run", project_name); printc(&format!(" cd {}\n dcr run", project_name), BOLD_CYAN);
0 0
} }

View File

@@ -1,5 +1,6 @@
use crate::cli::build::build; use crate::cli::build::build;
use crate::config::{PROFILE, flags}; use crate::config::{PROFILE, flags};
use crate::core::config::Config;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored}; use crate::utils::text::{BOLD_GREEN, colored};
@@ -14,6 +15,18 @@ pub fn run(args: &[String]) -> i32 {
return 1; 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("");
if let Some(first_arg) = args.first() { if let Some(first_arg) = args.first() {
if first_arg.starts_with("--") { if first_arg.starts_with("--") {
let candidate = first_arg.trim_start_matches("--"); let candidate = first_arg.trim_start_matches("--");
@@ -32,11 +45,11 @@ pub fn run(args: &[String]) -> i32 {
let build_status = build(args); let build_status = build(args);
if build_status == 0 { if build_status == 0 {
println!( println!(
"\n {} target/{active_profile}/main", "\n {} target/{active_profile}/{project_name}",
colored("Running", BOLD_GREEN) colored("Running", BOLD_GREEN)
); );
println!("--------------------------------"); println!("--------------------------------");
return Command::new(format!("./target/{active_profile}/main")) return Command::new(format!("./target/{active_profile}/{project_name}"))
.status() .status()
.map(|status| status.code().unwrap_or(1)) .map(|status| status.code().unwrap_or(1))
.unwrap_or(1); .unwrap_or(1);
@@ -47,7 +60,7 @@ pub fn run(args: &[String]) -> i32 {
.contains(&active_profile) .contains(&active_profile)
{ {
warn("Launch of the latest release"); warn("Launch of the latest release");
return Command::new(format!("./target/{active_profile}/main")) return Command::new(format!("./target/{active_profile}/{project_name}"))
.status() .status()
.map(|status| status.code().unwrap_or(1)) .map(|status| status.code().unwrap_or(1))
.unwrap_or(1); .unwrap_or(1);

View File

@@ -1,24 +1,4 @@
pub const PROJECT_VERSION: &str = "0.1.0";
pub const PROJECT_LANGUAGE: &str = "c";
pub const PROJECT_COMPILER: &str = "clang";
pub const PROFILE: &str = "debug"; pub const PROFILE: &str = "debug";
pub fn file_dcr_toml(project_name: &str) -> String {
format!(
r#"[package]
name = "{project_name}"
version = "{project_version}"
language = "{project_language}"
compiler = "{project_compiler}"
[dependencies]
"#,
project_version = PROJECT_VERSION,
project_language = PROJECT_LANGUAGE,
project_compiler = PROJECT_COMPILER
)
}
pub const FILE_MAIN_C: &str = r#"#include <stdio.h> pub const FILE_MAIN_C: &str = r#"#include <stdio.h>
int main(void) { int main(void) {

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

@@ -0,0 +1,81 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
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 mut cmd = Command::new(compiler);
let sources = collect_sources(ctx.language)?;
for source in &sources {
cmd.arg(source);
}
if !ctx.standard.is_empty() {
cmd.arg(format!("-std={}", ctx.standard));
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
cmd.arg("-o")
.arg(platform::bin_path(ctx.profile, ctx.project_name));
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();
let entries = fs::read_dir("./src").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_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 {
sources.push(file);
}
}
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
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",
],
_ => &[],
}
}

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

@@ -0,0 +1,81 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
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 mut cmd = Command::new(compiler);
let sources = collect_sources(ctx.language)?;
for source in &sources {
cmd.arg(source);
}
if !ctx.standard.is_empty() {
cmd.arg(format!("-std={}", ctx.standard));
}
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
}
cmd.arg("-o")
.arg(platform::bin_path(ctx.profile, ctx.project_name));
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();
let entries = fs::read_dir("./src").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_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 {
sources.push(file);
}
}
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
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",
],
_ => &[],
}
}

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

@@ -0,0 +1,28 @@
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 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)
}

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

@@ -0,0 +1,116 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
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 mut cmd = Command::new(compiler);
let sources = collect_sources(ctx.language)?;
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 source in &sources {
cmd.arg(source);
}
cmd.arg(format!(
"/Fe:{}",
platform::bin_path(ctx.profile, ctx.project_name)
));
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();
let entries = fs::read_dir("./src").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_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 {
sources.push(file);
}
}
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
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-"],
_ => &[],
}
}

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

@@ -0,0 +1,262 @@
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";
#[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"] {
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")));
}
}
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()),
);
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\n"));
out.push_str("[dependencies]\n");
if !deps.is_empty() {
let mut keys: Vec<&String> = deps.keys().collect();
keys.sort();
for key in keys {
let val = deps.get(key).and_then(|v| v.as_str()).unwrap_or("");
out.push_str(&format!("{key} = \"{val}\"\n"));
}
}
Ok(out)
}
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()))
}
}

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

@@ -0,0 +1,2 @@
pub mod builder;
pub mod config;

View File

@@ -1,6 +1,8 @@
use crate::utils::log::error; use crate::utils::log::error;
mod cli; mod cli;
mod config; mod config;
mod core;
mod platform;
mod utils; mod utils;
fn main() { fn main() {

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

@@ -0,0 +1,3 @@
pub fn bin_path(profile: &str, name: &str) -> String {
format!("./target/{profile}/{name}")
}

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

@@ -0,0 +1,3 @@
pub fn bin_path(profile: &str, name: &str) -> String {
format!("./target/{profile}/{name}")
}

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

@@ -0,0 +1,25 @@
#[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) -> String {
#[cfg(target_os = "linux")]
{
linux::bin_path(profile, name)
}
#[cfg(target_os = "macos")]
{
return macos::bin_path(profile, name);
}
#[cfg(target_os = "windows")]
{
return windows::bin_path(profile, name);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
format!("./target/{profile}/{name}")
}
}

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

@@ -0,0 +1,3 @@
pub fn bin_path(profile: &str, name: &str) -> String {
format!("./target/{profile}/{name}.exe")
}