38 Commits

Author SHA1 Message Date
9326e23a53 feat: add native Qt support and dcr lint command; refactor architecture and integration tests 2026-06-17 12:39:22 +08:00
21932ac94a feat(build): add freestanding, lto, strip, panic-abort, and codegen-units support in dcr.toml 2026-06-16 16:42:56 +08:00
3d596d7280 Update install docs 2026-06-15 22:35:38 +08:00
5cb4473bd1 Update README.md 2026-06-15 22:35:06 +08:00
33f955d4e3 refactor(updater): migrate from reqwest to ureq for lightweight builds 2026-06-13 16:15:24 +08:00
838be72dd2 refactor(deps): replace git2 with system git and add vcs support 2026-06-13 15:28:14 +08:00
39fad31c4f Merge pull request #18 from dexoron/master
v0.7.2
2026-06-07 22:31:43 +08:00
c8ef38a99b release: bump version to 0.7.2 and update changelog 2026-06-07 21:41:02 +08:00
8e0c535cfd fix: improve default target triple generation for various OS environments 2026-06-07 21:29:55 +08:00
de88c160ad Merge pull request #16 from XiaoPengYouCode/codex/fix-macos-host-target-link 2026-06-07 21:21:53 +08:00
4daca925a0 Merge pull request #17 from dexoron/master
master -> dev
2026-06-07 21:17:14 +08:00
595b7ac105 Merge pull request #15 from XiaoPengYouCode/codex/fix-readme-badges 2026-06-07 21:13:10 +08:00
XiaoPengYouCode
11ae3762d0 fix: use host macOS target for default builds 2026-06-07 05:41:07 +08:00
XiaoPengYouCode
4c439317e3 docs: fix README badge rendering 2026-06-07 05:09:38 +08:00
0aa3f87f99 feat: temporarily disable Snap Store publishing and update README instructions 2026-06-06 22:18:03 +08:00
a6ce0a3332 feat: enhance release notes extraction and include release name in GitHub release 2026-06-06 21:41:50 +08:00
cea43e99c8 feat: add Windows target dependencies and update README for MinGW 2026-06-06 21:39:17 +08:00
de4fb0bc29 feat: add .SRCINFO for AUR package metadata 2026-06-06 21:33:34 +08:00
59e27b6f5d Merge pull request #14 from dexoron/dev
v0.7.1 The Multi-Arch Expansion
2026-06-06 21:28:24 +08:00
c8cb155e5a feat: update help flag 2026-06-06 21:22:46 +08:00
6424aab69d feet: add license to docs and updated Cargo.toml to add build with no features flag 2026-06-06 21:19:04 +08:00
peoplemiau1
dabeb411d3 docs: update changelog for 0.7.1 (bilingual & multi-arch) 2026-06-02 16:22:57 +03:00
peoplemiau1
b8338c752b chore: remove i686-pc-windows-gnu from build matrix 2026-06-02 16:00:08 +03:00
b70438b16d feat: update README 2026-06-02 19:38:16 +08:00
0597b5ddb2 feat: add --help for all commands, man pages 2026-06-02 19:27:44 +08:00
peoplemiau1
8866fb0796 Bump version to 0.7.1 2026-06-02 13:55:54 +03:00
peoplemiau1
7d853482aa feat: build deb and rpm packages for all linux architectures 2026-06-02 13:50:57 +03:00
peoplemiau1
ef9ca57295 Add multi-arch support to release workflows and installation scripts 2026-06-02 13:38:42 +03:00
peoplemiau1
8e1fb60692 Add Linux i686, armv7, and riscv64 architectures support to release workflow and installation script 2026-06-02 13:25:38 +03:00
peoplemiau1
d6ddb5da3d Merge origin/master into dev and resolve conflicts in release.yml 2026-06-02 13:06:15 +03:00
peoplemiau1
d26e93dd47 Optimize release binary size and parse_d_file dependency parsing 2026-06-02 12:58:06 +03:00
97d2970727 feat: add dcr-dev and dcr-dev-bin publishing to AUR 2026-06-02 17:34:17 +08:00
kie
35f9b6be94 Update README.md 2026-06-02 11:27:09 +03:00
6885a82be7 feat: add package name validation to init and new commands 2026-06-02 16:26:11 +08:00
5d587db227 fix: update AUR publishing process to handle git initialization and improve build flags 2026-06-02 16:08:58 +08:00
424f4f6929 feat: add support for publishing Snap packages with dynamic file resolution 2026-06-02 16:08:58 +08:00
443306aa5f fix: update AUR publishing process to handle git initialization and improve build flags 2026-06-02 15:17:20 +08:00
f21f50facb feat: add support for publishing Snap packages with dynamic file resolution 2026-06-02 01:27:04 +08:00
90 changed files with 4114 additions and 2579 deletions

View File

@@ -21,6 +21,10 @@ on:
description: "Update AUR packages (dcr & dcr-bin)"
type: boolean
default: false
run_aur_dev:
description: "Update Dev AUR packages (dcr-dev, dcr-dev-bin)"
type: boolean
default: false
run_bsd:
description: "Build BSD"
type: boolean
@@ -37,10 +41,6 @@ on:
description: "Publish to crates.io"
type: boolean
default: false
run_snap:
description: "Publish to Snap Store (dcrup)"
type: boolean
default: false
run_brew:
description: "Update Homebrew tap"
type: boolean
@@ -56,6 +56,7 @@ jobs:
outputs:
tag: ${{ steps.tag.outputs.value }}
version: ${{ steps.tag.outputs.version }}
clean_version: ${{ steps.tag.outputs.clean_version }}
prerelease: ${{ steps.tag.outputs.prerelease }}
steps:
- name: Resolve tag
@@ -67,33 +68,53 @@ jobs:
TAG="${GITHUB_REF_NAME}"
fi
VERSION="${TAG#v}"
CLEAN_VERSION="${VERSION%-dev}"
[[ "$TAG" == *"-"* ]] && PRERELEASE="true" || PRERELEASE="false"
echo "value=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "clean_version=${CLEAN_VERSION}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
# ── Release Notes ──────────────────────────────────────────────────────────
notes:
needs: setup
runs-on: ubuntu-latest
outputs:
release_name: ${{ steps.extract.outputs.release_name }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Prepare Release Notes
id: extract
shell: bash
run: |
set -euo pipefail
tag="${{ needs.setup.outputs.version }}"
notes_file=".github/release-notes.md"
if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then
awk -v ver="$tag" '
# Strip -dev suffix to match base version in CHANGELOG
changelog_tag="${tag%-dev}"
if grep -q "^## \\[${changelog_tag}\\]" CHANGELOG.md; then
# Extract release name from header: ## [0.7.1] - 2026-06-02 "Title"
release_name=$(awk -v search_ver="$changelog_tag" -v display_ver="$tag" '
$0 ~ "^## \\[" search_ver "\\]" {
match($0, /"([^"]+)"/, arr)
if (arr[1] != "") {
print "v" display_ver " - " arr[1]
} else {
print "v" display_ver
}
}
' CHANGELOG.md)
echo "release_name=${release_name}" >> "$GITHUB_OUTPUT"
awk -v ver="$changelog_tag" '
$0 ~ "^## \\[" ver "\\]" {found=1; next}
found && $0 ~ "^## \\[" {exit}
found {print}
' CHANGELOG.md | sed -e '${/^$/d;}' > "$notes_file"
else
echo "release_name=v${tag}" >> "$GITHUB_OUTPUT"
echo "No CHANGELOG section for $tag." > "$notes_file"
git log -1 --pretty=format:"%s%n%n%b" >> "$notes_file"
fi
@@ -119,6 +140,37 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
name: ${{ needs.notes.outputs.release_name }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Man Pages Archive ──────────────────────────────────────────────────────
man-pages:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }}
needs: [setup, notes, create-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Create Man Pages Archive
run: |
tar czf "dcr-man-pages-${{ needs.setup.outputs.version }}.tar.gz" \
--transform 's/^man\/man1\//usr\/share\/man\/man1\//' \
man/man1/*.1
- name: Upload Man Pages to Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: dcr-man-pages-${{ needs.setup.outputs.version }}.tar.gz
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
@@ -135,6 +187,12 @@ jobs:
- target: aarch64-unknown-linux-gnu
- target: x86_64-unknown-linux-musl
- target: aarch64-unknown-linux-musl
- target: i686-unknown-linux-gnu
- target: i686-unknown-linux-musl
- target: armv7-unknown-linux-gnueabihf
- target: armv7-unknown-linux-musleabihf
- target: riscv64gc-unknown-linux-gnu
- target: riscv64gc-unknown-linux-musl
steps:
- uses: actions/checkout@v4
with:
@@ -192,41 +250,73 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- package_type: deb
artifact_name: debian-package
- package_type: rpm
artifact_name: rpm-package
package_type: [deb, rpm, arch, snap]
target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, i686-unknown-linux-gnu, armv7-unknown-linux-gnueabihf, riscv64gc-unknown-linux-gnu]
exclude:
- package_type: arch
artifact_name: arch-package
target: aarch64-unknown-linux-gnu
- package_type: arch
target: i686-unknown-linux-gnu
- package_type: arch
target: armv7-unknown-linux-gnueabihf
- package_type: arch
target: riscv64gc-unknown-linux-gnu
- package_type: snap
artifact_name: snap-package
target: aarch64-unknown-linux-gnu
- package_type: snap
target: i686-unknown-linux-gnu
- package_type: snap
target: armv7-unknown-linux-gnueabihf
- package_type: snap
target: riscv64gc-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Setup Rust (For non-container builds)
- name: Install cargo-zigbuild
if: matrix.package_type == 'deb' || matrix.package_type == 'rpm'
run: |
pip install cargo-zigbuild
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Setup Zig
if: (matrix.package_type == 'deb' || matrix.package_type == 'rpm') && matrix.target != 'x86_64-unknown-linux-gnu'
uses: mlugg/setup-zig@v1
with:
version: 0.13.0
- name: Setup Rust
if: matrix.package_type != 'arch'
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
# ── DEB Build ──
- name: Build Debian Package
if: matrix.package_type == 'deb'
run: |
cargo install cargo-deb
cargo build --release
cargo deb --no-build
mkdir -p dist-packages && cp target/debian/*.deb dist-packages/
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
cargo deb --no-build --target ${{ matrix.target }}
mkdir -p dist-packages && cp target/${{ matrix.target }}/debian/*.deb dist-packages/
# ── RPM Build ──
- name: Build RPM Package
if: matrix.package_type == 'rpm'
run: |
cargo install cargo-generate-rpm
cargo build --release
cargo generate-rpm
mkdir -p dist-packages && cp target/generate-rpm/*.rpm dist-packages/
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
cargo zigbuild --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
cargo generate-rpm --target ${{ matrix.target }}
mkdir -p dist-packages && cp target/${{ matrix.target }}/generate-rpm/*.rpm dist-packages/
# ── Snap Build ──
- name: Build Snap Package
@@ -235,6 +325,7 @@ jobs:
cargo build --release
mkdir -p packaging/snap/dist/bin
cp target/release/dcr packaging/snap/dist/bin/
cp -r man packaging/snap/dist/man
- name: Snapcraft Build Action
if: matrix.package_type == 'snap'
@@ -271,12 +362,17 @@ jobs:
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
unset CFLAGS CXXFLAGS LDFLAGS
export RUSTFLAGS="-C linker=cc"
cargo build --release --locked
}
package() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
cd "\$srcdir/dcr-${PKGVER_ORIG}"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
for manpage in man/man1/*.1; do
install -Dm644 "\$manpage" "\$pkgdir/usr/share/man/man1/\$(basename "\$manpage")"
done
}
PKG
docker run --rm \
@@ -296,7 +392,7 @@ jobs:
- name: Upload Package Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
name: ${{ matrix.package_type }}-package-${{ matrix.target }}
path: dist-packages/*
if-no-files-found: ignore
@@ -321,10 +417,10 @@ jobs:
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: |
collected-packages/debian-package/*
collected-packages/rpm-package/*
collected-packages/arch-package/*
collected-packages/snap-package/*
collected-packages/deb-package-*/*
collected-packages/rpm-package-*/*
collected-packages/arch-package-*/*
collected-packages/snap-package-*/*
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
@@ -450,8 +546,6 @@ jobs:
cp target/release/dcr "$ASSET"
echo "BSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
# --- UPLOAD TARGET ---
- name: Upload BSD Asset to Release
uses: softprops/action-gh-release@v2
@@ -472,6 +566,7 @@ jobs:
include:
- target: x86_64-pc-windows-msvc
- target: aarch64-pc-windows-msvc
- target: i686-pc-windows-msvc
- target: x86_64-pc-windows-gnu
steps:
- uses: actions/checkout@v4
@@ -552,14 +647,13 @@ jobs:
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source=("\$pkgname-\$pkgver.tar.gz::\${SRC_URL}")
source=("\$pkgname-\$pkgver.tar.gz::${SRC_URL}")
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="\${RUSTFLAGS:-}"
export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
unset CFLAGS CXXFLAGS LDFLAGS
export RUSTFLAGS="-C linker=cc"
cargo build --release --locked
}
@@ -567,6 +661,9 @@ jobs:
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
for manpage in man/man1/*.1; do
install -Dm644 "\$manpage" "\$pkgdir/usr/share/man/man1/\$(basename "\$manpage")"
done
}
PKG
@@ -603,22 +700,31 @@ jobs:
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 packages/aur/dcr/PKGBUILD aur-repo/PKGBUILD
cp packages/aur/dcr/.SRCINFO aur-repo/.SRCINFO
mkdir -p aur-repo
cd aur-repo
git init
git remote add origin "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git"
git fetch origin master || true
git checkout master || git checkout -b master
cp ../packages/aur/dcr/PKGBUILD .
cp ../packages/aur/dcr/.SRCINFO .
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
if git rev-parse --verify HEAD >/dev/null 2>&1; then
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr ${{ needs.setup.outputs.version }}"
git push
git push -u origin master
# ── Arch User Repository (dcr-bin) ────────────────────────────────
publish-aur-bin:
@@ -635,10 +741,22 @@ jobs:
shell: bash
run: |
set -euo pipefail
BIN_URL="https://github.com/dexoron/dcr/releases/download/${{ needs.setup.outputs.tag }}/dcr-x86_64-unknown-linux-musl-${{ needs.setup.outputs.version }}"
SHA256="$(curl -fsSL "$BIN_URL" | sha256sum | awk '{print $1}')"
echo "bin_url=$BIN_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
TAG="${{ needs.setup.outputs.tag }}"
VER="${{ needs.setup.outputs.version }}"
BASE_URL="https://github.com/dexoron/dcr/releases/download/${TAG}"
for arch in x86_64 aarch64 i686 armv7h riscv64; do
case "$arch" in
x86_64) triple="x86_64-unknown-linux-musl" ;;
aarch64) triple="aarch64-unknown-linux-musl" ;;
i686) triple="i686-unknown-linux-musl" ;;
armv7h) triple="armv7-unknown-linux-musleabihf" ;;
riscv64) triple="riscv64gc-unknown-linux-musl" ;;
esac
url="${BASE_URL}/dcr-${triple}-${VER}"
sha256="$(curl -fsSL "$url" | sha256sum | awk '{print $1}')"
echo "${arch}_url=${url}" >> "$GITHUB_OUTPUT"
echo "${arch}_sha256=${sha256}" >> "$GITHUB_OUTPUT"
done
- name: Render AUR Metadata
shell: bash
@@ -646,16 +764,13 @@ jobs:
set -euo pipefail
PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN="${PKGVER_ORIG//-/.}"
BIN_URL='${{ steps.meta.outputs.bin_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr-bin
cat > packages/aur/dcr-bin/PKGBUILD <<PKG
pkgname=dcr-bin
pkgver=${PKGVER_CLEAN}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)"
arch=('x86_64')
arch=('x86_64' 'aarch64' 'i686' 'armv7h' 'riscv64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
provides=('dcr')
@@ -664,8 +779,16 @@ jobs:
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source_x86_64=("dcr-\$pkgver::${BIN_URL}")
sha256sums_x86_64=('${SHA256}')
source_x86_64=("dcr-\$pkgver::${{ steps.meta.outputs.x86_64_url }}")
sha256sums_x86_64=('${{ steps.meta.outputs.x86_64_sha256 }}')
source_aarch64=("dcr-\$pkgver::${{ steps.meta.outputs.aarch64_url }}")
sha256sums_aarch64=('${{ steps.meta.outputs.aarch64_sha256 }}')
source_i686=("dcr-\$pkgver::${{ steps.meta.outputs.i686_url }}")
sha256sums_i686=('${{ steps.meta.outputs.i686_sha256 }}')
source_armv7h=("dcr-\$pkgver::${{ steps.meta.outputs.armv7h_url }}")
sha256sums_armv7h=('${{ steps.meta.outputs.armv7h_sha256 }}')
source_riscv64=("dcr-\$pkgver::${{ steps.meta.outputs.riscv64_url }}")
sha256sums_riscv64=('${{ steps.meta.outputs.riscv64_sha256 }}')
package() {
install -Dm755 "\$srcdir/dcr-\$pkgver" "\$pkgdir/usr/bin/dcr"
@@ -679,13 +802,25 @@ jobs:
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
arch = i686
arch = armv7h
arch = riscv64
license = GPL-3.0-or-later
provides = dcr
conflicts = dcr
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
source_x86_64 = dcr-${PKGVER_CLEAN}::${BIN_URL}
sha256sums_x86_64 = ${SHA256}
source_x86_64 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.x86_64_url }}
sha256sums_x86_64 = ${{ steps.meta.outputs.x86_64_sha256 }}
source_aarch64 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.aarch64_url }}
sha256sums_aarch64 = ${{ steps.meta.outputs.aarch64_sha256 }}
source_i686 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.i686_url }}
sha256sums_i686 = ${{ steps.meta.outputs.i686_sha256 }}
source_armv7h = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.armv7h_url }}
sha256sums_armv7h = ${{ steps.meta.outputs.armv7h_sha256 }}
source_riscv64 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.riscv64_url }}
sha256sums_riscv64 = ${{ steps.meta.outputs.riscv64_sha256 }}
pkgname = dcr-bin
SRC
@@ -703,22 +838,299 @@ jobs:
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 packages/aur/dcr-bin/PKGBUILD aur-repo/PKGBUILD
cp packages/aur/dcr-bin/.SRCINFO aur-repo/.SRCINFO
mkdir -p aur-repo
cd aur-repo
git init
git remote add origin "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git"
git fetch origin master || true
git checkout master || git checkout -b master
cp ../packages/aur/dcr-bin/PKGBUILD .
cp ../packages/aur/dcr-bin/.SRCINFO .
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
if git rev-parse --verify HEAD >/dev/null 2>&1; then
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr-bin ${{ needs.setup.outputs.version }}"
git push
git push -u origin master
# ── Arch User Repository (dcr-dev) ────────────────────────────────
publish-aur-dev:
if: ${{ (github.event_name == 'push' && needs.setup.outputs.prerelease == 'true') || github.event.inputs.run_aur_dev == 'true' }}
needs: [setup]
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive checksum
id: meta
shell: bash
run: |
set -euo pipefail
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/${{ needs.setup.outputs.tag }}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
echo "source_url=$SRC_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Render AUR metadata
shell: bash
run: |
set -euo pipefail
PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN='${{ needs.setup.outputs.clean_version }}'
SRC_URL='${{ steps.meta.outputs.source_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr-dev
cat > packages/aur/dcr-dev/PKGBUILD <<PKG
pkgname=dcr-dev
pkgver=${PKGVER_CLEAN}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects (development release)"
arch=('x86_64' 'aarch64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
depends=('gcc-libs')
makedepends=('cargo')
options=('!lto')
provides=('dcr')
conflicts=('dcr')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source=("\$pkgname-\$pkgver.tar.gz::${SRC_URL}")
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/dcr-${PKGVER_ORIG}"
unset CFLAGS CXXFLAGS LDFLAGS
export RUSTFLAGS="-C linker=cc"
cargo build --release --locked
}
package() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
for manpage in man/man1/*.1; do
install -Dm644 "\$manpage" "\$pkgdir/usr/share/man/man1/\$(basename "\$manpage")"
done
}
PKG
cat > packages/aur/dcr-dev/.SRCINFO <<SRC
pkgbase = dcr-dev
pkgdesc = Cargo-like utility to manage C/C++ projects (development release)
pkgver = ${PKGVER_CLEAN}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
license = GPL-3.0-or-later
makedepends = cargo
depends = gcc-libs
provides = dcr
conflicts = dcr
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
options = !lto
source = dcr-${PKGVER_CLEAN}.tar.gz::${SRC_URL}
sha256sums = ${SHA256}
pkgname = dcr-dev
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr-dev
shell: bash
run: |
set -euo pipefail
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'
mkdir -p aur-repo
cd aur-repo
git init
git remote add origin "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git"
git fetch origin master || true
git checkout master || git checkout -b master
cp ../packages/aur/dcr-dev/PKGBUILD .
cp ../packages/aur/dcr-dev/.SRCINFO .
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git rev-parse --verify HEAD >/dev/null 2>&1; then
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr-dev ${{ needs.setup.outputs.version }}"
git push -u origin master
# ── Arch User Repository (dcr-dev-bin) ────────────────────────────
publish-aur-dev-bin:
if: ${{ (github.event_name == 'push' && needs.setup.outputs.prerelease == 'true') || github.event.inputs.run_aur_dev == 'true' }}
needs: [setup, build-linux]
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive checksum
id: meta
shell: bash
run: |
set -euo pipefail
TAG="${{ needs.setup.outputs.tag }}"
VER="${{ needs.setup.outputs.version }}"
BASE_URL="https://github.com/dexoron/dcr/releases/download/${TAG}"
for arch in x86_64 aarch64 i686 armv7h riscv64; do
case "$arch" in
x86_64) triple="x86_64-unknown-linux-musl" ;;
aarch64) triple="aarch64-unknown-linux-musl" ;;
i686) triple="i686-unknown-linux-musl" ;;
armv7h) triple="armv7-unknown-linux-musleabihf" ;;
riscv64) triple="riscv64gc-unknown-linux-musl" ;;
esac
url="${BASE_URL}/dcr-${triple}-${VER}"
sha256="$(curl -fsSL "$url" | sha256sum | awk '{print $1}')"
echo "${arch}_url=${url}" >> "$GITHUB_OUTPUT"
echo "${arch}_sha256=${sha256}" >> "$GITHUB_OUTPUT"
done
- name: Render AUR Metadata
shell: bash
run: |
set -euo pipefail
PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN='${{ needs.setup.outputs.clean_version }}'
mkdir -p packages/aur/dcr-dev-bin
cat > packages/aur/dcr-dev-bin/PKGBUILD <<PKG
pkgname=dcr-dev-bin
pkgver=${PKGVER_CLEAN}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects (pre-compiled musl binary dev release)"
arch=('x86_64' 'aarch64' 'i686' 'armv7h' 'riscv64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
provides=('dcr')
conflicts=('dcr')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source_x86_64=("dcr-\$pkgver::${{ steps.meta.outputs.x86_64_url }}")
sha256sums_x86_64=('${{ steps.meta.outputs.x86_64_sha256 }}')
source_aarch64=("dcr-\$pkgver::${{ steps.meta.outputs.aarch64_url }}")
sha256sums_aarch64=('${{ steps.meta.outputs.aarch64_sha256 }}')
source_i686=("dcr-\$pkgver::${{ steps.meta.outputs.i686_url }}")
sha256sums_i686=('${{ steps.meta.outputs.i686_sha256 }}')
source_armv7h=("dcr-\$pkgver::${{ steps.meta.outputs.armv7h_url }}")
sha256sums_armv7h=('${{ steps.meta.outputs.armv7h_sha256 }}')
source_riscv64=("dcr-\$pkgver::${{ steps.meta.outputs.riscv64_url }}")
sha256sums_riscv64=('${{ steps.meta.outputs.riscv64_sha256 }}')
package() {
install -Dm755 "\$srcdir/dcr-\$pkgver" "\$pkgdir/usr/bin/dcr"
}
PKG
cat > packages/aur/dcr-dev-bin/.SRCINFO <<SRC
pkgbase = dcr-dev-bin
pkgdesc = Cargo-like utility to manage C/C++ projects (pre-compiled musl binary dev release)
pkgver = ${PKGVER_CLEAN}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
arch = i686
arch = armv7h
arch = riscv64
license = GPL-3.0-or-later
provides = dcr
conflicts = dcr
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
source_x86_64 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.x86_64_url }}
sha256sums_x86_64 = ${{ steps.meta.outputs.x86_64_sha256 }}
source_aarch64 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.aarch64_url }}
sha256sums_aarch64 = ${{ steps.meta.outputs.aarch64_sha256 }}
source_i686 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.i686_url }}
sha256sums_i686 = ${{ steps.meta.outputs.i686_sha256 }}
source_armv7h = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.armv7h_url }}
sha256sums_armv7h = ${{ steps.meta.outputs.armv7h_sha256 }}
source_riscv64 = dcr-${PKGVER_CLEAN}::${{ steps.meta.outputs.riscv64_url }}
sha256sums_riscv64 = ${{ steps.meta.outputs.riscv64_sha256 }}
pkgname = dcr-dev-bin
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr-dev-bin
shell: bash
run: |
set -euo pipefail
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'
mkdir -p aur-repo
cd aur-repo
git init
git remote add origin "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git"
git fetch origin master || true
git checkout master || git checkout -b master
cp ../packages/aur/dcr-dev-bin/PKGBUILD .
cp ../packages/aur/dcr-dev-bin/.SRCINFO .
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git rev-parse --verify HEAD >/dev/null 2>&1; then
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr-dev-bin ${{ needs.setup.outputs.version }}"
git push -u origin master
# ── crates.io ──────────────────────────────────────────────────────────────
publish-crates:
@@ -738,6 +1150,8 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish
# ── Homebrew tap ───────────────────────────────────────────────────────────
@@ -762,6 +1176,7 @@ jobs:
echo "linux_arm=$(download_sha "dcr-aarch64-unknown-linux-gnu-${VERSION}")" >> "$GITHUB_OUTPUT"
echo "macos_x86=$(download_sha "dcr-x86_64-apple-darwin-${VERSION}")" >> "$GITHUB_OUTPUT"
echo "macos_arm=$(download_sha "dcr-aarch64-apple-darwin-${VERSION}")" >> "$GITHUB_OUTPUT"
echo "man_pages=$(download_sha "dcr-man-pages-${VERSION}.tar.gz")" >> "$GITHUB_OUTPUT"
- name: Checkout homebrew-dexoron
uses: actions/checkout@v4
@@ -804,6 +1219,18 @@ jobs:
bin.install Dir["dcr*"].first => "dcr"
end
resource "man-pages" do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-man-pages-#{version}.tar.gz"
sha256 "${{ steps.sha.outputs.man_pages }}"
end
def install
bin.install Dir["dcr*"].first => "dcr"
resource("man-pages").stage do
man1.install Dir["usr/share/man/man1/*.1"]
end
end
test do
system "#{bin}/dcr", "--version"
end
@@ -819,24 +1246,11 @@ jobs:
git commit -m "dcr ${{ needs.setup.outputs.version }}"
git push
# ── Snap Store (dcrup) ─────────────────────────────────────────────────────
publish-snap:
if: |
(github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_snap == 'true')
needs: [setup, build-linux-packages]
runs-on: ubuntu-latest
steps:
- name: Download Built Packages
uses: actions/download-artifact@v4
with:
name: snap-package
path: dist-packages
- name: Publish to Snap Store
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
with:
snap: dist-packages/*.snap
release: stable
# ── Snap Store (dcrup) — temporarily disabled ────────────────────────────
# Classic confinement was denied for this project (too new).
# Snap artifact is still built and attached to the release via
# publish-dexoron-packages. Users can install manually:
# sudo snap install --classic --dangerous dcrup_*.snap
#
# publish-snap:
# if: ...

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ target/
sandbox/**
.idea/
.vscode/
.cargo/
.env
personal/

View File

@@ -1,5 +1,179 @@
# Changelog
## [0.7.4] - 2026-06-17 "Qt Поддержка, Linting и Рефакторинг Архитектуры / Qt Support, Linting, & Architecture Refactor"
### RU
**Добавлено:**
- **Нативная поддержка Qt** — добавлена автоматическая обработка мета-объектов (MOC, UIC, RCC) при включении `build.qt = true` в `dcr.toml`.
- **Команда `dcr lint`** — статический анализ C/C++ через `clang-tidy`. Поддерживает `--fix` для автоматического исправления. Уважает `build.roots` и `build.src_disable` из конфига.
- **Три новых ассемблерных бэкенда**:
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
- **LLVM IR** (`compiler = "llc"`) — компиляция `.ll` → объектный файл.
- **Общий ASM-пайплайн** — линковка и архивирование вынесены в `core/builder/asm.rs`. Новый ассемблерный бэкенд теперь пишется за ~30 строк.
- **Утилиты для файловой системы** — `to_hex()` и `home_dir()` вынесены в `utils/fs.rs`.
- **Утилиты для сборки** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` вынесены в `utils/build.rs`.
- **Утилиты для бэкендов** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` добавлены в `core/builder/common.rs`.
- **Полноценная автоматизация Bare-Metal / Freestanding** — добавлен конфигурационный флаг `build.freestanding = true`. При его включении (или при обнаружении bare-metal таргета) DCR автоматически инжектирует флаг `-ffreestanding` на этапе компиляции, а также `-nostdlib` и `-static` на этапе линковки.
- **Оптимизация артефактов (LTO и Strip)** — в `dcr.toml` добавлены параметры `build.lto` (авто-инжекция флага `-flto` для компилятора и линкера) и `build.strip` (автоматическое отсекание отладочной информации через флаг `-s` линкера).
- **Контроль многопоточности сборки** — добавлен параметр `build.codegen-units`, позволяющий жестко ограничить максимальное количество параллельных потоков сборки для кастомного движка `parallel_build`.
- **Управление паникой (Panic Abort)** — добавлен параметр `build.panic = "abort"`. Для C++ проектов он автоматически отключает генерацию таблиц раскрутки стека и исключений через флаги `-fno-exceptions`, `-fno-unwind-tables` и `-fno-asynchronous-unwind-tables`.
**Изменено:**
- **Интеллектуальные дефолтные флаги (DCR Defaults)** — генерация дефолтных флагов оптимизации (`-O3`/`-O0`), отладки (`-g`) и предупреждений (`-Wall -Wextra`) теперь автоматически отключается для bare-metal и freestanding целей, если пользователь переопределил `build.cflags`.
- **Глобальный вынос bare-metal детектора** — функция `is_bare_metal_target` вынесена в общие утилиты `src/utils/build.rs` для сквозного использования в конвейере сборки.
- **Рефакторинг архитектуры**:
- `src/config.rs` переименован в `src/templates.rs`.
- Начата консолидация конфигурационной логики и оркестрации сборки.
### EN
**Added:**
- **Native Qt support** — automatic meta-object handling (MOC, UIC, RCC) added when `build.qt = true` is set in `dcr.toml`.
- **`dcr lint` command** — C/C++ static analysis via `clang-tidy`. Supports `--fix` for automatic fixes. Respects `build.roots` and `build.src_disable` from config.
- **Three new assembler backends**:
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
- **LLVM IR** (`compiler = "llc"`) — compiles `.ll` files via `llc -filetype=obj`.
- **Shared ASM pipeline** — linking and archiving logic consolidated into `core/builder/asm.rs`. Adding a new assembler backend now takes ~30 lines.
- **Filesystem utilities** — `to_hex()` and `home_dir()` extracted to `utils/fs.rs`.
- **Build utilities** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` extracted to `utils/build.rs`.
- **Backend helpers** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` added to `core/builder/common.rs`.
- **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking.
- **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`.
- **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool.
- **Panic Behavior Management** — added `build.panic = "abort"` support. For C++ targets, it automatically strips exception handling and unwind tables via `-fno-exceptions`, `-fno-unwind-tables`, and `-fno-asynchronous-unwind-tables`.
**Changed:**
- **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden.
- **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core.
- **Architectural Refactoring**:
- Renamed `src/config.rs` to `src/templates.rs`.
- Started consolidation of configuration and build orchestration logic.
## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS"
### RU
**Добавлено:**
- **Параметр `--vcs` для команд `new` и `init`** — добавлена возможность явно указывать систему контроля версий через `--vcs <git|none>`.
- **Интеграция Git-информации в `--version`** — утилита теперь выводит текущий короткий хэш коммита и суффикс `-dirty`, если в репозитории DCR есть незакоммиченные изменения.
- **Автоматическая генерация `.gitignore`** — при инициализации Git-репозитория автоматически создается файл `.gitignore` с исключением директории `/target`.
- **Защита от вложенных репозиториев** — автоматическая инициализация Git теперь пропускается, если `dcr new` или `dcr init` запускаются внутри уже существующего Git-репозитория.
- **Тесты для верификации VCS** — добавлены интеграционные тесты `new_vcs_options_work` и `init_vcs_options_work` для проверки логики инициализации репозиториев.
**Изменено:**
- **Полный отказ от библиотеки `git2`** — работа с Git (клонирование зависимостей, инициализация VCS, проверка статуса) переведена на вызовы системного `git` через `Command`. Это облегчает сборку проекта и убирает необходимость в фичах `vendored` и `openssl`.
- **Миграция с `reqwest` на `ureq`** — в модуле обновления (`flag_update.rs`) тяжелый `reqwest` заменен на легковесный синхронный клиент `ureq 2.10`, что уменьшило оверхед и размер итогового бинарника.
### EN
**Added:**
- **`--vcs` option for `new` and `init` commands** — introduced explicit version control system selection via `--vcs <git|none>`.
- **Git metadata integration in `--version`** — the CLI now appends the current short commit hash and a `-dirty` suffix if there are uncommitted changes in the DCR repository.
- **Automatic `.gitignore` generation** — Git repository initialization now automatically writes a `.gitignore` file excluding the `/target` directory.
- **Nested repository prevention** — automatic Git initialization is now skipped if `dcr new` or `dcr init` is executed inside an already existing Git repository.
- **VCS verification tests** — added `new_vcs_options_work` and `init_vcs_options_work` integration tests to ensure reliable repository behavior.
**Changed:**
- **Complete removal of `git2` dependency** — all Git actions (dependency fetching, VCS setup, status checks) are now delegated to the system `git` executable via `Command`. This simplifies compilation and drops the need for `vendored` and `openssl` features.
- **Migrated from `reqwest` to `ureq`** — replaced the heavy `reqwest` crate in `flag_update.rs` with the lightweight synchronous `ureq 2.10` client, reducing overhead and the final binary size.
## [0.7.2] - 2026-06-07 "Исправление Таргетов и Линковки / Target & Linker Fixes"
### RU
**Добавлено:**
- Учтён `target_env` в `default_target_triple()` — Linux (gnu/musl) и Windows (msvc/gnu) теперь используют правильное окружение, а не хардкод.
**Исправлено:**
- **Сборка на macOS Apple Silicon падала с `_main` undefined** — `--target=` теперь передаётся и в ldflags, не только в cflags. `default_target_triple()` на macOS использует `std::env::consts::ARCH` вместо хардкода `x86_64`.
- **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
**Изменено:**
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
### EN
**Added:**
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
**Fixed:**
- **macOS Apple Silicon builds failed with `_main` undefined** — `--target=` is now passed to ldflags, not just cflags. `default_target_triple()` uses `std::env::consts::ARCH` on macOS instead of hardcoded `x86_64`.
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
**Changed:**
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean.
## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
### RU
**Добавлено:**
- Вывод `--help` для всех команд с цветным форматированием.
- man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах.
- Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`).
- Метаданные `documentation` и `homepage` в `Cargo.toml`.
- Оптимизация сборки: `opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, `strip = true`.
- Поддержка новых архитектур (i686, armv7, riscv64gc, aarch64) в рабочих процессах и установочных скриптах.
- Сборка пакетов `deb` и `rpm` для всех 5 архитектур Linux через `cargo-zigbuild`.
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
**Изменено:**
- Функция `validate_package_name` стала публичной.
- Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод.
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
- Отключена архитектура Windows 32-bit (GNU) из-за проблем со сборкой.
**Исправлено:**
- **`dcr new <invalid-name>` создаёт директорию, затем падает** — валидация теперь происходит до операций с файлами.
- **`dcr init` создаёт dcr.toml, затем падает при невалидном имени директории** — то же исправление.
- **Man-страницы отсутствовали после установки** — теперь устанавливаются через скрипты установки, пакетные менеджеры и Homebrew.
- **Ложное срабатывание на букву диска в Windows** — в парсинге файлов зависимостей.
- **CRLF `\r\n` ломает парсер зависимостей** — теперь обрабатываются смешанные окончания строк.
- **Новая строка после обратного слеша не поглощалась** — в обработке escape-последовательностей `parse_d_file`.
### EN
**Added:**
- `--help` for all commands with styled headers and usage lines.
- Man pages (12 troff man pages in `man/man1/`) and their installation across all packaging formats.
- Project name validation in `dcr init` and `dcr new` (ASCII letters, digits, `_`, `-` only).
- `documentation` and `homepage` metadata in `Cargo.toml`.
- Release profile optimizations (`opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`, `strip = true`).
- Linux i686, armv7, riscv64gc, aarch64 architecture support in release workflows and installation scripts.
- Multi-arch `deb` and `rpm` package builds for all 5 Linux architectures via `cargo-zigbuild`.
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
**Changed:**
- `validate_package_name` made public and callable from CLI commands directly.
- `gen.rs`, `tree.rs`, `fmt.rs`, `setup.rs` no longer ignore arguments and use styled output.
- README install instructions changed from `| bash` to `| sh` for POSIX compatibility.
- Disabled Windows 32-bit (GNU) target due to build issues.
Fixed:
- **`dcr new <invalid-name>` creates directory then fails** — validation now happens
before any file operations.
- **`dcr init` creates dcr.toml then fails on invalid directory name** — same fix.
- **Man pages missing after installation** — now installed via install scripts,
package managers, and Homebrew.
- **Windows drive letter false positive** — in dependency file parsing.
- **CRLF `\r\n` breaks dependency parser** — now handles mixed line endings.
- **Newline after backslash not consumed** — in `parse_d_file` escape handling.
## [0.7.0] - 2026-06-02
Added:

1037
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
[package]
name = "dcr"
version = "0.7.0"
version = "0.7.4"
edition = "2024"
build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
license = "GPL-3.0-or-later"
repository = "https://github.com/dexoron/dcr"
documentation = "https://dcr-tool.ru/docs"
homepage = "https://dcr-tool.ru"
[package.metadata.binstall]
pkg-fmt = "bin"
@@ -21,6 +23,18 @@ section = "devel"
priority = "optional"
assets = [
["target/release/dcr", "usr/bin/dcr", "755"],
["man/man1/dcr.1", "usr/share/man/man1/dcr.1", "644"],
["man/man1/dcr-add.1", "usr/share/man/man1/dcr-add.1", "644"],
["man/man1/dcr-build.1", "usr/share/man/man1/dcr-build.1", "644"],
["man/man1/dcr-clean.1", "usr/share/man/man1/dcr-clean.1", "644"],
["man/man1/dcr-fmt.1", "usr/share/man/man1/dcr-fmt.1", "644"],
["man/man1/dcr-gen.1", "usr/share/man/man1/dcr-gen.1", "644"],
["man/man1/dcr-init.1", "usr/share/man/man1/dcr-init.1", "644"],
["man/man1/dcr-new.1", "usr/share/man/man1/dcr-new.1", "644"],
["man/man1/dcr-run.1", "usr/share/man/man1/dcr-run.1", "644"],
["man/man1/dcr-setup.1", "usr/share/man/man1/dcr-setup.1", "644"],
["man/man1/dcr-test.1", "usr/share/man/man1/dcr-test.1", "644"],
["man/man1/dcr-tree.1", "usr/share/man/man1/dcr-tree.1", "644"],
]
[package.metadata.generate-rpm]
@@ -30,12 +44,24 @@ Cargo-like utility to manage C/C++ projects.
"""
assets = [
{ source = "target/release/dcr", dest = "/usr/bin/dcr", mode = "755" }
{ source = "target/release/dcr", dest = "/usr/bin/dcr", mode = "755" },
{ source = "man/man1/dcr.1", dest = "/usr/share/man/man1/dcr.1", mode = "644" },
{ source = "man/man1/dcr-add.1", dest = "/usr/share/man/man1/dcr-add.1", mode = "644" },
{ source = "man/man1/dcr-build.1", dest = "/usr/share/man/man1/dcr-build.1", mode = "644" },
{ source = "man/man1/dcr-clean.1", dest = "/usr/share/man/man1/dcr-clean.1", mode = "644" },
{ source = "man/man1/dcr-fmt.1", dest = "/usr/share/man/man1/dcr-fmt.1", mode = "644" },
{ source = "man/man1/dcr-gen.1", dest = "/usr/share/man/man1/dcr-gen.1", mode = "644" },
{ source = "man/man1/dcr-init.1", dest = "/usr/share/man/man1/dcr-init.1", mode = "644" },
{ source = "man/man1/dcr-new.1", dest = "/usr/share/man/man1/dcr-new.1", mode = "644" },
{ source = "man/man1/dcr-run.1", dest = "/usr/share/man/man1/dcr-run.1", mode = "644" },
{ source = "man/man1/dcr-setup.1", dest = "/usr/share/man/man1/dcr-setup.1", mode = "644" },
{ source = "man/man1/dcr-test.1", dest = "/usr/share/man/man1/dcr-test.1", mode = "644" },
{ source = "man/man1/dcr-tree.1", dest = "/usr/share/man/man1/dcr-tree.1", mode = "644" },
]
[dependencies]
glob = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
ureq = { version = "2.10", default-features = false, features = ["json", "native-certs"] }
serde = { version = "1", features = ["derive"] }
self-replace = "1"
sha2 = "0.10"
@@ -43,9 +69,17 @@ toml = "1.0.3"
serde_json = "1"
ctrlc = "3"
[target.'cfg(not(windows))'.dependencies]
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
openssl = { version = "0.10", features = ["vendored"] }
[profile.release]
opt-level = "z"
lto = "thin"
codegen-units = 1
panic = "abort"
strip = true
[target.'cfg(windows)'.dependencies]
git2 = { version = "0.20.4", features = ["vendored-libgit2"] }
[profile.fast]
inherits = "release"
opt-level = 1
lto = false
codegen-units = 16
panic = "abort"
strip = true

View File

@@ -1,31 +1,32 @@
<div align="center">
<h1 align="center">DCR — Dexoron Cargo Realization</h1>
# DCR — Dexoron Cargo Realization
<p align="center"><strong>A Cargo-style build tool for C/C++ projects</strong></p>
**A Cargo-style build tool for C/C++ projects**
<p align="center">
<a href="https://github.com/dexoron/dcr/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/dexoron/dcr/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://github.com/dexoron/dcr/releases/latest"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/dexoron/dcr"></a>
<a href="https://github.com/dexoron/dcr/releases"><img alt="Platform" src="https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey"></a>
<br>
<a href="https://aur.archlinux.org/packages/dcr"><img alt="AUR" src="https://img.shields.io/aur/version/dcr"></a>
<a href="https://crates.io/crates/dcr"><img alt="Crates.io" src="https://img.shields.io/crates/v/dcr"></a>
<a href="https://github.com/dexoron/homebrew-dexoron"><img alt="Homebrew" src="https://img.shields.io/badge/homebrew-dexoron%2Fdexoron-orange"></a>
<br>
<a href="https://github.com/dexoron/dcr/stargazers"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/dexoron/dcr?style=flat"></a>
<a href="LICENSE"><img alt="License: GPL-3.0" src="https://img.shields.io/badge/License-GPL--3.0-blue.svg"></a>
</p>
[![CI](https://github.com/dexoron/dcr/actions/workflows/ci.yml/badge.svg)](https://github.com/dexoron/dcr/actions/workflows/ci.yml)
[![GitHub Release](https://img.shields.io/github/v/release/dexoron/dcr)](https://github.com/dexoron/dcr/releases/latest)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/dexoron/dcr/releases)
<br/>
[![AUR](https://img.shields.io/aur/version/dcr)](https://aur.archlinux.org/packages/dcr)
[![Crates.io](https://img.shields.io/crates/v/dcr)](https://crates.io/crates/dcr)
[![Homebrew](https://img.shields.io/badge/homebrew-dexoron%2Fdexoron-orange)](https://github.com/dexoron/homebrew-dexoron)
<br/>
[![GitHub Stars](https://img.shields.io/github/stars/dexoron/dcr?style=flat)](https://github.com/dexoron/dcr/stargazers)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](LICENSE)
</div>
<!-- Snap badge removed: classic confinement was denied for this project. -->
---
## Why DCR?
- **No boilerplate** — one config file, predictable structure
- **Cargo-like workflow** — `build`, `run`, `clean`, `test`, `add`
- **Cargo-like workflow** — `build`, `run`, `clean`, `test`, `add`, `fmt`
- **Cross-compilation** — full target triple support with short names
- **IDE integration** — VS Code, CLion, `compile_commands.json` out of the box
- **Dependencies** — path, git, and registry-based with lock file
- **Man pages** — `man dcr` and per-command pages (`man dcr-build`, `man dcr-new`, etc.)
---
@@ -36,7 +37,7 @@
<td><b>Linux (Dexoron Packages)</b></td>
<td>
See https://dcr.dexoron.su for settings youre packages manager
See https://pkg.dexoron.su for settings youre packages manager
**Arch Linux:**
@@ -106,7 +107,7 @@ cargo install dcr
<td>
```sh
curl -fsSL https://dcr.dexoron.su/install.sh | bash
curl -fsSL https://dcr.dexoron.su/install.sh | sh
```
</td>
@@ -116,7 +117,20 @@ curl -fsSL https://dcr.dexoron.su/install.sh | bash
<td>
```sh
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | bash
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
```
</td>
</tr>
<tr>
<td><b>Snap (Linux)</b></td>
<td>
> Snap Store publishing temporarily disabled — see [#51671](https://forum.snapcraft.io/t/classic-confinement-request-for-dcrup/51671).
> Download the `.snap` from [GitHub Releases](https://github.com/dexoron/dcr/releases/latest) and install manually:
```sh
sudo snap install --classic --dangerous dcrup_*.snap
```
</td>
@@ -200,24 +214,26 @@ kind = "bin"
| `dcr gen <vscode\|clion\|compile-commands>` | Generate IDE integration |
| `dcr setup` | Initialize DCR registry |
other in [docs/](docs/) and `dcr --help`
other in [docs/](docs/), `dcr --help` and `dcr <command> --help`
---
## Platforms(pre-build source)
## Platforms (pre-built binary)
<div align="center">
| OS | x86_64 | aarch64 | armv7 | i686 | riscv64 | GNU | Musl | MSVC | MinGW |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Linux | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
| macOS | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Windows | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| FreeBSD | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| OpenBSD | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| NetBSD | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| OS | x86_64 | aarch64 | armv7 | i686 | riscv64 |
|:---:|:---:|:---:|:---:|:---:|:---:|
| Linux (GNU) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Linux (Musl) | ✅ | ✅ | ✅ | ✅ | 🚧 |
| macOS | ✅ | ✅ | ❌ | ❌ | ❌ |
| Windows (MSVC) | ✅ | ✅ | ❌ | ✅ | ❌ |
| Windows (MinGW) | ✅ | 🚧 | ❌ | ❌ | ❌ |
| FreeBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
| OpenBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
| NetBSD | ✅ | 🚧 | ❌ | ❌ | ❌ |
✅ officially supported · ⚠️ community / best-effort · ❌ not supported
✅ pre-built binary · 🚧 planned · ❌ not supported by platform or CI
</div>
@@ -257,6 +273,7 @@ cargo test
<div align="center">
GPL-3.0 — see [LICENSE](LICENSE)<br/>
DCR is a build tool, not a library. The GPL applies only to DCR's own source code, not to projects built with it.<br/>
Made with ❤️ by [Dexoron](https://github.com/dexoron) and contributors.
</div>
</div>

60
TODO.md
View File

@@ -2,38 +2,35 @@
Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers.
## 🔥 High Priority (Next Releases)
**Legend:** `[ ]` — not implemented | `[-]` — partially implemented | `[x]` — implemented
- [x] **Git dependencies support**
`git = "https://github.com/user/repo.git#branch"` and `git = "..."@tag`
- [ ] **Conan and vcpkg integration**
Automatic download, installation and linking of packages
- [ ] **Full freestanding / embedded support**
`-nostdlib`, linker scripts, bare-metal targets (i686-elf, aarch64-none-elf, etc.)
- [ ] **EFI support**
Dedicated platform configuration for EFI targets
- [ ] **Custom project name**
Override binary name in `dcr.toml`
- [ ] **Custom build steps / targets**
Ability to define custom pre-build, post-build, codegen steps in `dcr.toml`
- [ ] **Improved mixed C/C++/ASM projects**
Better handling of `.S` and `.asm` files with correct flags
- [ ] **dcr publish + official package registry**
Publish and fetch your own libraries easily
## 🔥 High Priority
- [ ] **CLI Output Refactoring & Cleanup**
- [ ] Move all terminal strings to a dedicated module (`src/ui/messages.rs`)
- [ ] Standardize logging formats (`error:`, `warning:`, `status:`)
- [x] Add build execution time statistics to the final output (e.g., `Finished in 4.23s`)
- [ ] **Internationalization (i18n) / Localization**
- [ ] Integrate a lightweight localization engine (`fluent` or `gettext` pure-Rust port)
- [ ] Create initial translation catalogs (`en.ftl`, `ru.ftl`)
- [x] **Self-Update Stability Validation**
- [x] Self-update implemented via `self-replace` + `ureq` in `flag_update.rs`
- [-] **Freestanding / Embedded Automation**
- *`-ffreestanding` injected at compile time, `-nostdlib -static` at link time for bare-metal targets or `build.freestanding = true`. Config option `build.freestanding` (bool) added.*
- [ ] **Conan and vcpkg integration** Automatic download, installation and linking of packages
## ✅ Medium Priority
- [ ] **Static analysis** (`dcr check`)
Integration with clang-tidy, cppcheck, include-what-you-use
- [ ] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
- [-] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
*`dcr fmt` (clang-format) implemented; `dcr lint` does not exist yet*
- [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.)
- [ ] **Better cross-compilation experience**
Ready-to-use toolchains + automatic download
- [ ] **Linker script support** + direct linker control
- [ ] **Workspaces improvements**
Per-member include directories, selective builds, better dependency graph
- [ ] **Build-time code generation**
Support for protobuf, flatbuffers, Qt moc, custom generators
- [-] **Workspaces improvements** Per-member include directories, selective builds, better dependency graph
*Basic workspaces with dep ordering + topological sort work; advanced features missing*
- [-] **Build-time code generation** Support for protobuf, flatbuffers, Qt moc, custom generators
*Generic `build.steps` machinery + Qt toolchain (uic/moc/rcc) work; no dedicated protobuf/flatbuffers generators*
- [-] **dcr publish + official package registry** *Registry consumption (fetch, resolve, link) works; `dcr publish` command / local packing missing*
## 📌 Low Priority / Nice to Have
@@ -57,13 +54,12 @@ Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers
---
**Currently in progress:**
- ...
- UI/UX terminal output unification and benchmarking metrics.
**Completed in recent releases:**
- Workspaces
- Cross-compilation
- Extreme binary size optimization (-50% weight reduction via `ureq` + `native-certs`)
- Complete removal of `git2` and `openssl` in favor of system Git execution
- Workspaces & Cross-compilation (`--target <triple>`)
- IDE config generation (VS Code, CLion, compile_commands.json)
- Self-update (`dcr --update`)
- Header tracking
- EFI support & Linker script support (`build.ldscript`)
- Mixed C/C++/ASM compilation with custom `std::thread::scope` work-stealing engine

View File

@@ -18,4 +18,37 @@
fn main() {
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
println!("cargo:rustc-env=DCR_TARGET={target}");
// Let's try to retrieve the commit hash for the version
let commit = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
});
// Check if there are any uncommitted changes in the repository
let is_dirty = std::process::Command::new("git")
.args(["status", "--porcelain"])
.output()
.ok()
.map(|o| o.status.success() && !o.stdout.is_empty())
.unwrap_or(false);
if let Some(commit_hash) = commit {
let dirty_suffix = if is_dirty { "-dirty" } else { "" };
println!(
"cargo:rustc-env=DCR_GIT_INFO= (git {}{})",
commit_hash, dirty_suffix
);
} else {
println!("cargo:rustc-env=DCR_GIT_INFO=");
}
}

View File

@@ -44,6 +44,7 @@ dcr gen vscode
| [Self Update](/docs/self-update) | Auto-update |
| [FAQ](/docs/faq) | Frequently asked questions |
| [Changelog](/docs/changelog) | Version history |
| [License](/docs/license) | DCR and vendored library licenses |
| [Contributing](/docs/contributing) | DCR development |
## Features
@@ -58,3 +59,5 @@ dcr gen vscode
- **Registry + Git + Path** — three dependency mechanisms
- **IDE generation** — VS Code, CLion, compile_commands.json, JSON metadata
- **pkg-config** — automatic system library discovery
- **Per-command `--help`** — `dcr build --help`, `dcr new --help`, etc.
- **Man pages** — `man dcr`, `man dcr-build`, `man dcr-new`

View File

@@ -4,6 +4,50 @@ sidebar_label: Changelog
# Changelog
## 0.7.1 (2026-06-02)
### Added
- **`--help` for all commands** — `dcr build --help`, `dcr run --help`, `dcr new --help`,
`dcr init --help`, `dcr clean --help`, `dcr add --help`, `dcr fmt --help`,
`dcr setup --help`, `dcr tree --help`, `dcr gen --help`, `dcr --update --help`,
`dcr test --help`. All output uses styled headers (green) and usage lines (cyan).
- **Man pages** — 12 troff pages in `man/man1/`: `dcr.1`, `dcr-build.1`,
`dcr-run.1`, `dcr-new.1`, `dcr-init.1`, `dcr-clean.1`, `dcr-add.1`, `dcr-test.1`,
`dcr-gen.1`, `dcr-fmt.1`, `dcr-tree.1`, `dcr-setup.1`.
- **Man pages in all packaging** — install.sh, install_bsd.sh, AUR PKGBUILD,
Debian (cargo-deb), RPM (cargo-generate-rpm), Nix (postInstall), Homebrew
(resource), Snap, GitHub Release assets.
- **Project name validation** — `dcr init` and `dcr new` now validate names before
creating files. Only ASCII letters, digits, `_` and `-` are allowed.
- **`documentation` and `homepage` fields** — added to `Cargo.toml`.
- **Release profile optimizations** — `opt-level = "z"`, LTO, `codegen-units = 1`,
`panic = "abort"`, `strip = true` for smaller binaries.
- **Linux i686, armv7, riscv64 targets** — added to release workflow and install scripts.
- **Linux musl i686, armv7 targets** — cross-compiled via zigbuild in CI.
- **AUR packages** — `dcr-dev`, `dcr-dev-bin` publishing workflow.
- **Snap publishing** — automated Snapcraft publishing in CI.
- **Snap badge** — in README.
### Changed
- **`validate_package_name` made public** — callable from CLI commands.
- **README platform table** — reorganized: libc variants as separate OS rows,
removed extra columns. Linux architecture list updated.
- **`gen.rs` no-args output** — now uses styled output instead of raw `eprintln!`.
- **`tree.rs`, `fmt.rs`, and `setup.rs`** — now accept arguments for `--help`.
- **README install commands** — `| bash` → `| sh` for POSIX compatibility.
### Fixed
- **`dcr new <invalid-name>` creates directory then fails** — validation now
happens before any file operations.
- **`dcr init` on invalid directory name** — same fix.
- **Man pages missing in installed packages** — now shipped in all formats.
- **Windows drive letter false positive** — in dependency file parser.
- **CRLF (`\r\n`) breaks dependency parser** — now handles mixed line endings.
- **Newline after backslash not consumed** — in `parse_d_file` escape handling.
## 0.7.0 (2026-06-01)
### Added

View File

@@ -8,6 +8,8 @@ sidebar_label: Project commands (new, init)
Creates a new project with a standard structure and `dcr.toml`. The `name` argument is required.
Only ASCII letters, digits, underscores `_` and hyphens `-` are allowed in the project name.
```bash
dcr new my-app
```
@@ -25,4 +27,4 @@ mkdir app && cd app
dcr init
```
The project name is taken from the current directory name.
The project name is taken from the current directory name. The directory name must follow the same naming rules as `dcr new`.

View File

@@ -42,3 +42,18 @@ dcr fmt
Processes: `src/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}` and `tests/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}`.
Uses `.clang-format` at the project root (if present), otherwise default clang-format style.
## `dcr lint`
Runs `clang-tidy` on C/C++ source files for static analysis.
```bash
dcr lint # show diagnostics
dcr lint --fix # apply fixes automatically
dcr lint --help # detailed help
```
Processes: `src/**/*.{c,cpp,cxx,cc}` and `tests/**/*.{c,cpp,cxx,cc}`.
Without `--fix`, clang-tidy reports warnings and errors without modifying files.
With `--fix`, clang-tidy applies automatic suggestions in place.

View File

@@ -6,7 +6,7 @@ sidebar_label: Installation
## Linux (Dexoron Packages)
Configure your package manager at https://dcr.dexoron.su and install:
Configure your package manager at https://pkg.dexoron.su and install:
**Arch Linux**
```sh
@@ -36,6 +36,12 @@ brew tap dexoron/dexoron
brew install dcr
```
## Snap (Linux)
```sh
sudo snap install dcrup
```
## Nix (flake)
```sh
@@ -53,13 +59,13 @@ cargo install dcr
## Linux / macOS (script)
```sh
curl -fsSL https://dcr.dexoron.su/install.sh | bash
curl -fsSL https://dcr.dexoron.su/install.sh | sh
```
## BSD (script)
```sh
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | bash
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
```
## Windows (PowerShell)
@@ -84,6 +90,13 @@ Verify the installation:
dcr --version
```
Man pages are available after installation:
```bash
man dcr
man dcr-build
```
To use the registry, create `~/.dcr/config.toml`:
```toml

46
docs/license.mdx Normal file
View File

@@ -0,0 +1,46 @@
---
sidebar_label: License
---
# License
## DCR
DCR itself is [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) licensed.
> **Note:** DCR is a build tool, not a library. The GPL applies only to DCR's own source code. Projects built with DCR are not subject to DCR's license — their licensing is determined solely by their own code and dependencies.
## Vendored libraries
DCR statically links the following C libraries through its vendored Rust dependencies:
| Library | License | Bundled by |
|---|---|---|
| [libgit2](https://libgit2.org) | MIT OR Apache-2.0 | `git2` / `libgit2-sys` |
| [libssh2](https://libssh2.org) | MIT OR Apache-2.0 | `libgit2-sys` |
| [zlib](https://zlib.net) | MIT OR Apache-2.0 | `libgit2-sys` |
| [OpenSSL](https://openssl.org) | Apache-2.0 | `openssl-sys` / `openssl-src` |
| [ring](https://github.com/briansmith/ring) | Apache-2.0 AND ISC | `rustls` |
## Rust dependencies
DCR is written in Rust. The following notable Rust crates are statically linked:
| Crate | License |
|---|---|
| [`reqwest`](https://crates.io/crates/reqwest) | MIT OR Apache-2.0 |
| [`rustls`](https://crates.io/crates/rustls) | Apache-2.0 OR ISC OR MIT |
| [`serde`](https://crates.io/crates/serde) | MIT OR Apache-2.0 |
| [`toml`](https://crates.io/crates/toml) | MIT OR Apache-2.0 |
| [`sha2`](https://crates.io/crates/sha2) | MIT OR Apache-2.0 |
| [`git2`](https://crates.io/crates/git2) | MIT OR Apache-2.0 |
| [`openssl`](https://crates.io/crates/openssl) | Apache-2.0 |
| [`tokio`](https://crates.io/crates/tokio) | MIT |
| [`hyper`](https://crates.io/crates/hyper) | MIT |
| [`ctrlc`](https://crates.io/crates/ctrlc) | MIT OR Apache-2.0 |
Full dependency tree (143 packages) is available in [`Cargo.lock`](https://github.com/dexoron/dcr/blob/main/Cargo.lock). The overwhelming majority are dual-licensed under MIT OR Apache-2.0.
## Additional credits
- DCR's CLI design and project model are inspired by [Cargo](https://doc.rust-lang.org/cargo/).

View File

@@ -32,11 +32,37 @@ Set `inherit = false` to disable array inheritance (only profile's own arrays ar
## Built-in profiles
| Profile | Default cflags |
The default flags for each profile are composed from three config fields:
| Field | debug default | release default |
|-------|--------------|----------------|
| `opt_level` | `"0"` | `"3"` |
| `debug` | `true` | `false` |
| `warnings` | `["all", "extra"]` | `[]` |
Which produce the equivalent compiler flags:
| Profile | Effective flags |
|---------|---------------|
| `debug` | `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` |
| `release` | `-O3 -DNDEBUG` |
Additional build options can be toggled per-profile:
```toml
[build.release]
opt_level = "z"
lto = true
strip = true
panic = "abort"
codegen-units = "1"
[build.debug]
opt_level = "1"
debug = false
warnings = ["all", "error"]
```
## Target-specific profiles
```toml

View File

@@ -6,30 +6,60 @@ sidebar_label: Build system
## Compiler Backends
DCR supports 4 compilation backends. Selection is automatic based on file extension.
DCR supports 7 compilation backends. Selection is automatic based on file extension and `build.compiler` value.
| Backend | Files | When used |
|---------|-------|-----------|
| `unix_cc` | `.c`, `.cpp`, `.cxx`, `.cc`, `.S` | gcc/clang on Linux/macOS/BSD |
| `msvc` | `.c`, `.cpp`, `.cxx`, `.cc` | Windows (cl, clang-cl) |
| `gas` | `.s` | ARM/ARM64 assembler (no preprocessor) |
| `nasm` | `.asm` | x86/x86_64 assembler |
| `nasm` | `.asm`, `.s` | x86/x86_64 NASM assembler |
| `masm` | `.asm` | MASM (ml/ml64) on Windows |
| `fasm` | `.asm`, `.fasm` | Flat Assembler |
| `llvm_ir` | `.ll` | LLVM IR via `llc -filetype=obj` |
## Qt Support
DCR provides native Qt support for automatic meta-object handling (MOC, UIC, RCC).
Enable it by setting `build.qt = true` in `dcr.toml`. DCR will automatically detect Qt-related files (`.ui`, `.qrc`, `.h` with `Q_OBJECT`) and process them.
```toml
[build]
qt = true
```
*Note: Requires `qt6` modules (Core, Widgets, Gui, Svg) installed via `pkg-config`.*
Advanced customization is still possible via `build.steps` if special handling is needed:
```toml
[build.steps]
moc = "moc {in} -o {out}"
```
### Unix CC
- Compiler resolved via `resolve_compiler()`: `DCR_COMPILER` > `DCR_CC` > `[toolchain]` > `build.compiler` > `PATH`
- Supports `.d` files for header dependency tracking
- Flags: `-std=`, `-MMD -MF`, `-c -o`, `-I`, `-L`, `-l`
- Conditional flags based on config:
- `freestanding` or bare-metal target: `-ffreestanding` (compile), `-nostdlib -static` (link)
- `lto`: `-flto` (compile + link)
- `panic = "abort"`: `-fno-exceptions` (C++ only), `-fno-unwind-tables`, `-fno-asynchronous-unwind-tables`
### MSVC
- Supports cl.exe and clang-cl.exe
- Flags: `/std:`, `/Fo:`, `/Fe:`, `/I`, `/link`
### GAS / NASM
### GAS / NASM / MASM / FASM / LLVM-IR
- GAS: `-I`, `-c -o`, `--defsym`
- NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32)
- MASM: `/nologo /c /Fo<obj> <source>`
- FASM: `<source> <output>` (output path is the object file)
- LLVM IR: `-filetype=obj <source> -o <output>`
## Incremental Builds
@@ -44,7 +74,7 @@ Three levels of incrementality:
- `thread::scope` for thread pool
- Atomic task queue (`AtomicU64`)
- Mutex on stdout (`OUTPUT_MUTEX`)
- Thread count = `available_parallelism()`
- Thread count = `available_parallelism()`, capped by `build.codegen-units` if set
## Build Kinds

View File

@@ -37,9 +37,19 @@ target = "aarch64-unknown-linux-gnu"
# Auto: cflags += ["--target=aarch64-unknown-linux-gnu"]
```
## Bare-Metal
## Bare-Metal / Freestanding
For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR default flags are disabled (no system include paths, no `-l` libc).
For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR automatically:
1. **Disables default flags** — no system include paths, no `-l` libc
2. **Injects `-ffreestanding`** at compile time and `-nostdlib -static` at link time
You can also enable freestanding mode explicitly:
```toml
[build]
freestanding = true
```
```bash
dcr build --target aarch64-none-elf

View File

@@ -56,6 +56,10 @@ author = "John Doe"
## [build]
...
- `build.qt` — (bool) Enable automatic Qt meta-object handling (MOC, UIC, RCC). Requires `qt6` modules installed via `pkg-config`.
| Field | Default | Description |
|-------|---------|-------------|
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` |
@@ -64,7 +68,7 @@ author = "John Doe"
| `compiler` | `"clang"` | Preferred compiler |
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` |
| `target` | host | Target triple for cross-compilation |
| `platform` | `"native"` | `native`, `efi`, `freestanding` |
| `platform` | `"native"` | `native`, `efi` |
| `cflags` | `[]` | Additional C/C++/ASM flags |
| `ldflags` | `[]` | Additional linker flags |
| `filename` | `""` | Custom output file name |
@@ -77,6 +81,15 @@ author = "John Doe"
| `clean` | `[]` | Glob patterns for custom clean paths |
| `out_dir` | `""` | Custom output directory |
| `workspace_only` | `false` | Workspace-only, not built standalone |
| `freestanding` | `false` | Compile in freestanding mode (`-ffreestanding` + `-nostdlib -static`) |
| `opt_level` | `"0"` (debug) / `"3"` (release) | Optimization level: `0`-`3`, `"s"`, `"z"` |
| `debug` | `true` (debug) / `false` (release) | Emit debug symbols (`-g`) |
| `lto` | `false` | Link-time optimization (`-flto` for both compiler and linker) |
| `strip` | `false` | Strip symbols from output (`-s` in ldflags) |
| `warnings` | `["all", "extra"]` (debug) / `[]` (release) | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`) |
| `panic` | `""` | Panic strategy: `"abort"` disables exceptions and unwind tables |
| `codegen-units` | `""` | Max parallel compilation jobs (`"0"` = auto) |
| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) |
Settings from raw config (not in typed struct):
- `pkg_config` — list of pkg-config packages
@@ -93,6 +106,11 @@ cxx_standard = "c++23"
compiler = "clang"
kind = "sharedlib"
cflags = ["-Wall", "-Wextra"]
opt_level = "z"
lto = true
strip = true
panic = "abort"
codegen-units = "2"
```
## [toolchain]

View File

@@ -64,6 +64,10 @@
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
postInstall = ''
install -Dm644 man/man1/*.1 -t $out/share/man/man1/
'';
# Tests require external services; run separately.
doCheck = false;
};

View File

@@ -47,10 +47,16 @@ function Select-Mode {
}
function Get-Target {
if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') {
Fail "Only x86_64 Windows is supported, current architecture: $env:PROCESSOR_ARCHITECTURE"
$arch = $env:PROCESSOR_ARCHITECTURE
if ($env:PROCESSOR_ARCHITEW6432) {
$arch = $env:PROCESSOR_ARCHITEW6432
}
switch ($arch) {
'AMD64' { return 'x86_64-pc-windows-msvc' }
'ARM64' { return 'aarch64-pc-windows-msvc' }
'x86' { return 'i686-pc-windows-msvc' }
default { Fail "Unsupported Windows architecture: $arch" }
}
return 'x86_64-pc-windows-msvc'
}
function Fetch-ReleaseJson($channel) {

View File

@@ -4,6 +4,7 @@ set -Eeuo pipefail
TMPDIR="/tmp/dcr-install"
INSTALL_PATH="$HOME/.local/share/dcr"
BINPATH="$HOME/.local/bin"
MANPATH="${XDG_DATA_HOME:-$HOME/.local/share}/man"
LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr"
GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
@@ -43,11 +44,20 @@ detect_target() {
case "$os:$arch" in
Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;;
Linux:aarch64|Linux:arm64) TARGET_TRIPLE="aarch64-unknown-linux-gnu" ;;
Linux:i686|Linux:i386) TARGET_TRIPLE="i686-unknown-linux-gnu" ;;
Linux:armv7l|Linux:armv7) TARGET_TRIPLE="armv7-unknown-linux-gnueabihf" ;;
Linux:riscv64) TARGET_TRIPLE="riscv64gc-unknown-linux-gnu" ;;
Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;;
FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;;
FreeBSD:aarch64|FreeBSD:arm64) TARGET_TRIPLE="aarch64-unknown-freebsd" ;;
FreeBSD:i686|FreeBSD:i386) TARGET_TRIPLE="i686-unknown-freebsd" ;;
OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;;
OpenBSD:aarch64|OpenBSD:arm64) TARGET_TRIPLE="aarch64-unknown-openbsd" ;;
OpenBSD:i686|OpenBSD:i386) TARGET_TRIPLE="i686-unknown-openbsd" ;;
NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;;
NetBSD:aarch64|NetBSD:arm64) TARGET_TRIPLE="aarch64-unknown-netbsd" ;;
NetBSD:i686|NetBSD:i386) TARGET_TRIPLE="i686-unknown-netbsd" ;;
*) error "Unsupported platform: $os/$arch"; exit 1 ;;
esac
@@ -190,6 +200,16 @@ install_link() {
success "Command 'dcr' added to $BINPATH"
}
install_man() {
log "Installing man pages..."
local man_src="$TMPDIR/man/man1"
if [[ -d "$man_src" ]]; then
mkdir -p "$MANPATH/man1"
cp "$man_src"/*.1 "$MANPATH/man1/"
success "Man pages installed to $MANPATH/man1"
fi
}
check_path() {
if ! echo "$PATH" | grep -q "$BINPATH"; then
warn "Directory $BINPATH not found in PATH"
@@ -222,6 +242,7 @@ main() {
fi
install_link
install_man
check_path
cleanup

View File

@@ -4,6 +4,7 @@ set -eu
TMPDIR="/tmp/dcr-install"
INSTALL_PATH="$HOME/.local/share/dcr"
BINPATH="$HOME/.local/bin"
MANPATH="${XDG_DATA_HOME:-$HOME/.local/share}/man"
LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr"
GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
@@ -39,8 +40,14 @@ detect_target() {
case "$os:$arch" in
FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;;
FreeBSD:aarch64|FreeBSD:arm64) TARGET_TRIPLE="aarch64-unknown-freebsd" ;;
FreeBSD:i686|FreeBSD:i386) TARGET_TRIPLE="i686-unknown-freebsd" ;;
OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;;
OpenBSD:aarch64|OpenBSD:arm64) TARGET_TRIPLE="aarch64-unknown-openbsd" ;;
OpenBSD:i686|OpenBSD:i386) TARGET_TRIPLE="i686-unknown-openbsd" ;;
NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;;
NetBSD:aarch64|NetBSD:arm64) TARGET_TRIPLE="aarch64-unknown-netbsd" ;;
NetBSD:i686|NetBSD:i386) TARGET_TRIPLE="i686-unknown-netbsd" ;;
DragonFly:x86_64|DragonFly:amd64) TARGET_TRIPLE="x86_64-unknown-dragonfly" ;;
*) error "Unsupported BSD platform: $os/$arch"; exit 1 ;;
esac
@@ -181,6 +188,16 @@ install_link() {
success "Command 'dcr' added to $BINPATH"
}
install_man() {
log "Installing man pages..."
man_src="$TMPDIR/man/man1"
if [ -d "$man_src" ]; then
mkdir -p "$MANPATH/man1"
cp "$man_src"/*.1 "$MANPATH/man1/"
success "Man pages installed to $MANPATH/man1"
fi
}
check_path() {
if ! echo "$PATH" | grep -q "$BINPATH"; then
warn "Directory $BINPATH not found in PATH"
@@ -213,6 +230,7 @@ main() {
fi
install_link
install_man
check_path
cleanup

42
man/man1/dcr-add.1 Normal file
View File

@@ -0,0 +1,42 @@
.TH DCR-ADD 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-add \- Add a dependency to a DCR project
.SH SYNOPSIS
.B dcr add \fI<name>\fR \fI<source>\fR
[\fB\-\-branch\fR \fI<b>\fR]
[\fB\-\-tag\fR \fI<t>\fR]
[\fB\-\-rev\fR \fI<r>\fR]
.SH DESCRIPTION
Adds a dependency to the project.
.SH SOURCES
.TP
.B user/repo
GitHub shorthand.
.TP
.B github:user/repo
Full GitHub repository.
.TP
.B gitlab:user/repo
GitLab repository.
.TP
.B git:host.com/user/repo
Generic git repository.
.TP
.B path:./path/to/lib
Local path dependency.
.TP
.B <version>
Version from registry.
.SH OPTIONS
.TP
.B \-\-branch \fI<name>\fR
Use a specific branch.
.TP
.B \-\-tag \fI<name>\fR
Use a specific tag.
.TP
.B \-\-rev \fI<hash>\fR
Use a specific commit hash.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-tree (1)

37
man/man1/dcr-build.1 Normal file
View File

@@ -0,0 +1,37 @@
.TH DCR-BUILD 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-build \- Compile a DCR project
.SH SYNOPSIS
.B dcr build
[\fB\-\-debug\fR | \fB\-\-release\fR]
[\fB\-\-target\fR \fI<triple>\fR]
[\fB\-\-force\fR]
[\fB\-\-clean\fR]
[\fB\-\-verbose\fR]
[\fB\-\-workspace\fR \fI<name>\fR]
.SH DESCRIPTION
Compiles the project. Default profile is \fB\-\-debug\fR.
.SH OPTIONS
.TP
.B \-\-debug
Build with debug profile (default). Enables debug symbols.
.TP
.B \-\-release
Build with release profile. Enables full optimization.
.TP
.B \-\-target \fI<triple>\fR
Cross-compile for the given target.
.TP
.B \-\-force
Force a full rebuild, ignoring cached artifacts.
.TP
.B \-\-clean
Clean before building.
.TP
.B \-\-verbose
Print detailed build output including compiler commands.
.TP
.B \-\-workspace \fI<name>\fR
Build a specific workspace member.
.SH SEE ALSO
.BR dcr (1)

26
man/man1/dcr-clean.1 Normal file
View File

@@ -0,0 +1,26 @@
.TH DCR-CLEAN 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-clean \- Remove build artifacts
.SH SYNOPSIS
.B dcr clean
[\fB\-\-debug\fR | \fB\-\-release\fR]
[\fB\-\-target\fR \fI<triple>\fR]
[\fB\-\-all\fR]
.SH DESCRIPTION
Removes build artifacts from the target directory.
.SH OPTIONS
.TP
.B \-\-debug
Clean debug artifacts (default).
.TP
.B \-\-release
Clean release artifacts.
.TP
.B \-\-target \fI<triple>\fR
Clean artifacts for a specific target.
.TP
.B \-\-all
Clean all workspace members.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-build (1)

10
man/man1/dcr-fmt.1 Normal file
View File

@@ -0,0 +1,10 @@
.TH DCR-FMT 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-fmt \- Format C/C++ source files
.SH SYNOPSIS
.B dcr fmt
.SH DESCRIPTION
Formats all C/C++ source files using clang-format. Scans \fBsrc/\fR
and \fBtests/\fR directories recursively.
.SH SEE ALSO
.BR dcr (1)

23
man/man1/dcr-gen.1 Normal file
View File

@@ -0,0 +1,23 @@
.TH DCR-GEN 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-gen \- Generate IDE and tooling integration files
.SH SYNOPSIS
.B dcr gen \fI<subcommand>\fR
.SH DESCRIPTION
Generates IDE and tooling integration files for DCR projects.
.SH SUBCOMMANDS
.TP
.B project-info
Print project metadata as JSON to stdout.
.TP
.B compile-commands
Generate \fBcompile_commands.json\fR for IDE tooling.
.TP
.B vscode
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
.TP
.B clion
Generate \fB.idea/\fR integration files for CLion.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-build (1)

11
man/man1/dcr-init.1 Normal file
View File

@@ -0,0 +1,11 @@
.TH DCR-INIT 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-init \- Initialize a directory as a DCR project
.SH SYNOPSIS
.B dcr init
.SH DESCRIPTION
Initializes the current directory as a DCR project. The directory must
be empty.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-new (1)

13
man/man1/dcr-new.1 Normal file
View File

@@ -0,0 +1,13 @@
.TH DCR-NEW 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-new \- Create a new DCR project
.SH SYNOPSIS
.B dcr new \fI<name>\fR
.SH DESCRIPTION
Creates a new C/C++ project with the given name. The name may only
contain ASCII letters, digits, underscores and hyphens.
.SH EXAMPLE
.B dcr new my-project
.SH SEE ALSO
.BR dcr (1),
.BR dcr-init (1)

35
man/man1/dcr-run.1 Normal file
View File

@@ -0,0 +1,35 @@
.TH DCR-RUN 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-run \- Build and run a DCR project
.SH SYNOPSIS
.B dcr run
[\fB\-\-debug\fR | \fB\-\-release\fR]
[\fB\-\-target\fR \fI<triple>\fR]
[\fB\-\-force\fR]
[\fB\-\-clean\fR]
[\fB\-\-verbose\fR]
.SH DESCRIPTION
Builds and runs the project. Only available for projects with
\fBkind = "bin"\fR.
.SH OPTIONS
.TP
.B \-\-debug
Run with debug profile (default).
.TP
.B \-\-release
Run with release profile.
.TP
.B \-\-target \fI<triple>\fR
Cross-compile for the given target.
.TP
.B \-\-force
Force a full rebuild.
.TP
.B \-\-clean
Clean before building.
.TP
.B \-\-verbose
Print detailed build output.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-build (1)

11
man/man1/dcr-setup.1 Normal file
View File

@@ -0,0 +1,11 @@
.TH DCR-SETUP 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-setup \- Set up DCR registries
.SH SYNOPSIS
.B dcr setup
.SH DESCRIPTION
Sets up DCR registries. Downloads and indexes package registries
for dependency resolution.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-add (1)

24
man/man1/dcr-test.1 Normal file
View File

@@ -0,0 +1,24 @@
.TH DCR-TEST 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-test \- Run project tests
.SH SYNOPSIS
.B dcr test
[\fB\-\-init\fR]
[\fB\-\-debug\fR | \fB\-\-release\fR]
.SH ALIASES
.B dcr tests
.SH DESCRIPTION
Runs project tests and prints a unified testsuite report.
.SH OPTIONS
.TP
.B \-\-init
Create \fBtests/dcr_test.h\fR in the current project.
.TP
.B \-\-debug
Build and run tests with debug profile (default).
.TP
.B \-\-release
Build and run tests with release profile.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-build (1)

11
man/man1/dcr-tree.1 Normal file
View File

@@ -0,0 +1,11 @@
.TH DCR-TREE 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr-tree \- Display the dependency tree
.SH SYNOPSIS
.B dcr tree
.SH DESCRIPTION
Displays the dependency tree of the current project, showing all
transitive dependencies and their versions.
.SH SEE ALSO
.BR dcr (1),
.BR dcr-add (1)

204
man/man1/dcr.1 Normal file
View File

@@ -0,0 +1,204 @@
.TH DCR 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.SH NAME
dcr \- Cargo\-like build tool and package manager for C/C++ projects
.SH SYNOPSIS
.B dcr
[\fIcommand\fR] [\fIoptions\fR]
.SH DESCRIPTION
DCR is a comprehensive build tool and package manager for C and C++ projects,
inspired by Cargo from the Rust ecosystem. It handles project creation,
building, dependency management, testing, formatting, and IDE integration.
.SH COMMANDS
.TP
.B new \fI<name>\fR
Create a new C/C++ project with the given name. Only ASCII letters, digits,
underscores and hyphens are allowed.
.TP
.B init
Initialize the current (empty) directory as a DCR project.
.TP
.B build [\fIoptions\fR]
Compile the project. Default profile is \fB\-\-debug\fR.
.TP
.B run [\fIoptions\fR]
Build and run the project. Only available for binary projects.
.TP
.B clean [\fIoptions\fR]
Remove build artifacts from the target directory.
.TP
.B test [\fI\-\-init\fR] [\fI\-\-debug\fR|\fI\-\-release\fR]
Run project tests (alias: \fBdcr tests\fR).
.TP
.B add \fI<name>\fR \fI<source>\fR [\fIoptions\fR]
Add a dependency to the project.
.TP
.B tree
Display the dependency tree of the current project.
.TP
.B fmt
Format all C/C++ source files using clang-format.
.TP
.B gen \fI<subcommand>\fR
Generate IDE integration files (vscode, clion, compile-commands, project-info).
.TP
.B setup
Set up DCR registries for dependency resolution.
.SH GLOBAL FLAGS
.TP
.B \-\-help
Show this help message and exit.
.TP
.B \-\-version
Print version information and exit.
.TP
.B \-\-update
Update DCR to the latest version from GitHub Releases.
.SH BUILD OPTIONS
The following options apply to \fBbuild\fR and \fBrun\fR commands:
.TP
.B \-\-debug
Build with debug profile (default). Enables debug symbols and minimal optimization.
.TP
.B \-\-release
Build with release profile. Enables full optimization without debug symbols.
.TP
.B \-\-target \fI<triple>\fR
Cross-compile for the given target (e.g. \fBaarch64-linux-gnu\fR, \fBx86_64-w64-mingw32\fR).
Short names like \fBlinux\fR, \fBmacos\fR, \fBwindows\fR are also supported.
.TP
.B \-\-force
Force a full rebuild, ignoring cached build artifacts.
.TP
.B \-\-clean
Clean build artifacts before building.
.TP
.B \-\-verbose
Print detailed build output including compiler commands.
.TP
.B \-\-workspace \fI<name>\fR
Build a specific workspace member.
.SH CLEAN OPTIONS
.TP
.B \-\-debug
Clean debug artifacts (default).
.TP
.B \-\-release
Clean release artifacts.
.TP
.B \-\-target \fI<triple>\fR
Clean artifacts for a specific target.
.TP
.B \-\-all
Clean all workspace members.
.SH ADD OPTIONS
.TP
.B \-\-branch \fI<name>\fR
Use a specific branch when adding a git dependency.
.TP
.B \-\-tag \fI<name>\fR
Use a specific tag when adding a git dependency.
.TP
.B \-\-rev \fI<hash>\fR
Use a specific commit hash when adding a git dependency.
.SH DEPENDENCY SOURCES
.TP
.B user/repo
GitHub shorthand (e.g. \fBuser/repo\fR).
.TP
.B github:user/repo
Full GitHub repository URL shorthand.
.TP
.B gitlab:user/repo
GitLab repository URL shorthand.
.TP
.B git:host.com/user/repo
Generic git repository.
.TP
.B path:./path/to/lib
Local path dependency.
.TP
.B \fI<version>\fR
Version from registry (e.g. \fB1.2.3\fR).
.SH GEN SUBCOMMANDS
.TP
.B project-info
Print project metadata as JSON to stdout.
.TP
.B compile-commands
Generate \fBcompile_commands.json\fR for IDE tooling.
.TP
.B vscode
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
.TP
.B clion
Generate \fB.idea/\fR integration files for CLion.
.SH TEST OPTIONS
.TP
.B \-\-init
Create \fBtests/dcr_test.h\fR in the current project.
.TP
.B \-\-debug
Build and run tests with debug profile (default).
.TP
.B \-\-release
Build and run tests with release profile.
.SH PROJECT CONFIGURATION
DCR uses \fBdcr.toml\fR for project configuration. Key settings include:
.TP
.B package.name
Project name (required).
.TP
.B package.version
Project version (default: \fB0.1.0\fR).
.TP
.B build.language
Programming language: \fBc\fR or \fBc++\fR (default: \fBc\fR).
.TP
.B build.standard
Language standard (e.g. \fBc11\fR, \fBc17\fR, \fBc++20\fR; default: \fBc11\fR).
.TP
.B build.compiler
Compiler to use: \fBclang\fR, \fBgcc\fR, \fBcl\fR, \fBclang-cl\fR (default: \fBclang\fR).
.TP
.B build.kind
Output type: \fBbin\fR, \fBlib\fR (static or dynamic), \fBstaticlib\fR, \fBsharedlib\fR (default: \fBbin\fR).
.SH FILES
.TP
.I dcr.toml
Project manifest file.
.TP
.I ~/.dcr/config.toml
User-level DCR configuration.
.TP
.I ~/.dcr/index.json
Local registry index.
.SH ENVIRONMENT
.TP
.B DCR_COMPILER
Override the compiler used for building.
.TP
.B DCR_CC
Alternative way to override the compiler.
.SH EXAMPLES
.TP
Create a new project:
.B dcr new hello
.TP
Initialize from current directory:
.B dcr init
.TP
Build with release profile:
.B dcr build \-\-release
.TP
Cross-compile for Windows:
.B dcr build \-\-target windows
.TP
Add a dependency:
.BR "dcr add curl git:https://github.com/curl/curl" .
.TP
Generate VS Code integration:
.B dcr gen vscode
.SH SEE ALSO
.IR https://dcr.dexoron.su ", " https://github.com/dexoron/dcr
.SH AUTHORS
Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>

Submodule packaging/aur/dcr-dev-git added at 93bdce858b

Submodule packaging/aur/dcr-git updated: 12038a613e...2c5743a456

View File

@@ -27,4 +27,7 @@ package() {
cd "$srcdir/$pkgname-$pkgver"
install -Dm755 "target/release/dcr" "$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
for manpage in man/man1/*.1; do
install -Dm644 "$manpage" "$pkgdir/usr/share/man/man1/$(basename "$manpage")"
done
}

View File

@@ -15,11 +15,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::deps::register;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use toml::Value;
use toml::map::Map;
@@ -34,6 +34,31 @@ pub struct AddArgs {
}
pub fn add(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Adds a dependency to the project.");
println!();
printc("SOURCES:", BOLD_GREEN);
println!(" user/repo GitHub shorthand");
println!(" github:user/repo GitHub repository");
println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository");
println!(" path:./path/to/lib Local path dependency");
println!(" <version> Version from registry");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --branch <b> Use a specific branch");
println!(" --tag <t> Use a specific tag");
println!(" --rev <r> Use a specific commit hash");
return 0;
}
let add_args = match parse_add_args(args) {
Ok(a) => a,
Err(code) => return code,

View File

@@ -17,19 +17,20 @@
use crate::cli::clean::clean;
use crate::cli::flags::parse_build_run_flags;
use crate::core::build_config::Config;
use crate::core::builder::common;
use crate::core::builder::{BuildContext, build as build_project, collect_sources};
use crate::core::config::Config;
use crate::core::deps::{register, resolve_deps};
use crate::core::workspace::parse_workspace;
use crate::utils::build::{
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile,
normalize_target_os, parse_version_info, profile_table, resolve_compiler,
resolve_pkg_config_flags, resolve_tool,
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform,
normalize_target, normalize_target_os, parse_version_info, prepend_clang_target_flag,
profile_table, resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars,
};
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob;
use sha2::{Digest, Sha256};
use std::fs;
@@ -42,6 +43,27 @@ static BUILD_INTERRUPTED: AtomicBool = AtomicBool::new(false);
static SIGNAL_HANDLER: Once = Once::new();
pub fn build(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Compiles the project. Default profile is --debug.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Build with debug profile (default)");
println!(" --release Build with release profile");
println!(" --target <triple> Cross-compile for the given target");
println!(" --force Force a full rebuild");
println!(" --clean Clean before building");
println!(" --verbose Print detailed build output");
println!(" --workspace <name> Build a specific workspace member");
return 0;
}
install_signal_handler();
BUILD_INTERRUPTED.store(false, Ordering::SeqCst);
let start_dir = match std::env::current_dir() {
@@ -79,27 +101,7 @@ pub fn build(args: &[String]) -> i32 {
}
}
if flags.target.is_none() {
let default_target = if cfg!(target_os = "linux") {
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
format!(
"{}-unknown-{}",
std::env::consts::ARCH,
std::env::consts::OS
)
} else {
"unknown".to_string()
};
flags.target = Some(default_target);
flags.target = Some(default_target_triple());
}
if flags.clean {
let mut clean_args = Vec::new();
@@ -631,6 +633,7 @@ fn build_project_at(
let project_compiler =
get_string_with_profile_and_target(&config, "compiler", profile, build_target);
let build_language = get_language_with_profile(&config, profile)?;
let build_qt = get_bool_with_profile(&config, "qt", profile, false);
let build_standard =
get_string_with_profile_and_target(&config, "standard", profile, build_target);
let build_cxx_standard =
@@ -681,6 +684,63 @@ fn build_project_at(
let build_roots =
get_list_with_profile_and_target(&config, "roots", profile, build_target)?;
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
let freestanding = get_bool_with_profile(&config, "freestanding", profile, false);
let lto = get_bool_with_profile(&config, "lto", profile, false);
let strip = get_bool_with_profile(&config, "strip", profile, false);
let opt_level =
get_string_with_profile_and_target(&config, "opt_level", profile, build_target);
let debug = get_bool_with_profile(&config, "debug", profile, profile != "release");
let warnings =
get_list_with_profile_and_target(&config, "warnings", profile, build_target)?;
let panic_val = get_string_with_profile_and_target(&config, "panic", profile, build_target);
let panic_abort = panic_val == "abort";
let codegen_units: usize =
get_string_with_profile_and_target(&config, "codegen-units", profile, build_target)
.parse()
.unwrap_or(0);
if build_cflags.is_empty() && !freestanding && !is_bare_metal_target(build_target) {
if !opt_level.is_empty() {
build_cflags.push(format!("-O{opt_level}"));
} else {
build_cflags.push(match profile {
"release" => "-O3".to_string(),
_ => "-O0".to_string(),
});
}
if debug {
build_cflags.push("-g".to_string());
}
if warnings.is_empty() {
if profile == "debug" {
build_cflags.push("-Wall".to_string());
build_cflags.push("-Wextra".to_string());
}
} else {
for w in &warnings {
build_cflags.push(format!("-W{w}"));
}
}
match profile {
"debug" => {
build_cflags.push("-fno-omit-frame-pointer".to_string());
build_cflags.push("-DDCR_DEBUG".to_string());
}
"release" => {
build_cflags.push("-DNDEBUG".to_string());
}
_ => {}
}
}
if lto {
build_cflags.push("-flto".to_string());
build_ldflags.push("-flto".to_string());
}
if strip {
build_ldflags.push("-s".to_string());
}
let build_expects =
get_list_with_profile_and_target(&config, "expect", profile, build_target)?;
let pkg_configs =
@@ -697,19 +757,13 @@ fn build_project_at(
tc_cxx.as_deref(),
tc_as.as_deref(),
);
if let Some(t) = build_target
&& !t.trim().is_empty()
&& resolved_compiler.to_lowercase().contains("clang")
{
let target_flag = format!("--target={}", t.trim());
if !build_cflags
.iter()
.any(|f| f == &target_flag || f.starts_with("--target="))
{
build_cflags.insert(0, target_flag);
}
}
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler);
prepend_clang_target_flag(
&mut build_ldflags,
build_target,
resolved_linker.as_deref().unwrap_or(&resolved_compiler),
);
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile);
@@ -877,21 +931,19 @@ fn build_project_at(
}
let version_info = parse_version_info(&project_version);
let substitute = |s: &str| -> String {
s.replace("{profile}", profile)
.replace("{name}", &project_name)
.replace("{version}", &version_info.full)
.replace("{version_major}", &version_info.major)
.replace("{version_minor}", &version_info.minor)
.replace("{version_patch}", &version_info.patch)
.replace("{version_suffix}", &version_info.suffix)
.replace("{version_suffix_dash}", &version_info.suffix_dash)
};
let resolved_cflags: Vec<String> = resolved_cflags.iter().map(|f| substitute(f)).collect();
let modified_ldflags: Vec<String> =
resolved_ldflags.iter().map(|f| substitute(f)).collect();
let merged_include_dirs: Vec<String> =
merged_include_dirs.iter().map(|f| substitute(f)).collect();
let resolved_cflags: Vec<String> = resolved_cflags
.iter()
.map(|f| substitute_vars(f, &version_info, profile, &project_name))
.collect();
let modified_ldflags: Vec<String> = resolved_ldflags
.iter()
.map(|f| substitute_vars(f, &version_info, profile, &project_name))
.collect();
let merged_include_dirs: Vec<String> = merged_include_dirs
.iter()
.map(|f| substitute_vars(f, &version_info, profile, &project_name))
.collect();
let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs);
let ctx = BuildContext {
profile,
project_name: &project_name,
@@ -909,11 +961,17 @@ fn build_project_at(
platform: normalize_platform(&build_platform),
linker: resolved_linker.as_deref(),
archiver: resolved_archiver.as_deref(),
moc: Some(&tool_execs.moc),
uic: Some(&tool_execs.uic),
rcc: Some(&tool_execs.rcc),
package_type: if build_type.is_empty() {
None
} else {
Some(build_type.as_str())
},
freestanding,
panic_abort,
codegen_units,
source_roots: &source_roots,
exclude_dirs: &combined_excludes,
include_paths: &combined_includes,
@@ -933,15 +991,8 @@ fn build_project_at(
Some(output_extension.as_str())
},
verbose,
qt: build_qt,
};
if std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] debug: compiler={}", ctx.compiler);
eprintln!("[dcr] debug: cflags={:?}", ctx.cflags);
eprintln!("[dcr] debug: ldflags={:?}", ctx.ldflags);
eprintln!("[dcr] debug: lib_dirs={:?}", ctx.lib_dirs);
eprintln!("[dcr] debug: libs={:?}", ctx.libs);
}
let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs);
let step_flags =
build_step_flags(&resolved_cflags, &resolved.include_dirs, &resolved_compiler);
let step_vars = StepVars {
@@ -1049,29 +1100,6 @@ fn package_library(ctx: &BuildContext, headers: &[PathBuf]) -> Result<(), String
Ok(())
}
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(target.trim());
if trimmed.is_empty() {
None
} else {
Some(format!("target/{trimmed}/{profile}"))
}
}
fn normalize_kind(kind: &str) -> &str {
let trimmed = kind.trim();
if trimmed.is_empty() { "bin" } else { trimmed }
}
fn normalize_platform(platform: &str) -> Option<&str> {
let trimmed = platform.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
struct ToolchainExecs {
uic: String,
moc: String,
@@ -1186,12 +1214,11 @@ fn detect_qt6_tool_variant(tool: &str) -> Option<String> {
}
fn is_on_path(cmd: &str) -> bool {
std::process::Command::new("sh")
.arg("-c")
.arg(format!("command -v {cmd}"))
.output()
.map(|o| o.status.success())
.unwrap_or(false)
if Path::new(cmd).is_file() {
return true;
}
std::env::var_os("PATH")
.is_some_and(|paths| std::env::split_paths(&paths).any(|dir| dir.join(cmd).is_file()))
}
#[derive(Clone)]
@@ -1314,21 +1341,15 @@ fn substitute_step_cmd(
stem: &str,
vars: &StepVars,
) -> String {
template
.replace("{in}", &input.to_string_lossy())
let info = make_version_info(vars);
let s = substitute_vars(template, &info, vars.profile, "");
s.replace("{in}", &input.to_string_lossy())
.replace("{out}", &output.to_string_lossy())
.replace("{uic}", &tools.uic)
.replace("{moc}", &tools.moc)
.replace("{rcc}", &tools.rcc)
.replace("{cflags}", step_flags)
.replace("{stem}", stem)
.replace("{profile}", vars.profile)
.replace("{version}", vars.version)
.replace("{version_major}", vars.version_major)
.replace("{version_minor}", vars.version_minor)
.replace("{version_patch}", vars.version_patch)
.replace("{version_suffix}", vars.version_suffix)
.replace("{version_suffix_dash}", vars.version_suffix_dash)
}
fn build_step_flags(cflags: &[String], include_dirs: &[String], compiler: &str) -> String {
@@ -1351,14 +1372,9 @@ fn build_step_flags(cflags: &[String], include_dirs: &[String], compiler: &str)
out.push(format!("/I{dir}"));
}
}
let mut dedup = Vec::new();
for item in out {
if !dedup.contains(&item) {
dedup.push(item);
}
}
dedup
.into_iter()
out.sort();
out.dedup();
out.into_iter()
.map(quote_step_arg)
.collect::<Vec<_>>()
.join(" ")
@@ -1419,16 +1435,21 @@ fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, St
Ok(false)
}
fn make_version_info(vars: &StepVars) -> crate::utils::build::VersionInfo {
crate::utils::build::VersionInfo {
full: vars.version.to_string(),
major: vars.version_major.to_string(),
minor: vars.version_minor.to_string(),
patch: vars.version_patch.to_string(),
suffix: vars.version_suffix.to_string(),
suffix_dash: vars.version_suffix_dash.to_string(),
}
}
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
template
.replace("{stem}", stem)
.replace("{profile}", vars.profile)
.replace("{version}", vars.version)
.replace("{version_major}", vars.version_major)
.replace("{version_minor}", vars.version_minor)
.replace("{version_patch}", vars.version_patch)
.replace("{version_suffix}", vars.version_suffix)
.replace("{version_suffix_dash}", vars.version_suffix_dash)
let info = make_version_info(vars);
let s = substitute_vars(template, &info, vars.profile, "");
s.replace("{stem}", stem)
}
struct StepVars<'a> {
@@ -1660,9 +1681,5 @@ fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), Strin
}
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
crate::utils::fs::to_hex(bytes)
}

View File

@@ -15,18 +15,35 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::flags;
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::parse_version_info;
use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_GREEN, colored};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob;
use std::fs;
use std::path::Path;
pub fn clean(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr clean [--debug | --release] [--target <triple>] [--all]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Removes build artifacts from the target directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Clean debug artifacts (default)");
println!(" --release Clean release artifacts");
println!(" --target <triple> Clean artifacts for a specific target");
println!(" --all Clean all workspace members");
return 0;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
@@ -88,7 +105,7 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
}
if arg.starts_with("--") {
let candidate = arg.trim_start_matches("--").to_string();
if flags(&candidate).is_some() {
if !default_profile_flags(&candidate).is_empty() {
if profile.is_some() {
return Err("Duplicate profile flag".to_string());
}
@@ -118,28 +135,7 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.or_else(|| {
Some(if cfg!(target_os = "linux") {
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
format!(
"{}-unknown-{}",
std::env::consts::ARCH,
std::env::consts::OS
)
} else {
"unknown".to_string()
})
});
.or_else(|| Some(default_target_triple()));
if flags.all
&& let Some(workspace) = parse_workspace(
@@ -230,7 +226,7 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
.filter_map(|item| item.as_str())
.map(|s| {
let value = s.replace("{profile}", profile);
substitute_version_vars(&value, config)
substitute_clean_vars(&value, config)
})
.collect::<Vec<String>>(),
None => Vec::new(),
@@ -251,17 +247,11 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> {
Ok(())
}
fn substitute_version_vars(value: &str, config: &Config) -> String {
fn substitute_clean_vars(value: &str, config: &Config) -> String {
let version = config
.get("package.version")
.and_then(|v| v.as_str())
.unwrap_or("");
let info = parse_version_info(version);
value
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
crate::utils::build::substitute_version_vars(value, &info)
}

View File

@@ -16,9 +16,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::{error, warn};
use reqwest::blocking::Client;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use serde::Deserialize;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use std::process::Command;
@@ -39,6 +40,15 @@ struct ReleaseAsset {
}
pub fn flag_update(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr --update", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Updates DCR to the latest version from GitHub Releases.");
return 0;
}
if !args.is_empty() {
warn("Command does not support additional arguments");
return 1;
@@ -62,15 +72,8 @@ pub fn flag_update(args: &[String]) -> i32 {
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) {
let release = match fetch_latest_release() {
Ok(release) => release,
Err(err) => {
error(&format!("Failed to check for updates: {err}"));
@@ -94,7 +97,7 @@ pub fn flag_update(args: &[String]) -> i32 {
return 1;
};
let bytes = match download_asset(&client, &asset.browser_download_url) {
let bytes = match download_asset(&asset.browser_download_url) {
Ok(bytes) => bytes,
Err(err) => {
error(&format!("Failed to download update: {err}"));
@@ -121,35 +124,35 @@ pub fn flag_update(args: &[String]) -> i32 {
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()));
}
fn fetch_latest_release() -> Result<Release, String> {
let response = ureq::get(LATEST_RELEASE_URL)
.set("User-Agent", "dcr-updater")
.call()
.map_err(|e| match e {
ureq::Error::Status(code, _) => format!("GitHub API returned status {code}"),
ureq::Error::Transport(e) => format!("GitHub API request failed: {e}"),
})?;
response
.json::<Release>()
.into_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())?;
fn download_asset(url: &str) -> Result<Vec<u8>, String> {
let response = ureq::get(url)
.set("User-Agent", "dcr-updater")
.call()
.map_err(|e| match e {
ureq::Error::Status(code, _) => format!("Download returned status {code}"),
ureq::Error::Transport(e) => format!("Download request failed: {e}"),
})?;
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())
let mut reader = response.into_reader();
let mut data = Vec::new();
reader
.read_to_end(&mut data)
.map_err(|_| "Failed to read downloaded data".to_string())?;
Ok(data)
}
fn asset_candidates(target: &str) -> Vec<String> {

View File

@@ -16,11 +16,21 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob;
use std::process::Command;
pub fn fmt(_args: &[String]) -> i32 {
pub fn fmt(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr fmt", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Formats all C/C++ source files using clang-format.");
println!(" Scans src/ and tests/ directories recursively.");
return 0;
}
let patterns = [
"src/**/*.c",
"src/**/*.cpp",

View File

@@ -15,18 +15,19 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build_config::Config;
use crate::core::builder::BuildContext;
use crate::core::builder::collect_sources;
use crate::core::builder::common;
use crate::core::config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::{
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,
get_list_with_profile, get_string_with_profile, normalize_target_os, resolve_compiler,
resolve_pkg_config_flags_lossy,
get_list_with_profile, get_string_with_profile, normalize_kind, normalize_platform,
normalize_target, resolve_compiler, resolve_pkg_config_flags_lossy,
};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::path::{Path, PathBuf};
/// Like `deps::resolve_deps` but does NOT require lib directories to exist.
@@ -186,13 +187,30 @@ pub struct ProjectInfo {
pub fn r#gen(args: &[String]) -> i32 {
let subcommand = match args.first() {
Some(s) if s == "--help" => {
printc("USAGE:", BOLD_GREEN);
printc(" dcr gen <subcommand>", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Generates IDE and tooling integration files.");
println!();
printc("SUBCOMMANDS:", BOLD_GREEN);
println!(" project-info Print project metadata as JSON");
println!(" compile-commands Generate compile_commands.json");
println!(" vscode Generate .vscode/ integration files");
println!(" clion Generate .idea/ integration files");
return 0;
}
Some(s) => s.as_str(),
None => {
eprintln!("Usage: dcr gen <subcommand>");
eprintln!(" project-info Print project metadata as JSON");
eprintln!(" compile-commands Generate compile_commands.json");
eprintln!(" vscode Generate .vscode/ integration files");
eprintln!(" clion Generate .idea/ integration files");
printc("USAGE:", BOLD_GREEN);
printc(" dcr gen <subcommand>", BOLD_CYAN);
println!();
printc("SUBCOMMANDS:", BOLD_GREEN);
println!(" project-info Print project metadata as JSON");
println!(" compile-commands Generate compile_commands.json");
println!(" vscode Generate .vscode/ integration files");
println!(" clion Generate .idea/ integration files");
return 1;
}
};
@@ -348,7 +366,13 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
platform: normalize_platform(&platform),
linker: resolved_linker.as_deref(),
archiver: resolved_archiver.as_deref(),
moc: None,
uic: None,
rcc: None,
package_type: None,
freestanding: false,
panic_abort: false,
codegen_units: 0,
source_roots: &source_roots,
exclude_dirs: &combined_excludes,
include_paths: &combined_includes,
@@ -360,6 +384,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
output_filename: None,
output_extension: None,
verbose: false,
qt: false,
};
let sources = collect_sources(&ctx).map_err(|e| format!("Failed to collect sources: {e}"))?;
@@ -585,7 +610,7 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
cmd.push("-c".to_string());
// ASM x flag — must be before source file
if let Some(flag) = asm_lang_flag(source) {
if let Some(flag) = common::asm_lang_flag(source) {
cmd.push("-x".to_string());
cmd.push(flag.to_string());
}
@@ -663,15 +688,6 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
cmd
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
fn strip_src_prefix(p: &Path) -> PathBuf {
// Try to strip leading ./src or src
let s = p.to_string_lossy();
@@ -1220,25 +1236,6 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
Ok((root, profile))
}
fn normalize_target(s: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(s.trim());
if trimmed.is_empty() {
None
} else {
Some(format!("target/{trimmed}/{profile}"))
}
}
fn normalize_kind(s: &str) -> &str {
let t = s.trim();
if t.is_empty() { "bin" } else { t }
}
fn normalize_platform(s: &str) -> Option<&str> {
let t = s.trim();
if t.is_empty() { None } else { Some(t) }
}
fn json_str(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 2);
result.push('"');

View File

@@ -16,7 +16,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C;
use crate::core::config::Config;
use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
@@ -25,7 +26,35 @@ use std::io::Write;
use toml::Value;
pub fn init(args: &[String]) -> i32 {
if !args.is_empty() {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Initializes the current directory as a DCR project.");
println!(" The directory must be empty.");
return 0;
}
let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--vcs" {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
vcs_str = Some(stripped.to_string());
} else {
clean_args.push(arg.clone());
}
}
if !clean_args.is_empty() {
warn("Command does not support additional arguments");
return 1;
}
@@ -41,6 +70,15 @@ pub fn init(args: &[String]) -> i32 {
return 1;
}
if let Err(e) = validate_package_name(&project_name) {
error(&format!(
"Invalid project name `{}`: {}",
colored(&project_name, BOLD_CYAN),
e
));
return 1;
}
let cwd = std::env::current_dir()
.map(|v| v.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string());
@@ -87,6 +125,40 @@ pub fn init(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN)
);
// settings VCS
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
return 1;
}
}
} else if let Ok(cwd_path) = std::env::current_dir()
&& crate::core::vcs::find_existing_vcs(&cwd_path).is_some()
{
vcs_kind = VcsKind::None;
}
if vcs_kind == VcsKind::Git {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(".");
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!(
"Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)

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

@@ -0,0 +1,189 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build_config::Config;
use crate::core::builder::common;
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn lint(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr lint [--fix]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Runs clang-tidy on all C/C++ source files.");
println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --fix Apply clang-tidy suggestions automatically");
return 0;
}
let do_fix = args.iter().any(|a| a == "--fix");
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(r)) => r,
Ok(None) => {
error("dcr.toml not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
return 1;
}
};
let config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(c) => c,
Err(e) => {
error(&format!("Failed to load dcr.toml: {e}"));
return 1;
}
};
let profile = "debug";
let build_roots = get_list_with_profile(&config, "roots", profile);
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
let mut source_roots: Vec<String> = Vec::new();
for raw in &build_roots {
let p = Path::new(raw);
if p.is_absolute() {
source_roots.push(raw.clone());
} else {
source_roots.push(root.join(p).to_string_lossy().to_string());
}
}
if !src_disable && source_roots.is_empty() {
source_roots.push(root.join("src").to_string_lossy().to_string());
}
let cxx_extensions = ["c", "cpp", "cxx", "cc"];
let exclude_dirs: Vec<PathBuf> = Vec::new();
let include_paths: Vec<String> = Vec::new();
let roots: Vec<PathBuf> = source_roots.iter().map(PathBuf::from).collect();
let mut files =
match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) {
Ok(f) => f,
Err(e) => {
error(&format!("Failed to collect source files: {e}"));
return 1;
}
};
let tests_dir = root.join("tests");
if tests_dir.is_dir() {
let test_roots = vec![tests_dir];
if let Ok(mut test_files) =
common::collect_sources(&test_roots, &cxx_extensions, &exclude_dirs, &include_paths)
{
files.append(&mut test_files);
}
}
files.sort();
if files.is_empty() {
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
return 0;
}
println!(
" {} {} files {}",
colored("Linting", BOLD_GREEN),
files.len(),
if do_fix { "(with --fix)" } else { "" }
);
let mut total_issues = 0u32;
let mut failed = 0u32;
for file in &files {
let mut cmd = Command::new("clang-tidy");
if do_fix {
cmd.arg("--fix");
}
cmd.arg("--quiet");
cmd.arg(file);
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
error(&format!("Failed to execute clang-tidy: {}", e));
return 1;
}
};
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}{}", stdout, stderr);
let issues: Vec<&str> = combined
.lines()
.filter(|l| l.contains("warning:") || l.contains("error:"))
.collect();
let issue_count = issues.len();
if issue_count > 0 {
total_issues += issue_count as u32;
failed += 1;
print!(" {} ", colored("×", BOLD_YELLOW));
println!("{} ({} issues)", file, issue_count);
for issue in &issues {
println!(" {}", issue);
}
} else {
print!(" {} ", colored("✓", BOLD_GREEN));
println!("{}", file);
}
}
let total = files.len();
let passed = total - failed as usize;
if failed == 0 {
println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
total
);
} else {
println!(
" {} {}/{} files passed, {} issues found",
colored("Lint", BOLD_YELLOW),
passed,
total,
total_issues
);
}
0
}

View File

@@ -24,6 +24,7 @@ pub mod fmt;
pub mod r#gen;
pub mod help;
pub mod init;
pub mod lint;
pub mod new;
pub mod run;
pub mod setup;

View File

@@ -16,7 +16,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::FILE_MAIN_C;
use crate::core::config::Config;
use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
@@ -25,23 +26,60 @@ use std::io::Write;
use toml::Value;
pub fn new(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Creates a new C/C++ project with the given name.");
println!(" The name may only contain ASCII letters, digits, '_' and '-'.");
return 0;
}
let items = check_dir(None).unwrap_or_default();
if args.is_empty() {
let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--vcs" {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
vcs_str = Some(stripped.to_string());
} else {
clean_args.push(arg.clone());
}
}
if clean_args.is_empty() {
error("Project name not specified");
return 1;
}
if args.len() > 1 {
if clean_args.len() > 1 {
warn("Command does not support additional arguments");
return 1;
}
let project_name = &args[0];
let project_name = &clean_args[0];
println!(
"Creating a Project `{}`...",
colored(project_name, BOLD_CYAN)
);
if let Err(e) = validate_package_name(project_name) {
error(&format!(
"Invalid project name `{}`: {}",
colored(project_name, BOLD_CYAN),
e
));
return 1;
}
if items.contains(project_name) {
error(&format!(
"Directory `{}` already exists\n",
@@ -108,6 +146,40 @@ pub fn new(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN)
);
// settings VCS
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
return 1;
}
}
} else if let Ok(cwd) = std::env::current_dir()
&& crate::core::vcs::find_existing_vcs(&cwd).is_some()
{
vcs_kind = VcsKind::None;
}
if vcs_kind == VcsKind::Git {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!(
"Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN)

View File

@@ -17,13 +17,15 @@
use crate::cli::build::build;
use crate::cli::flags::parse_build_run_flags;
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::runner::run_binary;
use crate::utils::build::{normalize_target_os, parse_version_info};
use crate::utils::build::{
default_target_triple, normalize_target_os, parse_version_info, substitute_vars,
};
use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir;
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::path::Path;
use std::process::Command;
@@ -56,6 +58,26 @@ fn get_run_cmd(
}
pub fn run(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
BOLD_CYAN,
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Builds and runs the project. Only available for kind = \"bin\".");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Run with debug profile (default)");
println!(" --release Run with release profile");
println!(" --target <triple> Cross-compile for the given target");
println!(" --force Force a full rebuild");
println!(" --clean Clean before building");
println!(" --verbose Print detailed build output");
return 0;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
@@ -158,27 +180,7 @@ fn run_project(
// If no target specified, use default host target for target-specific config
let mut target = flags.target.clone();
if target.is_none() {
let default_target = if cfg!(target_os = "linux") {
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
format!(
"{}-unknown-{}",
std::env::consts::ARCH,
std::env::consts::OS
)
} else {
"unknown".to_string()
};
target = Some(default_target);
target = Some(default_target_triple());
}
let build_kind = config
@@ -195,11 +197,11 @@ fn run_project(
match workspace_root {
Some(wr) => target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile))
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile))
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
None => target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)),
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)),
}
};
@@ -285,11 +287,5 @@ fn run_shell(cmd: &str) -> i32 {
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
let info = parse_version_info(version);
cmd.replace("{profile}", profile)
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
substitute_vars(cmd, &info, profile, "")
}

View File

@@ -15,7 +15,19 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub fn setup(_args: &[String]) -> i32 {
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
pub fn setup(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr setup", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Sets up DCR registries. Downloads and indexes");
println!(" package registries for dependency resolution.");
return 0;
}
println!("Setting up DCR registries...");
match crate::core::registry::RegistryManager::load() {
Ok(manager) => {

View File

@@ -17,14 +17,14 @@
use crate::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::build::{
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
};
use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, BOLD_RED, RESET, colored};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_RED, RESET, colored, printc};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -36,16 +36,16 @@ pub fn test(args: &[String]) -> i32 {
let mut profile = "debug";
for arg in args {
if arg == "--help" {
println!("USAGE:");
println!(" dcr test [--init] [--debug | --release]");
printc("USAGE:", BOLD_GREEN);
printc(" dcr test [--init] [--debug | --release]", BOLD_CYAN);
println!();
println!("ALIASES:");
printc("ALIASES:", BOLD_GREEN);
println!(" dcr tests");
println!();
println!("DESCRIPTION:");
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Runs project tests and prints a unified testsuite report.");
println!();
println!("OPTIONS:");
printc("OPTIONS:", BOLD_GREEN);
println!(" --init Create tests/dcr_test.h in current project");
println!(" --debug Build and run tests with debug profile (default)");
println!(" --release Build and run tests with release profile");

View File

@@ -15,14 +15,23 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, printc};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use toml::Value;
pub fn tree(_args: &[String]) -> i32 {
pub fn tree(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr tree", BOLD_CYAN);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
println!(" Displays the dependency tree of the current project.");
return 0;
}
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let config = match Config::open("dcr.toml") {
Ok(c) => c,

View File

@@ -97,16 +97,6 @@ int main(int argc, char **argv) {
"#;
pub fn flags(profile: &str) -> Option<&'static [&'static str]> {
match profile {
"release" => Some(&["-O3", "-DNDEBUG"]),
"debug" => Some(&[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
]),
_ => None,
}
let f = crate::utils::build::default_profile_flags(profile);
if f.is_empty() { None } else { Some(f) }
}

View File

@@ -138,6 +138,8 @@ pub struct BuildConfig {
pub src_disable: Option<bool>,
#[serde(default)]
pub workspace_only: bool,
#[serde(default)]
pub qt: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
@@ -447,7 +449,7 @@ fn parse_typed_config(value: &Value) -> Result<DcrConfig, ConfigError> {
value.clone().try_into().map_err(ConfigError::TomlDe)
}
fn validate_package_name(name: &str) -> Result<(), ConfigError> {
pub fn validate_package_name(name: &str) -> Result<(), ConfigError> {
let trimmed = name.trim();
if trimmed != name {
return Err(ConfigError::Invalid(

170
src/core/builder/asm.rs Normal file
View File

@@ -0,0 +1,170 @@
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build_assembly<F>(
ctx: &BuildContext,
backend_name: &str,
default_assembler: &str,
extensions: &[&str],
build_object: F,
) -> Result<f64, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err(format!(
"{backend_name} backend requires build.language = \"asm\""
));
}
let assembler = if ctx.compiler.is_empty() {
default_assembler
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = common::collect_sources(
ctx.source_roots,
extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?;
if ctx.kind == "staticlib" {
return archive_staticlib(ctx, &objects, start_time);
}
link_objects(ctx, &objects, start_time)
}
fn build_objects_common<F>(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
build_object: &F,
) -> Result<Vec<String>, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, "o"))
.collect();
common::parallel_build(
sources.len(),
|i| build_object(assembler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects)
}
fn archive_staticlib(
ctx: &BuildContext,
objects: &[String],
start_time: Instant,
) -> Result<f64, String> {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(objects, &lib_path) {
return Ok(common::elapsed_secs(start_time));
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => Ok(common::elapsed_secs(start_time)),
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
fn link_objects(
ctx: &BuildContext,
objects: &[String],
start_time: Instant,
) -> Result<f64, String> {
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(objects, &out_path) {
return Ok(common::elapsed_secs(start_time));
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => Ok(common::elapsed_secs(start_time)),
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}

View File

@@ -19,6 +19,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
@@ -164,13 +165,48 @@ pub fn has_glob_magic(value: &str) -> bool {
value.chars().any(|c| matches!(c, '*' | '?' | '['))
}
pub fn parallel_build<F>(total_tasks: usize, task_fn: F) -> Result<(), String>
pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
}
pub fn source_extensions(language: &str) -> Vec<&'static str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => out.extend(["s", "S", "asm"]),
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
}
pub fn elapsed_secs(start: Instant) -> f64 {
((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
}
pub fn parallel_build<F>(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String>
where
F: Fn(usize) -> Result<(), String> + Sync + Send,
{
let num_threads = std::thread::available_parallelism()
let available = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let num_threads = if max_jobs > 0 {
max_jobs.min(available)
} else {
available
};
let counter = std::sync::atomic::AtomicUsize::new(0);
let err_msg = std::sync::Mutex::new(None);
@@ -329,17 +365,16 @@ pub fn needs_link(objects: &[String], output_path: &str) -> bool {
fn parse_d_file(content: &str) -> Vec<String> {
let mut deps = Vec::new();
let text = content.replace("\\\n", " ").replace("\\\r\n", " ");
let mut target_end = 0;
let chars: Vec<char> = text.chars().collect();
for i in 0..chars.len() {
if chars[i] == ':' {
let bytes = content.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b':' {
if i == 1
&& chars[0].is_ascii_alphabetic()
&& i + 1 < chars.len()
&& (chars[i + 1] == '\\' || chars[i + 1] == '/')
&& bytes[0].is_ascii_alphabetic()
&& i + 1 < bytes.len()
&& (bytes[i + 1] == b'\\' || bytes[i + 1] == b'/')
{
continue; // Windows drive letter
continue;
}
target_end = i + 1;
break;
@@ -347,17 +382,23 @@ fn parse_d_file(content: &str) -> Vec<String> {
}
let deps_str = if target_end > 0 {
&text[target_end..]
&content[target_end..]
} else {
&text
content
};
let mut current_path = String::new();
let mut in_escape = false;
let mut chars = deps_str.chars().peekable();
for c in deps_str.chars() {
while let Some(c) = chars.next() {
if in_escape {
if c != '\n' && c != '\r' {
if c == '\n' {
} else if c == '\r' {
if chars.peek() == Some(&'\n') {
chars.next();
}
} else {
current_path.push(c);
}
in_escape = false;
@@ -786,4 +827,44 @@ mod tests {
"changed header should trigger rebuild"
);
}
#[test]
fn asm_lang_flag_returns_correct_flag() {
assert_eq!(asm_lang_flag("foo.S"), Some("assembler-with-cpp"));
assert_eq!(asm_lang_flag("bar.s"), Some("assembler"));
assert_eq!(asm_lang_flag("baz.asm"), Some("assembler"));
assert_eq!(asm_lang_flag("main.c"), None);
}
#[test]
fn source_extensions_parses_language_string() {
let c = source_extensions("c");
assert!(c.contains(&"c"));
assert!(!c.contains(&"cpp"));
let cpp = source_extensions("c++");
assert!(cpp.contains(&"cpp"));
assert!(cpp.contains(&"cxx"));
assert!(cpp.contains(&"cc"));
let asm = source_extensions("asm");
assert!(asm.contains(&"s"));
assert!(asm.contains(&"S"));
assert!(asm.contains(&"asm"));
let mixed = source_extensions("c, c++");
assert!(mixed.contains(&"c"));
assert!(mixed.contains(&"cpp"));
let unknown = source_extensions("rust");
assert_eq!(unknown, vec!["c"]);
}
#[test]
fn elapsed_secs_returns_positive() {
let start = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_millis(50));
let elapsed = elapsed_secs(start);
assert!(elapsed > 0.0);
}
}

47
src/core/builder/fasm.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
&["asm", "fasm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg(source).arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -16,121 +16,17 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err("GAS backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
"as"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
asm::build_assembly(ctx, "GAS", "as", &["s"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
// GAS handles only lowercase .s files
common::collect_sources(
ctx.source_roots,
&["s"],
@@ -139,25 +35,6 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,

View File

@@ -0,0 +1,43 @@
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "LLVM-IR", "llc", &["ll"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
&["ll"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("-filetype=obj").arg(source).arg("-o").arg(obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
common::run_command_sync_output(&mut cmd)
}

51
src/core/builder/masm.rs Normal file
View File

@@ -0,0 +1,51 @@
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
asm::build_assembly(ctx, "MASM", "ml", &["asm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
common::collect_sources(
ctx.source_roots,
&["asm"],
ctx.exclude_dirs,
ctx.include_paths,
)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if !common::needs_rebuild(source, obj_path) {
return Ok(());
}
let mut cmd = Command::new(assembler);
cmd.arg("/nologo")
.arg("/c")
.arg("/Fo")
.arg(obj_path)
.arg(source);
for flag in ctx.cflags {
cmd.arg(flag);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
common::run_command_sync_output(&mut cmd)
}

View File

@@ -15,10 +15,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod asm;
pub mod common;
pub mod fasm;
pub mod gas;
pub mod llvm_ir;
pub mod masm;
pub mod msvc;
pub mod nasm;
pub mod qt;
pub mod unix_cc;
pub struct BuildContext<'a> {
@@ -34,7 +39,13 @@ pub struct BuildContext<'a> {
pub platform: Option<&'a str>,
pub linker: Option<&'a str>,
pub archiver: Option<&'a str>,
pub moc: Option<&'a str>,
pub uic: Option<&'a str>,
pub rcc: Option<&'a str>,
pub package_type: Option<&'a str>,
pub freestanding: bool,
pub panic_abort: bool,
pub codegen_units: usize,
pub source_roots: &'a [std::path::PathBuf],
pub exclude_dirs: &'a [std::path::PathBuf],
pub include_paths: &'a [String],
@@ -46,6 +57,7 @@ pub struct BuildContext<'a> {
pub output_filename: Option<&'a str>,
pub output_extension: Option<&'a str>,
pub verbose: bool,
pub qt: bool,
}
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
@@ -62,9 +74,18 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if compiler == "as" || compiler.contains("gas") {
return gas::build(ctx);
}
if compiler.contains("llc") {
return llvm_ir::build(ctx);
}
if compiler.contains("fasm") {
return fasm::build(ctx);
}
if compiler.contains("nasm") {
return nasm::build(ctx);
}
if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") {
return masm::build(ctx);
}
if compiler == "cl" || compiler.contains("msvc") {
return msvc::build(ctx);
}
@@ -87,8 +108,17 @@ pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
if compiler == "as" || compiler.contains("gas") {
return gas::collect_sources(ctx);
}
if compiler.contains("llc") {
return llvm_ir::collect_sources(ctx);
}
if compiler.contains("fasm") {
return fasm::collect_sources(ctx);
}
if compiler.contains("nasm") {
return nasm::collect_sources(ctx);
}
if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") {
return masm::collect_sources(ctx);
}
unix_cc::collect_sources(ctx)
}

View File

@@ -44,7 +44,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
@@ -57,7 +57,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
@@ -123,17 +123,17 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
cmd.arg(format!("/Fe:{out_path}"));
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
@@ -151,21 +151,8 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => {}
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::builder::common::source_extensions(language)
}
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
@@ -228,9 +215,11 @@ fn build_objects(
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
common::parallel_build(
sources.len(),
|i| build_object(compiler, &sources[i], &objects[i], ctx),
ctx.codegen_units,
)?;
Ok(objects)
}

View File

@@ -16,117 +16,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::asm;
use crate::core::builder::common;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let lang = ctx.language.to_lowercase();
if lang.split(',').any(|p| p.trim() != "asm") {
return Err("NASM backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
"nasm"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let name = ctx.output_filename.unwrap_or(ctx.project_name);
let ext = ctx.output_extension.unwrap_or("");
let final_name = if ext.is_empty() {
name.to_string()
} else {
format!("{}.{}", name, ext)
};
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
} else if ctx.kind == "elf" {
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
asm::build_assembly(ctx, "NASM", "nasm", &["asm", "s"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
@@ -139,32 +36,10 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
let format = nasm_format(ctx.platform);
common::parallel_build(sources.len(), |i| {
build_object(assembler, &sources[i], &objects[i], format, ctx)
})?;
Ok(objects)
}
fn build_object(
assembler: &str,
source: &str,
obj_path: &str,
format: &str,
ctx: &BuildContext,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
@@ -175,6 +50,8 @@ fn build_object(
return Ok(());
}
let format = nasm_format(ctx.platform);
let mut cmd = Command::new(assembler);
cmd.arg("-f")
.arg(format)

141
src/core/builder/qt.rs Normal file
View File

@@ -0,0 +1,141 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::utils::build::run_pkg_config;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf>, String> {
let qt_dir = build_dir.join("qt");
fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?;
let extensions = vec!["ui", "qrc", "h", "hpp", "hxx"];
let files = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
// Fetch Qt system include paths and defines for MOC
let mut qt_moc_args = Vec::new();
let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"];
for module in qt_modules {
if let Ok(cflags) = run_pkg_config(module, "--cflags") {
for flag in cflags.split_whitespace() {
// Pass both -I and -D flags to moc
if flag.starts_with("-I") || flag.starts_with("-D") {
qt_moc_args.push(flag.to_string());
}
}
}
}
let mut generated_any = false;
for file in files {
let path = Path::new(&file);
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"ui" => {
if let Some(uic) = ctx.uic {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("ui_{out_name}.h"));
run_command(
uic,
&[path.to_str().unwrap(), "-o", out_file.to_str().unwrap()],
ctx.verbose,
)?;
generated_any = true;
}
}
"qrc" => {
if let Some(rcc) = ctx.rcc {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("qrc_{out_name}.cpp"));
run_command(
rcc,
&[
path.to_str().unwrap(),
"-name",
out_name,
"-o",
out_file.to_str().unwrap(),
],
ctx.verbose,
)?;
generated_any = true;
}
}
"h" | "hpp" | "hxx" => {
if let Some(moc) = ctx.moc {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
if content.contains("Q_OBJECT") {
let out_name = path.file_stem().unwrap().to_str().unwrap();
let out_file = qt_dir.join(format!("moc_{out_name}.cpp"));
let mut moc_args = Vec::new();
// Add Qt system cflags (includes + defines)
for arg in &qt_moc_args {
moc_args.push(arg.as_str());
}
// Add project include paths
for inc in ctx.include_paths.iter().chain(ctx.include_dirs.iter()) {
moc_args.push("-I");
moc_args.push(inc);
}
moc_args.push(path.to_str().unwrap());
moc_args.push("-o");
moc_args.push(out_file.to_str().unwrap());
run_command(moc, &moc_args, ctx.verbose)?;
generated_any = true;
}
}
}
_ => {}
}
}
if generated_any {
Ok(Some(qt_dir))
} else {
Ok(None)
}
}
fn run_command(cmd_path: &str, args: &[&str], verbose: bool) -> Result<(), String> {
let mut cmd = Command::new(cmd_path);
cmd.args(args);
if verbose || std::env::var("DCR_DEBUG").is_ok() {
eprintln!("[dcr] {:?}", cmd);
}
match cmd.status() {
Ok(s) if s.success() => Ok(()),
Ok(s) => Err(format!("Command {} failed with status {}", cmd_path, s)),
Err(e) => Err(format!("Failed to execute {}: {}", cmd_path, e)),
}
}

View File

@@ -18,8 +18,9 @@
use crate::core::builder::BuildContext;
use crate::core::builder::common;
use crate::platform;
use crate::utils::build::is_bare_metal_target;
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
@@ -30,23 +31,34 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
ctx.compiler
};
let start_time = Instant::now();
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
ctx.source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let obj_dir = match ctx.target_dir {
Some(dir) => Path::new(dir).join("obj"),
None => Path::new("./target").join(ctx.profile).join("obj"),
};
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?;
let qt_include_path = if ctx.qt {
crate::core::builder::qt::process_qt(ctx, &obj_dir)?
} else {
None
};
let mut all_source_roots = ctx.source_roots.to_vec();
if let Some(qt_path) = &qt_include_path {
all_source_roots.push(qt_path.clone());
}
let extensions = source_extensions(ctx.language);
let sources = common::collect_sources(
&all_source_roots,
&extensions,
ctx.exclude_dirs,
ctx.include_paths,
)?;
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?;
if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
if !common::needs_link(&objects, &lib_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
@@ -59,7 +71,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
@@ -80,6 +92,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
cmd.arg("-nostdlib");
cmd.arg("-Wl,-dll");
cmd.arg("-Wl,--subsystem,10");
} else if ctx.freestanding || is_bare_metal_target(ctx.target) {
cmd.arg("-nostdlib");
cmd.arg("-static");
}
for obj in &objects {
cmd.arg(obj);
@@ -112,7 +127,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
};
if !common::needs_link(&objects, &out_path) {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
return Ok(elapsed);
}
cmd.arg("-o").arg(out_path);
@@ -122,7 +137,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
let elapsed = common::elapsed_secs(start_time);
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
@@ -140,49 +155,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
fn source_extensions(language: &str) -> Vec<&str> {
let mut out = Vec::new();
for part in language.split(',').map(|s| s.trim()) {
let lang = part.to_lowercase();
match lang.as_str() {
"c" => out.extend(["c"]),
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
"asm" => out.extend(["s", "S", "asm"]),
_ => {}
}
}
if out.is_empty() {
out.extend(["c"]);
}
out
fn source_extensions(language: &str) -> Vec<&'static str> {
crate::core::builder::common::source_extensions(language)
}
fn default_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
],
_ => &[],
}
}
fn is_bare_metal_target(target: Option<&str>) -> bool {
if let Some(t) = target {
let lower = t.to_lowercase();
lower.contains("none")
|| lower.contains("-elf")
|| lower.contains("eabi")
|| lower.contains("baremetal")
|| lower.contains("bare-metal")
} else {
false
}
crate::utils::build::default_profile_flags(profile)
}
fn build_objects(
@@ -191,15 +169,26 @@ fn build_objects(
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
qt_include_dir: Option<PathBuf>,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = sources
.iter()
.map(|s| common::object_path(obj_dir, s, obj_ext))
.collect();
common::parallel_build(sources.len(), |i| {
build_object(compiler, &sources[i], &objects[i], ctx)
})?;
common::parallel_build(
sources.len(),
|i| {
build_object(
compiler,
&sources[i],
&objects[i],
ctx,
qt_include_dir.as_deref(),
)
},
ctx.codegen_units,
)?;
Ok(objects)
}
@@ -209,6 +198,7 @@ fn build_object(
source: &str,
obj_path: &str,
ctx: &BuildContext,
qt_include_dir: Option<&Path>,
) -> Result<(), String> {
if let Some(parent) = Path::new(obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
@@ -231,6 +221,23 @@ fn build_object(
cmd.arg("-fPIC");
}
if (ctx.freestanding || is_bare_metal_target(ctx.target))
&& ctx.language.to_lowercase() != "asm"
{
cmd.arg("-ffreestanding");
}
if ctx.panic_abort && ctx.language.to_lowercase() != "asm" {
if ctx.language.contains("++")
|| ctx.language.contains("cpp")
|| ctx.language.contains("cxx")
{
cmd.arg("-fno-exceptions");
}
cmd.arg("-fno-unwind-tables");
cmd.arg("-fno-asynchronous-unwind-tables");
}
if let Some(platform) = ctx.platform
&& !platform.trim().is_empty()
{
@@ -250,7 +257,8 @@ fn build_object(
}
}
let use_dcr_defaults = ctx.cflags.is_empty() && !is_bare_metal_target(ctx.target);
let use_dcr_defaults =
ctx.cflags.is_empty() && !(ctx.freestanding || is_bare_metal_target(ctx.target));
if use_dcr_defaults {
for flag in default_flags(ctx.profile) {
cmd.arg(flag);
@@ -265,6 +273,10 @@ fn build_object(
cmd.arg(format!("-I{dir}"));
}
if let Some(path) = qt_include_dir {
cmd.arg(format!("-I{}", path.display()));
}
let d_path = Path::new(obj_path).with_extension("d");
cmd.arg("-MMD").arg("-MF").arg(&d_path);
@@ -276,10 +288,5 @@ fn build_object(
}
fn asm_lang_flag(source: &str) -> Option<&'static str> {
let ext = Path::new(source).extension().and_then(|v| v.to_str())?;
match ext {
"S" => Some("assembler-with-cpp"),
"s" | "asm" => Some("assembler"),
_ => None,
}
crate::core::builder::common::asm_lang_flag(source)
}

View File

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

View File

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

View File

@@ -15,10 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use toml::Value;
#[derive(Debug, Clone)]
pub struct DepLock {
@@ -73,65 +71,3 @@ fn quote_list(items: &[String]) -> String {
fn escape_value(input: &str) -> String {
input.replace('\\', "\\\\").replace('"', "\\\"")
}
#[allow(dead_code)]
pub fn read_dep_version(dep_path: &Path) -> Option<String> {
let path = dep_path.join("dcr.toml");
let content = fs::read_to_string(path).ok()?;
let value: Value = content.parse().ok()?;
value
.get("package")
.and_then(|v| v.as_table())
.and_then(|t| t.get("version"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[allow(dead_code)]
pub fn compute_checksum(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
collect_files(root, root, &mut files)?;
files.sort();
let mut hasher = Sha256::new();
for rel in files {
hasher.update(rel.as_bytes());
let data =
fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?;
hasher.update(&data);
}
let hash = hasher.finalize();
Ok(to_hex(&hash))
}
#[allow(dead_code)]
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), String> {
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? {
let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" {
continue;
}
if path.is_dir() {
collect_files(root, &path, out)?;
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
out.push(rel);
}
}
Ok(())
}
#[allow(dead_code)]
fn to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{:02x}", b));
}
out
}

View File

@@ -16,11 +16,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod common;
pub mod git;
pub mod lock;
pub mod register;
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock};
use std::path::Path;

View File

@@ -33,13 +33,7 @@ pub struct DcrConfig {
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
crate::utils::fs::home_dir()
}
fn dcr_config_dir() -> Option<PathBuf> {

View File

@@ -15,9 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod build_config;
pub mod builder;
pub mod config;
pub mod deps;
pub mod registry;
pub mod runner;
pub mod vcs;
pub mod workspace;

View File

@@ -38,13 +38,7 @@ pub struct RegistryManager {
}
fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
crate::utils::fs::home_dir()
}
impl RegistryManager {

32
src/core/vcs/git.rs Normal file
View File

@@ -0,0 +1,32 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
// creating git init and .gitignore
pub fn init(path: &Path) -> Result<(), String> {
crate::utils::git::git_init(path)?;
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
fs::write(&gitignore_path, "/target\n")
.map_err(|e| format!("failed to create .gitignore: {}", e))?;
}
Ok(())
}

61
src/core/vcs/mod.rs Normal file
View File

@@ -0,0 +1,61 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod git;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VcsKind {
Git,
None,
}
impl VcsKind {
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"git" => Ok(Self::Git),
"none" => Ok(Self::None),
_ => Err(format!("Unknown VCS: {}", s)),
}
}
}
pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
match kind {
VcsKind::Git => git::init(path),
VcsKind::None => Ok(()),
}
}
// look for git repo in parents to avoid nested repos
pub fn find_existing_vcs(path: &Path) -> Option<VcsKind> {
let mut current = if path.is_file() {
path.parent()
} else {
Some(path)
};
while let Some(dir) = current {
if dir.join(".git").exists() {
return Some(VcsKind::Git);
}
current = dir.parent();
}
None
}

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::build::normalize_target_os;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

View File

@@ -41,10 +41,16 @@ fn main() {
"tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest),
"fmt" => cli::fmt::fmt(rest),
"lint" => cli::lint::lint(rest),
"clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest),
"--version" => {
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET"));
println!(
"dcr {}{} ({})",
env!("CARGO_PKG_VERSION"),
env!("DCR_GIT_INFO"),
env!("DCR_TARGET")
);
0
}
"--help" => cli::help::help(),

View File

@@ -26,6 +26,16 @@ pub mod bsd;
pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
pub mod posix;
#[cfg(target_os = "windows")]
pub mod windows;
@@ -61,10 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
return posix::bin_path(profile, name, target_dir);
}
}
@@ -100,10 +107,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}"),
}
return posix::elf_path(profile, name, target_dir);
}
}
@@ -139,10 +143,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.efi"),
}
return posix::efi_path(profile, name, target_dir);
}
}
@@ -178,10 +179,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.a"),
}
return posix::lib_path(profile, name, target_dir);
}
}
@@ -217,9 +215,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly"
)))]
{
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
}
return posix::shared_lib_path(profile, name, target_dir);
}
}

31
src/platform/posix.rs Normal file
View File

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

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::config::Config;
use crate::core::build_config::Config;
use crate::utils::log::warn;
pub struct VersionInfo {
@@ -32,17 +32,16 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
if full.is_empty() {
full = "0.0.0".to_string();
}
let full_clone = full.clone();
let (base, suffix) = match full_clone.split_once('-') {
Some((head, tail)) => (head, tail),
None => (full_clone.as_str(), ""),
let (base, suffix) = match full.split_once('-') {
Some((head, tail)) => (head.to_string(), tail.to_string()),
None => (full.clone(), String::new()),
};
let mut parts = base.split('.');
let major = parts.next().unwrap_or("0").to_string();
let minor = parts.next().unwrap_or("0").to_string();
let patch = parts.next().unwrap_or("0").to_string();
let suffix_dash = if suffix.is_empty() {
"".to_string()
String::new()
} else {
format!("-{suffix}")
};
@@ -51,11 +50,51 @@ pub fn parse_version_info(version: &str) -> VersionInfo {
major,
minor,
patch,
suffix: suffix.to_string(),
suffix,
suffix_dash,
}
}
pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String {
let s = template
.replace("{profile}", profile)
.replace("{name}", name);
substitute_version_vars(&s, info)
}
pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String {
template
.replace("{version}", &info.full)
.replace("{version_major}", &info.major)
.replace("{version_minor}", &info.minor)
.replace("{version_patch}", &info.patch)
.replace("{version_suffix}", &info.suffix)
.replace("{version_suffix_dash}", &info.suffix_dash)
}
pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(target.trim());
if trimmed.is_empty() {
None
} else {
Some(format!("target/{trimmed}/{profile}"))
}
}
pub fn normalize_kind(kind: &str) -> &str {
let trimmed = kind.trim();
if trimmed.is_empty() { "bin" } else { trimmed }
}
pub fn normalize_platform(platform: &str) -> Option<&str> {
let trimmed = platform.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
pub fn normalize_target_os(target: &str) -> &str {
match target {
"linux" => "x86_64-unknown-linux-gnu",
@@ -72,6 +111,53 @@ pub fn normalize_target_os(target: &str) -> &str {
}
}
pub fn default_target_triple() -> String {
let arch = std::env::consts::ARCH;
if cfg!(target_os = "linux") {
let env = if cfg!(target_env = "musl") {
"musl"
} else {
"gnu"
};
format!("{arch}-unknown-linux-{env}")
} else if cfg!(target_os = "macos") {
format!("{arch}-apple-darwin")
} else if cfg!(target_os = "windows") {
let env = if cfg!(target_env = "gnu") {
"gnu"
} else {
"msvc"
};
format!("{arch}-pc-windows-{env}")
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
format!("{arch}-unknown-{}", std::env::consts::OS)
} else {
"unknown".to_string()
}
}
pub fn prepend_clang_target_flag(flags: &mut Vec<String>, target: Option<&str>, tool: &str) {
let Some(target) = target.map(str::trim).filter(|t| !t.is_empty()) else {
return;
};
if !tool.to_lowercase().contains("clang") {
return;
}
let target_flag = format!("--target={target}");
if !flags
.iter()
.any(|f| f == &target_flag || f.starts_with("--target="))
{
flags.insert(0, target_flag);
}
}
pub fn get_config_str(config: &Config, key: &str) -> String {
config
.get(key)
@@ -140,6 +226,43 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec
out
}
fn contains_ignore_case(text: &str, pattern: &str) -> bool {
if pattern.len() > text.len() {
return false;
}
text.as_bytes()
.windows(pattern.len())
.any(|w| w.eq_ignore_ascii_case(pattern.as_bytes()))
}
pub fn is_bare_metal_target(target: Option<&str>) -> bool {
match target {
Some(t) => {
contains_ignore_case(t, "none")
|| contains_ignore_case(t, "-elf")
|| contains_ignore_case(t, "eabi")
|| contains_ignore_case(t, "baremetal")
|| contains_ignore_case(t, "bare-metal")
}
None => false,
}
}
pub fn default_profile_flags(profile: &str) -> &'static [&'static str] {
match profile {
"release" => &["-O3", "-DNDEBUG"],
"debug" => &[
"-O0",
"-g",
"-Wall",
"-Wextra",
"-fno-omit-frame-pointer",
"-DDCR_DEBUG",
],
_ => &[],
}
}
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
let base = config
.get(&format!("build.{field}"))
@@ -316,7 +439,7 @@ pub fn resolve_pkg_config_flags_lossy(
}
}
fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
let output = std::process::Command::new("pkg-config")
.arg(arg)
.arg(pkg)
@@ -380,6 +503,61 @@ mod tests {
assert_eq!(normalize_target_os("unknown"), "unknown");
}
#[test]
fn default_target_triple_uses_host_arch() {
let target = default_target_triple();
if cfg!(target_os = "linux") {
assert_eq!(
target,
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
);
} else if cfg!(target_os = "macos") {
assert_eq!(target, format!("{}-apple-darwin", std::env::consts::ARCH));
} else if cfg!(target_os = "windows") {
assert_eq!(
target,
format!("{}-pc-windows-msvc", std::env::consts::ARCH)
);
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
assert_eq!(
target,
format!(
"{}-unknown-{}",
std::env::consts::ARCH,
std::env::consts::OS
)
);
} else {
assert_eq!(target, "unknown");
}
}
#[test]
fn prepend_clang_target_flag_adds_once_for_clang() {
let mut flags = vec!["-O2".to_string()];
prepend_clang_target_flag(&mut flags, Some("aarch64-apple-darwin"), "clang");
prepend_clang_target_flag(&mut flags, Some("aarch64-apple-darwin"), "clang");
assert_eq!(
flags,
vec![
"--target=aarch64-apple-darwin".to_string(),
"-O2".to_string()
]
);
}
#[test]
fn prepend_clang_target_flag_ignores_non_clang_tools() {
let mut flags = vec!["-O2".to_string()];
prepend_clang_target_flag(&mut flags, Some("aarch64-apple-darwin"), "gcc");
assert_eq!(flags, vec!["-O2".to_string()]);
}
#[test]
fn parse_version_parts() {
let info = parse_version_info("1.2.3-beta");
@@ -390,4 +568,52 @@ mod tests {
assert_eq!(info.suffix, "beta");
assert_eq!(info.suffix_dash, "-beta");
}
#[test]
fn normalize_target_with_profile() {
assert_eq!(
normalize_target("linux", "debug"),
Some("target/x86_64-unknown-linux-gnu/debug".into())
);
assert_eq!(
normalize_target("x86_64-unknown-linux-gnu", "release"),
Some("target/x86_64-unknown-linux-gnu/release".into())
);
assert_eq!(normalize_target("", "debug"), None);
}
#[test]
fn normalize_kind_empty_defaults_to_bin() {
assert_eq!(normalize_kind(""), "bin");
assert_eq!(normalize_kind(" "), "bin");
assert_eq!(normalize_kind("staticlib"), "staticlib");
assert_eq!(normalize_kind(" elf "), "elf");
}
#[test]
fn normalize_platform_empty_is_none() {
assert_eq!(normalize_platform(""), None);
assert_eq!(normalize_platform(" "), None);
assert_eq!(normalize_platform("native"), Some("native"));
assert_eq!(normalize_platform(" efi "), Some("efi"));
}
#[test]
fn default_profile_flags_by_profile() {
assert!(default_profile_flags("debug").contains(&"-O0"));
assert!(default_profile_flags("debug").contains(&"-g"));
assert!(default_profile_flags("release").contains(&"-O3"));
assert!(default_profile_flags("release").contains(&"-DNDEBUG"));
assert!(default_profile_flags("unknown").is_empty());
}
#[test]
fn is_bare_metal_detects_embedded_targets() {
assert!(is_bare_metal_target(Some("aarch64-none-elf")));
assert!(is_bare_metal_target(Some("i686-elf")));
assert!(is_bare_metal_target(Some("armv7e-m-none-eabi")));
assert!(is_bare_metal_target(Some("riscv32-unknown-none-elf")));
assert!(!is_bare_metal_target(Some("x86_64-unknown-linux-gnu")));
assert!(!is_bare_metal_target(None));
}
}

View File

@@ -18,6 +18,24 @@
use std::io;
use std::path::{Path, PathBuf};
pub fn home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(profile) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(profile));
}
None
}
pub 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
}
#[allow(dead_code)]
pub fn check_dir(dir: Option<&str>) -> io::Result<Vec<String>> {
let path: PathBuf = match dir {

28
src/utils/git.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::path::Path;
use std::process::Command;
pub fn is_git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn git_init(path: &Path) -> Result<(), String> {
if !is_git_available() {
return Err("git executable not found in PATH".to_string());
}
let status = Command::new("git")
.arg("init")
.current_dir(path)
.status()
.map_err(|e| format!("failed to execute git init: {}", e))?;
if !status.success() {
return Err("git init failed".to_string());
}
Ok(())
}

View File

@@ -17,5 +17,6 @@
pub mod build;
pub mod fs;
pub mod git;
pub mod log;
pub mod text;

View File

@@ -1,502 +0,0 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
run_dcr_env(args, cwd, &[])
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
fn available_compiler() -> Option<&'static str> {
for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if ok {
return Some(candidate);
}
}
None
}
#[test]
fn help_and_version_work() {
let dir = unique_sandbox_dir("help");
let out = run_dcr(&["--help"], &dir);
assert!(out.status.success(), "--help should succeed");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success(), "--version should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("dcr"), "version output should mention dcr");
}
#[test]
fn new_creates_project_layout() {
let dir = unique_sandbox_dir("new");
let out = run_dcr(&["new", "hello"], &dir);
assert!(out.status.success(), "dcr new should succeed");
let project_dir = dir.join("hello");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
assert!(
project_dir.join("src").join("main.c").is_file(),
"src/main.c missing"
);
}
#[test]
fn init_and_clean_remove_target() {
let dir = unique_sandbox_dir("init");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let target_debug = dir.join("target").join("debug");
std::fs::create_dir_all(&target_debug).expect("failed to create target/debug");
std::fs::write(target_debug.join("dummy.o"), "x").expect("failed to write dummy file");
let out = run_dcr(&["clean"], &dir);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn build_run_clean_flags_normal_project() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping build/run test");
return;
};
let dir = unique_sandbox_dir("normal");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "dcr build should succeed");
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!(
!dir.join(&target_dir).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join(&target_dir).join("debug").is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
);
let out = run_dcr_env(&["clean"], &dir, &envs);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn workspace_build_and_clean_all() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping workspace test");
return;
};
let root = unique_sandbox_dir("workspace");
let out = run_dcr(&["init"], &root);
assert!(out.status.success(), "root init should succeed");
let members = [
("userspace", &[][..]),
("core", &["userspace"][..]),
("kernel", &["core"][..]),
];
for (name, _) in &members {
let member_dir = root.join("src").join(name);
std::fs::create_dir_all(&member_dir).expect("failed to create member dir");
let out = run_dcr(&["init"], &member_dir);
assert!(out.status.success(), "member init should succeed");
}
let workspace_toml = "[package]\nname = \"ws-root\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[workspace]\nuserspace = { path = \"src/userspace\", deps = [] }\ncore = { path = \"src/core\", deps = [\"userspace\"] }\nkernel = { path = \"src/kernel\", deps = [\"core\"] }\n\n[dependencies]\n";
std::fs::write(root.join("dcr.toml"), workspace_toml).expect("failed to write root dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build", "--release"], &root, &envs);
assert!(
out.status.success(),
"workspace build --release should succeed"
);
let out = run_dcr_env(&["clean", "--release", "--all"], &root, &envs);
assert!(
out.status.success(),
"workspace clean --all --release should succeed"
);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!root.join(target_dir).join("release").exists(),
"root target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
root.join(target_dir).join("debug").exists(),
"root target/x86_64-unknown-linux-gnu/debug should remain"
);
}
#[test]
fn dcr_add_dependencies() {
let dir = unique_sandbox_dir("add_dep");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
// Test path: prefix
let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir);
assert!(out.status.success(), "dcr add path should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("mylib = { path = \"./libs/mylib\" }"),
"path dep not found in toml"
);
// Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"),
"github dep not found in toml"
);
// Test git: prefix (generic)
let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir);
assert!(out.status.success(), "dcr add custom git should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"),
"custom git dep not found in toml"
);
// Test git: prefix (github default)
let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir);
assert!(out.status.success(), "dcr add git short should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("git_short = { git = \"https://github.com/user/repo\" }"),
"git short dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
&dir,
);
assert!(out.status.success(), "dcr add with branch should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"),
"branch lib not found in toml"
);
// Test failure on no prefix
let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir);
assert!(!out.status.success(), "dcr add without prefix should fail");
}
#[test]
fn dcr_builds_lib_package() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping lib package test");
return;
};
let dir = unique_sandbox_dir("lib_package");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
let updated_toml = toml
.replace("kind = \"bin\"", "kind = \"staticlib\"")
.replace("type = \"none\"", "type = \"lib\"");
std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml");
std::fs::write(dir.join("src").join("my_lib.h"), "void hello();")
.expect("failed to write header");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(out.status.success(), "dcr build should succeed");
let target_dir = dir.join("target");
assert!(
target_dir.join("include").join("my_lib.h").is_file(),
"include/my_lib.h missing"
);
assert!(target_dir.join("lib").exists(), "lib directory missing");
}
#[test]
fn registry_dependency_is_built_from_cache() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping registry dependency build test");
return;
};
let root = unique_sandbox_dir("registry_dep");
let home = root.join("home");
let dcr_home = home.join(".dcr");
let dep = root.join("cache").join("mylib");
let app = root.join("app");
std::fs::create_dir_all(dcr_home.as_path()).expect("failed to create dcr home");
std::fs::create_dir_all(dep.join("src")).expect("failed to create dep src");
std::fs::create_dir_all(app.join("src")).expect("failed to create app src");
std::fs::write(
dcr_home.join("config.toml"),
"[registry.local]\nurl = \"file://local\"\npriority = 1\n",
)
.expect("failed to write registry config");
std::fs::write(
dcr_home.join("index.json"),
format!(
"{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}",
dep.display()
),
)
.expect("failed to write registry index");
std::fs::write(
dep.join("dcr.toml"),
"[package]\nname = \"mylib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\n",
)
.expect("failed to write dep dcr.toml");
std::fs::write(dep.join("src").join("mylib.h"), "int answer(void);\n")
.expect("failed to write header");
std::fs::write(
dep.join("src").join("mylib.c"),
"int answer(void) { return 42; }\n",
)
.expect("failed to write dep source");
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\ntype = \"none\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmylib = \"0.1.0\"\n",
)
.expect("failed to write app dcr.toml");
std::fs::write(
app.join("src").join("main.c"),
"#include \"mylib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
)
.expect("failed to write app source");
let index_path = dcr_home.join("index.json");
let envs = [
("DCR_COMPILER", compiler),
("HOME", home.to_str().unwrap()),
("DCR_INDEX_PATH", index_path.to_str().unwrap()),
];
let out = run_dcr_env(&["build"], &app, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"registry dependency build should succeed"
);
assert!(
dep.join("target").join("include").join("mylib.h").is_file(),
"registry dependency headers were not packaged"
);
assert!(
dep.join("target").join("lib").exists(),
"registry dependency library directory missing"
);
}
fn parse_project_name(toml: &str) -> String {
toml.lines()
.find(|l| l.trim().starts_with("name ="))
.and_then(|l| l.split('=').nth(1))
.map(|s| s.trim().trim_matches('"').to_string())
.expect("could not parse project name from dcr.toml")
}
fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
project_root
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug")
.join(project_name)
}
#[test]
fn build_with_target_config() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping target config test");
return;
};
let dir = unique_sandbox_dir("target_cfg");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\ntarget = \"linux\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(
out.status.success(),
"dcr build with target = \"linux\" should succeed"
);
let artifact = default_artifact_path(&dir, &project_name);
assert!(
artifact.is_file(),
"artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}",
project_name
);
}
#[test]
fn build_with_out_dir() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping out_dir test");
return;
};
let dir = unique_sandbox_dir("out_dir");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\nout_dir = \"./_BUILD\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"dcr build with out_dir should succeed"
);
let artifact = dir.join("_BUILD").join(&project_name);
assert!(
artifact.is_file(),
"artifact should be at _BUILD/{} (custom out_dir)",
project_name
);
let default_path = default_artifact_path(&dir, &project_name);
assert!(
!default_path.exists(),
"artifact should NOT be at default path when out_dir is set"
);
}
#[test]
fn dcr_test_runs_without_sandbox_dependency() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping dcr test integration");
return;
};
let dir = unique_sandbox_dir("dcr_test_independent");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_CC", compiler)];
let out_init = run_dcr_env(&["test", "--init"], &dir, &envs);
assert!(out_init.status.success(), "dcr test --init should succeed");
let out = run_dcr_env(&["test"], &dir, &envs);
assert!(out.status.success(), "dcr test should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
assert!(
stdout.contains("PASS: 1"),
"PASS summary line missing\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
}

130
tests/cli_build.rs Normal file
View File

@@ -0,0 +1,130 @@
mod common;
use common::*;
#[test]
fn init_and_clean_remove_target() {
let dir = unique_sandbox_dir("init");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let target_debug = dir.join("target").join("debug");
std::fs::create_dir_all(&target_debug).expect("failed to create target/debug");
std::fs::write(target_debug.join("dummy.o"), "x").expect("failed to write dummy file");
let out = run_dcr(&["clean"], &dir);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn build_run_clean_flags_normal_project() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping build/run test");
return;
};
let dir = unique_sandbox_dir("normal");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "dcr build should succeed");
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!(
!dir.join(&target_dir).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join(&target_dir).join("debug").is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
);
let out = run_dcr_env(&["clean"], &dir, &envs);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}
#[test]
fn build_with_target_config() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping target config test");
return;
};
let dir = unique_sandbox_dir("target_cfg");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\ntarget = \"linux\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
assert!(
out.status.success(),
"dcr build with target = \"linux\" should succeed"
);
let artifact = default_artifact_path(&dir, &project_name);
assert!(
artifact.is_file(),
"artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}",
project_name
);
}
#[test]
fn build_with_out_dir() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping out_dir test");
return;
};
let dir = unique_sandbox_dir("out_dir");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let project_name = parse_project_name(&toml);
let updated = toml.replace("[build]", "[build]\nout_dir = \"./_BUILD\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &dir, &envs);
if !out.status.success() {
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
}
assert!(
out.status.success(),
"dcr build with out_dir should succeed"
);
let artifact = dir.join("_BUILD").join(&project_name);
assert!(
artifact.is_file(),
"artifact should be at _BUILD/{} (custom out_dir)",
project_name
);
let default_path = default_artifact_path(&dir, &project_name);
assert!(
!default_path.exists(),
"artifact should NOT be at default path when out_dir is set"
);
}

176
tests/cli_deps.rs Normal file
View File

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

66
tests/cli_lint.rs Normal file
View File

@@ -0,0 +1,66 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
#[test]
fn lint_command_runs() {
let dir = unique_sandbox_dir("lint");
let out = run_dcr_env(&["init"], &dir, &[]);
assert!(out.status.success(), "dcr init should succeed");
// Linting a clean project should succeed or at least run without crashing
let out = run_dcr_env(&["lint"], &dir, &[]);
assert!(out.status.success(), "dcr lint should run on clean project");
}

75
tests/cli_new.rs Normal file
View File

@@ -0,0 +1,75 @@
mod common;
use common::*;
#[test]
fn help_and_version_work() {
let dir = unique_sandbox_dir("help");
let out = run_dcr(&["--help"], &dir);
assert!(out.status.success(), "--help should succeed");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success(), "--version should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("dcr"), "version output should mention dcr");
}
#[test]
fn new_creates_project_layout() {
let dir = unique_sandbox_dir("new");
let out = run_dcr(&["new", "hello"], &dir);
assert!(out.status.success(), "dcr new should succeed");
let project_dir = dir.join("hello");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
assert!(
project_dir.join("src").join("main.c").is_file(),
"src/main.c missing"
);
}
#[test]
fn new_vcs_options_work() {
// 1. Test --vcs none
let dir = unique_sandbox_dir("new_vcs_none");
let out = run_dcr(&["new", "hello_none", "--vcs", "none"], &dir);
assert!(out.status.success(), "dcr new --vcs none should succeed");
let project_dir = dir.join("hello_none");
assert!(project_dir.is_dir());
assert!(!project_dir.join(".git").exists());
assert!(!project_dir.join(".gitignore").exists());
// 2. Test --vcs git
if is_git_in_path() {
let dir = unique_sandbox_dir("new_vcs_git");
let out = run_dcr(&["new", "hello_git", "--vcs", "git"], &dir);
assert!(out.status.success(), "dcr new --vcs git should succeed");
let project_dir = dir.join("hello_git");
assert!(project_dir.is_dir());
assert!(project_dir.join(".git").is_dir());
assert!(project_dir.join(".gitignore").is_file());
let gitignore = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/target"));
}
}
#[test]
fn init_vcs_options_work() {
// 1. Test --vcs none
let dir = unique_sandbox_dir("init_vcs_none");
let out = run_dcr(&["init", "--vcs", "none"], &dir);
assert!(out.status.success(), "dcr init --vcs none should succeed");
assert!(!dir.join(".git").exists());
assert!(!dir.join(".gitignore").exists());
// 2. Test --vcs git
if is_git_in_path() {
let dir = unique_sandbox_dir("init_vcs_git");
let out = run_dcr(&["init", "--vcs", "git"], &dir);
assert!(out.status.success(), "dcr init --vcs git should succeed");
assert!(dir.join(".git").is_dir());
assert!(dir.join(".gitignore").is_file());
let gitignore = std::fs::read_to_string(dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/target"));
}
}

79
tests/cli_qt.rs Normal file
View File

@@ -0,0 +1,79 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
#[test]
fn qt_support_config_detected() {
let dir = unique_sandbox_dir("qt");
let out = run_dcr_env(&["init"], &dir, &[]);
assert!(out.status.success(), "dcr init should succeed");
let toml_path = dir.join("dcr.toml");
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
let updated = toml.replace("[build]", "[build]\nqt = true\nlanguage = \"cpp\"");
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
// This might fail if Qt is not installed, but it should at least try to run Qt processing
// We just check if it doesn't crash immediately due to bad config
let out = run_dcr_env(&["build"], &dir, &[]);
// Depending on environment, this might be success or failure, but it should not panic.
// If it fails with "qt not found", it means it parsed the config and attempted to use it.
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("panicked"),
"dcr should not panic on Qt config"
);
}

30
tests/cli_test.rs Normal file
View File

@@ -0,0 +1,30 @@
mod common;
use common::*;
#[test]
fn dcr_test_runs_without_sandbox_dependency() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping dcr test integration");
return;
};
let dir = unique_sandbox_dir("dcr_test_independent");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let envs = [("DCR_CC", compiler)];
let out_init = run_dcr_env(&["test", "--init"], &dir, &envs);
assert!(out_init.status.success(), "dcr test --init should succeed");
let out = run_dcr_env(&["test"], &dir, &envs);
assert!(out.status.success(), "dcr test should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
assert!(
stdout.contains("PASS: 1"),
"PASS summary line missing\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
}

58
tests/cli_workspace.rs Normal file
View File

@@ -0,0 +1,58 @@
mod common;
use common::*;
#[test]
fn workspace_build_and_clean_all() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping workspace test");
return;
};
let root = unique_sandbox_dir("workspace");
let out = run_dcr(&["init"], &root);
assert!(out.status.success(), "root init should succeed");
let members = [
("userspace", &[][..]),
("core", &["userspace"][..]),
("kernel", &["core"][..]),
];
for (name, _) in &members {
let member_dir = root.join("src").join(name);
std::fs::create_dir_all(&member_dir).expect("failed to create member dir");
let out = run_dcr(&["init"], &member_dir);
assert!(out.status.success(), "member init should succeed");
}
let workspace_toml = "[package]\nname = \"ws-root\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[workspace]\nuserspace = { path = \"src/userspace\", deps = [] }\ncore = { path = \"src/core\", deps = [\"userspace\"] }\nkernel = { path = \"src/kernel\", deps = [\"core\"] }\n\n[dependencies]\n";
std::fs::write(root.join("dcr.toml"), workspace_toml).expect("failed to write root dcr.toml");
let envs = [("DCR_COMPILER", compiler)];
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build"], &root, &envs);
assert!(out.status.success(), "workspace build should succeed");
let out = run_dcr_env(&["build", "--release"], &root, &envs);
assert!(
out.status.success(),
"workspace build --release should succeed"
);
let out = run_dcr_env(&["clean", "--release", "--all"], &root, &envs);
assert!(
out.status.success(),
"workspace clean --all --release should succeed"
);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!root.join(target_dir).join("release").exists(),
"root target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
root.join(target_dir).join("debug").exists(),
"root target/x86_64-unknown-linux-gnu/debug should remain"
);
}

101
tests/common/mod.rs Normal file
View File

@@ -0,0 +1,101 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
pub fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
pub fn unique_sandbox_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("sandbox");
path.push("cli-tests");
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
pub fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
run_dcr_env(args, cwd, &[])
}
pub fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output {
let mut cmd = Command::new(bin_path());
cmd.args(args).current_dir(cwd);
for (k, v) in envs {
cmd.env(k, v);
}
cmd.output().expect("failed to run dcr")
}
pub fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
#[allow(dead_code)]
pub fn available_compiler() -> Option<&'static str> {
for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if ok {
return Some(candidate);
}
}
None
}
#[allow(dead_code)]
pub fn is_git_in_path() -> bool {
std::process::Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[allow(dead_code)]
pub fn parse_project_name(toml: &str) -> String {
toml.lines()
.find(|l| l.trim().starts_with("name ="))
.and_then(|l| l.split('=').nth(1))
.map(|s| s.trim().trim_matches('"').to_string())
.expect("could not parse project name from dcr.toml")
}
#[allow(dead_code)]
pub fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
project_root
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug")
.join(project_name)
}