37 Commits

Author SHA1 Message Date
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
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
78160e2bb9 feat: set MAKE environment variable for NetBSD build process 2026-05-28 16:55:51 +08:00
7f9cd3d643 fix: correct path for Snap package output in release workflow 2026-05-28 16:53:14 +08:00
4d1bd1dbdc feat: update snap packaging directory structure and fix NetBSD package installation command 2026-05-28 16:43:27 +08:00
7bc571ea27 feat: update snap packaging paths and improve asset configuration in Cargo.toml 2026-05-28 16:10:28 +08:00
f431c3d018 feat: update package metadata for RPM generation in Cargo.toml and fix snap package path 2026-05-28 15:34:24 +08:00
9fd1652ebb chore: release v0.6.9 2026-05-28 15:18:03 +08:00
c59d6de54f feat: enhance release workflow with Arch Linux native build and AUR update options 2026-05-28 00:13:01 +08:00
73b6ce903e feat: add Arch Linux native package build process to release workflow 2026-05-28 00:07:36 +08:00
e97b9b6e00 feat: enhance package database update process with checks for existing files 2026-05-27 23:58:53 +08:00
12d1bc5111 feat: add Dexoron Packages Index workflow for automatic package updates 2026-05-27 23:55:11 +08:00
8c9dd39503 feat: update version to 0.6.9 and add packaging for Linux and BSD
- Bump version from 0.6.8 to 0.6.9 in Cargo.toml and Cargo.lock.
- Enhance GitHub Actions workflow to build and publish .deb, .rpm, and Snap packages.
- Implement AUR publishing for both dcr and dcr-bin packages.
- Add support for workspace-only projects in the build system.
- Introduce new `workspace` flag for build and run commands.
- Create snapcraft.yaml for Snap package configuration.
- Update build.rs and related files to handle workspace members and inheritance.
- Modify config handling to support optional package and build configurations.
2026-05-27 23:14:49 +08:00
e3c35793ec feat: enhance FreeBSD, OpenBSD, and NetBSD build processes in release workflow 2026-05-27 20:42:30 +08:00
50 changed files with 2323 additions and 355 deletions

View File

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

View File

@@ -0,0 +1,109 @@
name: Dexoron Packages Index
on:
workflow_dispatch:
inputs:
tag:
description: "Версия (тег) для ручного импорта пакетов (например, v0.6.9)"
required: true
type: string
workflow_run:
workflows: ["Release"]
types:
- completed
permissions:
contents: read
jobs:
update-packages-index:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || github.event_workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Central Packages Repo
uses: actions/checkout@v4
with:
repository: 'dexoron/packages'
token: ${{ secrets.PACKAGES_REPO_TOKEN }}
path: 'central-packages'
- name: Import GPG Key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
# --- Загрузка артефактов (сохранена прежняя логика) ---
- name: Download Artifacts
if: ${{ github.event_name == 'workflow_run' }}
uses: actions/download-artifact@v4
with:
path: central-packages/
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Download Packages from Release Assets
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ inputs.tag }}
run: |
gh release download "$TAG" --repo dexoron/dcr --pattern "*.pkg.tar.zst" --dir central-packages/archlinux/x86_64/ --clobber
gh release download "$TAG" --repo dexoron/dcr --pattern "*.deb" --dir central-packages/debian/ --clobber
gh release download "$TAG" --repo dexoron/dcr --pattern "*.rpm" --dir central-packages/fedora/x86_64/ --clobber
# --- ИНДЕКСАЦИЯ И ПОДПИСЬ ---
- name: Update Pacman Database
run: |
docker run --rm \
-v ${{ github.workspace }}/central-packages:/packages \
-v ~/.gnupg:/root/.gnupg \
archlinux:latest \
bash -c '
cd /packages/archlinux/x86_64 && \
packages=(*.pkg.tar.zst) && \
repo-add --sign dexoron.db.tar.gz "${packages[@]}" && \
ln -sf dexoron.db.tar.gz dexoron.db && \
ln -sf dexoron.files.tar.gz dexoron.files
'
- name: Fix GPG permissions after Docker
run: sudo chown -R runner:runner ~/.gnupg
- name: Update Debian APT Repository Index
run: |
sudo apt-get update && sudo apt-get install -y dpkg-dev
cd central-packages/debian
dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz
echo "Origin: Dexoron Packages" > Release
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
run: |
sudo apt-get update && sudo apt-get install -y createrepo-c
cd central-packages/fedora/x86_64
createrepo_c .
gpg --batch --yes --detach-sign --armor --output repodata/repomd.xml.asc repodata/repomd.xml
- name: Push to dexoron/packages
run: |
cd central-packages
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .
if ! git diff --cached --quiet; then
git commit -m "chore: update and sign repositories"
git push
fi

View File

@@ -13,6 +13,14 @@ on:
description: "Build Linux" description: "Build Linux"
type: boolean type: boolean
default: true default: true
run_linux_packages:
description: "Build Linux Packages (.deb, .rpm, .snap, .pkg.tar.zst)"
type: boolean
default: true
run_aur:
description: "Update AUR packages (dcr & dcr-bin)"
type: boolean
default: false
run_bsd: run_bsd:
description: "Build BSD" description: "Build BSD"
type: boolean type: boolean
@@ -92,10 +100,28 @@ jobs:
name: release-notes name: release-notes
path: .github/release-notes.md path: .github/release-notes.md
# ── Linux ────────────────────────────────────────────────────────────────── # ── 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 ──────────────────────────────────────────────────
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
@@ -121,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:
@@ -148,10 +180,154 @@ jobs:
body_path: .github/release-notes.md body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── macOS ────────────────────────────────────────────────────────────────── # ── Linux Packages Matrix (.deb, .rpm, .pkg.tar.zst, .snap) ────────────────
build-linux-packages:
if: ${{ github.event_name == 'push' || github.event.inputs.run_linux_packages == 'true' }}
needs: [setup, notes]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- package_type: deb
artifact_name: debian-package
- package_type: rpm
artifact_name: rpm-package
- package_type: arch
artifact_name: arch-package
- package_type: snap
artifact_name: snap-package
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Setup Rust (For non-container builds)
if: matrix.package_type != 'arch'
uses: dtolnay/rust-toolchain@stable
# ── DEB Build ──
- name: Build Debian Package
if: matrix.package_type == 'deb'
run: |
cargo install cargo-deb
cargo build --release
cargo deb --no-build
mkdir -p dist-packages && cp target/debian/*.deb dist-packages/
# ── RPM Build ──
- name: Build RPM Package
if: matrix.package_type == 'rpm'
run: |
cargo install cargo-generate-rpm
cargo build --release
cargo generate-rpm
mkdir -p dist-packages && cp target/generate-rpm/*.rpm dist-packages/
# ── Snap Build ──
- name: Build Snap Package
if: matrix.package_type == 'snap'
run: |
cargo build --release
mkdir -p packaging/snap/dist/bin
cp target/release/dcr packaging/snap/dist/bin/
- name: Snapcraft Build Action
if: matrix.package_type == 'snap'
uses: snapcore/action-build@v1
with:
path: packaging/snap
- name: Prepare Snap Output
if: matrix.package_type == 'snap'
run: mkdir -p dist-packages && cp packaging/snap/*.snap dist-packages/
# ── Arch Linux Native Build ──
- name: Build Arch Package inside Container
if: matrix.package_type == 'arch'
run: |
PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN="${PKGVER_ORIG//-/.}"
TAG='${{ needs.setup.outputs.tag }}'
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/${TAG}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
mkdir -p arch-build
cat > arch-build/PKGBUILD <<PKG
pkgname=dcr
pkgver=${PKGVER_CLEAN}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
depends=('gcc-libs')
makedepends=('cargo')
options=('!lto')
source=("\$pkgname-\$pkgver.tar.gz::\$url/archive/refs/tags/${TAG}.tar.gz")
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
cargo build --release --locked
}
package() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
}
PKG
docker run --rm \
-v ${{ github.workspace }}/arch-build:/build \
archlinux:latest \
bash -c "
pacman -Syu --noconfirm base-devel cargo gcc-libs git && \
useradd -m builduser && \
echo 'builduser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers && \
chown -R builduser:builduser /build && \
cd /build && \
su builduser -c 'makepkg -s --noconfirm'
"
mkdir -p dist-packages && cp arch-build/*.pkg.tar.zst dist-packages/
# ── Upload Artifacts ──
- name: Upload Package Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: dist-packages/*
if-no-files-found: ignore
# ── Publish Packages to Release (Out of Matrix) ────────────────────────────
publish-dexoron-packages:
needs: [setup, notes, build-linux-packages, create-release]
runs-on: ubuntu-latest
steps:
- name: Download Release Notes
uses: actions/download-artifact@v4
with:
name: release-notes
path: .github
- name: Download Built Packages
uses: actions/download-artifact@v4
with:
path: collected-packages
- name: Upload Everything to GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.setup.outputs.tag }}
files: |
collected-packages/debian-package/*
collected-packages/rpm-package/*
collected-packages/arch-package/*
collected-packages/snap-package/*
body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── 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:
@@ -194,17 +370,15 @@ jobs:
body_path: .github/release-notes.md body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── BSD ──────────────────────────────────────────────────────────────────── # ── 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
matrix: matrix:
include: system: [freebsd, openbsd, netbsd]
- target: x86_64-unknown-freebsd
# При необходимости сюда можно докинуть x86_64-unknown-netbsd
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -216,37 +390,77 @@ jobs:
name: release-notes name: release-notes
path: .github path: .github
- name: Install cargo-zigbuild # --- FREEBSD ---
run: | - name: Build in FreeBSD VM
pip install cargo-zigbuild if: matrix.system == 'freebsd'
echo "$HOME/.local/bin" >> $GITHUB_PATH uses: vmactions/freebsd-vm@v1
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} usesh: true
run: |
export PATH="/usr/local/bin:/usr/local/sbin:$PATH"
pkg install -y rust perl5 gmake
cargo build --release
- name: Build - name: Prepare FreeBSD Artifact
run: cargo zigbuild --release --target ${{ matrix.target }} if: matrix.system == 'freebsd'
- name: Prepare Artifact
run: | run: |
ASSET="dcr-${{ matrix.target }}-${{ needs.setup.outputs.version }}" ASSET="dcr-x86_64-unknown-freebsd-${{ needs.setup.outputs.version }}"
cp target/${{ matrix.target }}/release/dcr "$ASSET" cp target/release/dcr "$ASSET"
echo "ASSET_NAME=${ASSET}" >> "$GITHUB_ENV" echo "BSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
- name: Upload Release Asset # --- OPENBSD ---
- name: Build in OpenBSD VM
if: matrix.system == 'openbsd'
uses: vmactions/openbsd-vm@v1
with:
usesh: true
run: |
export PATH="/usr/local/bin:$PATH"
pkg_add rust perl gmake
cargo build --release
- name: Prepare OpenBSD Artifact
if: matrix.system == 'openbsd'
run: |
ASSET="dcr-x86_64-unknown-openbsd-${{ needs.setup.outputs.version }}"
cp target/release/dcr "$ASSET"
echo "BSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
# --- NETBSD ---
- name: Build in NetBSD VM
if: matrix.system == 'netbsd'
uses: vmactions/netbsd-vm@v1
with:
usesh: true
run: |
pkg_add rust perl 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
- name: Prepare NetBSD Artifact
if: matrix.system == 'netbsd'
run: |
ASSET="dcr-x86_64-unknown-netbsd-${{ needs.setup.outputs.version }}"
cp target/release/dcr "$ASSET"
echo "BSD_ASSET=${ASSET}" >> "$GITHUB_ENV"
# --- UPLOAD TARGET ---
- name: Upload BSD Asset to Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ needs.setup.outputs.tag }} tag_name: ${{ needs.setup.outputs.tag }}
files: ${{ env.ASSET_NAME }} files: ${{ env.BSD_ASSET }}
body_path: .github/release-notes.md body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Windows ──────────────────────────────────────────────────────────────── # ── 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
@@ -288,6 +502,219 @@ jobs:
files: ${{ env.ASSET_NAME }} files: ${{ env.ASSET_NAME }}
body_path: .github/release-notes.md body_path: .github/release-notes.md
prerelease: ${{ needs.setup.outputs.prerelease == 'true' }} prerelease: ${{ needs.setup.outputs.prerelease == 'true' }}
# ── Arch User Repository (dcr) ───────────────────────────────────
publish-aur:
if: ${{ (github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') || github.event.inputs.run_aur == 'true' }}
needs: [setup]
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive checksum
id: meta
shell: bash
run: |
set -euo pipefail
SRC_URL="https://github.com/dexoron/dcr/archive/refs/tags/${{ needs.setup.outputs.tag }}.tar.gz"
SHA256="$(curl -fsSL "$SRC_URL" | sha256sum | awk '{print $1}')"
echo "source_url=$SRC_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Render AUR metadata
shell: bash
run: |
set -euo pipefail
PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN="${PKGVER_ORIG//-/.}"
SRC_URL='${{ steps.meta.outputs.source_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr
cat > packages/aur/dcr/PKGBUILD <<PKG
pkgname=dcr
pkgver=${PKGVER_CLEAN}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects"
arch=('x86_64' 'aarch64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
depends=('gcc-libs')
makedepends=('cargo')
options=('!lto')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source=("\$pkgname-\$pkgver.tar.gz::\${SRC_URL}")
sha256sums=('${SHA256}')
build() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
export CARGO_PROFILE_RELEASE_LTO=false
export RUSTFLAGS="\${RUSTFLAGS:-}"
export RUSTFLAGS="\${RUSTFLAGS/-C link-arg=-fuse-ld=lld/}"
cargo build --release --locked
}
package() {
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
install -Dm755 "target/release/dcr" "\$pkgdir/usr/bin/dcr"
install -Dm644 LICENSE "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE"
}
PKG
cat > packages/aur/dcr/.SRCINFO <<SRC
pkgbase = dcr
pkgdesc = Cargo-like utility to manage C/C++ projects
pkgver = ${PKGVER_CLEAN}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
arch = aarch64
license = GPL-3.0-or-later
makedepends = cargo
depends = gcc-libs
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
options = !lto
source = dcr-${PKGVER_CLEAN}.tar.gz::${SRC_URL}
sha256sums = ${SHA256}
pkgname = dcr
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr
shell: bash
run: |
set -euo pipefail
install -d -m 700 ~/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
export GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes'
git clone "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git" aur-repo
cp packages/aur/dcr/PKGBUILD aur-repo/PKGBUILD
cp packages/aur/dcr/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr ${{ needs.setup.outputs.version }}"
git push
# ── Arch User Repository (dcr-bin) ────────────────────────────────
publish-aur-bin:
if: ${{ (github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') || github.event.inputs.run_aur == 'true' }}
needs: [setup, build-linux]
runs-on: ubuntu-latest
environment: aur
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive checksum
id: meta
shell: bash
run: |
set -euo pipefail
BIN_URL="https://github.com/dexoron/dcr/releases/download/${{ needs.setup.outputs.tag }}/dcr-x86_64-unknown-linux-musl-${{ needs.setup.outputs.version }}"
SHA256="$(curl -fsSL "$BIN_URL" | sha256sum | awk '{print $1}')"
echo "bin_url=$BIN_URL" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Render AUR Metadata
shell: bash
run: |
set -euo pipefail
PKGVER_ORIG='${{ needs.setup.outputs.version }}'
PKGVER_CLEAN="${PKGVER_ORIG//-/.}"
BIN_URL='${{ steps.meta.outputs.bin_url }}'
SHA256='${{ steps.meta.outputs.sha256 }}'
mkdir -p packages/aur/dcr-bin
cat > packages/aur/dcr-bin/PKGBUILD <<PKG
pkgname=dcr-bin
pkgver=${PKGVER_CLEAN}
pkgrel=1
pkgdesc="Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)"
arch=('x86_64')
url="https://github.com/dexoron/dcr"
license=('GPL-3.0-or-later')
provides=('dcr')
conflicts=('dcr')
optdepends=(
'gcc: build C/C++ projects with GCC'
'clang: build C/C++ projects with Clang'
)
source_x86_64=("dcr-\$pkgver::${BIN_URL}")
sha256sums_x86_64=('${SHA256}')
package() {
install -Dm755 "\$srcdir/dcr-\$pkgver" "\$pkgdir/usr/bin/dcr"
}
PKG
cat > packages/aur/dcr-bin/.SRCINFO <<SRC
pkgbase = dcr-bin
pkgdesc = Cargo-like utility to manage C/C++ projects (pre-compiled musl binary)
pkgver = ${PKGVER_CLEAN}
pkgrel = 1
url = https://github.com/dexoron/dcr
arch = x86_64
license = GPL-3.0-or-later
provides = dcr
conflicts = dcr
optdepends = gcc: build C/C++ projects with GCC
optdepends = clang: build C/C++ projects with Clang
source_x86_64 = dcr-${PKGVER_CLEAN}::${BIN_URL}
sha256sums_x86_64 = ${SHA256}
pkgname = dcr-bin
SRC
- name: Push to AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
AUR_PACKAGE: dcr-bin
shell: bash
run: |
set -euo pipefail
install -d -m 700 ~/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
export GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes'
git clone "ssh://aur@aur.archlinux.org/${AUR_PACKAGE}.git" aur-repo
cp packages/aur/dcr-bin/PKGBUILD aur-repo/PKGBUILD
cp packages/aur/dcr-bin/.SRCINFO aur-repo/.SRCINFO
cd aur-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "No AUR changes"
exit 0
fi
git add PKGBUILD .SRCINFO
git commit -m "dcr-bin ${{ needs.setup.outputs.version }}"
git push
# ── crates.io ────────────────────────────────────────────────────────────── # ── crates.io ──────────────────────────────────────────────────────────────
publish-crates: publish-crates:
@@ -296,6 +723,9 @@ jobs:
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_publish == 'true') (github.event_name == 'workflow_dispatch' && github.event.inputs.run_publish == 'true')
needs: [setup, build-linux, build-macos, build-windows, build-bsd] needs: [setup, build-linux, build-macos, build-windows, build-bsd]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -304,7 +734,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- name: Publish to crates.io - name: Publish to crates.io
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish
# ── Homebrew tap ─────────────────────────────────────────────────────────── # ── Homebrew tap ───────────────────────────────────────────────────────────
publish-brew: publish-brew:

View File

@@ -1,5 +1,95 @@
# 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
Added:
- **Dexoron Packages Index** — new standalone CI workflow (`.github/workflows/dexoron-packages-index.yml`)
that automatically updates the `dexoron/packages` repository indexes after a release.
Supports Arch Linux (`repo-add`), Debian (`dpkg-scanpackages`), and Fedora (`createrepo_c`).
Triggered by `workflow_dispatch` or `workflow_run` on the Release workflow.
- **Arch Linux native package** (`.pkg.tar.zst`) build inside the release workflow —
builds from source in an `archlinux:latest` Docker container using a dynamically
generated PKGBUILD.
- **AUR publish support** — new `run_aur` workflow dispatch input and `publish-aur` /
`publish-aur-bin` jobs that push PKGBUILD / `.SRCINFO` to `aur.archlinux.org`.
Changed:
- **Release workflow refactored** — Linux packages (deb, rpm, snap, arch) and BSD
builds now use `strategy.matrix` instead of separate jobs. A new
`publish-dexoron-packages` job collects all package artifacts and uploads them
to the GitHub Release in one step. Removed standalone `build-arch-package`,
`build-snap`, `build-deb-rpm` jobs. The old `run_arch_native` input is now
`run_linux_packages`.
- **Dexoron Packages Index workflow** extracted from `release.yml` into its own
file with support for Debian and RPM repository indexes in addition to Arch.
- **git2 vendoring** — enabled `vendored-libgit2` and `vendored-openssl` features
for reproducible CI builds without system libraries.
- **cargo-generate-rpm metadata** — updated to the current `package.metadata.rpm`
format (uses `files` table instead of `assets` array).
Fixed:
- **`--target=<triple>` no longer passed to GCC** — the flag is now only injected into
cflags when the resolved compiler name contains `"clang"`. GCC rejects this flag
with `unrecognized command-line option`.
- **Workspace clean test** — removed assertions for member-local `target/`
directories. Workspace members share the root `target/` directory, so their
individual `src/<name>/target/` is never created.
- **Release build test path** — corrected expected directory from `target/release`
to `target/x86_64-unknown-linux-gnu/release`.
## [0.6.8] - 2026-05-27 ## [0.6.8] - 2026-05-27
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.8" version = "0.7.0"
dependencies = [ dependencies = [
"ctrlc", "ctrlc",
"git2", "git2",

View File

@@ -1,12 +1,38 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.6.8" 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."
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/dexoron/dcr" repository = "https://github.com/dexoron/dcr"
[package.metadata.binstall]
pkg-fmt = "bin"
pkg-url = "{ repo }/releases/download/v{ version }/dcr-{ target }-{ version }{ bin-suffix }"
[package.metadata.deb]
maintainer = "Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>"
copyright = "2026, Dexoron"
extended-description = """
Cargo-like utility to manage C/C++ projects.
"""
section = "devel"
priority = "optional"
assets = [
["target/release/dcr", "usr/bin/dcr", "755"],
]
[package.metadata.generate-rpm]
summary = "Cargo-like utility to manage C/C++ projects"
description = """
Cargo-like utility to manage C/C++ projects.
"""
assets = [
{ source = "target/release/dcr", dest = "/usr/bin/dcr", mode = "755" }
]
[dependencies] [dependencies]
glob = "0.3" glob = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
@@ -14,9 +40,12 @@ serde = { version = "1", features = ["derive"] }
self-replace = "1" self-replace = "1"
sha2 = "0.10" sha2 = "0.10"
toml = "1.0.3" toml = "1.0.3"
git2 = "0.20.4"
serde_json = "1" serde_json = "1"
ctrlc = "3" ctrlc = "3"
[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)
@@ -121,14 +122,15 @@ rev = "string" # optional (git commit)
include = ["string", "..."] # optional (include dirs) 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:

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
} }
@@ -41,15 +41,26 @@ detect_target() {
arch="$(uname -m)" arch="$(uname -m)"
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" ;;
Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;; Linux:aarch64|Linux:arm64) TARGET_TRIPLE="aarch64-unknown-linux-gnu" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;; Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;;
FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;;
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() {
@@ -83,15 +94,16 @@ 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]')"
result="$(printf '%s' "$json" | python3 - <<'EOF' else
result="$(printf '%s' "$json" | python3 - <<'EOF'
import sys, json import sys, json
releases = json.load(sys.stdin) releases = json.load(sys.stdin)
pre = [r for r in releases if r.get("prerelease", False)] pre = [r for r in releases if r.get("prerelease", False)]
@@ -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 "$@"

1
packaging/aur/dcr-git Submodule

Submodule packaging/aur/dcr-git added at 12038a613e

View File

@@ -0,0 +1,18 @@
name: dcr
base: core22
version: '0.6.9'
summary: Cargo-like utility to manage C/C++ projects
description: |
A build tool and package manager for C/C++ projects inspired by Rust's Cargo.
grade: stable
confinement: classic
apps:
dcr:
command: bin/dcr
parts:
dcr:
plugin: dump
source: dist/

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

@@ -69,7 +69,9 @@ pub fn build(args: &[String]) -> i32 {
if flags.target.is_none() { if flags.target.is_none() {
let config_path = root.join("dcr.toml"); let config_path = root.join("dcr.toml");
if let Ok(config) = Config::open(config_path.to_str().unwrap()) { if let Ok(config) = Config::open(config_path.to_str().unwrap())
&& !config.is_workspace_only()
{
let bt = get_build_string_with_profile(&config, "target", "debug"); let bt = get_build_string_with_profile(&config, "target", "debug");
if !bt.is_empty() { if !bt.is_empty() {
flags.target = Some(bt); flags.target = Some(bt);
@@ -78,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();
@@ -100,6 +113,7 @@ pub fn build(args: &[String]) -> i32 {
flags.target.as_deref(), flags.target.as_deref(),
flags.force, flags.force,
flags.verbose, flags.verbose,
flags.workspace.as_deref(),
) )
}) { }) {
Ok(()) => 0, Ok(()) => 0,
@@ -197,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)
} }
@@ -372,8 +386,13 @@ fn get_build_steps_with_profile(
get_build_steps(config, &format!("build.{field}")) get_build_steps(config, &format!("build.{field}"))
} }
fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<String>) { fn ensure_target_dirs(
if !items.contains(&"target".to_string()) { items: &[String],
profile: &str,
target_dir: Option<String>,
skip_local_target: bool,
) {
if !skip_local_target && !items.contains(&"target".to_string()) {
let _ = fs::create_dir("./target"); let _ = fs::create_dir("./target");
} }
if let Some(dir) = &target_dir { if let Some(dir) = &target_dir {
@@ -382,6 +401,15 @@ fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option<String
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()
}; };
@@ -414,8 +442,38 @@ fn build_from_root(
target: Option<&str>, target: Option<&str>,
force: bool, force: bool,
verbose: bool, verbose: bool,
workspace: Option<&str>,
) -> Result<(), String> { ) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
if config.is_workspace_only() {
let ws = parse_workspace(&config, profile, target, root)?
.ok_or_else(|| "Workspace root has no members defined".to_string())?;
let members: Vec<_> = if let Some(filter_name) = workspace {
ws.members
.into_iter()
.filter(|m| m.name == filter_name)
.collect()
} else {
ws.members
};
if members.is_empty() {
return Err("No matching workspace members to build".to_string());
}
for member in &members {
build_project_at(
&member.path,
profile,
target,
&[],
force,
verbose,
Some(root),
)?;
}
return Ok(());
}
let project_name = get_config_str(&config, "package.name"); let project_name = get_config_str(&config, "package.name");
let project_version = get_config_str(&config, "package.version"); let project_version = get_config_str(&config, "package.version");
@@ -454,20 +512,50 @@ fn build_from_root(
) )
); );
} }
if let Some(workspace) = parse_workspace(&config, profile, build_target.as_deref(), root)? { if let Some(ws) = parse_workspace(&config, profile, build_target.as_deref(), root)? {
build_workspace(&workspace, profile, build_target.as_deref(), force, verbose)?; if let Some(filter_name) = workspace {
let excludes: Vec<std::path::PathBuf> = for member in ws.members.iter().filter(|m| m.name == filter_name) {
workspace.members.iter().map(|m| m.path.clone()).collect(); build_project_at(
&member.path,
profile,
build_target.as_deref(),
&[],
force,
verbose,
Some(root),
)?;
}
} else {
build_workspace(
&ws,
profile,
build_target.as_deref(),
force,
verbose,
Some(root),
)?;
let excludes: Vec<std::path::PathBuf> =
ws.members.iter().map(|m| m.path.clone()).collect();
build_project_at(
root,
profile,
build_target.as_deref(),
&excludes,
force,
verbose,
None,
)?;
}
} else {
build_project_at( build_project_at(
root, root,
profile, profile,
build_target.as_deref(), build_target.as_deref(),
&excludes, &[],
force, force,
verbose, verbose,
None,
)?; )?;
} else {
build_project_at(root, profile, build_target.as_deref(), &[], force, verbose)?;
} }
} }
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
@@ -485,9 +573,18 @@ fn build_workspace(
target: Option<&str>, target: Option<&str>,
force: bool, force: bool,
verbose: bool, verbose: bool,
workspace_root: Option<&Path>,
) -> Result<(), String> { ) -> Result<(), String> {
for member in &workspace.members { for member in &workspace.members {
build_project_at(&member.path, profile, target, &[], force, verbose)?; build_project_at(
&member.path,
profile,
target,
&[],
force,
verbose,
workspace_root,
)?;
} }
Ok(()) Ok(())
} }
@@ -499,6 +596,7 @@ fn build_project_at(
exclude_dirs: &[std::path::PathBuf], exclude_dirs: &[std::path::PathBuf],
force: bool, force: bool,
verbose: bool, verbose: bool,
workspace_root: Option<&Path>,
) -> Result<(), String> { ) -> Result<(), String> {
with_dir(project_root, || { with_dir(project_root, || {
check_interrupted()?; check_interrupted()?;
@@ -506,7 +604,22 @@ fn build_project_at(
if !items.contains(&"dcr.toml".to_string()) { if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string()); return Err("dcr.toml file not found".to_string());
} }
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let mut config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
// Inherit build fields from workspace root if configured
if let Some(root) = workspace_root {
let has_inherit = config
.get("build.inherit")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if has_inherit {
let root_path = root.join("dcr.toml");
if root_path.is_file() {
let parent_config =
Config::open(&root_path.to_string_lossy()).map_err(|e| e.to_string())?;
config.merge_parent(&parent_config);
}
}
}
let project_name = get_config_str(&config, "package.name"); let project_name = get_config_str(&config, "package.name");
let project_version = get_config_str(&config, "package.version"); let project_version = get_config_str(&config, "package.version");
let build_target_config = get_build_string_with_profile(&config, "target", profile); let build_target_config = get_build_string_with_profile(&config, "target", profile);
@@ -548,17 +661,6 @@ fn build_project_at(
.or_else(|| get_config_opt(&config, "toolchain.rcc")); .or_else(|| get_config_opt(&config, "toolchain.rcc"));
let mut build_cflags = let mut build_cflags =
get_list_with_profile_and_target(&config, "cflags", profile, build_target)?; get_list_with_profile_and_target(&config, "cflags", profile, build_target)?;
if let Some(t) = build_target
&& !t.trim().is_empty()
{
let target_flag = format!("--target={}", t.trim());
if !build_cflags
.iter()
.any(|f| f == &target_flag || f.starts_with("--target="))
{
build_cflags.insert(0, target_flag);
}
}
let mut build_ldflags = let mut build_ldflags =
get_list_with_profile_and_target(&config, "ldflags", profile, build_target)?; get_list_with_profile_and_target(&config, "ldflags", profile, build_target)?;
let build_ldscript = let build_ldscript =
@@ -595,11 +697,53 @@ fn build_project_at(
tc_cxx.as_deref(), tc_cxx.as_deref(),
tc_as.as_deref(), tc_as.as_deref(),
); );
if let Some(t) = build_target
&& !t.trim().is_empty()
&& resolved_compiler.to_lowercase().contains("clang")
{
let target_flag = format!("--target={}", t.trim());
if !build_cflags
.iter()
.any(|f| f == &target_flag || f.starts_with("--target="))
{
build_cflags.insert(0, target_flag);
}
}
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); 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 = build_target.map(|t| format!("target/{t}/{profile}")); let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile);
ensure_target_dirs(&items, profile, target_dir); let target_dir_binding = if !out_dir_config.is_empty() {
out_dir_config
} else {
match workspace_root {
Some(root) => {
let target_str = build_target
.map(normalize_target_os)
.filter(|t| !t.is_empty());
let rel: PathBuf = match target_str {
Some(ref t) => Path::new("target").join(t).join(profile),
None => {
let default_dir = if cfg!(target_os = "linux") {
let arch = std::env::consts::ARCH;
format!("{arch}-unknown-linux-gnu/{profile}")
} else {
profile.to_string()
};
Path::new("target").join(&default_dir)
}
};
root.join(&rel).to_string_lossy().to_string()
}
None => normalize_target(build_target.unwrap_or(""), profile).unwrap_or_default(),
}
};
let target_dir = if target_dir_binding.is_empty() {
None
} else {
Some(target_dir_binding.clone())
};
ensure_target_dirs(&items, profile, target_dir, workspace_root.is_some());
let deps_table = config.get("dependencies").and_then(|v| v.as_table()); let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let resolved = resolve_deps(&config, profile, build_target, project_root)?; let resolved = resolve_deps(&config, profile, build_target, project_root)?;
@@ -635,7 +779,15 @@ fn build_project_at(
dep_root.display() dep_root.display()
)); ));
} }
build_project_at(&dep_root, profile, build_target, &[], force, verbose)?; build_project_at(
&dep_root,
profile,
build_target,
&[],
force,
verbose,
None,
)?;
print!( print!(
"\r{:100}\r {} {} v{}", "\r{:100}\r {} {} v{}",
"", "",
@@ -724,7 +876,6 @@ fn build_project_at(
} }
} }
let target_dir_binding = normalize_target(build_target.unwrap_or(""), profile);
let version_info = parse_version_info(&project_version); let version_info = parse_version_info(&project_version);
let substitute = |s: &str| -> String { let substitute = |s: &str| -> String {
s.replace("{profile}", profile) s.replace("{profile}", profile)
@@ -749,7 +900,11 @@ fn build_project_at(
standard: &build_standard, standard: &build_standard,
cxx_standard: &build_cxx_standard, cxx_standard: &build_cxx_standard,
target: build_target, target: build_target,
target_dir: target_dir_binding.as_deref(), target_dir: if target_dir_binding.is_empty() {
None
} else {
Some(target_dir_binding.as_str())
},
kind: normalize_kind(&build_kind), kind: normalize_kind(&build_kind),
platform: normalize_platform(&build_platform), platform: normalize_platform(&build_platform),
linker: resolved_linker.as_deref(), linker: resolved_linker.as_deref(),

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()
}) })

View File

@@ -21,6 +21,7 @@ use crate::utils::log::warn;
pub struct BuildRunFlags { pub struct BuildRunFlags {
pub profile: String, pub profile: String,
pub target: Option<String>, pub target: Option<String>,
pub workspace: Option<String>,
pub force: bool, pub force: bool,
pub clean: bool, pub clean: bool,
pub verbose: bool, pub verbose: bool,
@@ -29,6 +30,7 @@ pub struct BuildRunFlags {
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> { pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
let mut profile = PROFILE.to_string(); let mut profile = PROFILE.to_string();
let mut target = None; let mut target = None;
let mut workspace = None;
let mut force = false; let mut force = false;
let mut clean = false; let mut clean = false;
let mut verbose = false; let mut verbose = false;
@@ -52,6 +54,15 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
verbose = true; verbose = true;
continue; continue;
} }
if candidate == "workspace" {
if let Some(w) = iter.next() {
workspace = Some(w.clone());
} else {
warn("--workspace requires a value");
return Err(1);
}
continue;
}
if candidate == "target" { if candidate == "target" {
if let Some(t) = iter.next() { if let Some(t) = iter.next() {
target = Some(t.clone()); target = Some(t.clone());
@@ -76,6 +87,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
Ok(BuildRunFlags { Ok(BuildRunFlags {
profile, profile,
target, target,
workspace,
force, force,
clean, clean,
verbose, verbose,

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

@@ -24,6 +24,7 @@ use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir; use crate::utils::fs::with_dir;
use crate::utils::log::error; use crate::utils::log::error;
use crate::utils::text::{BOLD_GREEN, colored}; use crate::utils::text::{BOLD_GREEN, colored};
use std::path::Path;
use std::process::Command; use std::process::Command;
fn get_run_cmd( fn get_run_cmd(
@@ -82,28 +83,102 @@ pub fn run(args: &[String]) -> i32 {
return 1; return 1;
} }
}; };
let flags = match parse_build_run_flags(args) {
Ok(v) => v,
Err(_) => return 1,
};
// Handle workspace-only root: delegate to workspace member
if config.is_workspace_only() {
let ws = match crate::core::workspace::parse_workspace(
&config,
&flags.profile,
flags.target.as_deref(),
&root,
) {
Ok(Some(ws)) => ws,
Ok(None) => {
error("Workspace root has no members defined");
return 1;
}
Err(e) => {
error(&e);
return 1;
}
};
let member = match &flags.workspace {
Some(name) => ws.members.iter().find(|m| m.name == *name),
None => ws.main_member(),
};
let member = match member {
Some(m) => m,
None => {
if let Some(name) = &flags.workspace {
error(&format!("Workspace member '{name}' not found"));
} else {
error("No workspace member to run (set `main = true` on one member)");
}
return 1;
}
};
// Build and run from the member's directory
return match with_dir(&member.path, || {
run_project(&member.path, &flags, Some(root.as_path()))
}) {
Ok(code) => code,
Err(e) => {
error(&e);
1
}
};
}
match run_project(&root, &flags, None) {
Ok(code) => code,
Err(e) => {
error(&e);
1
}
}
}
fn run_project(
root: &Path,
flags: &crate::cli::flags::BuildRunFlags,
workspace_root: Option<&Path>,
) -> Result<i32, String> {
let config =
Config::open(root.join("dcr.toml").to_str().unwrap()).map_err(|err| err.to_string())?;
let project_name: &str = config let project_name: &str = config
.get("package.name") .get("package.name")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let mut flags = match parse_build_run_flags(args) {
Ok(v) => v,
Err(_) => return 1,
};
// If no target specified, use default host target for target-specific config // If no target specified, use default host target for target-specific config
if flags.target.is_none() { let mut target = flags.target.clone();
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()
}; };
flags.target = Some(default_target.to_string()); target = Some(default_target);
} }
let build_kind = config let build_kind = config
@@ -112,25 +187,36 @@ pub fn run(args: &[String]) -> i32 {
.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 = flags let out_dir =
.target crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
.as_ref() let normalized_target_dir = if !out_dir.is_empty() {
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)); Some(out_dir)
} else {
match workspace_root {
Some(wr) => target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile))
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
None => target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)),
}
};
let version = config let version = config
.get("package.version") .get("package.version")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let run_cmd = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), version); let run_cmd = get_run_cmd(&config, &flags.profile, target.as_deref(), version);
let kind = build_kind.trim(); let kind = build_kind.trim();
if run_cmd.is_none() if run_cmd.is_none()
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf") && (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf")
{ {
error("Cannot run library build"); return Err("Cannot run library build".to_string());
return 1;
} }
let build_status = build(args);
let build_status = build(&args_for_build(flags));
let bin_path = crate::platform::bin_path( let bin_path = crate::platform::bin_path(
&flags.profile, &flags.profile,
project_name, project_name,
@@ -140,15 +226,15 @@ pub fn run(args: &[String]) -> i32 {
if let Some(cmd) = run_cmd { if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd); println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------"); println!("--------------------------------");
return run_shell(&cmd); return Ok(run_shell(&cmd));
} }
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------"); println!("--------------------------------");
return run_binary( return Ok(run_binary(
project_name, project_name,
&flags.profile, &flags.profile,
normalized_target_dir.as_deref(), normalized_target_dir.as_deref(),
); ));
} }
let fallback_code = if let Some(cmd) = run_cmd { let fallback_code = if let Some(cmd) = run_cmd {
@@ -161,11 +247,27 @@ pub fn run(args: &[String]) -> i32 {
) )
}; };
if fallback_code != 1 { if fallback_code != 1 {
return fallback_code; return Ok(fallback_code);
} }
error("Fix errors in the code to run the project"); Err("Fix errors in the code to run the project".to_string())
1 }
fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
let mut args = Vec::new();
args.push(format!("--{}", flags.profile));
if let Some(ref target) = flags.target {
args.push("--target".to_string());
args.push(target.clone());
}
if let Some(ref name) = flags.workspace {
args.push("--workspace".to_string());
args.push(name.clone());
}
if flags.verbose {
args.push("--verbose".to_string());
}
args
} }
fn run_shell(cmd: &str) -> i32 { fn run_shell(cmd: &str) -> i32 {

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 {
@@ -77,8 +85,10 @@ pub struct Config {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct DcrConfig { pub struct DcrConfig {
pub package: PackageConfig, #[serde(default)]
pub build: BuildConfig, pub package: Option<PackageConfig>,
#[serde(default)]
pub build: Option<BuildConfig>,
#[serde(default)] #[serde(default)]
pub dependencies: BTreeMap<String, DependencyConfig>, pub dependencies: BTreeMap<String, DependencyConfig>,
#[serde(default)] #[serde(default)]
@@ -99,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,
@@ -108,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>,
@@ -123,6 +136,8 @@ pub struct BuildConfig {
pub clean: Vec<String>, pub clean: Vec<String>,
#[serde(default)] #[serde(default)]
pub src_disable: Option<bool>, pub src_disable: Option<bool>,
#[serde(default)]
pub workspace_only: bool,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@@ -201,13 +216,100 @@ impl Config {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn package(&self) -> &PackageConfig { pub fn package(&self) -> Option<&PackageConfig> {
&self.typed.package self.typed.package.as_ref()
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn build_config(&self) -> &BuildConfig { pub fn build_config(&self) -> Option<&BuildConfig> {
&self.typed.build self.typed.build.as_ref()
}
pub fn is_workspace_only(&self) -> bool {
self.typed
.build
.as_ref()
.map(|b| b.workspace_only)
.unwrap_or(false)
&& !self.typed.workspace.is_empty()
}
/// Merge build fields from parent config into self where self has empty/default values.
/// Used for workspace member inheritance (build.inherit = true).
pub fn merge_parent(&mut self, parent: &Config) {
let fields: &[&str] = &[
"language",
"standard",
"cxx_standard",
"compiler",
"kind",
"target",
"platform",
"cflags",
"ldflags",
"exclude",
"include",
"roots",
"clean",
"src_disable",
];
let parent_build = match parent.data.get("build").and_then(|v| v.as_table()) {
Some(t) => t.clone(),
None => return,
};
let self_build = self
.data
.get("build")
.and_then(|v| v.as_table())
.cloned()
.unwrap_or_default();
let mut changes: Vec<(String, toml::Value)> = Vec::new();
for field in fields {
let val = match parent_build.get(*field) {
Some(v) => v,
None => continue,
};
let self_val = self_build.get(*field);
let should_inherit = match val {
toml::Value::Array(_) => self_val.is_none(),
toml::Value::String(_) => self_val
.and_then(|v| v.as_str())
.map(|s| s.trim().is_empty())
.unwrap_or(true),
_ => self_val.is_none(),
};
if should_inherit {
changes.push((field.to_string(), val.clone()));
}
}
if changes.is_empty() {
return;
}
if !self.data.is_table() {
let mut m = Map::new();
m.insert("build".to_string(), toml::Value::Table(Map::new()));
self.data = toml::Value::Table(m);
}
let tbl = self.data.as_table_mut().unwrap();
let build_tbl = tbl
.entry("build".to_string())
.or_insert_with(|| toml::Value::Table(Map::new()));
if let Some(build_tbl) = build_tbl.as_table_mut() {
for (key, val) in changes {
build_tbl.entry(key).or_insert(val);
}
}
if let Ok(typed) = parse_typed_config(&self.data) {
self.typed = typed;
}
} }
pub fn get(&self, key: &str) -> Option<&Value> { pub fn get(&self, key: &str) -> Option<&Value> {
@@ -229,15 +331,29 @@ impl Config {
} }
pub fn validate(&self) -> Result<(), ConfigError> { pub fn validate(&self) -> Result<(), ConfigError> {
if self.typed.package.name.trim().is_empty() { if self.is_workspace_only() {
return self.validate_workspace();
}
let pkg =
self.typed.package.as_ref().ok_or_else(|| {
ConfigError::Invalid("missing [package] section (required)".into())
})?;
let build = self
.typed
.build
.as_ref()
.ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?;
if pkg.name.trim().is_empty() {
return Err(ConfigError::Invalid("package.name is empty".into())); return Err(ConfigError::Invalid("package.name is empty".into()));
} }
if self.typed.package.version.trim().is_empty() { if pkg.version.trim().is_empty() {
return Err(ConfigError::Invalid("package.version is empty".into())); return Err(ConfigError::Invalid("package.version is empty".into()));
} }
validate_package_name(&self.typed.package.name)?; validate_package_name(&pkg.name)?;
if let Some(pkg_type) = &self.typed.package.pkg_type { if let Some(pkg_type) = &pkg.pkg_type {
let pkg_type = pkg_type.trim(); let pkg_type = pkg_type.trim();
if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none" if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none"
{ {
@@ -247,34 +363,44 @@ impl Config {
} }
} }
validate_language_config(&self.typed.build.language, "build.language")?; validate_language_config(&build.language, "build.language")?;
if self.typed.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 self.typed.build.compiler.trim().is_empty() { if build.compiler.trim().is_empty() {
return Err(ConfigError::Invalid("build.compiler is empty".into())); return Err(ConfigError::Invalid("build.compiler is empty".into()));
} }
if let Some(platform) = &self.typed.build.platform if let Some(platform) = &build.platform
&& platform.trim().is_empty() && platform.trim().is_empty()
{ {
return Err(ConfigError::Invalid("build.platform is empty".into())); return Err(ConfigError::Invalid("build.platform is empty".into()));
} }
validate_toolchain(self.typed.toolchain.as_ref())?; validate_toolchain(self.typed.toolchain.as_ref())?;
if let Some(kind) = &self.typed.build.kind { if let Some(kind) = &build.kind {
let kind = kind.trim(); let kind = kind.trim();
if !kind.is_empty() && !VALID_KINDS.contains(&kind) { if !kind.is_empty() && !VALID_KINDS.contains(&kind) {
return Err(ConfigError::Invalid("build.kind is invalid".into())); return Err(ConfigError::Invalid("build.kind is invalid".into()));
} }
} }
validate_string_list(&self.typed.build.exclude, "build.exclude")?; validate_string_list(&build.exclude, "build.exclude")?;
validate_string_list(&self.typed.build.include, "build.include")?; validate_string_list(&build.include, "build.include")?;
validate_string_list(&self.typed.build.roots, "build.roots")?; validate_string_list(&build.roots, "build.roots")?;
validate_string_list(&self.typed.build.clean, "build.clean")?; validate_string_list(&build.clean, "build.clean")?;
validate_string_list(&self.typed.build.cflags, "build.cflags")?; validate_string_list(&build.cflags, "build.cflags")?;
validate_string_list(&self.typed.build.ldflags, "build.ldflags")?; validate_string_list(&build.ldflags, "build.ldflags")?;
if let Some(target) = &self.typed.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
@@ -418,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() {
@@ -649,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"));
} }
out.push_str(&format!("standard = \"{standard}\"\n")); if !standard.is_empty() {
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")
@@ -661,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)));
} }
@@ -824,10 +959,10 @@ mod tests {
"[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n", "[package]\nname = \"typed\"\nversion = \"1.2.3\"\ntype = \"lib\"\n\n[build]\nlanguage = [\"c\", \"c++\"]\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\ncflags = [\"-Wall\"]\n\n[dependencies]\nfoo = \"1.0.0\"\n",
); );
let config = Config::open(&path.to_string_lossy()).unwrap(); let config = Config::open(&path.to_string_lossy()).unwrap();
assert_eq!(config.package().name, "typed"); assert_eq!(config.package().unwrap().name, "typed");
assert_eq!(config.typed().package.version, "1.2.3"); assert_eq!(config.typed().package.as_ref().unwrap().version, "1.2.3");
assert_eq!(config.build_config().compiler, "clang"); assert_eq!(config.build_config().unwrap().compiler, "clang");
assert_eq!(config.build_config().cflags, ["-Wall"]); assert_eq!(config.build_config().unwrap().cflags, ["-Wall"]);
assert!(config.typed().dependencies.contains_key("foo")); assert!(config.typed().dependencies.contains_key("foo"));
} }

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

@@ -26,6 +26,7 @@ pub struct WorkspaceMember {
pub name: String, pub name: String,
pub path: PathBuf, pub path: PathBuf,
pub deps: Vec<String>, pub deps: Vec<String>,
pub main: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -33,6 +34,15 @@ pub struct Workspace {
pub members: Vec<WorkspaceMember>, pub members: Vec<WorkspaceMember>,
} }
impl Workspace {
pub fn main_member(&self) -> Option<&WorkspaceMember> {
self.members
.iter()
.find(|m| m.main)
.or_else(|| self.members.first())
}
}
pub fn parse_workspace( pub fn parse_workspace(
config: &Config, config: &Config,
profile: &str, profile: &str,
@@ -85,6 +95,7 @@ pub fn parse_workspace(
name: name.to_string(), name: name.to_string(),
path, path,
deps, deps,
main: tbl.get("main").and_then(|v| v.as_bool()).unwrap_or(false),
}); });
} }

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

@@ -198,18 +198,6 @@ fn workspace_build_and_clean_all() {
"workspace clean --all --release should succeed" "workspace clean --all --release should succeed"
); );
for (name, _) in &members {
let member = root.join("src").join(name);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!member.join(target_dir).join("release").exists(),
"member target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
member.join(target_dir).join("debug").exists(),
"member target/x86_64-unknown-linux-gnu/debug should remain"
);
}
let target_dir = "target/x86_64-unknown-linux-gnu"; let target_dir = "target/x86_64-unknown-linux-gnu";
assert!( assert!(
!root.join(target_dir).join("release").exists(), !root.join(target_dir).join("release").exists(),
@@ -395,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 {

View File

@@ -280,8 +280,11 @@ fn build_release_profile() {
let out = run_dcr_env(&["build", "--release"], &dir, &envs); let out = run_dcr_env(&["build", "--release"], &dir, &envs);
assert!(out.status.success(), "release build should succeed"); assert!(out.status.success(), "release build should succeed");
assert!( assert!(
dir.join("target").join("release").is_dir(), dir.join("target")
"target/release should exist" .join("x86_64-unknown-linux-gnu")
.join("release")
.is_dir(),
"target/x86_64-unknown-linux-gnu/release should exist"
); );
} }