31 Commits

Author SHA1 Message Date
c756d94f96 Merge pull request #10 and #8
build(nix): configure flake for dcr project
Update user input prompts to read from /dev/tty 
Create FUNDING.yml
2026-06-01 20:29:07 +08:00
Alexandr_Croitor
8e21432764 chore: remove unused dev shell tools 2026-06-01 13:23:12 +03:00
Alexandr_Croitor
772d50811f build(nix): configure flake for dcr project
- Set package name/version from Cargo.toml (dcr 0.6.9)
- Add cmake and perl for vendored libgit2/openssl builds
- Switch to stable toolchain (complete was overkill)
- Generate flake.lock
2026-05-31 22:37:57 +03:00
ea1546b13a Create FUNDING.yml 2026-06-01 02:49:06 +08:00
f5d5795872 docsL fix URL 2026-05-30 17:15:55 +08:00
e6788b4d71 fix: add missing .cxx/.cc/.hxx/.hh extensions to fmt; update docs for 0.7.0 2026-05-30 15:32:16 +08:00
865d8c52ff fix: split target into target (triple) and out_dir (output directory)
build.target now holds the target triple (e.g. x86_64-unknown-linux-gnu)
build.out_dir is a custom output directory that overrides the default
target/<triple>/<profile> path.
2026-05-30 14:18:54 +08:00
94aa6c0f01 Merge pull request #8 from babichgh/master
Update user input prompts to read from /dev/tty
2026-05-30 13:28:40 +08:00
John Babich
e462de6272 Update user input prompts to read from /dev/tty
Redirect input for user prompts to /dev/tty for better compatibility.
2026-05-29 23:58:03 +03:00
peoplemiau1
0587965283 Make standard optional, allow empty src_disable, and add none/custom kinds 2026-05-29 18:57:45 +03:00
peoplemiau1
478d1ad0c5 Remove DragonFly BSD support 2026-05-29 09:57:07 +03:00
peoplemiau1
060b92111f Fix dcr run to inherit stdout/stderr properly 2026-05-29 09:54:13 +03:00
peoplemiau1
86f0566dc2 Trigger release 2026-05-29 09:21:03 +03:00
peoplemiau1
768e2fc0dc Fix release creation race condition by adding a dedicated create-release job 2026-05-29 09:18:27 +03:00
peoplemiau1
619493bcbd Fix install.sh: support Linux ARM64 and BSD systems, add robust JSON parser checks 2026-05-29 09:13:37 +03:00
peoplemiau1
1dac8661b6 Implement incremental linking and add DragonFly BSD VM build 2026-05-29 09:09:03 +03:00
peoplemiau1
80a6d574a3 Rewrite install_bsd.sh for POSIX sh compliance 2026-05-29 08:27:23 +03:00
peoplemiau1
daf465368f Add BSD install script and update compatibility table in README 2026-05-29 07:51:10 +03:00
peoplemiau1
800fe38fba Fix GHA release: resolve stable toolchain override and sanitize Arch Linux package version 2026-05-28 23:20:48 +03:00
peoplemiau1
a7f9b79b93 Fix release CI/CD: make git2 target-specific, install zig for cross-compilation, set version to 0.7.0 2026-05-28 23:15:09 +03:00
peoplemiau1
b03660a9d7 Bump version to 0.7.0-dev and update changelog for pre-release 2026-05-28 23:01:15 +03:00
peoplemiau1
2cb02aa102 Add full FreeBSD, OpenBSD, NetBSD, and DragonflyBSD target support 2026-05-28 22:55:53 +03:00
peoplemiau1
06a8458ca7 feat: add fmt command 2026-05-28 22:55:26 +03:00
c2ef5483a0 update: update dexoron packages 2026-05-28 21:11:49 +08:00
eb24d616c5 fix: Fix GPG permissions after Docker 2026-05-28 18:16:18 +08:00
4f76f54b63 fix: update GPG signing process to resolve permission issues and clarify Release generation 2026-05-28 18:11:34 +08:00
37b9b855ec feat: streamline artifact download process and enhance repository indexing with signing 2026-05-28 18:08:06 +08:00
8f0f67ad62 fix: correct paths for RPM package artifacts and update repository indexing 2026-05-28 17:49:29 +08:00
49945aa65d feat: add --clobber flag to Download Packages from Release Assets 2026-05-28 17:44:51 +08:00
83593aa592 feat: enhance Dexoron Packages Index workflow with manual dispatch and improved artifact handling 2026-05-28 17:41:50 +08:00
2835188da9 feat: update Dexoron Packages Index workflow and improve release process for NetBSD 2026-05-28 17:09:36 +08:00
47 changed files with 1636 additions and 225 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: 'https://dalink.to/dexoron'

View File

@@ -2,6 +2,11 @@ name: Dexoron Packages Index
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
tag:
description: "Версия (тег) для ручного импорта пакетов (например, v0.6.9)"
required: true
type: string
workflow_run: workflow_run:
workflows: ["Release"] workflows: ["Release"]
types: types:
@@ -13,7 +18,7 @@ permissions:
jobs: jobs:
update-packages-index: update-packages-index:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event_name == 'workflow_dispatch' || github.event_workflow_run.conclusion == 'success' }}
steps: steps:
- name: Checkout Central Packages Repo - name: Checkout Central Packages Repo
@@ -23,105 +28,82 @@ jobs:
token: ${{ secrets.PACKAGES_REPO_TOKEN }} token: ${{ secrets.PACKAGES_REPO_TOKEN }}
path: 'central-packages' path: 'central-packages'
# --- СКАЧИВАНИЕ АРТЕФАКТОВ ИЗ РЕЛИЗА --- - name: Import GPG Key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
- name: Download Arch package artifact # --- Загрузка артефактов (сохранена прежняя логика) ---
- name: Download Artifacts
if: ${{ github.event_name == 'workflow_run' }} if: ${{ github.event_name == 'workflow_run' }}
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: arch-package path: central-packages/
path: central-packages/archlinux/x86_64/ github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.PACKAGES_REPO_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
continue-on-error: true
- name: Download Debian package artifact - name: Download Packages from Release Assets
if: ${{ github.event_name == 'workflow_run' }} if: ${{ github.event_name == 'workflow_dispatch' }}
uses: actions/download-artifact@v4 env:
with: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: debian-package TAG: ${{ inputs.tag }}
path: central-packages/debian/ run: |
github-token: ${{ secrets.PACKAGES_REPO_TOKEN }} gh release download "$TAG" --repo dexoron/dcr --pattern "*.pkg.tar.zst" --dir central-packages/archlinux/x86_64/ --clobber
run-id: ${{ github.event.workflow_run.id }} gh release download "$TAG" --repo dexoron/dcr --pattern "*.deb" --dir central-packages/debian/ --clobber
continue-on-error: true gh release download "$TAG" --repo dexoron/dcr --pattern "*.rpm" --dir central-packages/fedora/x86_64/ --clobber
- name: Download RPM package artifact # --- ИНДЕКСАЦИЯ И ПОДПИСЬ ---
if: ${{ github.event_name == 'workflow_run' }}
uses: actions/download-artifact@v4
with:
name: rpm-package
path: central-packages/rpm/x86_64/
github-token: ${{ secrets.PACKAGES_REPO_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
continue-on-error: true
# --- ИНДЕКСАЦИЯ REPO --- - name: Update Pacman Database
- name: Update Pacman Database (Inside Arch Container)
run: | run: |
docker run --rm \ docker run --rm \
-v ${{ github.workspace }}/central-packages:/packages \ -v ${{ github.workspace }}/central-packages:/packages \
-v ~/.gnupg:/root/.gnupg \
archlinux:latest \ archlinux:latest \
bash -c ' bash -c '
cd /packages/archlinux/x86_64 && \ cd /packages/archlinux/x86_64 && \
rm -f dexoron.db dexoron.files && \ packages=(*.pkg.tar.zst) && \
shopt -s nullglob repo-add --sign dexoron.db.tar.gz "${packages[@]}" && \
packages=(*.pkg.tar.zst)
if [ ${#packages[@]} -gt 0 ]; then
echo "Found ${#packages[@]} Arch packages. Updating database..."
repo-add dexoron.db.tar.gz "${packages[@]}" && \
ln -sf dexoron.db.tar.gz dexoron.db && \ ln -sf dexoron.db.tar.gz dexoron.db && \
ln -sf dexoron.files.tar.gz dexoron.files ln -sf dexoron.files.tar.gz dexoron.files
else
echo "No .pkg.tar.zst files found. Skipping."
fi
' '
- name: Fix GPG permissions after Docker
run: sudo chown -R runner:runner ~/.gnupg
- name: Update Debian APT Repository Index - name: Update Debian APT Repository Index
run: | run: |
sudo apt-get update && sudo apt-get install -y dpkg-dev sudo apt-get update && sudo apt-get install -y dpkg-dev
cd central-packages/debian cd central-packages/debian
shopt -s nullglob
deb_packages=(*.deb)
if [ ${#deb_packages[@]} -gt 0 ]; then
echo "Found ${#deb_packages[@]} Debian packages. Generating Packages.gz..."
# Генерируем индекс для плоского (flat) репозитория
dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz
else
echo "No .deb files found. Skipping Debian index update." echo "Origin: Dexoron Packages" > Release
fi echo "Label: Dexoron Packages" >> Release
echo "Suite: stable" >> Release
echo "Codename: stable" >> Release
echo "Architectures: amd64" >> Release
echo "Components: main" >> Release
echo "Description: Dexoron binary package repository" >> Release
echo "Date: $(date -uR)" >> Release
echo "SHA256:" >> Release
echo " $(sha256sum Packages.gz | awk '{print $1" "$2}')" >> Release
gpg --batch --yes --pinentry-mode loopback --local-user 74322D39483E9C9E8C86C0AA0770A5C2A2A82DAC --detach-sign --armor --output Release.gpg Release
gpg --batch --yes --pinentry-mode loopback --local-user 74322D39483E9C9E8C86C0AA0770A5C2A2A82DAC --clearsign --output InRelease Release
- name: Update Fedora DNF Repository Index - name: Update Fedora DNF Repository Index
run: | run: |
sudo apt-get update && sudo apt-get install -y createrepo-c sudo apt-get update && sudo apt-get install -y createrepo-c
cd central-packages/rpm/x86_64 cd central-packages/fedora/x86_64
shopt -s nullglob
rpm_packages=(*.rpm)
if [ ${#rpm_packages[@]} -gt 0 ]; then
echo "Found ${#rpm_packages[@]} RPM packages. Generating repodata..."
# Пересоздаем метаданные для dnf
createrepo_c . createrepo_c .
else gpg --batch --yes --detach-sign --armor --output repodata/repomd.xml.asc repodata/repomd.xml
echo "No .rpm files found. Skipping Fedora index update."
fi
# --- ПУШ ИЗМЕНЕНИЙ ---
- name: Push to dexoron/packages - name: Push to dexoron/packages
run: | run: |
cd central-packages cd central-packages
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .
# Проверяем изменения во всех трех целевых папках if ! git diff --cached --quiet; then
if [ -n "$(git status --porcelain)" ]; then git commit -m "chore: update and sign repositories"
git add archlinux/ debian/ rpm/
git commit -m "chore: update repositories index (arch, debian, rpm)"
git push git push
else
echo "No changes detected in repository databases."
fi fi

View File

@@ -100,10 +100,28 @@ jobs:
name: release-notes name: release-notes
path: .github/release-notes.md path: .github/release-notes.md
# ── Create Release (prevent race conditions) ───────────────────────────────
create-release:
needs: [setup, notes]
runs-on: ubuntu-latest
steps:
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Linux Binaries Matrix ────────────────────────────────────────────────── # ── Linux Binaries Matrix ──────────────────────────────────────────────────
build-linux: build-linux:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }} if: ${{ github.event_name == 'push' || github.event.inputs.run_linux == 'true' }}
needs: [setup, notes] needs: [setup, notes, create-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
@@ -129,6 +147,12 @@ jobs:
pip install cargo-zigbuild pip install cargo-zigbuild
echo "$HOME/.local/bin" >> $GITHUB_PATH echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Setup Zig
if: matrix.target != 'x86_64-unknown-linux-gnu'
uses: mlugg/setup-zig@v1
with:
version: 0.13.0
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
@@ -222,14 +246,15 @@ jobs:
- name: Build Arch Package inside Container - name: Build Arch Package inside Container
if: matrix.package_type == 'arch' if: matrix.package_type == 'arch'
run: | run: |
PKGVER='${{ needs.setup.outputs.version }}' PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN="${PKGVER_ORIG//-/.}"
TAG='${{ needs.setup.outputs.tag }}' TAG='${{ needs.setup.outputs.tag }}'
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/${TAG}.tar.gz" SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/${TAG}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')" SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
mkdir -p arch-build mkdir -p arch-build
cat > arch-build/PKGBUILD <<PKG cat > arch-build/PKGBUILD <<PKG
pkgname=dcr pkgname=dcr
pkgver=${PKGVER} pkgver=${PKGVER_CLEAN}
pkgrel=1 pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects" pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64') arch=('x86_64')
@@ -238,14 +263,14 @@ jobs:
depends=('gcc-libs') depends=('gcc-libs')
makedepends=('cargo') makedepends=('cargo')
options=('!lto') options=('!lto')
source=("\$pkgname-\$pkgver.tar.gz::\$url/archive/refs/tags/v\$pkgver.tar.gz") source=("\$pkgname-\$pkgver.tar.gz::\$url/archive/refs/tags/${TAG}.tar.gz")
sha256sums=('${SHA256}') sha256sums=('${SHA256}')
build() { build() {
cd "\$srcdir/\$pkgname-\$pkgver" cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
cargo build --release --locked cargo build --release --locked
} }
package() { package() {
cd "\$srcdir/\$pkgname-\$pkgver" cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr" install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE" install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
} }
@@ -273,7 +298,7 @@ jobs:
# ── Publish Packages to Release (Out of Matrix) ──────────────────────────── # ── Publish Packages to Release (Out of Matrix) ────────────────────────────
publish-dexoron-packages: publish-dexoron-packages:
needs: [setup, notes, build-linux-packages] needs: [setup, notes, build-linux-packages, create-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download Release Notes - name: Download Release Notes
@@ -302,7 +327,7 @@ jobs:
# ── macOS Matrix ─────────────────────────────────────────────────────────── # ── macOS Matrix ───────────────────────────────────────────────────────────
build-macos: build-macos:
if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }} if: ${{ github.event_name == 'push' || github.event.inputs.run_macos == 'true' }}
needs: [setup, notes] needs: [setup, notes, create-release]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -348,7 +373,7 @@ jobs:
# ── BSD Systems Matrix ───────────────────────────────────────────────────── # ── BSD Systems Matrix ─────────────────────────────────────────────────────
build-bsd: build-bsd:
if: ${{ github.event_name == 'push' || github.event.inputs.run_bsd == 'true' }} if: ${{ github.event_name == 'push' || github.event.inputs.run_bsd == 'true' }}
needs: [setup, notes] needs: [setup, notes, create-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
@@ -408,9 +433,10 @@ jobs:
with: with:
usesh: true usesh: true
run: | run: |
export PATH="/usr/pkg/bin:/usr/pkg/sbin:$PATH"
pkg_add rust perl gmake pkg_add rust perl gmake
export MAKE=gmake mkdir -p /tmp/bin
ln -sf /usr/pkg/bin/gmake /tmp/bin/make
export PATH="/tmp/bin:/usr/pkg/bin:/usr/pkg/sbin:$PATH"
cargo build --release cargo build --release
- name: Prepare NetBSD Artifact - name: Prepare NetBSD Artifact
@@ -420,6 +446,8 @@ jobs:
cp target/release/dcr "$ASSET" cp target/release/dcr "$ASSET"
echo "BSD_ASSET=${ASSET}" >> "$GITHUB_ENV" echo "BSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
# --- UPLOAD TARGET --- # --- UPLOAD TARGET ---
- name: Upload BSD Asset to Release - name: Upload BSD Asset to Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@@ -432,7 +460,7 @@ jobs:
# ── Windows Matrix ───────────────────────────────────────────────────────── # ── Windows Matrix ─────────────────────────────────────────────────────────
build-windows: build-windows:
if: ${{ github.event_name == 'push' || github.event.inputs.run_windows == 'true' }} if: ${{ github.event_name == 'push' || github.event.inputs.run_windows == 'true' }}
needs: [setup, notes] needs: [setup, notes, create-release]
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
@@ -499,14 +527,15 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
PKGVER='${{ needs.setup.outputs.version }}' PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN="${PKGVER_ORIG//-/.}"
SRC_URL='${{ steps.meta.outputs.source_url }}' SRC_URL='${{ steps.meta.outputs.source_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}' SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr mkdir -p packages/aur/dcr
cat > packages/aur/dcr/PKGBUILD <<PKG cat > packages/aur/dcr/PKGBUILD <<PKG
pkgname=dcr pkgname=dcr
pkgver=${PKGVER} pkgver=${PKGVER_CLEAN}
pkgrel=1 pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects" pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
@@ -519,11 +548,11 @@ jobs:
'gcc: build C/C++ projects with GCC' 'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang' 'clang: build C/C++ projects with Clang'
) )
source=("\$pkgname-\$pkgver.tar.gz::\$url/archive/refs/tags/v\$pkgver.tar.gz") source=("\$pkgname-\$pkgver.tar.gz::\${SRC_URL}")
sha256sums=('${SHA256}') sha256sums=('${SHA256}')
build() { build() {
cd "\$srcdir/\$pkgname-\$pkgver" cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
export CARGO_PROFILE_RELEASE_LTO=false export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="\${RUSTFLAGS:-}" export RUSTFLAGS="\${RUSTFLAGS:-}"
export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}" export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
@@ -531,7 +560,7 @@ jobs:
} }
package() { package() {
cd "\$srcdir/\$pkgname-\$pkgver" cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr" install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE" install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
} }
@@ -540,7 +569,7 @@ jobs:
cat > packages/aur/dcr/.SRCINFO <<SRC cat > packages/aur/dcr/.SRCINFO <<SRC
pkgbase = dcr pkgbase = dcr
pkgdesc = Cargo-like utility to manage C/C++ projects pkgdesc = Cargo-like utility to manage C/C++ projects
pkgver = ${PKGVER} pkgver = ${PKGVER_CLEAN}
pkgrel = 1 pkgrel = 1
url = https://github.com/dexoron/dcr url = https://github.com/dexoron/dcr
arch = x86_64 arch = x86_64
@@ -551,7 +580,7 @@ jobs:
optdepends = gcc: build C/C++ projects with GCC optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang optdepends = clang: build C/C++ projects with Clang
options = !lto options = !lto
source = dcr-${PKGVER}.tar.gz::${SRC_URL} source = dcr-${PKGVER_CLEAN}.tar.gz::${SRC_URL}
sha256sums = ${SHA256} sha256sums = ${SHA256}
pkgname = dcr pkgname = dcr
@@ -611,14 +640,15 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
PKGVER='${{ needs.setup.outputs.version }}' PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN="${PKGVER_ORIG//-/.}"
BIN_URL='${{ steps.meta.outputs.bin_url }}' BIN_URL='${{ steps.meta.outputs.bin_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}' SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr-bin mkdir -p packages/aur/dcr-bin
cat > packages/aur/dcr-bin/PKGBUILD <<PKG cat > packages/aur/dcr-bin/PKGBUILD <<PKG
pkgname=dcr-bin pkgname=dcr-bin
pkgver=${PKGVER} pkgver=${PKGVER_CLEAN}
pkgrel=1 pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)" pkgdesc="Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)"
arch=('x86_64') arch=('x86_64')
@@ -641,7 +671,7 @@ jobs:
cat > packages/aur/dcr-bin/.SRCINFO <<SRC cat > packages/aur/dcr-bin/.SRCINFO <<SRC
pkgbase = dcr-bin pkgbase = dcr-bin
pkgdesc = Cargo-like utility to manage C/C++ projects (pre-compiled musl binary) pkgdesc = Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)
pkgver = ${PKGVER} pkgver = ${PKGVER_CLEAN}
pkgrel = 1 pkgrel = 1
url = https://github.com/dexoron/dcr url = https://github.com/dexoron/dcr
arch = x86_64 arch = x86_64
@@ -650,7 +680,7 @@ jobs:
conflicts = dcr conflicts = dcr
optdepends = gcc: build C/C++ projects with GCC optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang optdepends = clang: build C/C++ projects with Clang
source_x86_64 = dcr-${PKGVER}::${BIN_URL} source_x86_64 = dcr-${PKGVER_CLEAN}::${BIN_URL}
sha256sums_x86_64 = ${SHA256} sha256sums_x86_64 = ${SHA256}
pkgname = dcr-bin pkgname = dcr-bin

View File

@@ -1,5 +1,48 @@
# Changelog # Changelog
## [0.7.0] - 2026-05-30
Added:
- **OpenBSD and NetBSD target support** — full platform routing with dynamically generated target triples using `std::env::consts::ARCH` and `std::env::consts::OS`. Affects `build`, `run`, `clean`, and the platform module.
- **`src/platform/bsd.rs`** — new dedicated BSD platform module shared by FreeBSD, OpenBSD, NetBSD, providing `bin_path`, `lib_path`, `elf_path`, `efi_path`, and `shared_lib_path`.
- **`build.out_dir` configuration option** — custom output directory that overrides the default `target/<triple>/<profile>` path for final artifacts. Supported in `build`, `run`, and config validation.
- **`dcr fmt` command** — new CLI command that formats all C/C++ source files (`*.c`, `*.cpp`, `*.h`, `*.hpp`) in `src/` and `tests/` using `clang-format`.
- **Incremental linking** — `needs_link()` function in `common.rs` that checks whether any object file is newer than the linked output, skipping unnecessary re-linking. Implemented for all backends (`unix_cc`, `msvc`, `gas`, `nasm`).
- **`build.kind = "none"` and `"custom"`** — two new project kind types for special build scenarios where no standard artifact is produced.
- **`install_bsd.sh`** — dedicated POSIX-compliant install script for BSD systems (FreeBSD, OpenBSD, NetBSD) with binary download and source build modes.
- **Linux ARM64 support in `install.sh`** — added `Linux:aarch64|Linux:arm64` target triple detection for pre-built binary downloads.
- **BSD OS detection in `install.sh`** — FreeBSD, OpenBSD, NetBSD detection and target triple resolution.
- **`rust-toolchain.toml`** — explicit `stable` channel toolchain pinning.
- **Integration tests** — `build_with_target_config` (verifies `build.target = "linux"`) and `build_with_out_dir` (verifies custom output directory).
- **`get_build_string_with_profile` made `pub`** — so `run.rs` can resolve custom output directory configuration.
Changed:
- **`build.target` semantics changed** — now strictly holds the target triple (e.g. `x86_64-unknown-linux-gnu`) or a short name (`linux`, `macos`, `windows`). No longer doubles as a custom output directory — that functionality moved to `build.out_dir`.
- **`build.standard` made optional** — changed from `String` to `Option<String>`. Validation only enforces non-empty for non-ASM languages. Skipped in `dcr.toml` output when empty.
- **`dcr run` with `out_dir`** — now resolves target directory respecting `build.out_dir` when configured, via `get_build_string_with_profile()` from `build.rs`.
- **`collect_sources()` returns empty vec** instead of error when no source files are found, allowing `kind = "none"` or `kind = "custom"` projects to have no source files.
- **CI/CD release workflow refactored** — `git2` made target-specific (no vendored-openssl on Windows), Zig-based cross-compilation for non-x86_64 Linux targets, Arch Linux package version sanitization (dashes → dots), NetBSD `gmake` symlink.
- **README compatibility table** — FreeBSD, OpenBSD, NetBSD build/runtime status upgraded from community/best-effort to officially supported.
- **Documentation updated** — `build-section.md` documents new `build.target` and `build.out_dir` fields. `target-directory.md` rewritten to clarify the distinction.
- **Dev channel dependency check in `install.sh`** — checks for `python3` or `jq` before attempting dev channel installations.
Fixed:
- **`dcr run` stdout/stderr not inherited** — child process output was captured and manually printed, breaking interactive programs. Fixed by switching to `Command::status()`.
- **Release CI race condition** — build matrix jobs could upload assets to a release that did not yet exist. Fixed by adding a dedicated `create-release` job.
- **GHA release — stable toolchain override** — resolved Rust toolchain override issues in CI.
- **Arch Linux package version sanitization** — version strings with dashes (e.g. `0.7.0-dev`) are invalid for Arch Linux `pkgver`. Fixed by replacing dashes with dots.
- **GPG permissions after Docker** — Docker operations changed GPG directory ownership. Fixed by running `chown` after Docker commands.
- **RPM package artifact paths** — RPM artifacts placed under `rpm/x86_64/` instead of `fedora/x86_64/`. Corrected in Dexoron Packages Index workflow.
- **`install.sh` JSON parsing robustness** — added `jq` as primary parser with `python3` fallback for dev channel release lookups.
Removed:
- **`format_roots()` helper** — removed from `common.rs`. Was only used by the old error path in `collect_sources()`.
- **Individual per-distro artifact download steps** — three separate `actions/download-artifact` steps replaced with unified `gh release download --clobber`.
## [0.6.9] - 2026-05-28 ## [0.6.9] - 2026-05-28
Added: Added:

View File

@@ -33,7 +33,7 @@ DCR supports cross-compilation via `--target <triple>` or short names:
- Configure multiple targets in `dcr.toml`: - Configure multiple targets in `dcr.toml`:
```toml ```toml
[build.targets] [build]
targets = ["linux", "macos"] targets = ["linux", "macos"]
[build.linux] [build.linux]

2
Cargo.lock generated
View File

@@ -112,7 +112,7 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.6.9" version = "0.7.0"
dependencies = [ dependencies = [
"ctrlc", "ctrlc",
"git2", "git2",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.6.9" version = "0.7.0"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -42,7 +42,10 @@ sha2 = "0.10"
toml = "1.0.3" toml = "1.0.3"
serde_json = "1" serde_json = "1"
ctrlc = "3" ctrlc = "3"
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
[target.'cfg(windows)'.dependencies]
git2 = { version = "0.20.4", features = ["vendored-libgit2"] }

View File

@@ -33,6 +33,32 @@
<table> <table>
<tr> <tr>
<td><b>Linux (Dexoron Packages)</b></td>
<td>
See https://dcr.dexoron.su for settings youre packages manager
**Arch Linux:**
```sh
sudo pacman -Sy dcr
```
**Debian/Ubuntu:**
```sh
sudo apt update && sudo apt install dcr
```
**Fedora/RHEL:**
```sh
sudo dnf install dcr
```
</td>
</tr>
<tr>
<td><b>Arch Linux (AUR)</b></td> <td><b>Arch Linux (AUR)</b></td>
<td> <td>
@@ -71,6 +97,16 @@ cargo install dcr
curl -fsSL https://dcr.dexoron.su/install.sh | bash curl -fsSL https://dcr.dexoron.su/install.sh | bash
``` ```
</td>
</tr>
<tr>
<td><b>BSD (script)</b></td>
<td>
```sh
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | bash
```
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -142,15 +178,21 @@ kind = "bin"
|---|---| |---|---|
| `dcr new <name>` | Create a new project | | `dcr new <name>` | Create a new project |
| `dcr init` | Initialize current directory | | `dcr init` | Initialize current directory |
| `dcr add <dep>` | Add a dependency |
| `dcr build [--release]` | Build the project | | `dcr build [--release]` | Build the project |
| `dcr run [--release]` | Build and run | | `dcr run [--release]` | Build and run |
| `dcr clean` | Remove build artifacts | | `dcr clean` | Remove build artifacts |
| `dcr test` | Run project tests |
| `dcr fmt` | Format C/C++ sources (clang-format) |
| `dcr tree` | View dependency tree |
| `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/) and `dcr --help`
--- ---
## Platforms ## Platforms(pre-build source)
<div align="center"> <div align="center">
@@ -159,9 +201,9 @@ other in [docs/](docs/) and `dcr --help`
| Linux | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | | Linux | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
| macOS | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | macOS | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Windows | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | | Windows | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| FreeBSD | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ⚠️ | ❌ | ❌ | ❌ | | FreeBSD | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| OpenBSD | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ⚠️ | ❌ | ❌ | ❌ | | OpenBSD | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| NetBSD | ⚠️ | ⚠️ | ❌ | ❌ | ❌ | ⚠️ | ❌ | ❌ | ❌ | | NetBSD | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
✅ officially supported · ⚠️ community / best-effort · ❌ not supported ✅ officially supported · ⚠️ community / best-effort · ❌ not supported

View File

@@ -27,6 +27,7 @@
* [dcr clean](commands/dcr-clean.md) * [dcr clean](commands/dcr-clean.md)
* [dcr gen](commands/dcr-gen.md) * [dcr gen](commands/dcr-gen.md)
* [dcr test](commands/dcr-test.md) * [dcr test](commands/dcr-test.md)
* [dcr fmt](commands/dcr-fmt.md)
* [dcr tree](commands/dcr-tree.md) * [dcr tree](commands/dcr-tree.md)
## Dependencies & Build ## Dependencies & Build

View File

@@ -30,9 +30,12 @@ dcr build --debug --clean
- `build.compiler` - `build.compiler`
- `build.language` - `build.language`
- `build.standard` - `build.standard`
- `build.cxx_standard`
- `build.kind` - `build.kind`
- `build.target` - `build.target`
- `build.out_dir`
- `build.platform` - `build.platform`
- `package.type`
- `build.cflags` - `build.cflags`
- `build.ldflags` - `build.ldflags`
- `build.ldscript` - `build.ldscript`
@@ -58,8 +61,9 @@ dcr build --debug --clean
- Profile flag (`--debug` / `--release`) can appear in any argument position (duplicates are rejected). - Profile flag (`--debug` / `--release`) can appear in any argument position (duplicates are rejected).
- Unknown profile flags return an error. - Unknown profile flags return an error.
- `--target <triple>` builds for specified target (short names: `linux`, `macos`, `windows`). - `--target <triple>` builds for specified target (short names: `linux`, `macos`, `windows`, `freebsd`, `openbsd`, `netbsd`).
- `--verbose` prints the compiler/linker command lines to stderr (also works via `DCR_DEBUG` env var). - `--verbose` prints the compiler/linker command lines to stderr (also works via `DCR_DEBUG` env var).
- `--workspace <name>` filters build to a specific workspace member.
- If no `--target`, builds for `build.targets` or native. - If no `--target`, builds for `build.targets` or native.
- Incremental rebuild checks source/object mtime and tracked header dependencies. - Incremental rebuild checks source/object mtime and tracked header dependencies.
- `--force` skips build cache checks and recompiles. - `--force` skips build cache checks and recompiles.

19
docs/commands/dcr-fmt.md Normal file
View File

@@ -0,0 +1,19 @@
# dcr fmt
Formats all C/C++ source files using `clang-format`.
## Usage
```sh
dcr fmt
```
## Behavior
1. Scans `src/` and `tests/` directories recursively.
2. Formats all C/C++ source and header files (`.c`, `.cpp`, `.cxx`, `.cc`, `.h`, `.hpp`, `.hxx`, `.hh`) using `clang-format`.
3. Uses `.clang-format` if present in the project root, otherwise falls back to LLVM style.
## Requirements
- `clang-format` must be installed and available in PATH.

View File

@@ -23,11 +23,12 @@ dcr run --debug --clean
## Restrictions ## Restrictions
- If `build.kind` (or `build.{profile}.kind`) is `"staticlib"` or `"sharedlib"`, `run` exits with error. - If `build.kind` (or `build.{profile}.kind`) is `"staticlib"`, `"sharedlib"`, `"efi"`, or `"elf"`, `run` exits with error.
- The profile is selected from the first argument. - The profile is selected from the first argument.
- `--target <triple>` runs the binary for specified target (short names: `linux`, `macos`, `windows`). - `--target <triple>` runs the binary for specified target (short names: `linux`, `macos`, `windows`, `freebsd`, `openbsd`, `netbsd`).
- `--force` skips build cache checks and recompiles. - `--force` skips build cache checks and recompiles.
- `--clean` removes `target/<profile>` and `build.clean` paths before building. - `--clean` removes `target/<profile>` and `build.clean` paths before building.
- `--workspace <name>` filters build to a specific workspace member.
## `run.cmd` variables ## `run.cmd` variables

View File

@@ -30,10 +30,13 @@ You can also add `--force` or `--clean` to `build`/`run`.
## Artifacts ## Artifacts
By default: By default (for `kind = "bin"`):
- `target/debug/<name>` (or `.exe` on Windows) - `target/<target>/debug/<name>` (or `.exe` on Windows)
- `target/release/<name>` (or `.exe` on Windows) - `target/<target>/release/<name>` (or `.exe` on Windows)
Where `<target>` is `native` or a target triple.
Use `build.out_dir` to override the output directory.
Default compiler flags: Default compiler flags:
@@ -58,6 +61,14 @@ Shared library mode (`kind = "sharedlib"`):
- macOS: `lib<name>.dylib` - macOS: `lib<name>.dylib`
- Windows: `<name>.dll` - Windows: `<name>.dll`
UEFI executable mode (`kind = "efi"`):
- All platforms: `<name>.efi`
Bare-metal ELF mode (`kind = "elf"`):
- All platforms: `<name>` (no extension)
## Profile overrides ## Profile overrides
Use `[build.debug]` and `[build.release]` to override build settings per profile. Use `[build.debug]` and `[build.release]` to override build settings per profile.

View File

@@ -13,7 +13,8 @@ kind = "bin"
# optional: # optional:
# inherit = true # Inherit from base (default true) # inherit = true # Inherit from base (default true)
# targets = ["linux", "macos"] # Multiple targets to build # targets = ["linux", "macos"] # Multiple targets to build
# target = "./dist" # out_dir = "./dist"
# target = "x86_64-unknown-linux-gnu"
# platform = "x86_64" # platform = "x86_64"
# cflags = ["-Wall"] # cflags = ["-Wall"]
# ldflags = ["-lm"] # ldflags = ["-lm"]
@@ -31,17 +32,17 @@ kind = "bin"
- `language` (string or array, required): `c`, `c++`, `cpp`, `cxx`, or `asm`. - `language` (string or array, required): `c`, `c++`, `cpp`, `cxx`, or `asm`.
Mixed languages can be specified as an array, for example `["c", "c++", "asm"]`. Mixed languages can be specified as an array, for example `["c", "c++", "asm"]`.
- `standard` (string, required): C language standard passed to compiler (e.g. `c11`, `c17`, `gnu11`). - `standard` (string, optional): C language standard passed to compiler (e.g. `c11`, `c17`, `gnu11`). Defaults to `c11`.
- `cxx_standard` (string, optional): C++ language standard passed to compiler for `.cpp`/`.cxx`/`.cc` files - `cxx_standard` (string, optional): C++ language standard passed to compiler for `.cpp`/`.cxx`/`.cc` files
(e.g. `c++17`, `gnu++17`, `c++20`). When set, overrides `standard` for C++ sources. (e.g. `c++17`, `gnu++17`, `c++20`). When set, overrides `standard` for C++ sources.
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`). - `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
- `kind` (string, required): `bin`, `staticlib`, `sharedlib`, `efi`, or `elf`. - `kind` (string, optional): `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, or `custom`. Defaults to `bin`.
- `filename` (string, optional): custom output filename (without extension). Overrides `package.name` for the final artifact. - `filename` (string, optional): custom output filename (without extension). Overrides `package.name` for the final artifact.
- `extension` (string, optional): custom file extension without the leading dot (e.g. `BIN`, `efi`, `EFI`). Combined with `filename`. - `extension` (string, optional): custom file extension without the leading dot (e.g. `BIN`, `efi`, `EFI`). Combined with `filename`.
- `inherit` (bool, optional): inherit settings from base `[build]` (default `true`). If `false`, only use target/profile specific settings. - `inherit` (bool, optional): inherit settings from base `[build]` (default `true`). If `false`, only use target/profile specific settings.
- `targets` (string array, optional): list of targets to build simultaneously. Supports short names (`linux`, `macos`, `windows`). - `targets` (string array, optional): list of targets to build simultaneously. Supports short names (`linux`, `macos`, `windows`).
- `target` (string, optional): custom output directory for final artifact. - `target` (string, optional): platform target triple (e.g. `x86_64-unknown-linux-gnu`) or short name (`linux`, `macos`, `windows`). Same as `--target` CLI flag.
When set, DCR automatically injects `--target=<value>` into `cflags` (useful for clang cross-compilation). - `out_dir` (string, optional): custom output directory for final artifact (overrides default `target/<target>/<profile>/`).
- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`). - `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`).
- `cflags` (string array, optional): extra compile flags. - `cflags` (string array, optional): extra compile flags.
Supports variable substitution — see [Variable substitution](#variable-substitution) below. Supports variable substitution — see [Variable substitution](#variable-substitution) below.

View File

@@ -35,17 +35,16 @@ Workspace-only root:
- `package.name` - `package.name`
- `package.version` - `package.version`
- `build.language` - `build.language`
- `build.standard`
- `build.compiler` - `build.compiler`
- `build.kind`
`build.kind` must be either `bin`, `staticlib`, or `sharedlib`.
## Optional keys ## Optional keys
- `build.standard` (default: `c11`)
- `build.kind` (default: `bin`; valid: `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom`)
- `build.inherit` - `build.inherit`
- `build.targets` - `build.targets`
- `build.target` - `build.target`
- `build.out_dir`
- `build.platform` - `build.platform`
- `build.cflags` - `build.cflags`
- `build.ldflags` - `build.ldflags`

View File

@@ -18,6 +18,8 @@ frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"
### Registry dependencies ### Registry dependencies
Defined by name and version string. DCR automatically resolves the package from the configured registry index. Defined by name and version string. DCR automatically resolves the package from the configured registry index.
- `version` (string, required): version requirement.
- `features` (string array, optional): enable specific features.
### Git dependencies ### Git dependencies
Defined by repository URL and optionally branch, tag, or revision. Defined by repository URL and optionally branch, tag, or revision.

View File

@@ -23,6 +23,9 @@ dcr run --target linux --release
- `linux` → `x86_64-unknown-linux-gnu` - `linux` → `x86_64-unknown-linux-gnu`
- `macos` → `x86_64-apple-darwin` - `macos` → `x86_64-apple-darwin`
- `windows` → `x86_64-pc-windows-msvc` - `windows` → `x86_64-pc-windows-msvc`
- `freebsd` → `x86_64-unknown-freebsd`
- `openbsd` → `x86_64-unknown-openbsd`
- `netbsd` → `x86_64-unknown-netbsd`
### Full Target Triples ### Full Target Triples
DCR supports any valid target triple in the format `<arch>-<vendor>-<os>-<abi>`. Common examples: DCR supports any valid target triple in the format `<arch>-<vendor>-<os>-<abi>`. Common examples:
@@ -32,6 +35,9 @@ DCR supports any valid target triple in the format `<arch>-<vendor>-<os>-<abi>`.
- `aarch64-apple-darwin` - `aarch64-apple-darwin`
- `x86_64-pc-windows-msvc` - `x86_64-pc-windows-msvc`
- `i686-pc-windows-gnu` - `i686-pc-windows-gnu`
- `x86_64-unknown-freebsd`
- `x86_64-unknown-openbsd`
- `x86_64-unknown-netbsd`
## Configuration ## Configuration
@@ -40,11 +46,11 @@ DCR supports any valid target triple in the format `<arch>-<vendor>-<os>-<abi>`.
Configure multiple targets to build simultaneously: Configure multiple targets to build simultaneously:
```toml ```toml
[build.targets] [build]
targets = ["linux", "macos", "windows"] targets = ["linux", "macos", "windows"]
# Or per profile # Or per profile
[build.release.targets] [build.release]
targets = ["linux", "macos"] targets = ["linux", "macos"]
``` ```
@@ -66,6 +72,10 @@ cflags = ["-Wall", "-O2", "-arch", "x86_64"]
[build.windows] [build.windows]
compiler = "x86_64-w64-mingw32-gcc" compiler = "x86_64-w64-mingw32-gcc"
ldflags = ["-static"] ldflags = ["-static"]
[build.freebsd]
compiler = "clang"
cflags = ["-Wall", "-O2"]
``` ```
### Target-Specific Toolchain ### Target-Specific Toolchain
@@ -99,6 +109,9 @@ cmd = "open ./target/macos/release/myapp.app"
[run.windows] [run.windows]
cmd = "wine ./target/windows/release/myapp.exe" cmd = "wine ./target/windows/release/myapp.exe"
[run.freebsd]
cmd = "./target/freebsd/release/myapp"
``` ```
## Workspace and Dependencies Inheritance ## Workspace and Dependencies Inheritance
@@ -151,7 +164,10 @@ target/
├── x86_64-unknown-linux-gnu/ ├── x86_64-unknown-linux-gnu/
│ ├── debug/ │ ├── debug/
│ └── release/ │ └── release/
└── aarch64-linux-gnu/ ├── aarch64-linux-gnu/
│ ├── debug/
│ └── release/
└── x86_64-unknown-freebsd/
├── debug/ ├── debug/
└── release/ └── release/
``` ```
@@ -200,4 +216,4 @@ dcr build --target windows --release
- **Linking errors**: Check `ldflags` and library paths for cross-compilation. - **Linking errors**: Check `ldflags` and library paths for cross-compilation.
- **Header issues**: Include paths may differ; use target-specific `cflags`. - **Header issues**: Include paths may differ; use target-specific `cflags`.
For more help, see [Troubleshooting map](troubleshooting-map.md). For more help, see [Troubleshooting map](../reference-and-help/troubleshooting-map.md).

View File

@@ -26,6 +26,8 @@ json = { git = "https://github.com/nlohmann/json", tag = "v3.11.2" }
- `include`: optional include subpaths (absolute or relative to dep root). - `include`: optional include subpaths (absolute or relative to dep root).
- `lib`: optional library subpaths (absolute or relative to dep root). - `lib`: optional library subpaths (absolute or relative to dep root).
- `libs`: optional linker library names. - `libs`: optional linker library names.
- `features`: optional feature flags for registry dependencies.
- `default-features`: whether to include default features (default `true`).
## Common valid compact forms ## Common valid compact forms
@@ -35,6 +37,8 @@ mylib = { path = "./libs/mylib" }
gitlib = { git = "https://github.com/user/repo" } gitlib = { git = "https://github.com/user/repo" }
# Shortest form for path # Shortest form for path
simple = "./libs/simple" simple = "./libs/simple"
# Registry with features
sdl2 = { version = "2.0.0", features = ["vulkan"] }
``` ```
In these forms DCR will try defaults for include/lib dirs and use the dependency name as the linker library name. In these forms DCR will try defaults for include/lib dirs and use the dependency name as the linker library name.

View File

@@ -2,22 +2,29 @@
By default, DCR writes final artifacts to `target/<target>/<profile>/` (where `<target>` is `native` or target triple). By default, DCR writes final artifacts to `target/<target>/<profile>/` (where `<target>` is `native` or target triple).
You can override this with `build.target`: You can override this with `build.out_dir`:
```toml ```toml
[build] [build]
target = "./dist" out_dir = "./dist"
``` ```
## Behavior ## Behavior
- Binary mode: - Binary mode:
- Linux/macOS: `<target>/<name>` - Linux/macOS: `<out_dir>/<name>`
- Windows: `<target>/<name>.exe` - Windows: `<out_dir>/<name>.exe`
- Static library mode: - Static library mode:
- Linux/macOS: `<target>/lib<name>.a` - Linux/macOS: `<out_dir>/lib<name>.a`
- Windows: `<target>/<name>.lib` - Windows: `<out_dir>/<name>.lib`
## Important detail ## Important detail
`build.target` changes only the final artifact location. Object cache and dependency sync directories still use `target/<target>/<profile>/...`. `build.out_dir` changes only the final artifact location. Object cache and dependency sync directories still use `target/<target>/<profile>/...`.
## Distinction from platform targets
- `build.out_dir` — custom **output directory** path.
- `build.target` / `--target <triple>` CLI flag — **platform target** for cross-compilation (e.g. `x86_64-unknown-linux-gnu`, or short name `linux`).
- `build.targets` (array) — list of **platform targets** to build simultaneously.
- `[build.<target>]` sections — **target-specific config overrides** (e.g. `[build.linux]`).

View File

@@ -37,4 +37,4 @@ project/
- `target/<target>/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`. - `target/<target>/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`.
- `target/<target>/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`. - `target/<target>/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`.
If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<target>/<profile>/obj`. If `build.out_dir` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<target>/<profile>/obj`.

View File

@@ -13,6 +13,8 @@ dcr clean [--debug|--release] [--all]
dcr tree dcr tree
dcr test dcr test
dcr gen <subcommand> dcr gen <subcommand>
dcr fmt
dcr setup
dcr --help dcr --help
dcr --version dcr --version
dcr --update dcr --update
@@ -20,11 +22,13 @@ dcr --update
## Notes on argument parsing ## Notes on argument parsing
- `build`/`run` parse flags in any order (`--debug|--release`, `--force`, `--clean`, `--verbose`) and reject duplicates. - `build`/`run` parse flags in any order (`--debug|--release`, `--force`, `--clean`, `--verbose`, `--workspace`) and reject duplicates.
- `new` requires exactly one argument. - `new` requires exactly one argument.
- `init` and `--update` do not accept extra arguments. - `init` and `--update` do not accept extra arguments.
- `clean` accepts `--debug|--release` and optional `--all`. - `clean` accepts `--debug|--release` and optional `--all`.
- `gen` supports `project-info`, `compile-commands`, `vscode`, `clion`. - `gen` supports `project-info`, `compile-commands`, `vscode`, `clion`.
- `fmt` formats C/C++ sources (`.c`, `.cpp`, `.cxx`, `.cc`, `.h`, `.hpp`, `.hxx`, `.hh`) in `src/` and `tests/` using `clang-format`.
- `setup` initializes DCR registries and local configuration.
- `tree` shows a dependency tree (similar to `cargo tree`). - `tree` shows a dependency tree (similar to `cargo tree`).
- `test` / `tests` runs the project's tests. - `test` / `tests` runs the project's tests.

View File

@@ -19,13 +19,14 @@ categories = ["string", "..."] # optional
[build] [build]
language = "c|c++|cpp|cxx|asm" # required (string or array) language = "c|c++|cpp|cxx|asm" # required (string or array)
standard = "string" # required (e.g. "c11", "gnu11") standard = "string" # optional (default "c11")
cxx_standard = "string" # optional (e.g. "c++17", "gnu++17") cxx_standard = "string" # optional (e.g. "c++17", "gnu++17")
compiler = "string" # required (e.g. "clang", "gcc") compiler = "string" # required (e.g. "clang", "gcc")
kind = "bin|staticlib|sharedlib|efi|elf" # required kind = "bin|staticlib|sharedlib|efi|elf|none|custom" # optional (default "bin")
target = "string" # optional (cross-compilation target triple) target = "string" # optional (cross-compilation target triple)
out_dir = "string" # optional (custom output directory)
platform = "string" # optional (architecture hint, -march) platform = "string" # optional (architecture hint, -march)
type = "string" # optional
cflags = ["string", "..."] # optional (supports {version} etc.) cflags = ["string", "..."] # optional (supports {version} etc.)
ldflags = ["string", "..."] # optional (supports {version} etc.) ldflags = ["string", "..."] # optional (supports {version} etc.)
ldscript = "string" # optional (linker script path) ldscript = "string" # optional (linker script path)
@@ -122,13 +123,14 @@ include = ["string", "..."] # optional (include dirs)
lib = ["string", "..."] # optional (lib dirs) lib = ["string", "..."] # optional (lib dirs)
libs = ["string", "..."] # optional (link libs) libs = ["string", "..."] # optional (link libs)
features = ["string", "..."] # optional (registry features) features = ["string", "..."] # optional (registry features)
default-features = true # optional (include default features)
``` ```
## Validation rules ## Validation rules
- `[package]`, `[build]`, `[dependencies]` must exist. - `[package]`, `[build]`, `[dependencies]` must exist.
- Required string fields must be non-empty. - Required string fields must be non-empty.
- `build.kind` must be `bin`, `staticlib`, `sharedlib`, `efi`, or `elf`. - `build.kind` must be `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, or `custom`.
- Dependency fields `include/lib/libs` must be string arrays when provided. - Dependency fields `include/lib/libs` must be string arrays when provided.
- `build.exclude`/`build.include` must be string arrays when provided. - `build.exclude`/`build.include` must be string arrays when provided.
- `build.roots` must be a string array when provided. - `build.roots` must be a string array when provided.

View File

@@ -2,7 +2,7 @@
## What commands are available? ## What commands are available?
`new`, `init`, `build`, `run`, `clean`, `gen`, `--help`, `--version`, `--update`. `new`, `init`, `add`, `build`, `run`, `clean`, `test`, `fmt`, `tree`, `gen`, `setup`, `--help`, `--version`, `--update`.
## Is `dcr.toml` mandatory? ## Is `dcr.toml` mandatory?

View File

@@ -8,8 +8,11 @@ Used with `dcr build` and `dcr run`:
- `--debug`: select debug profile. - `--debug`: select debug profile.
- `--release`: select release profile. - `--release`: select release profile.
- `--target <triple>`: build for specified target (short names: `linux`, `macos`, `windows`, `freebsd`, `openbsd`, `netbsd`).
- `--force`: ignore incremental cache checks and rebuild. - `--force`: ignore incremental cache checks and rebuild.
- `--clean`: remove `target/<profile>` and `build.clean` paths before build. - `--clean`: remove `target/<profile>` and `build.clean` paths before build.
- `--verbose`: print compiler/linker command lines to stderr.
- `--workspace <name>`: filter build to a specific workspace member.
Notes: Notes:

135
flake.lock generated Normal file
View File

@@ -0,0 +1,135 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1780218205,
"narHash": "sha256-c4MSlBQw5MIDRkad9UNUUnBaOHAHnhuCXrDt90FDVrg=",
"owner": "nix-community",
"repo": "fenix",
"rev": "96e0fc9f1a9b37f6477fa11c3fd48575354773ed",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1770107345,
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"treefmt-nix": "treefmt-nix"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1780179631,
"narHash": "sha256-61A3gj4V1DyPP6lVHTdui4+dvG66pkMZSMd1Kke3Rz8=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "cce33f2f1565958787bb763ae751a6a5f56b95e3",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

109
flake.nix Normal file
View File

@@ -0,0 +1,109 @@
{
description = "DCR — Cargo-like utility for managing C/C++ projects";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
treefmt-nix.url = "github:numtide/treefmt-nix";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
flake-utils,
treefmt-nix,
fenix,
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
};
packageName = "dcr";
rustToolchain = fenix.packages.${system}.stable.toolchain;
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
treefmtEval = treefmt-nix.lib.evalModule pkgs {
projectRootFile = "flake.nix";
programs.alejandra.enable = true;
programs.rustfmt.enable = true;
programs.taplo.enable = true;
};
app = rustPlatform.buildRustPackage {
pname = packageName;
version = "0.6.9";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [
pkgs.pkg-config
pkgs.cmake
pkgs.perl
];
buildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin [
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
# Tests require external services; run separately.
doCheck = false;
};
in {
packages = {
default = app;
dcr = app;
};
apps = let
defaultApp =
(flake-utils.lib.mkApp {
drv = app;
})
// {
meta.description = "Run ${packageName}";
};
in {
default = defaultApp;
dcr = defaultApp;
};
checks = {
dcr = app;
formatting = treefmtEval.config.build.check self;
};
formatter = treefmtEval.config.build.wrapper;
devShells.default = pkgs.mkShell {
inputsFrom = [app];
packages = [
rustToolchain
pkgs.rust-analyzer
treefmtEval.config.build.wrapper
];
shellHook = ''
export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"
'';
};
});
}

View File

@@ -30,8 +30,8 @@ CHANNEL=""
check_os() { check_os() {
case "$(uname -s)" in case "$(uname -s)" in
Linux|Darwin) ;; Linux|Darwin|FreeBSD|OpenBSD|NetBSD) ;;
*) error "Only Linux and macOS are supported"; exit 1 ;; *) error "Only Linux, macOS, and BSD systems are supported"; exit 1 ;;
esac esac
} }
@@ -42,14 +42,25 @@ detect_target() {
case "$os:$arch" in case "$os:$arch" in
Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;; Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;;
Linux:aarch64|Linux:arm64) TARGET_TRIPLE="aarch64-unknown-linux-gnu" ;;
Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;; Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;; Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;;
FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;;
OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;;
NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;;
*) error "Unsupported platform: $os/$arch"; exit 1 ;; *) error "Unsupported platform: $os/$arch"; exit 1 ;;
esac esac
} }
check_common_dependencies() { check_common_dependencies() {
command -v curl >/dev/null 2>&1 || { error "curl is not installed"; exit 1; } command -v curl >/dev/null 2>&1 || { error "curl is not installed"; exit 1; }
if [[ "$CHANNEL" == "dev" ]]; then
if ! command -v python3 >/dev/null 2>&1 && ! command -v jq >/dev/null 2>&1; then
error "For dev installations, either 'python3' or 'jq' must be installed to parse GitHub API response."
exit 1
fi
fi
} }
check_build_dependencies() { check_build_dependencies() {
@@ -61,7 +72,7 @@ select_channel() {
echo "Choose channel:" echo "Choose channel:"
echo " 1) Latest stable release (default)" echo " 1) Latest stable release (default)"
echo " 2) Latest dev (pre-release)" echo " 2) Latest dev (pre-release)"
read -r -p "Enter 1 or 2 [1]: " choice read -r -p "Enter 1 or 2 [1]: " choice < /dev/tty
case "${choice:-1}" in case "${choice:-1}" in
1) CHANNEL="stable" ;; 1) CHANNEL="stable" ;;
@@ -74,7 +85,7 @@ select_install_mode() {
echo "Choose installation mode:" echo "Choose installation mode:"
echo " 1) Download prebuilt binary from GitHub Release (recommended)" echo " 1) Download prebuilt binary from GitHub Release (recommended)"
echo " 2) Build from git" echo " 2) Build from git"
read -r -p "Enter 1 or 2 [1]: " choice read -r -p "Enter 1 or 2 [1]: " choice < /dev/tty
case "${choice:-1}" in case "${choice:-1}" in
1) INSTALL_MODE="release" ;; 1) INSTALL_MODE="release" ;;
@@ -83,14 +94,15 @@ select_install_mode() {
esac esac
} }
# Возвращает JSON нужного релиза в stdout # Returns JSON of the requested release to stdout
fetch_release_json() { fetch_release_json() {
if [[ "$CHANNEL" == "dev" ]]; then if [[ "$CHANNEL" == "dev" ]]; then
log "Looking for latest dev (pre-release)..." log "Looking for latest dev (pre-release)..."
local json local json result
json="$(curl -fsSL "$GITHUB_API_ALL")" json="$(curl -fsSL "$GITHUB_API_ALL")"
# Первый pre-release в списке if command -v jq >/dev/null 2>&1; then
local result result="$(printf '%s' "$json" | jq -e -c 'map(select(.prerelease == true))[0]')"
else
result="$(printf '%s' "$json" | python3 - <<'EOF' result="$(printf '%s' "$json" | python3 - <<'EOF'
import sys, json import sys, json
releases = json.load(sys.stdin) releases = json.load(sys.stdin)
@@ -101,7 +113,9 @@ else:
print(json.dumps(pre[0]), end="") print(json.dumps(pre[0]), end="")
EOF EOF
)" )"
if [[ -z "$result" || "$result" == "{}" ]]; then fi
if [[ -z "$result" || "$result" == "{}" || "$result" == "null" ]]; then
error "No dev (pre-release) found on GitHub" error "No dev (pre-release) found on GitHub"
exit 1 exit 1
fi fi
@@ -123,9 +137,9 @@ download_binary() {
exit 1 exit 1
fi fi
# Версия без ведущего 'v' # Version without leading 'v'
version="${tag#v}" version="${tag#v}"
# Имя бинарника: dcr-<triple>-<version> # Binary name: dcr-<triple>-<version>
asset_name="dcr-${TARGET_TRIPLE}-${version}" asset_name="dcr-${TARGET_TRIPLE}-${version}"
log "Fetching release ${tag} (channel: ${CHANNEL})..." log "Fetching release ${tag} (channel: ${CHANNEL})..."
@@ -149,7 +163,7 @@ prepare_sources() {
rm -rf "$TMPDIR" rm -rf "$TMPDIR"
git clone --depth 1 "$REPO_URL" "$TMPDIR" git clone --depth 1 "$REPO_URL" "$TMPDIR"
if [[ "$CHANNEL" == "dev" ]]; then if [[ "$CHANNEL" == "dev" ]]; then
# Клонируем dev ветку, если она есть # Clone dev branch if it exists
git clone --depth 1 --branch dev "$REPO_URL" "$TMPDIR" 2>/dev/null || \ git clone --depth 1 --branch dev "$REPO_URL" "$TMPDIR" 2>/dev/null || \
git clone --depth 1 "$REPO_URL" "$TMPDIR" git clone --depth 1 "$REPO_URL" "$TMPDIR"
fi fi
@@ -193,8 +207,8 @@ main() {
check_os check_os
detect_target detect_target
check_common_dependencies
select_channel select_channel
check_common_dependencies
select_install_mode select_install_mode
cleanup cleanup

223
install_bsd.sh Executable file
View File

@@ -0,0 +1,223 @@
#!/bin/sh
set -eu
TMPDIR="/tmp/dcr-install"
INSTALL_PATH="$HOME/.local/share/dcr"
BINPATH="$HOME/.local/bin"
LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr"
GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
GITHUB_API_ALL="https://api.github.com/repos/dexoron/dcr/releases"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
mkdir -p "$(dirname "$LOGFILE")"
log() { printf "${BLUE}[%s]${NC} %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOGFILE"; }
success() { printf "${GREEN}✔ %s${NC}\n" "$1" | tee -a "$LOGFILE"; }
warn() { printf "${YELLOW}⚠ %s${NC}\n" "$1" | tee -a "$LOGFILE"; }
error() { printf "${RED}✖ %s${NC}\n" "$1" | tee -a "$LOGFILE" >&2; }
INSTALL_MODE=""
CHANNEL=""
check_os() {
case "$(uname -s)" in
FreeBSD|OpenBSD|NetBSD|DragonFly) ;;
*) error "Only BSD systems (FreeBSD, OpenBSD, NetBSD, DragonFly) are supported by this script"; exit 1 ;;
esac
}
detect_target() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os:$arch" in
FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;;
OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;;
NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;;
DragonFly:x86_64|DragonFly:amd64) TARGET_TRIPLE="x86_64-unknown-dragonfly" ;;
*) error "Unsupported BSD platform: $os/$arch"; exit 1 ;;
esac
}
check_common_dependencies() {
command -v curl >/dev/null 2>&1 || { error "curl is not installed"; exit 1; }
if [ "$CHANNEL" = "dev" ]; then
if ! command -v python3 >/dev/null 2>&1 && ! command -v jq >/dev/null 2>&1; then
error "For dev installations, either 'python3' or 'jq' must be installed to parse GitHub API response."
exit 1
fi
fi
}
check_build_dependencies() {
command -v git >/dev/null 2>&1 || { error "git is not installed"; exit 1; }
command -v cargo >/dev/null 2>&1 || { error "cargo is not installed"; exit 1; }
}
select_channel() {
echo "Choose channel:"
echo " 1) Latest stable release (default)"
echo " 2) Latest dev (pre-release)"
printf "Enter 1 or 2 [1]: "
read -r choice
case "${choice:-1}" in
1) CHANNEL="stable" ;;
2) CHANNEL="dev" ;;
*) error "Unknown option"; exit 1 ;;
esac
}
select_install_mode() {
echo "Choose installation mode:"
echo " 1) Download prebuilt binary from GitHub Release (recommended)"
echo " 2) Build from git"
printf "Enter 1 or 2 [1]: "
read -r choice
case "${choice:-1}" in
1) INSTALL_MODE="release" ;;
2) INSTALL_MODE="build" ;;
*) error "Unknown option"; exit 1 ;;
esac
}
fetch_release_json() {
if [ "$CHANNEL" = "dev" ]; then
log "Looking for latest dev (pre-release)..."
local json result
json="$(curl -fsSL "$GITHUB_API_ALL")"
if command -v jq >/dev/null 2>&1; then
result="$(printf '%s' "$json" | jq -e -c 'map(select(.prerelease == true))[0]')"
else
result="$(printf '%s' "$json" | python3 - <<'EOF'
import sys, json
releases = json.load(sys.stdin)
pre = [r for r in releases if r.get("prerelease", False)]
if not pre:
print("{}", end="")
else:
print(json.dumps(pre[0]), end="")
EOF
)"
fi
if [ -z "$result" ] || [ "$result" = "{}" ] || [ "$result" = "null" ]; then
error "No dev (pre-release) found on GitHub"
exit 1
fi
printf '%s' "$result"
else
curl -fsSL "$GITHUB_API_LATEST"
fi
}
download_binary() {
local release_json tag version asset_name download_url
release_json="$(fetch_release_json)"
tag="$(printf '%s\n' "$release_json" | \
sed -n 's/.*"tag_name": "\([^"]*\)".*/\1/p' | head -n1)"
if [ -z "$tag" ]; then
error "Failed to determine release version"
exit 1
fi
version="${tag#v}"
asset_name="dcr-${TARGET_TRIPLE}-${version}"
log "Fetching release ${tag} (channel: ${CHANNEL})..."
download_url="$(printf '%s\n' "$release_json" | \
sed -n "s#.*\"browser_download_url\": \"\([^\"]*/${asset_name}\)\".*#\1#p" | head -n1)"
if [ -z "$download_url" ]; then
error "Asset ${asset_name} not found in release ${tag}"
exit 1
fi
mkdir -p "$INSTALL_PATH"
curl -fL "$download_url" -o "$INSTALL_PATH/dcr"
chmod +x "$INSTALL_PATH/dcr"
success "Binary ${asset_name} downloaded (${tag})"
}
prepare_sources() {
log "Fetching sources..."
rm -rf "$TMPDIR"
git clone --depth 1 "$REPO_URL" "$TMPDIR"
if [ "$CHANNEL" = "dev" ]; then
git clone --depth 1 --branch dev "$REPO_URL" "$TMPDIR" 2>/dev/null || \
git clone --depth 1 "$REPO_URL" "$TMPDIR"
fi
success "Sources fetched"
}
build_binary() {
log "Building release binary..."
(cd "$TMPDIR" && cargo build --release)
success "Build completed"
}
install_built_binary() {
mkdir -p "$INSTALL_PATH"
cp "$TMPDIR/target/release/dcr" "$INSTALL_PATH/dcr"
chmod +x "$INSTALL_PATH/dcr"
success "Binary installed from source"
}
install_link() {
log "Creating symlink..."
mkdir -p "$BINPATH"
ln -sf "$INSTALL_PATH/dcr" "$BINPATH/dcr"
success "Command 'dcr' added to $BINPATH"
}
check_path() {
if ! echo "$PATH" | grep -q "$BINPATH"; then
warn "Directory $BINPATH not found in PATH"
echo "Add this to ~/.bashrc, ~/.zshrc or ~/.profile:"
echo "export PATH=\"$BINPATH:\$PATH\""
fi
}
cleanup() {
rm -rf "$TMPDIR" 2>/dev/null || true
}
main() {
log "Starting DCR installation for BSD"
check_os
select_channel
check_common_dependencies
detect_target
select_install_mode
cleanup
if [ "$INSTALL_MODE" = "build" ]; then
check_build_dependencies
prepare_sources
build_binary
install_built_binary
else
download_binary
fi
install_link
check_path
cleanup
success "Installation completed successfully"
log "Log saved to $LOGFILE"
}
main "$@"

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "stable"

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Sync all GitHub Releases (with assets) to GitLab Releases."""
import json
import os
import sys
import time
import urllib.error
import urllib.request
import uuid
GL_TOKEN = os.environ.get("GL_TOKEN")
if not GL_TOKEN:
print("FATAL: GL_TOKEN environment variable not set", file=sys.stderr)
sys.exit(1)
GL_PROJECT = "dexoron-studio%2Fdcr"
GL_PROJECT_RAW = "dexoron-studio/dcr"
GL_API = "https://gitlab.com/api/v4"
GH_REPO = "dexoron/dcr"
def gl_api(method, path, data=None, filepath=None, retries=5):
url = f"{GL_API}/{path}"
headers = {"PRIVATE-TOKEN": GL_TOKEN}
if data is not None and filepath is None:
headers["Content-Type"] = "application/json"
body = json.dumps(data).encode()
elif filepath:
boundary = uuid.uuid4().hex
with open(filepath, "rb") as f:
fdata = f.read()
fname = os.path.basename(filepath)
parts = []
parts.append(f"--{boundary}".encode())
parts.append(f'Content-Disposition: form-data; name="file"; filename="{fname}"'.encode())
parts.append(b"Content-Type: application/octet-stream")
parts.append(b"")
parts.append(fdata)
parts.append(f"--{boundary}--".encode())
parts.append(b"")
body = b"\r\n".join(parts)
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
else:
body = None
for attempt in range(retries):
try:
req = urllib.request.Request(url, data=body, headers=headers, method=method)
with urllib.request.urlopen(req) as resp:
text = resp.read().decode()
return json.loads(text) if text else None
except urllib.error.HTTPError as e:
body_text = e.read().decode() if e.fp else ""
if e.code in (409, 429) and attempt < retries - 1:
time.sleep(3)
continue
if attempt < retries - 1:
time.sleep(1)
continue
print(f" HTTP {e.code}: {body_text[:300]}", file=sys.stderr)
return None
except Exception as e:
if attempt < retries - 1:
time.sleep(1)
continue
print(f" Error: {e}", file=sys.stderr)
return None
return None
def download_file(url, dest):
with urllib.request.urlopen(url) as resp:
with open(dest, "wb") as f:
f.write(resp.read())
# --- MAIN ---
print("Fetching GitHub releases...")
with urllib.request.urlopen(f"https://api.github.com/repos/{GH_REPO}/releases?per_page=50") as r:
gh_releases = json.loads(r.read().decode())
print(f"Found {len(gh_releases)} releases on GitHub")
# Reverse to process oldest first (GitHub returns newest first)
gh_releases.reverse()
print("Fetching existing GitLab releases and their assets...")
existing = gl_api("GET", f"projects/{GL_PROJECT}/releases?per_page=100") or []
existing = existing if isinstance(existing, list) else []
# Build map: tag -> set of existing asset names
gl_assets = {}
for rel in existing:
tag = rel["tag_name"]
links = rel.get("assets", {}).get("links", []) or rel.get("assets", {}).get("sources", []) or []
gl_assets[tag] = {l.get("name", "") for l in links if isinstance(l, dict)}
# Also get the release description to check if it needs updating
gl_assets[tag + "__desc"] = rel.get("description", "")
print(f"Existing GitLab releases: {len(existing)}")
download_dir = "/tmp/gh-assets"
os.makedirs(download_dir, exist_ok=True)
# Reverse: process older first so newer releases with many assets get their description too
# Actually, process in original order
total = len(gh_releases)
for i, rel in enumerate(gh_releases):
tag = rel["tag_name"]
body = rel.get("body", "") or ""
assets = rel.get("assets", [])
if isinstance(assets, dict):
assets = assets.get("nodes", []) or []
print(f"\n{'='*60}")
print(f"[{i+1}/{total}] {tag} ({len(assets)} assets)")
# Create or update release
existing_tag = tag in gl_assets
if not existing_tag:
print(f" Creating release...")
result = gl_api("POST", f"projects/{GL_PROJECT}/releases", data={
"tag_name": tag,
"name": rel.get("name", tag),
"description": body,
"ref": "master",
})
if result is None:
print(f" FAILED — skip")
continue
print(f" OK")
else:
# Update description if needed
existing_desc = gl_assets.get(tag + "__desc", "")
if body and body != existing_desc:
print(f" Updating description...")
gl_api("PUT", f"projects/{GL_PROJECT}/releases/{urllib.request.quote(tag, safe='')}",
data={"description": body})
existing_links = gl_assets.get(tag, set())
for j, asset in enumerate(assets):
aname = asset.get("name", "")
aurl = asset.get("browser_download_url", "")
if not aurl:
continue
if aname in existing_links:
print(f" [{j+1}/{len(assets)}] {aname} — already exists")
continue
fpath = os.path.join(download_dir, aname)
print(f" [{j+1}/{len(assets)}] {aname}...", end=" ", flush=True)
# Download from GitHub
try:
download_file(aurl, fpath)
except Exception as e:
print(f"download failed: {e}")
continue
# Upload to GitLab
up = gl_api("POST", f"projects/{GL_PROJECT}/uploads", filepath=fpath)
if not up or "url" not in up:
size_mb = os.path.getsize(fpath) / (1024 * 1024)
print(f"upload failed ({size_mb:.1f} MB)")
os.remove(fpath)
continue
file_url = up["url"]
full_url = f"https://gitlab.com/{GL_PROJECT_RAW}{file_url}"
# Link asset to release
encoded_tag = urllib.request.quote(tag, safe='')
link = gl_api("POST",
f"projects/{GL_PROJECT}/releases/{encoded_tag}/assets/links",
data={"name": aname, "url": full_url},
retries=5)
if link:
print(f"linked")
existing_links.add(aname)
else:
print(f"link failed")
os.remove(fpath)
time.sleep(0.3)
print(f"\n{'='*60}")
print("Done!")

View File

@@ -80,15 +80,26 @@ pub fn build(args: &[String]) -> i32 {
} }
if flags.target.is_none() { if flags.target.is_none() {
let default_target = if cfg!(target_os = "linux") { let default_target = if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu" format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
"x86_64-apple-darwin" "x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") { } else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc" "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 { } else {
"unknown" "unknown".to_string()
}; };
flags.target = Some(default_target.to_string()); flags.target = Some(default_target);
} }
if flags.clean { if flags.clean {
let mut clean_args = Vec::new(); let mut clean_args = Vec::new();
@@ -200,7 +211,7 @@ fn get_string_with_profile_and_target(
} }
} }
fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String { pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
get_string_with_profile_and_target(config, field, profile, None) get_string_with_profile_and_target(config, field, profile, None)
} }
@@ -390,6 +401,15 @@ fn ensure_target_dirs(
let default_dir = if cfg!(target_os = "linux") { let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH; let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}") format!("{arch}-unknown-linux-gnu/{profile}")
} else if cfg!(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
format!("{arch}-unknown-{os}/{profile}")
} else { } else {
profile.to_string() profile.to_string()
}; };
@@ -692,7 +712,11 @@ fn build_project_at(
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
let target_dir_binding = match workspace_root { let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile);
let target_dir_binding = if !out_dir_config.is_empty() {
out_dir_config
} else {
match workspace_root {
Some(root) => { Some(root) => {
let target_str = build_target let target_str = build_target
.map(normalize_target_os) .map(normalize_target_os)
@@ -712,6 +736,7 @@ fn build_project_at(
root.join(&rel).to_string_lossy().to_string() root.join(&rel).to_string_lossy().to_string()
} }
None => normalize_target(build_target.unwrap_or(""), profile).unwrap_or_default(), None => normalize_target(build_target.unwrap_or(""), profile).unwrap_or_default(),
}
}; };
let target_dir = if target_dir_binding.is_empty() { let target_dir = if target_dir_binding.is_empty() {
None None

View File

@@ -120,11 +120,22 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
}) })
.or_else(|| { .or_else(|| {
Some(if cfg!(target_os = "linux") { Some(if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu".to_string() format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string() "x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") { } else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string() "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 { } else {
"unknown".to_string() "unknown".to_string()
}) })

81
src/cli/fmt.rs Normal file
View File

@@ -0,0 +1,81 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored};
use glob::glob;
use std::process::Command;
pub fn fmt(_args: &[String]) -> i32 {
let patterns = [
"src/**/*.c",
"src/**/*.cpp",
"src/**/*.cxx",
"src/**/*.cc",
"src/**/*.h",
"src/**/*.hpp",
"src/**/*.hxx",
"src/**/*.hh",
"tests/**/*.c",
"tests/**/*.cpp",
"tests/**/*.cxx",
"tests/**/*.cc",
"tests/**/*.h",
"tests/**/*.hpp",
"tests/**/*.hxx",
"tests/**/*.hh",
];
let mut files = Vec::new();
for pattern in patterns {
if let Ok(paths) = glob(pattern) {
for path in paths.flatten() {
if let Some(s) = path.to_str() {
files.push(s.to_string());
}
}
}
}
if files.is_empty() {
println!(" {} No files to format", colored("Format", BOLD_GREEN));
return 0;
}
println!(
" {} {} files",
colored("Formatting", BOLD_GREEN),
files.len()
);
let status = Command::new("clang-format").arg("-i").args(&files).status();
match status {
Ok(s) if s.success() => {
println!(" {} successful", colored("Format", BOLD_GREEN));
0
}
Ok(s) => {
error(&format!("clang-format failed with status: {}", s));
1
}
Err(e) => {
error(&format!("Failed to execute clang-format: {}", e));
1
}
}
}

View File

@@ -31,6 +31,7 @@ pub fn help() -> i32 {
println!(" run [--profile] Build and run the project (default: --debug)"); println!(" run [--profile] Build and run the project (default: --debug)");
println!(" tree Display the dependency tree"); println!(" tree Display the dependency tree");
println!(" test Run project tests (alias: tests)"); println!(" test Run project tests (alias: tests)");
println!(" fmt Format all C/C++ source files using clang-format");
println!(" clean Remove the target directory"); println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files"); println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN); printc("FLAGS:", BOLD_GREEN);

View File

@@ -20,6 +20,7 @@ pub mod build;
pub mod clean; pub mod clean;
pub mod flag_update; pub mod flag_update;
pub mod flags; pub mod flags;
pub mod fmt;
pub mod r#gen; pub mod r#gen;
pub mod help; pub mod help;
pub mod init; pub mod init;

View File

@@ -83,7 +83,6 @@ pub fn run(args: &[String]) -> i32 {
return 1; return 1;
} }
}; };
let flags = match parse_build_run_flags(args) { let flags = match parse_build_run_flags(args) {
Ok(v) => v, Ok(v) => v,
Err(_) => return 1, Err(_) => return 1,
@@ -160,15 +159,26 @@ fn run_project(
let mut target = flags.target.clone(); let mut target = flags.target.clone();
if target.is_none() { if target.is_none() {
let default_target = if cfg!(target_os = "linux") { let default_target = if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu" format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
"x86_64-apple-darwin" "x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") { } else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc" "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 { } else {
"unknown" "unknown".to_string()
}; };
target = Some(default_target.to_string()); target = Some(default_target);
} }
let build_kind = config let build_kind = config
@@ -177,7 +187,12 @@ fn run_project(
.or_else(|| config.get("build.kind").and_then(|v| v.as_str())) .or_else(|| config.get("build.kind").and_then(|v| v.as_str()))
.unwrap_or(""); .unwrap_or("");
let normalized_target_dir = match workspace_root { let out_dir =
crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
let normalized_target_dir = if !out_dir.is_empty() {
Some(out_dir)
} else {
match workspace_root {
Some(wr) => target Some(wr) => target
.as_ref() .as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)) .and_then(|t| crate::cli::build::normalize_target(t, &flags.profile))
@@ -185,6 +200,7 @@ fn run_project(
None => target None => target
.as_ref() .as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)), .and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)),
}
}; };
let version = config let version = config

View File

@@ -37,9 +37,6 @@ pub fn collect_sources(
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?; collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
} }
sources.sort(); sources.sort();
if sources.is_empty() {
return Err(format!("No source files found in {}", format_roots(roots)));
}
Ok(sources) Ok(sources)
} }
@@ -89,17 +86,6 @@ fn collect_sources_rec(
Ok(()) Ok(())
} }
fn format_roots(roots: &[PathBuf]) -> String {
if roots.is_empty() {
return "<none>".to_string();
}
let mut out = Vec::new();
for root in roots {
out.push(root.to_string_lossy().to_string());
}
out.join(", ")
}
pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool { pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[String]) -> bool {
if matches_patterns(path, include_paths, true) { if matches_patterns(path, include_paths, true) {
return false; return false;
@@ -316,6 +302,31 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
false false
} }
pub fn needs_link(objects: &[String], output_path: &str) -> bool {
let out_meta = match fs::metadata(output_path) {
Ok(m) => m,
Err(_) => return true,
};
let out_time = match out_meta.modified() {
Ok(t) => t,
Err(_) => return true,
};
for obj in objects {
let obj_meta = match fs::metadata(obj) {
Ok(m) => m,
Err(_) => return true,
};
let obj_time = match obj_meta.modified() {
Ok(t) => t,
Err(_) => return true,
};
if obj_time > out_time {
return true;
}
}
false
}
fn parse_d_file(content: &str) -> Vec<String> { fn parse_d_file(content: &str) -> Vec<String> {
let mut deps = Vec::new(); let mut deps = Vec::new();
let text = content.replace("\\\n", " ").replace("\\\r\n", " "); let text = content.replace("\\\n", " ").replace("\\\r\n", " ");
@@ -418,6 +429,55 @@ mod tests {
assert_eq!(result, "target/debug/obj/main.c.obj"); assert_eq!(result, "target/debug/obj/main.c.obj");
} }
#[test]
fn needs_link_no_output() {
let dir = temp_dir("link_no_out");
let obj1 = dir.join("test1.o");
let obj2 = dir.join("test2.o");
let out = dir.join("app");
fs::write(&obj1, "").unwrap();
fs::write(&obj2, "").unwrap();
let objs = vec![
obj1.to_string_lossy().to_string(),
obj2.to_string_lossy().to_string(),
];
assert!(needs_link(&objs, &out.to_string_lossy()));
}
#[test]
fn needs_link_fresh() {
let dir = temp_dir("link_fresh");
let obj1 = dir.join("test1.o");
let obj2 = dir.join("test2.o");
let out = dir.join("app");
fs::write(&obj1, "").unwrap();
fs::write(&obj2, "").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&out, "").unwrap();
let objs = vec![
obj1.to_string_lossy().to_string(),
obj2.to_string_lossy().to_string(),
];
assert!(!needs_link(&objs, &out.to_string_lossy()));
}
#[test]
fn needs_link_stale() {
let dir = temp_dir("link_stale");
let obj1 = dir.join("test1.o");
let obj2 = dir.join("test2.o");
let out = dir.join("app");
fs::write(&obj1, "").unwrap();
fs::write(&out, "").unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
fs::write(&obj2, "").unwrap();
let objs = vec![
obj1.to_string_lossy().to_string(),
obj2.to_string_lossy().to_string(),
];
assert!(needs_link(&objs, &out.to_string_lossy()));
}
#[test] #[test]
fn needs_rebuild_no_object() { fn needs_rebuild_no_object() {
let dir = temp_dir("rebuild_no_obj"); let dir = temp_dir("rebuild_no_obj");
@@ -550,7 +610,8 @@ mod tests {
let result = collect_sources(&roots, &["c"], &[], &include); let result = collect_sources(&roots, &["c"], &[], &include);
std::env::set_current_dir(prev).unwrap(); std::env::set_current_dir(prev).unwrap();
assert!(result.is_err(), "empty src should return error"); let sources = result.expect("empty src should return Ok");
assert!(sources.is_empty(), "empty src should return empty vec");
} }
#[test] #[test]

View File

@@ -43,6 +43,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); 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") { let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib" "lib"
} else { } else {
@@ -105,6 +109,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else { } else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir) 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); cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {

View File

@@ -43,6 +43,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); 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 mut cmd = Command::new(ctx.archiver.unwrap_or("lib")); let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}")); cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
for obj in &objects { for obj in &objects {
@@ -117,6 +121,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else { } else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir) platform::bin_path(ctx.profile, ctx.project_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(format!("/Fe:{out_path}")); cmd.arg(format!("/Fe:{out_path}"));
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {

View File

@@ -43,6 +43,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); 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") { let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib" "lib"
} else { } else {
@@ -105,6 +109,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else { } else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir) 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); cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {

View File

@@ -45,6 +45,10 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); 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 mut cmd = Command::new(ctx.archiver.unwrap_or("ar")); let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
cmd.arg("rcs").arg(&lib_path); cmd.arg("rcs").arg(&lib_path);
for obj in &objects { for obj in &objects {
@@ -106,6 +110,11 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else { } else {
platform::bin_path(ctx.profile, &final_name, ctx.target_dir) 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); cmd.arg("-o").arg(out_path);
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {

View File

@@ -27,7 +27,15 @@ const DEFAULT_LANGUAGE: &str = "c";
const DEFAULT_STANDARD: &str = "c11"; const DEFAULT_STANDARD: &str = "c11";
const DEFAULT_COMPILER: &str = "clang"; const DEFAULT_COMPILER: &str = "clang";
const DEFAULT_KIND: &str = "bin"; const DEFAULT_KIND: &str = "bin";
const VALID_KINDS: &[&str] = &["bin", "staticlib", "sharedlib", "efi", "elf"]; const VALID_KINDS: &[&str] = &[
"bin",
"staticlib",
"sharedlib",
"efi",
"elf",
"none",
"custom",
];
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum ConfigError {
@@ -101,7 +109,8 @@ pub struct PackageConfig {
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct BuildConfig { pub struct BuildConfig {
pub language: LanguageConfig, pub language: LanguageConfig,
pub standard: String, #[serde(default)]
pub standard: Option<String>,
#[serde(default)] #[serde(default)]
pub cxx_standard: Option<String>, pub cxx_standard: Option<String>,
pub compiler: String, pub compiler: String,
@@ -110,6 +119,8 @@ pub struct BuildConfig {
#[serde(default)] #[serde(default)]
pub target: Option<String>, pub target: Option<String>,
#[serde(default)] #[serde(default)]
pub out_dir: Option<String>,
#[serde(default)]
pub platform: Option<String>, pub platform: Option<String>,
#[serde(default)] #[serde(default)]
pub cflags: Vec<String>, pub cflags: Vec<String>,
@@ -353,7 +364,14 @@ impl Config {
} }
validate_language_config(&build.language, "build.language")?; validate_language_config(&build.language, "build.language")?;
if build.standard.trim().is_empty() { let lang_is_asm = match &build.language {
LanguageConfig::One(s) => s.trim().eq_ignore_ascii_case("asm"),
LanguageConfig::Many(v) => v.len() == 1 && v[0].trim().eq_ignore_ascii_case("asm"),
};
if let Some(ref std) = build.standard
&& std.trim().is_empty()
&& !lang_is_asm
{
return Err(ConfigError::Invalid("build.standard is empty".into())); return Err(ConfigError::Invalid("build.standard is empty".into()));
} }
if build.compiler.trim().is_empty() { if build.compiler.trim().is_empty() {
@@ -380,6 +398,9 @@ impl Config {
if let Some(target) = &build.target { if let Some(target) = &build.target {
validate_non_empty_string(target, "build.target")?; validate_non_empty_string(target, "build.target")?;
} }
if let Some(out_dir) = &build.out_dir {
validate_non_empty_string(out_dir, "build.out_dir")?;
}
for profile in ["release", "debug"] { for profile in ["release", "debug"] {
if let Some(section) = self if let Some(section) = self
.data .data
@@ -523,7 +544,9 @@ impl Config {
if let Some(lang) = table.get("language") { if let Some(lang) = table.get("language") {
self.validate_language(lang)?; self.validate_language(lang)?;
} }
for key in ["standard", "compiler", "kind", "target", "platform"] { for key in [
"standard", "compiler", "kind", "target", "out_dir", "platform",
] {
if let Some(value) = table.get(key) { if let Some(value) = table.get(key) {
let s = value.as_str().unwrap_or(""); let s = value.as_str().unwrap_or("");
if s.trim().is_empty() { if s.trim().is_empty() {
@@ -754,7 +777,9 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
} else { } else {
out.push_str(&format!("language = \"{language}\"\n")); out.push_str(&format!("language = \"{language}\"\n"));
} }
if !standard.is_empty() {
out.push_str(&format!("standard = \"{standard}\"\n")); out.push_str(&format!("standard = \"{standard}\"\n"));
}
out.push_str(&format!("compiler = \"{compiler}\"\n")); out.push_str(&format!("compiler = \"{compiler}\"\n"));
let kind = build let kind = build
.get("kind") .get("kind")
@@ -766,6 +791,11 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
{ {
out.push_str(&format!("target = \"{target}\"\n")); out.push_str(&format!("target = \"{target}\"\n"));
} }
if let Some(out_dir) = build.get("out_dir").and_then(|v| v.as_str())
&& !out_dir.trim().is_empty()
{
out.push_str(&format!("out_dir = \"{out_dir}\"\n"));
}
if let Some(cflags) = build.get("cflags") { if let Some(cflags) = build.get("cflags") {
out.push_str(&format!("cflags = {}\n", format_string_array(cflags))); out.push_str(&format!("cflags = {}\n", format_string_array(cflags)));
} }

View File

@@ -22,12 +22,10 @@ use std::process::Command;
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 { pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
let bin_path = platform::bin_path(profile, project_name, target_dir); let bin_path = platform::bin_path(profile, project_name, target_dir);
if Path::new(&bin_path).exists() { if Path::new(&bin_path).exists() {
let output = Command::new(&bin_path).output(); let status = Command::new(&bin_path).status();
match output { match status {
Ok(o) => { Ok(s) => {
print!("{}", String::from_utf8_lossy(&o.stdout)); return s.code().unwrap_or(0);
eprint!("{}", String::from_utf8_lossy(&o.stderr));
return o.status.code().unwrap_or(0);
} }
Err(_) => { Err(_) => {
return 1; return 1;

View File

@@ -14,7 +14,6 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::error; use crate::utils::log::error;
mod cli; mod cli;
mod config; mod config;
@@ -41,6 +40,7 @@ fn main() {
"run" => cli::run::run(rest), "run" => cli::run::run(rest),
"tree" => cli::tree::tree(rest), "tree" => cli::tree::tree(rest),
"test" | "tests" => cli::test::test(rest), "test" | "tests" => cli::test::test(rest),
"fmt" => cli::fmt::fmt(rest),
"clean" => cli::clean::clean(rest), "clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest), "gen" => cli::r#gen::r#gen(rest),
"--version" => { "--version" => {

105
src/platform/bsd.rs Normal file
View File

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

View File

@@ -15,6 +15,13 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
pub mod bsd;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod linux; pub mod linux;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -35,7 +42,24 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
{ {
return windows::bin_path(profile, name, target_dir); return windows::bin_path(profile, name, target_dir);
} }
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] #[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
{
return bsd::bin_path(profile, name, target_dir);
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
{ {
match target_dir { match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
@@ -57,7 +81,24 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
{ {
return windows::elf_path(profile, name, target_dir); return windows::elf_path(profile, name, target_dir);
} }
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] #[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
{
return bsd::elf_path(profile, name, target_dir);
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
{ {
match target_dir { match target_dir {
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
@@ -79,7 +120,24 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
{ {
return windows::efi_path(profile, name, target_dir); return windows::efi_path(profile, name, target_dir);
} }
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] #[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
{
return bsd::efi_path(profile, name, target_dir);
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
{ {
match target_dir { match target_dir {
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name), Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
@@ -101,7 +159,24 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
{ {
return windows::lib_path(profile, name, target_dir); return windows::lib_path(profile, name, target_dir);
} }
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] #[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
{
return bsd::lib_path(profile, name, target_dir);
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
{ {
match target_dir { match target_dir {
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name), Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
@@ -123,7 +198,24 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
{ {
return windows::shared_lib_path(profile, name, target_dir); return windows::shared_lib_path(profile, name, target_dir);
} }
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] #[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
{
return bsd::shared_lib_path(profile, name, target_dir);
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)))]
{ {
match target_dir { match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name), Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),

View File

@@ -383,6 +383,96 @@ fn registry_dependency_is_built_from_cache() {
); );
} }
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] #[test]
fn dcr_test_runs_without_sandbox_dependency() { fn dcr_test_runs_without_sandbox_dependency() {
let Some(compiler) = available_compiler() else { let Some(compiler) = available_compiler() else {