mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 20:29:36 +00:00
Compare commits
40 Commits
v0.7.0
...
803d5891ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 803d5891ce | |||
| 9326e23a53 | |||
| 21932ac94a | |||
| 97db82e4f8 | |||
| 3d596d7280 | |||
| 5cb4473bd1 | |||
| 33f955d4e3 | |||
| 838be72dd2 | |||
| 39fad31c4f | |||
| c8ef38a99b | |||
| 8e0c535cfd | |||
| de88c160ad | |||
| 4daca925a0 | |||
| 595b7ac105 | |||
|
|
11ae3762d0 | ||
|
|
4c439317e3 | ||
| 0aa3f87f99 | |||
| a6ce0a3332 | |||
| cea43e99c8 | |||
| de4fb0bc29 | |||
| 59e27b6f5d | |||
| c8cb155e5a | |||
| 6424aab69d | |||
|
|
dabeb411d3 | ||
|
|
b8338c752b | ||
| b70438b16d | |||
| 0597b5ddb2 | |||
|
|
8866fb0796 | ||
|
|
7d853482aa | ||
|
|
ef9ca57295 | ||
|
|
8e1fb60692 | ||
|
|
d6ddb5da3d | ||
|
|
d26e93dd47 | ||
| 97d2970727 | |||
|
|
35f9b6be94 | ||
| 6885a82be7 | |||
| 5d587db227 | |||
| 424f4f6929 | |||
| 443306aa5f | |||
| f21f50facb |
576
.github/workflows/release.yml
vendored
576
.github/workflows/release.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -2,6 +2,7 @@ target/
|
||||
sandbox/**
|
||||
.idea/
|
||||
.vscode/
|
||||
.cargo/
|
||||
.env
|
||||
|
||||
personal/
|
||||
174
CHANGELOG.md
174
CHANGELOG.md
@@ -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
1037
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -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
|
||||
79
README.md
79
README.md
@@ -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>
|
||||
|
||||
[](https://github.com/dexoron/dcr/actions/workflows/ci.yml)
|
||||
[](https://github.com/dexoron/dcr/releases/latest)
|
||||
[](https://github.com/dexoron/dcr/releases)
|
||||
<br/>
|
||||
[](https://aur.archlinux.org/packages/dcr)
|
||||
[](https://crates.io/crates/dcr)
|
||||
[](https://github.com/dexoron/homebrew-dexoron)
|
||||
<br/>
|
||||
[](https://github.com/dexoron/dcr/stargazers)
|
||||
[](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
60
TODO.md
@@ -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
|
||||
33
build.rs
33
build.rs
@@ -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=");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
46
docs/license.mdx
Normal 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/).
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
12
install.ps1
12
install.ps1
@@ -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) {
|
||||
|
||||
21
install.sh
21
install.sh
@@ -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
|
||||
|
||||
|
||||
@@ -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
42
man/man1/dcr-add.1
Normal 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
37
man/man1/dcr-build.1
Normal 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
26
man/man1/dcr-clean.1
Normal 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
10
man/man1/dcr-fmt.1
Normal 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
23
man/man1/dcr-gen.1
Normal 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
11
man/man1/dcr-init.1
Normal 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
13
man/man1/dcr-new.1
Normal 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
35
man/man1/dcr-run.1
Normal 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
11
man/man1/dcr-setup.1
Normal 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
24
man/man1/dcr-test.1
Normal 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
11
man/man1/dcr-tree.1
Normal 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
204
man/man1/dcr.1
Normal 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>
|
||||
1
packaging/aur/dcr-dev-git
Submodule
1
packaging/aur/dcr-dev-git
Submodule
Submodule packaging/aur/dcr-dev-git added at 93bdce858b
Submodule packaging/aur/dcr-git updated: 12038a613e...2c5743a456
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
259
src/cli/build.rs
259
src/cli/build.rs
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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('"');
|
||||
|
||||
@@ -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
189
src/cli/lint.rs
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, "")
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
170
src/core/builder/asm.rs
Normal 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}")),
|
||||
}
|
||||
}
|
||||
@@ -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
47
src/core/builder/fasm.rs
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
43
src/core/builder/llvm_ir.rs
Normal file
43
src/core/builder/llvm_ir.rs
Normal 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
51
src/core/builder/masm.rs
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
141
src/core/builder/qt.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
32
src/core/vcs/git.rs
Normal 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
61
src/core/vcs/mod.rs
Normal 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
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
31
src/platform/posix.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
28
src/utils/git.rs
Normal 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(())
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
|
||||
pub mod build;
|
||||
pub mod fs;
|
||||
pub mod git;
|
||||
pub mod log;
|
||||
pub mod text;
|
||||
|
||||
@@ -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
130
tests/cli_build.rs
Normal 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
176
tests/cli_deps.rs
Normal 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
66
tests/cli_lint.rs
Normal 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
75
tests/cli_new.rs
Normal 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
79
tests/cli_qt.rs
Normal 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
30
tests/cli_test.rs
Normal 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
58
tests/cli_workspace.rs
Normal 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
101
tests/common/mod.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user