38 Commits
v0.8.5 ... dev

Author SHA1 Message Date
6f47e81610 docs: remove obsolete cmd bootstrap and delete dcrup.cmd 2026-10-11 15:12:40 +08:00
d49b271e6a fix: restore Unicode characters in PowerShell installer 2026-10-11 15:09:09 +08:00
f17e4c6642 fix dcrup installers and installation docs 2026-10-11 15:04:16 +08:00
a890f7e6eb Merge pull request #42 from alex-xr555/cleaned_cli_styles
refactor: Changing the style system
2026-09-25 23:29:39 +08:00
alex-xr555
3b6251ec33 refactor: the style system has been changed and replaced functions (printc, colored) have been removed in src/utils/cli_styles.rs. Most files in src/cli have been heavily modified to migrate to the new system 2026-09-24 01:10:19 +03:00
alex-xr555
e9d09eacaf refactor: created new macro sprintln! and sprint! (src/utils/log.rs) for print stylized text to the console. An additional comment has been created, and access to owo_colors via pub use in src/utils/cli_styles.rs 2026-09-24 00:36:51 +03:00
alex-xr555
2f4cde9177 refactor: renamed src/utils/text.rs to cli_styles 2026-09-24 00:20:43 +03:00
552f6c8e4f Merge pull request #41 from alex-xr555/refactor
refactor: Simplify use blocks
2026-09-20 18:28:22 +08:00
alex-xr555
da84916e10 Simplified use blocks 2026-09-19 18:11:28 +03:00
cb6ae3dcc7 Merge pull request #40 from alex-xr555/log 2026-09-19 09:28:18 +08:00
alex-xr555
10123ddedb Translation of comments into English, minor changes for completeness. 2026-09-15 18:34:54 +03:00
alex-xr555
0a140c75d8 Clarified the use block 2026-09-15 18:34:54 +03:00
alex-xr555
aa1d4ba807 The logging system has been brought to a fully functional level. 2026-09-15 18:34:54 +03:00
alex-xr555
991d1c20af Absolutely all logs have been replaced with the new system on log. 2026-09-15 18:34:54 +03:00
alex-xr555
df674defe1 Start of the logging system replacement 2026-09-15 18:34:54 +03:00
7d2340fb52 Merge pull request #39 from dexoron/dev
v0.9.1
2026-09-13 15:02:16 +08:00
6c79340736 feat: add package name validation in default manifest generation; clarify pkg-config dependency requirements 2026-09-13 14:50:40 +08:00
be14cfb8e2 Update release date for version 0.9.1 2026-09-13 13:58:26 +08:00
3305915c07 Update release date for version 0.9.1 2026-09-13 13:58:04 +08:00
5cb51878a5 feat: add pkg-config system dependencies and fix dcr new in non-ascii paths 2026-09-08 20:40:49 +08:00
d587eba5a4 Update release.yml to use VERSION variable 2026-09-05 21:39:33 +08:00
bfd2b9d251 Merge pull request #36 from dexoron/dev
Release v0.9.0
2026-08-31 14:16:57 +08:00
1e3eb658dc test: normalize git dependency paths on Windows 2026-08-31 13:58:42 +08:00
f5dad74498 Release v0.9.0: complete path and git dependencies 2026-08-31 13:42:16 +08:00
3c25539c11 Merge pull request #35 from dexoron/master
update dev branch
2026-08-29 12:33:55 +08:00
54653dfd80 feat: add download mirrors and drop legacy install scripts
Introduce `--mirror` and `DCRUP_MIRROR` so `dcrup` can fetch prebuilt
binaries from multiple CDNs. Default `auto` tries cf → gh → sf → ya
and uses the first successful download.

- dcrup.sh / dcrup.ps1: mirror chain, CLI flag, env override
- remove install.sh / install.ps1 / install_bsd.sh (superseded by dcrup)
- GitHub Actions: upload releases to Cloudflare R2 next to Yandex S3
- docs: README, FAQ, changelog, installation, env vars, contributing
2026-08-13 12:59:12 +08:00
f3605a86a2 Add Yandex S3 2026-08-12 23:35:44 +08:00
9d21294cb4 Update README.md 2026-08-12 21:27:21 +08:00
8609d12179 fix release sync 2026-08-12 20:49:36 +08:00
b6ecb2aa79 fix release sync 2026-08-12 20:48:12 +08:00
dd89109f92 fix release sync 2026-08-12 20:30:45 +08:00
c6af6c5196 fix release sync 2026-08-12 20:25:36 +08:00
b6153dbaac fix release sync 2026-08-12 20:13:30 +08:00
9e2b7a897a fix release sync 2026-08-12 19:45:38 +08:00
9add069879 fix release sync 2026-08-12 19:24:31 +08:00
5eb00037ea fix release sync 2026-08-12 19:14:13 +08:00
f7353f9d03 fix git sync 2026-08-12 18:03:20 +08:00
05e49838f7 feat: add automated repository mirroring workflows and update docs
- Add GitHub Actions workflows for code and release mirroring
- Remove manual sync-releases-to-gitlab.py script
- Update .gitignore to track .github/ directory
- Document repository mirrors and VS Code extension recommendations
2026-08-12 17:58:30 +08:00
75 changed files with 3276 additions and 1922 deletions

View File

@@ -6,7 +6,19 @@ on:
- master
- main
- dev
paths:
- 'src/**'
- 'tests/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'
pull_request:
paths:
- 'src/**'
- 'tests/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'
env:
CARGO_TERM_COLOR: always

38
.github/workflows/code-mirror.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Mirror source code to GitVerse
on:
push:
branches:
- '**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: code-mirror-${{ github.ref }}
cancel-in-progress: true
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mirror all branches
env:
GITVERSE_PAT: ${{ secrets.GITVERSE_PAT }}
GITVERSE_ORG: ${{ vars.GITVERSE_ORG || 'dcr_labs' }}
GITVERSE_REPO: ${{ vars.GITVERSE_REPO || 'dcr' }}
run: |
set -euo pipefail
test -n "$GITVERSE_PAT" || { echo 'GITVERSE_PAT is not configured' >&2; exit 1; }
remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git"
git remote add gitverse "$remote"
# Use a private ref namespace: Git will not overwrite the checked-out
# branch, and broken legacy submodule metadata is not traversed.
git fetch --no-recurse-submodules origin '+refs/heads/*:refs/mirror/*'
git push --prune --force gitverse 'refs/mirror/*:refs/heads/*'

290
.github/workflows/release-mirror.yml vendored Normal file
View File

@@ -0,0 +1,290 @@
name: Mirror releases to GitVerse, SourceCraft, and GitLab
on:
workflow_run:
workflows:
- Release
types:
- completed
workflow_dispatch:
inputs:
tag:
description: Git tag of the GitHub release to mirror (for example v0.7.1)
required: true
type: string
permissions:
contents: read
jobs:
setup:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.release.outputs.tag }}
found: ${{ steps.release.outputs.found }}
steps:
- name: Resolve release tag
id: release
env:
GH_TOKEN: ${{ github.token }}
MANUAL_TAG: ${{ inputs.tag }}
WORKFLOW_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
tag="$MANUAL_TAG"
if [ -z "$tag" ] && [[ "$WORKFLOW_HEAD_BRANCH" == v* ]]; then
tag="$WORKFLOW_HEAD_BRANCH"
fi
if [ -z "$tag" ] && [ -n "$WORKFLOW_HEAD_SHA" ]; then
releases=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
-H 'Accept: application/vnd.github+json' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100")
while IFS= read -r candidate; do
ref=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
-H 'Accept: application/vnd.github+json' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${candidate}")
sha=$(jq -r '.object.sha' <<< "$ref")
if [ "$(jq -r '.object.type' <<< "$ref")" = tag ]; then
sha=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
-H 'Accept: application/vnd.github+json' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/git/tags/${sha}" | jq -r '.object.sha')
fi
if [ "$sha" = "$WORKFLOW_HEAD_SHA" ]; then
tag="$candidate"
break
fi
done < <(jq -r '.[].tag_name' <<< "$releases")
fi
if [ -z "$tag" ] || [[ "$tag" != v* ]]; then
echo 'No release tag was found; release.yml may have failed before creating a GitHub release.'
echo 'found=false' >> "$GITHUB_OUTPUT"
exit 0
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo 'found=true' >> "$GITHUB_OUTPUT"
- name: Download GitHub release metadata and assets
if: steps.release.outputs.found == 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
mkdir -p assets
curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
-H 'Accept: application/vnd.github+json' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" > release.json
jq -e '.tag_name == $tag' --arg tag "$TAG" release.json >/dev/null
total=$(jq '.assets | length' release.json)
downloaded=0
failed=0
while IFS=$' ' read -r name url; do
if curl -fsSL -L -H "Authorization: Bearer $GH_TOKEN" \
-H 'Accept: application/octet-stream' -o "assets/$name" "$url"; then
downloaded=$((downloaded+1))
else
echo "::warning::Failed to download asset: $name"
failed=$((failed+1))
fi
done < <(jq -r '.assets[] | [.name, .browser_download_url] | @tsv' release.json)
echo "Downloaded ${downloaded}/${total} assets (${failed} failed)"
[ "$failed" -eq 0 ] || { echo 'Not all release assets were downloaded.' >&2; exit 1; }
- name: Upload release metadata artifact
if: steps.release.outputs.found == 'true'
uses: actions/upload-artifact@v4
with:
name: release-mirror-data
path: |
release.json
assets/*
if-no-files-found: error
mirror-gitverse:
needs: setup
if: needs.setup.outputs.found == 'true'
runs-on: ubuntu-latest
steps:
- name: Download release metadata
uses: actions/download-artifact@v4
with:
name: release-mirror-data
- name: Sync tag and release to GitVerse
env:
GITVERSE_PAT: ${{ secrets.GITVERSE_PAT }}
GITVERSE_ORG: ${{ vars.GITVERSE_ORG || 'dcr_labs' }}
GITVERSE_REPO: ${{ vars.GITVERSE_REPO || 'dcr' }}
TAG: ${{ needs.setup.outputs.tag }}
run: |
set -euo pipefail
test -n "$GITVERSE_PAT" || { echo 'GITVERSE_PAT is not configured' >&2; exit 1; }
project="${GITVERSE_ORG}%2F${GITVERSE_REPO}"
remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git"
git clone --mirror "https://github.com/${GITHUB_REPOSITORY}.git" mirror.git
git -C mirror.git remote add gitverse "$remote"
git -C mirror.git push gitverse "refs/tags/${TAG}:refs/tags/${TAG}"
title=$(jq -r '.name // .tag_name' release.json)
notes=$(jq -r '.body // ""' release.json)
payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \
'{name:$title, tag_name:$tag, description:$notes, ref:$tag}')
http=$(curl -sS -o gitverse-release.json -w '%{http_code}' \
-H "PRIVATE-TOKEN: $GITVERSE_PAT" \
"https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}" || true)
if [ "$http" = '200' ]; then
echo "GitVerse release ${TAG} already exists; skipping create."
else
curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \
-H 'Content-Type: application/json' --data "$payload" \
"https://gitverse.ru/api/v4/projects/${project}/releases" || \
curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITVERSE_PAT" \
-H 'Content-Type: application/json' --data "$payload" \
"https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}"
fi
uploaded=0; failed=0
for file in assets/*; do
[ -f "$file" ] || continue
name=$(basename "$file")
upload_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \
-F "file=@$file" "https://gitverse.ru/api/v4/projects/${project}/uploads")
http_code=$(echo "$upload_response" | tail -n1)
body=$(echo "$upload_response" | sed '$d')
if [ "$http_code" != "201" ] && [ "$http_code" != "200" ]; then
echo "::warning::GitVerse: failed to upload $name (HTTP $http_code)"
failed=$((failed+1))
continue
fi
url=$(jq -r '.url // .file_path' <<< "$body" 2>/dev/null || echo "")
if [ -z "$url" ]; then
echo "::warning::GitVerse: failed to parse upload response for $name"
failed=$((failed+1))
continue
fi
link=$(jq -n --arg name "$name" --arg url "$url" '{name:$name,url:$url}')
link_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \
-H 'Content-Type: application/json' --data "$link" \
"https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}/assets/links")
link_http=$(echo "$link_response" | tail -n1)
if [ "$link_http" = "201" ] || [ "$link_http" = "200" ] || [ "$link_http" = "409" ]; then
uploaded=$((uploaded+1))
else
echo "::warning::GitVerse: failed to attach asset $name (HTTP $link_http)"
failed=$((failed+1))
fi
done
echo "GitVerse: uploaded ${uploaded}, failed ${failed}"
[ "$failed" -eq 0 ] || exit 1
mirror-sourcecraft:
needs: setup
if: needs.setup.outputs.found == 'true'
runs-on: ubuntu-latest
steps:
- name: Download release metadata
uses: actions/download-artifact@v4
with:
name: release-mirror-data
- name: Sync tag and release to SourceCraft
env:
SOURCECRAFT_PAT: ${{ secrets.SOURCECRAFT_PAT }}
SOURCECRAFT_ORG: ${{ vars.SOURCECRAFT_ORG || 'dexoron-dev' }}
SOURCECRAFT_REPO: ${{ vars.SOURCECRAFT_REPO || 'dcr' }}
TAG: ${{ needs.setup.outputs.tag }}
run: |
set -euo pipefail
test -n "$SOURCECRAFT_PAT" || { echo 'SOURCECRAFT_PAT is not configured' >&2; exit 1; }
remote="https://anyname:${SOURCECRAFT_PAT}@git.sourcecraft.dev/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}.git"
git clone "$remote" sourcecraft
git -C sourcecraft remote add github "https://github.com/${GITHUB_REPOSITORY}.git"
git -C sourcecraft fetch github "refs/tags/${TAG}:refs/tags/${TAG}"
git -C sourcecraft push origin "refs/tags/${TAG}"
title=$(jq -r '.name // .tag_name' release.json)
notes=$(jq -r '.body // ""' release.json)
payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \
'{tag:$tag,title:$title,release_notes:$notes,publish:true}')
base="https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}"
http=$(curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer $SOURCECRAFT_PAT" "${base}/releases/tag/${TAG}" || true)
if [ "$http" != '200' ]; then
curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \
-H 'Content-Type: application/json' --data "$payload" "${base}/releases" || \
curl -fsSL -X PUT -H "Authorization: Bearer $SOURCECRAFT_PAT" \
-H 'Content-Type: application/json' --data "$payload" "${base}/releases/${TAG}" || \
echo 'SourceCraft release already exists; continuing with assets.'
else
echo "SourceCraft release ${TAG} already exists; continuing with assets."
fi
uploaded=0; failed=0
for file in assets/*; do
[ -f "$file" ] || continue
name=$(basename "$file")
upload_response=$(curl -sS -w "\n%{http_code}" -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \
-F "file=@$file" \
"${base}/releases/tag/${TAG}/assets")
http_code=$(echo "$upload_response" | tail -n1)
if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then
uploaded=$((uploaded+1))
elif [ "$http_code" = "409" ]; then
uploaded=$((uploaded+1))
else
echo "::warning::SourceCraft: failed to attach asset $name (HTTP $http_code)"
failed=$((failed+1))
fi
done
echo "SourceCraft: uploaded ${uploaded}, failed ${failed}"
[ "$failed" -eq 0 ] || exit 1
mirror-gitlab:
needs: setup
if: needs.setup.outputs.found == 'true'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Download release metadata
uses: actions/download-artifact@v4
with:
name: release-mirror-data
- name: Sync release to GitLab
env:
GITLAB_PAT: ${{ secrets.GITLAB_PAT }}
GITLAB_PROJECT: ${{ vars.GITLAB_PROJECT || 'dexoron-dev/dcr-tool/dcr' }}
TAG: ${{ needs.setup.outputs.tag }}
run: |
set -euo pipefail
test -n "$GITLAB_PAT" || { echo 'GITLAB_PAT is not configured' >&2; exit 0; }
project_id="${GITLAB_PROJECT//\//%2F}"
api="https://gitlab.com/api/v4/projects/${project_id}"
title=$(jq -r '.name // .tag_name' release.json)
notes=$(jq -r '.body // ""' release.json)
payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \
'{tag_name:$tag, name:$title, description:$notes}')
http=$(curl -sS -o /dev/null -w '%{http_code}' \
-H "PRIVATE-TOKEN: $GITLAB_PAT" "${api}/releases/${TAG}" || true)
if [ "$http" = '200' ]; then
echo "GitLab release ${TAG} already exists; skipping"
exit 0
fi
echo "Creating release on GitLab..."
curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \
-H 'Content-Type: application/json' --data "$payload" "${api}/releases" || true

View File

@@ -45,6 +45,10 @@ on:
description: "Update Homebrew tap"
type: boolean
default: false
run_s3:
description: "Mirror Release to Yandex Object Storage (S3)"
type: boolean
default: false
permissions:
contents: write
@@ -1187,6 +1191,7 @@ jobs:
- name: Update Formula/dcr.rb
run: |
VERSION="${{ needs.setup.outputs.version }}"
cat > Formula/dcr.rb <<FORMULA
class Dcr < Formula
desc "Cargo-like utility to manage C/C++ projects"
@@ -1195,37 +1200,36 @@ jobs:
on_macos do
on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-apple-darwin-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-apple-darwin-${VERSION}"
sha256 "${{ steps.sha.outputs.macos_x86 }}"
end
on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-apple-darwin-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-apple-darwin-${VERSION}"
sha256 "${{ steps.sha.outputs.macos_arm }}"
end
end
on_linux do
on_intel do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-x86_64-unknown-linux-gnu-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-x86_64-unknown-linux-gnu-${VERSION}"
sha256 "${{ steps.sha.outputs.linux_x86 }}"
end
on_arm do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-aarch64-unknown-linux-gnu-#{version}"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-aarch64-unknown-linux-gnu-${VERSION}"
sha256 "${{ steps.sha.outputs.linux_arm }}"
end
end
def install
bin.install Dir["dcr*"].first => "dcr"
end
resource "man-pages" do
url "https://github.com/dexoron/dcr/releases/download/v#{version}/dcr-man-pages-#{version}.tar.gz"
url "https://github.com/dexoron/dcr/releases/download/v${VERSION}/dcr-man-pages-${VERSION}.tar.gz"
sha256 "${{ steps.sha.outputs.man_pages }}"
end
def install
bin.install Dir["dcr*"].first => "dcr"
resource("man-pages").stage do
man1.install Dir["usr/share/man/man1/*.1"]
end
@@ -1254,3 +1258,72 @@ jobs:
#
# publish-snap:
# if: ...
# ── Mirror Release to Yandex Object Storage (S3) ─────────────────────────
publish-s3:
if: |
(github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_s3 == 'true')
needs:
- setup
- create-release
- man-pages
- build-linux
- build-macos
- build-bsd
- build-windows
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.tag }}
- name: Prepare Directory and Download GitHub Release Assets
id: fetch-assets
shell: bash
run: |
set -euo pipefail
TAG="${{ needs.setup.outputs.tag }}"
mkdir -p dist
cd dist
echo "Downloading assets from GitHub Release for tag: ${TAG}"
gh release download "${TAG}" --dir . --repo "${{ github.repository }}"
cd ..
echo "Archiving source code..."
git archive --format=tar.gz --prefix="dcr-${TAG}/" "${TAG}" -o "dist/dcr-src-${TAG}.tar.gz"
git archive --format=zip --prefix="dcr-${TAG}/" "${TAG}" -o "dist/dcr-src-${TAG}.zip"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Everything to Yandex Object Storage
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_EC2_METADATA_DISABLED: true
shell: bash
run: |
set -euo pipefail
TAG="${{ needs.setup.outputs.tag }}"
aws s3 sync dist/ "s3://${{ secrets.S3_BUCKET_NAME }}/${TAG}/" \
--endpoint-url https://storage.yandexcloud.net \
--region ru-central1
- name: Upload Everything to Cloudflare R2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_EC2_METADATA_DISABLED: true
shell: bash
run: |
set -euo pipefail
TAG="${{ needs.setup.outputs.tag }}"
aws s3 sync dist/ "s3://${{ secrets.R2_BUCKET_NAME }}/${TAG}/" \
--endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \
--region auto

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ sandbox/**
personal/
.*
!.gitignore
!.github/
graphify-out/

View File

@@ -1,5 +1,67 @@
# Changelog
## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies"
### RU
**Добавлено:**
- **Зависимости через `pkg-config`** — `fmt = { pkg-config = "fmt" }` или массив имён пакетов (`pkg-config = ["openssl", "libcrypto"]`) подключает системные библиотеки через `.pc`-метаданные.
- **Поддержка префиксов в `dcr add`** — команды `dcr add fmt pkg-config:fmt` и `dcr add openssl pkg:openssl,libcrypto` добавляют зависимости от системных пакетов в `dcr.toml`.
- **Полный набор флагов** — DCR собирает и пробрасывает compiler/linker flags (`-I`, `-L`, `-l`, `-pthread` и другие), включая транзитивные зависимости и интеграцию с `dcr gen` / `dcr test`.
- **Фиксация в `dcr.lock`** — записывается источник `pkg-config+...` и обнаруженная версия пакета.
- **Отображение в `dcr tree`** — `pkg-config` зависимости наглядно помечаются в дереве зависимостей.
**Исправлено:**
- **Ошибка создания проекта в папках со спецсимволами** — `dcr new` больше не завершается ошибкой `Failed to create dcr.toml`, если родительская директория содержит пробелы или не-ASCII символы.
### EN
**Added:**
- **`pkg-config` dependencies** — `fmt = { pkg-config = "fmt" }` or an array of package names (`pkg-config = ["openssl", "libcrypto"]`) connects system libraries through their `.pc` metadata.
- **Prefix support in `dcr add`** — `dcr add fmt pkg-config:fmt` and `dcr add openssl pkg:openssl,libcrypto` add system package dependencies to `dcr.toml`.
- **Complete flag propagation** — compiler/linker flags (`-I`, `-L`, `-l`, `-pthread`, and others) are preserved and passed to builds, `dcr test`, and IDE integrations via `dcr gen`.
- **Lockfile entries** — `dcr.lock` records the `pkg-config+...` source and detected package version.
- **`dcr tree` representation** — `pkg-config` dependencies are clearly labeled in the dependency tree.
**Fixed:**
- **Project creation failure in special character directories** — `dcr new` no longer fails with `Failed to create dcr.toml` when invoked inside parent folders with spaces or non-ASCII characters.
## [v0.9.0] - 2026-08-31 "Полные Path/Git-зависимости и готовые библиотеки / Complete Path/Git Dependencies & Prebuilt Libraries"
### RU
**Добавлено:**
- **Зеркала загрузок в `dcrup`** — `--mirror auto|cf|gh|sf|ya` и `DCRUP_MIRROR`.
По умолчанию `auto`: **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
- **Полные Path/Git-зависимости** — DCR-пакеты из `path` и `git` собираются перед потребителем; Git-код кешируется в `~/.dcr/git`, поддерживает `branch`/`tag`/`rev` и фиксирует точный commit в `dcr.lock`.
- **Транзитивные зависимости и защита от циклов** — DCR рекурсивно собирает DCR-пакеты и показывает цепочку циклической зависимости.
- **Внешние готовые библиотеки через path/Git** — `include`, `lib` и `libs` подключают библиотеку без `dcr.toml`; Git prebuilt-источники тоже фиксируются по точному commit.
- **Явный режим зависимости** — `mode = "build"` и `mode = "prebuild"` позволяют выбрать сборку DCR-пакета или подключение готовых артефактов, даже когда репозиторий содержит оба варианта.
**Удалено:**
- **`install.sh` / `install.ps1` / `install_bsd.sh`** — устарели, заменены `dcrup`.
### EN
**Added:**
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.
Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
- **Complete Path/Git dependencies** — DCR packages from `path` and `git` are built before their consumer; Git sources are cached in `~/.dcr/git`, support `branch`/`tag`/`rev`, and lock the exact commit in `dcr.lock`.
- **Transitive dependencies and cycle protection** — DCR recursively builds DCR packages and reports the dependency cycle chain.
- **Prebuilt external libraries through path/Git** — `include`, `lib`, and `libs` link libraries without `dcr.toml`; Git prebuilt sources are locked to their exact commit as well.
- **Explicit dependency mode** — `mode = "build"` and `mode = "prebuild"` select DCR package builds or ready-made artifacts when a repository contains both forms.
**Removed:**
- **`install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`.
## [v0.8.5] - 2026-08-08 "Фиксация версии dcr-version и предупреждения совместимости / Config Version Pinning & Compatibility Warnings"
### RU

View File

@@ -11,6 +11,7 @@ Thanks for your interest in the project.
```bash
git clone https://github.com/dexoron/dcr.git
# or a source mirror: GitVerse / SourceCraft / SourceForge / GitLab (see README)
cd dcr
cargo check
```

287
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -11,6 +20,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.102"
@@ -106,6 +165,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -154,11 +219,14 @@ dependencies = [
[[package]]
name = "dcr"
version = "0.8.5"
version = "0.9.1"
dependencies = [
"ctrlc",
"env_logger",
"fatfs",
"glob",
"log",
"owo-colors",
"self-replace",
"serde",
"serde_json",
@@ -168,6 +236,37 @@ dependencies = [
"ureq",
]
[[package]]
name = "defmt"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
dependencies = [
"defmt-parser",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -198,7 +297,30 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
name = "env_filter"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
@@ -475,12 +597,54 @@ dependencies = [
"serde_core",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
dependencies = [
"defmt",
"jiff-core",
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
dependencies = [
"defmt",
]
[[package]]
name = "jiff-static"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
dependencies = [
"jiff-core",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "js-sys"
version = "0.3.103"
@@ -518,9 +682,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "log"
version = "0.4.30"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
@@ -570,12 +734,24 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "owo-colors"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -588,6 +764,21 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -604,7 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -631,6 +822,35 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustix"
version = "1.1.4"
@@ -757,7 +977,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -828,6 +1048,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -836,7 +1067,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -852,6 +1083,26 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.5",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -976,6 +1227,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -1032,7 +1289,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wasm-bindgen-shared",
]
@@ -1100,7 +1357,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1111,7 +1368,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -1271,7 +1528,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
"syn",
"syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -1287,7 +1544,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@@ -1354,7 +1611,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"synstructure",
]
@@ -1375,7 +1632,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"synstructure",
]
@@ -1415,7 +1672,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "dcr"
version = "0.8.5"
version = "0.9.1"
edition = "2024"
build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -75,6 +75,11 @@ serde_json = "1"
ctrlc = "3"
fatfs = { version = "0.3", optional = true }
# Logging
log = "0.4.34"
env_logger = "0.11.11"
owo-colors = "4"
[profile.release]
opt-level = "z"
lto = "thin"
@@ -89,3 +94,12 @@ lto = false
codegen-units = 16
panic = "abort"
strip = true
[profile.dev]
incremental = false
opt-level = 0
debug = "line-tables-only"
[profile.dev.package."*"]
opt-level = 1
debug = false

11
FAQ.md
View File

@@ -61,8 +61,19 @@ Available installation methods may vary by project version.
Recommended path:
- check instructions in `README.md`;
- use `dcrup install stable` (default mirror chain: **cf → gh → sf → ya**);
- pin a host with `--mirror cf|gh|sf|ya` or `DCRUP_MIRROR` if needed;
- if there is no prebuilt package for your platform, build from source using `cargo build --release`.
## How do I choose a download mirror?
`dcrup` defaults to `--mirror auto`: CloudFlare CDN, then GitHub, then SourceForge, then Yandex CDN.
```sh
dcrup install stable --mirror cf
export DCRUP_MIRROR=ya
```
## How can I get started quickly?
Minimal workflow:

View File

@@ -39,7 +39,10 @@
```sh
yay -S dcr
# or
paru -S dcr
```
> 💡 Do you like DCR? Vote for the package on the [AUR](https://aur.archlinux.org/packages/dcr) page—it will help the project get into the official "extra" repository!
</td>
</tr>
@@ -83,7 +86,9 @@ cargo install dcr
```sh
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable
dcrup install stable # auto: cf → gh → sf → ya
# dcrup install stable --mirror cf # CloudFlare CDN
# dcrup install stable --mirror ya # Yandex CDN (RU)
```
</td>
@@ -98,7 +103,9 @@ irm https://ps1.dcr-tool.ru | iex
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
# powershell -File .\dcrup.ps1 self-install
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
dcrup install stable
dcrup install stable # auto: cf → gh → sf → ya
# dcrup install stable --mirror cf # CloudFlare CDN
# dcrup install stable --mirror ya # Yandex CDN (RU)
```
</td>
@@ -216,6 +223,41 @@ Full docs at **[dcr.dexoron.su](https://dcr.dexoron.su)** or in the [`docs/`](do
---
## Repository Mirrors
The primary repository is hosted on GitHub. Source and release mirrors:
| Region | Mirror | Contents |
|---|---|---|
| Worldwide | [GitHub](https://github.com/dexoron/dcr) | Source code and releases |
| Worldwide | [SourceForge](https://sourceforge.net/projects/dcr-tool/) | Source code and releases |
| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) | Source code |
| Russia | [SourceCraft](https://sourcecraft.dev/dexoron-dev/dcr) | Source code |
Release CDN (used by `dcrup`):
| Code | Host | Role |
|---|---|---|
| `cf` | [cdn-global.dcr-tool.ru](https://cdn-global.dcr-tool.ru) (CloudFlare) | Worldwide CDN |
| `gh` | GitHub Releases | Primary host |
| `sf` | SourceForge | Worldwide fallback |
| `ya` | [cdn-ru.dcr-tool.ru](https://cdn-ru.dcr-tool.ru) (Yandex) | Paid RU fallback for restrictions |
`dcrup install` defaults to `--mirror auto` and tries **cf → gh → sf → ya**. Override with `--mirror cf|gh|sf|ya` or `DCRUP_MIRROR`.
---
## VS Code Extension
Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr) for VS Code. Do not install Microsoft C/C++ extensions for DCR projects; DCR uses `clangd` instead. If you need debugging with Microsoft's debugger, use MS C/C++ only for its debugger component.
Recommended companion extensions:
- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only
---
## Contributors
<div align="center">

View File

@@ -1,13 +0,0 @@
@echo off
setlocal
set "SCRIPT_DIR=%~dp0"
if exist "%SCRIPT_DIR%dcrup.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%dcrup.ps1" %*
exit /b %ERRORLEVEL%
)
if exist "%USERPROFILE%\.dcr\bin\dcrup.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.dcr\bin\dcrup.ps1" %*
exit /b %ERRORLEVEL%
)
echo [error] dcrup.ps1 not found near dcrup.cmd >&2
exit /b 1

136
dcrup.ps1
View File

@@ -1,4 +1,4 @@
#Requires -Version 5.1
#Requires -Version 5.1
$ErrorActionPreference = 'Stop'
$DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' }
@@ -6,6 +6,8 @@ $DcrupBin = if ($env:DCRUP_BIN) { $env:DCRUP_BIN } else { Join-Path $DcrupHom
$DcrupTc = Join-Path $DcrupHome 'toolchains'
$DcrupMeta = Join-Path $DcrupHome 'meta'
$RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' }
$InstallUrl = if ($env:DCRUP_INSTALL_URL) { $env:DCRUP_INSTALL_URL } else { 'https://ps1.dcr-tool.ru' }
$DcrupMirror = if ($env:DCRUP_MIRROR) { $env:DCRUP_MIRROR } else { 'auto' }
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
$Features = if ($env:DCR_FEATURES) { $env:DCR_FEATURES } else { 'archive' }
@@ -20,7 +22,7 @@ function Show-Usage {
dcrup — install and switch DCR versions (Windows)
Usage:
dcrup install <spec> [--build|--release] [--force]
dcrup install <spec> [--build|--release] [--force] [--mirror auto|cf|gh|sf|ya]
dcrup default <spec>
dcrup update
dcrup list
@@ -38,15 +40,24 @@ Spec:
night → always build from branch "dev" HEAD
Modes:
--release download GitHub Release binary (default for stable/dev)
--release download prebuilt binary (default for stable/dev)
--build cargo build --release --features archive
night always --build (prebuilt not available)
Mirrors (prebuilt download only):
--mirror auto try cf → gh → sf → ya, first successful download wins (default)
--mirror cf download from cdn-global.dcr-tool.ru
--mirror gh download from github.com/dexoron/dcr
--mirror sf download from sourceforge.net/projects/dcr-tool
--mirror ya download from cdn-ru.dcr-tool.ru
DCRUP_MIRROR env override (default: auto)
Env:
DCRUP_HOME default ~/.dcr
DCRUP_BIN default $DCRUP_HOME/bin
DCR_FEATURES default archive
DCRUP_REPO git/GitHub repo URL
DCRUP_MIRROR auto|cf|gh|sf|ya (default: auto)
'@ | Write-Host
}
@@ -123,6 +134,65 @@ function Link-Default([string]$id) {
Ok "default → $id ($dst)"
}
function Resolve-InstalledId([string]$spec) {
Parse-Spec $spec
if ($script:SpecChannel -eq 'night' -or $script:SpecFloating) {
$item = Get-ChildItem $DcrupTc -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "$($script:SpecChannel)-*" } |
Sort-Object Name | Select-Object -Last 1
if ($item) { return $item.Name }
return $null
}
return (Toolchain-Id)
}
function Set-Mirror([string]$value) {
if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) {
Die "invalid --mirror: $value (use auto|cf|gh|sf|ya)"
}
$script:DcrupMirror = $value
}
function Get-MirrorUrl([string]$mirror, [string]$tag, [string]$asset) {
switch ($mirror) {
'cf' { return "https://cdn-global.dcr-tool.ru/$tag/$asset" }
'gh' { return "https://github.com/dexoron/dcr/releases/download/$tag/$asset" }
'sf' { return "https://sourceforge.net/projects/dcr-tool/files/$tag/$asset/download" }
'ya' { return "https://cdn-ru.dcr-tool.ru/$tag/$asset" }
default { Die "invalid mirror: $mirror" }
}
}
function Get-MirrorsToTry {
switch ($script:DcrupMirror) {
'auto' { return @('cf', 'gh', 'sf', 'ya') }
{ $_ -in @('cf', 'gh', 'sf', 'ya') } { return @($script:DcrupMirror) }
default { Die "invalid mirror: $($script:DcrupMirror) (use auto|cf|gh|sf|ya)" }
}
}
function Download-FromMirrors([string]$tag, [string]$asset, [string]$destFile) {
$tmp = "$destFile.download"
foreach ($m in (Get-MirrorsToTry)) {
$url = Get-MirrorUrl $m $tag $asset
Info "trying mirror $m …"
Remove-Item $tmp -ErrorAction SilentlyContinue
try {
Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent 'dcrup'
$size = (Get-Item $tmp).Length
if ($size -gt 100000) {
Move-Item $tmp $destFile -Force
return $m
}
Warn "mirror $m returned a too-small file ($size bytes)"
} catch {
Warn "mirror $m failed"
}
Remove-Item $tmp -ErrorAction SilentlyContinue
}
return $null
}
function Fetch-Release($channel, $version) {
if ($version) {
return Invoke-RestMethod -Uri "https://api.github.com/repos/dexoron/dcr/releases/tags/v$version"
@@ -142,8 +212,6 @@ function Install-Prebuilt([switch]$Force) {
$tag = $rel.tag_name
$ver = Normalize-Version $tag
$assetName = "dcr-$triple-$ver.exe"
$asset = $rel.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
if (-not $asset) { Die "asset not found: $assetName" }
$id = if ($script:SpecFloating) { Toolchain-Id $ver } else { Toolchain-Id }
$dest = Join-Path $DcrupTc $id
@@ -155,16 +223,19 @@ function Install-Prebuilt([switch]$Force) {
}
Ensure-Dirs
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Info "downloading $assetName …"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $bin
Info "downloading $assetName (mirror=$($script:DcrupMirror)) …"
$mirror = Download-FromMirrors $tag $assetName $bin
if (-not $mirror) { Die "no mirror reachable for $assetName (try: --mirror cf|gh|sf|ya)" }
Info "mirror → $mirror"
@"
channel=$($script:SpecChannel)
version=$ver
tag=$tag
source=release
triple=$triple
mirror=$mirror
"@ | Set-Content (Join-Path $dest 'dcrup-meta')
Ok "installed prebuilt $id"
Ok "installed prebuilt $id ($triple via $mirror)"
Link-Default $id
}
@@ -270,23 +341,32 @@ function Cmd-Install {
$mode = 'release'
$force = $false
$spec = $null
foreach ($a in $Args) {
$i = 0
while ($i -lt $Args.Count) {
$a = $Args[$i]
switch -Regex ($a) {
'^--build$' { $mode = 'build'; continue }
'^--release$' { $mode = 'release'; continue }
'^--force$' { $force = $true; continue }
'^--build$' { $mode = 'build' }
'^--release$' { $mode = 'release' }
'^--force$' { $force = $true }
'^--mirror$' {
if ($i + 1 -ge $Args.Count) { Die '--mirror needs auto|cf|gh|sf|ya' }
$i++
Set-Mirror $Args[$i]
}
'^--mirror=(.+)$' { Set-Mirror $Matches[1] }
'^-' { Die "unknown flag: $a" }
default {
if ($spec) { Die "unexpected: $a" }
$spec = $a
}
}
$i++
}
if (-not $spec) { Die 'usage: dcrup install <spec> [--build|--release] [--force]' }
if (-not $spec) { Die 'usage: dcrup install <spec> [--build|--release] [--force] [--mirror auto|cf|gh|sf|ya]' }
Parse-Spec $spec
Ensure-Dirs
if ($script:SpecChannel -eq 'night') { $mode = 'build' }
Info "install spec=$spec channel=$($script:SpecChannel) mode=$mode"
Info "install spec=$spec channel=$($script:SpecChannel) mode=$mode mirror=$($script:DcrupMirror)"
if ($mode -eq 'build') { Install-Build -Force:$force } else { Install-Prebuilt -Force:$force }
Ensure-UserPath
Install-CmdShim
@@ -312,7 +392,19 @@ function Cmd-Show {
function Cmd-SelfInstall {
Ensure-Dirs
if ([string]::IsNullOrWhiteSpace($PSCommandPath) -or -not (Test-Path $PSCommandPath)) {
# With `irm URL | iex`, PowerShell evaluates text and there is no
# source path. Download the canonical installer for self-install.
$tmp = Join-Path $env:TEMP ("dcrup-" + [guid]::NewGuid().ToString('N') + '.ps1')
try {
Invoke-WebRequest -Uri $InstallUrl -OutFile $tmp -UseBasicParsing -UserAgent 'dcrup'
Copy-Item $tmp (Join-Path $DcrupBin 'dcrup.ps1') -Force
} finally {
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
}
} else {
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
}
Install-CmdShim
Ensure-UserPath
Ok "dcrup installed to $DcrupBin (use: dcrup …)"
@@ -350,7 +442,7 @@ switch ($cmd) {
if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') }
elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') }
elseif ($a -like 'dev-*') { Cmd-Install @('dev', '--force') }
else { Ok "pinned $a — install a newer version explicitly" }
else { Ok "pinned $a - install a newer version explicitly" }
}
'uninstall' {
if ($rest -contains '--all') {
@@ -359,9 +451,21 @@ switch ($cmd) {
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
Ok 'removed all'
} else {
Die 'use: dcrup uninstall --all (or install a replacement first)'
if ($rest.Count -ne 1) { Die 'usage: dcrup uninstall <spec>|--all' }
$id = Resolve-InstalledId $rest[0]
if (-not $id) { Die "not installed: $($rest[0])" }
$dir = Join-Path $DcrupTc $id
if (-not (Test-Path $dir)) { Die "not installed: $($rest[0])" }
Remove-Item -Recurse -Force $dir
if ((Read-Active) -eq $id) {
Remove-Item (Join-Path $DcrupBin 'dcr.exe') -ErrorAction SilentlyContinue
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
Warn 'removed active toolchain; install or select another default'
}
Ok "removed $id"
}
}
'self-install' { Cmd-SelfInstall }
default { Die "unknown command: $cmd" }
}

153
dcrup.sh Executable file → Normal file
View File

@@ -6,6 +6,8 @@ DCRUP_BIN="${DCRUP_BIN:-$DCRUP_HOME/bin}"
DCRUP_TC="${DCRUP_HOME}/toolchains"
DCRUP_META="${DCRUP_HOME}/meta"
REPO_URL="${DCRUP_REPO:-https://github.com/dexoron/dcr}"
DCRUP_INSTALL_URL="${DCRUP_INSTALL_URL:-https://sh.dcr-tool.ru}"
DCRUP_MIRROR="${DCRUP_MIRROR:-auto}"
API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
API_ALL="https://api.github.com/repos/dexoron/dcr/releases"
FEATURES="${DCR_FEATURES:-archive}"
@@ -19,14 +21,17 @@ NC='\033[0m'
log() { printf "${CYAN}[dcrup]${NC} %s\n" "$*"; }
ok() { printf "${GREEN}[ok]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[warn]${NC} %s\n" "$*"; }
die() { printf "${RED}[error]${NC} %s\n" "$*" >&2; exit 1; }
die() {
printf "${RED}[error]${NC} %s\n" "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
dcrup — install and switch DCR versions
Usage:
dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto]
dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]
dcrup default <spec>
dcrup update
dcrup list
@@ -44,7 +49,7 @@ Spec:
night → always build from branch "dev" HEAD
Modes:
--release download GitHub Release binary (default for stable/dev)
--release download prebuilt binary (default for stable/dev)
--build cargo build --release --features archive
night always --build (prebuilt not available)
@@ -53,12 +58,21 @@ Linux libc (prebuilt triple only):
--libc musl x86_64-unknown-linux-musl
--libc auto musl if host ldd/libc is musl, else gnu
Mirrors (prebuilt download only):
--mirror auto try cf → gh → sf → ya, first successful download wins (default)
--mirror cf download from cdn-global.dcr-tool.ru
--mirror gh download from github.com/dexoron/dcr
--mirror sf download from sourceforge.net/projects/dcr-tool
--mirror ya download from cdn-ru.dcr-tool.ru
DCRUP_MIRROR env override (default: auto)
Env:
DCRUP_HOME default ~/.dcr
DCRUP_BIN default $DCRUP_HOME/bin
DCR_FEATURES default archive
DCRUP_REPO git/GitHub repo URL
DCRUP_LIBC gnu|musl|auto (default: gnu)
DCRUP_MIRROR auto|cf|gh|sf|ya (default: auto)
EOF
}
@@ -225,6 +239,8 @@ link_default() {
local id="$1"
local src="$DCRUP_TC/$id/dcr"
[[ -x "$src" ]] || die "toolchain not installed: $id"
# Resolve the source so a relative DCRUP_HOME cannot create a broken link.
src="$(cd "$(dirname "$src")" && pwd)/$(basename "$src")"
ln -sfn "$src" "$DCRUP_BIN/dcr"
write_active "$id"
ok "default → $id ($DCRUP_BIN/dcr)"
@@ -239,9 +255,57 @@ json_tag() {
printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1
}
json_asset_url() {
local json="$1" asset="$2"
printf '%s' "$json" | sed -n "s#.*\"browser_download_url\"[[:space:]]*:[[:space:]]*\"\([^\"]*/${asset}\)\"#\1#p" | head -n1
set_mirror() {
case "$1" in
auto | cf | gh | sf | ya) DCRUP_MIRROR="$1" ;;
*) die "invalid --mirror: $1 (use auto|cf|gh|sf|ya)" ;;
esac
}
mirror_url() {
local mirror="$1" tag="$2" asset="$3"
case "$mirror" in
cf) printf 'https://cdn-global.dcr-tool.ru/%s/%s' "$tag" "$asset" ;;
gh) printf 'https://github.com/dexoron/dcr/releases/download/%s/%s' "$tag" "$asset" ;;
sf) printf 'https://sourceforge.net/projects/dcr-tool/files/%s/%s/download' "$tag" "$asset" ;;
ya) printf 'https://cdn-ru.dcr-tool.ru/%s/%s' "$tag" "$asset" ;;
*) return 1 ;;
esac
}
mirror_chain() { echo "cf gh sf ya"; }
mirrors_to_try() {
case "$DCRUP_MIRROR" in
auto) mirror_chain ;;
cf | gh | sf | ya) echo "$DCRUP_MIRROR" ;;
*) die "invalid mirror: $DCRUP_MIRROR (use auto|cf|gh|sf|ya)" ;;
esac
}
download_from_mirrors() {
local tag="$1" asset="$2" dest="$3"
local tmp="$dest/dcr.download" m url size
USED_MIRROR=""
for m in $(mirrors_to_try); do
url="$(mirror_url "$m" "$tag" "$asset")"
log "trying mirror $m …"
rm -f "$tmp"
if curl -fL --retry 2 --retry-delay 1 --connect-timeout 10 --max-time 180 \
-A "dcrup" "$url" -o "$tmp"; then
size="$(wc -c <"$tmp" | tr -d ' ')"
if [[ "${size:-0}" -gt 100000 ]]; then
mv "$tmp" "$dest/dcr"
USED_MIRROR="$m"
return 0
fi
warn "mirror $m returned a too-small file (${size:-0} bytes)"
else
warn "mirror $m failed"
fi
rm -f "$tmp"
done
return 1
}
latest_stable_json() {
@@ -281,7 +345,7 @@ release_json_for_spec() {
}
install_prebuilt() {
local triple asset tag version url json id dest
local triple asset tag version id dest mirror
need_cmd curl
triple="$(detect_triple)"
json="$(release_json_for_spec)"
@@ -292,8 +356,6 @@ install_prebuilt() {
case "$triple" in
*windows*) asset="${asset}.exe" ;;
esac
url="$(json_asset_url "$json" "$asset")"
[[ -n "$url" ]] || die "asset not found: $asset (tag $tag)"
id="$(toolchain_id "$version")"
dest="$DCRUP_TC/$id"
@@ -303,9 +365,12 @@ install_prebuilt() {
return 0
fi
log "downloading $asset …"
mkdir -p "$dest"
curl -fL "$url" -o "$dest/dcr"
log "downloading $asset (mirror=$DCRUP_MIRROR) …"
download_from_mirrors "$tag" "$asset" "$dest" ||
die "no mirror reachable for $asset (try: --mirror cf|gh|sf|ya)"
mirror="$USED_MIRROR"
log "mirror → $mirror"
chmod +x "$dest/dcr"
printf '%s\n' \
"channel=$SPEC_CHANNEL" \
@@ -313,9 +378,10 @@ install_prebuilt() {
"tag=$tag" \
"source=release" \
"triple=$triple" \
"mirror=$mirror" \
"libc=${DCRUP_LIBC:-gnu}" \
>"$dest/dcrup-meta"
ok "installed prebuilt $id ($triple)"
ok "installed prebuilt $id ($triple via $mirror)"
link_default "$id"
}
@@ -330,8 +396,8 @@ install_build() {
if [[ "$SPEC_CHANNEL" == "night" ]]; then
log "cloning $REPO_URL (branch dev) for night …"
git clone --depth 1 --branch dev "$REPO_URL" "$tmp/src" 2>/dev/null \
|| git clone --depth 1 "$REPO_URL" "$tmp/src"
git clone --depth 1 --branch dev "$REPO_URL" "$tmp/src" 2>/dev/null ||
git clone --depth 1 "$REPO_URL" "$tmp/src"
sha="$(git -C "$tmp/src" rev-parse --short HEAD)"
version="night"
id="$(toolchain_id "$sha")"
@@ -343,8 +409,8 @@ install_build() {
version="$(normalize_version "$tag")"
id="$(toolchain_id "$version")"
log "cloning $REPO_URL @ $tag …"
git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null \
|| {
git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null ||
{
git clone --depth 1 "$REPO_URL" "$tmp/src"
git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
git -C "$tmp/src" checkout "$tag"
@@ -355,8 +421,8 @@ install_build() {
id="$(toolchain_id)"
local tag="v${SPEC_VERSION}"
log "cloning $REPO_URL @ $tag …"
git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null \
|| {
git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null ||
{
git clone --depth 1 "$REPO_URL" "$tmp/src"
git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
git -C "$tmp/src" checkout "$tag"
@@ -393,9 +459,18 @@ cmd_install() {
local libc_cli=""
while [[ $# -gt 0 ]]; do
case "$1" in
--build) mode="build"; shift ;;
--release) mode="release"; shift ;;
--force) FORCE=1; shift ;;
--build)
mode="build"
shift
;;
--release)
mode="release"
shift
;;
--force)
FORCE=1
shift
;;
--libc)
[[ $# -ge 2 ]] || die "--libc needs gnu|musl|auto"
libc_cli="$2"
@@ -405,7 +480,19 @@ cmd_install() {
libc_cli="${1#--libc=}"
shift
;;
-h|--help) usage; exit 0 ;;
--mirror)
[[ $# -ge 2 ]] || die "--mirror needs auto|cf|gh|sf|ya"
set_mirror "$2"
shift 2
;;
--mirror=*)
set_mirror "${1#--mirror=}"
shift
;;
-h | --help)
usage
exit 0
;;
-*) die "unknown flag: $1" ;;
*)
[[ -z "$spec" ]] || die "unexpected argument: $1"
@@ -414,7 +501,7 @@ cmd_install() {
;;
esac
done
[[ -n "$spec" ]] || die "usage: dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto]"
[[ -n "$spec" ]] || die "usage: dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]"
parse_spec "$spec"
ensure_dirs
@@ -431,7 +518,7 @@ cmd_install() {
fi
export FORCE
log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC"
log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC mirror=$DCRUP_MIRROR"
if [[ "$mode" == "build" ]]; then
install_build
else
@@ -564,14 +651,28 @@ cmd_self_install() {
local dest="$DCRUP_BIN"
while [[ $# -gt 0 ]]; do
case "$1" in
--to) dest="$2"; shift 2 ;;
--to)
dest="$2"
shift 2
;;
*) die "usage: dcrup self-install [--to DIR]" ;;
esac
done
ensure_dirs
mkdir -p "$dest"
local self
self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || true)"
if [[ -z "$self" || ! -f "$self" ]]; then
# When invoked as `curl ... | sh`, the shell has no script file to copy.
# Fetch the same trusted installer URL and install that exact script.
need_cmd curl
self="$(mktemp "${TMPDIR:-/tmp}/dcrup.XXXXXX")"
curl -fsSL --proto '=https' --tlsv1.2 "$DCRUP_INSTALL_URL" -o "$self" || {
rm -f "$self"
die "unable to download installer for self-install"
}
trap 'rm -f "$self"' EXIT
fi
cp "$self" "$dest/dcrup"
chmod +x "$dest/dcrup"
ok "dcrup installed to $dest/dcrup"

View File

@@ -11,7 +11,8 @@ sidebar_label: Introduction
# Installation (dcrup)
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable
dcrup install stable # auto: cf → gh → sf → ya
# dcrup install stable --mirror cf # CloudFlare / GitHub / SourceForge / Yandex
# Create a project
dcr new my-app

View File

@@ -4,6 +4,24 @@ sidebar_label: Changelog
# Changelog
## 0.9.1 (2026-09-13)
- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`.
- **`dcr add` support for `pkg-config`** — easily add system package dependencies with `dcr add fmt pkg-config:fmt` or `dcr add openssl pkg:openssl,libcrypto`.
- **`dcr tree` & IDE integration** — `pkg-config` packages are marked in `dcr tree` and resolved into compiler flags in `dcr gen` (`compile_commands.json`).
- **Fixed `dcr new` in non-ASCII / spaced paths** — initializing projects from folders with non-ASCII characters or spaces no longer fails validation.
## 0.9.0 (2026-08-31)
- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`.
Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN).
- **Complete Path/Git dependencies** — DCR packages from `path` and `git` are built before their consumer. Git sources are cached in `~/.dcr/git`, support `branch`/`tag`/`rev`, and record the exact commit in `dcr.lock`.
- **Build and prebuild modes** — `path` and `git` dependencies can build a DCR package from `dcr.toml` or link existing `include`/`lib`/`libs` artifacts; `mode = "build"` / `"prebuild"` selects explicitly when both forms are present.
- **Prebuilt dependency examples** — documentation now shows the `dcr-math-lib` Git repository and recommended build/prebuilt layouts for both `git` and `path`.
- **Transitive dependency builds and cycle reporting** — DCR recursively builds DCR packages and reports a clear dependency chain for cycles.
- **Bare path arguments for `dcr add`** — `dcr add mylib ../mylib` now works in addition to the explicit `path:` form.
- **Removed `install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`.
## 0.8.5 (2026-08-08)
- **`package.dcr-version`** — optional CLI version pin in `dcr.toml`.

View File

@@ -15,6 +15,8 @@ dcr add mylib path:./lib # explicit path prefix
dcr add mylib git:https://github.com/user/repo # explicit git prefix
dcr add mylib github:user/repo # GitHub shorthand
dcr add mylib gitlab:user/repo # GitLab shorthand
dcr add fmt pkg-config:fmt # system package via pkg-config
dcr add openssl pkg:openssl,libcrypto # multiple pkg-config packages
```
Source prefixes:
@@ -22,6 +24,7 @@ Source prefixes:
- `git:` — generic git URL
- `github:` — expands to `https://github.com/<user>/<repo>`
- `gitlab:` — expands to `https://gitlab.com/<user>/<repo>`
- `pkg-config:` / `pkg:` — one or more packages resolved by `pkg-config`
- `http://` / `https://` / `git@` — full URL
Flags:
@@ -29,7 +32,12 @@ Flags:
- `--tag <name>` — git tag
- `--rev <hash>` — git commit
If source is omitted — DCR searches connected registries.
If source is omitted — DCR searches connected registries. A bare relative or absolute path is accepted as a path dependency.
For `pkg-config` dependencies, the referenced packages must already be installed
and provide `.pc` files. DCR invokes `pkg-config` during the build (using
`PATH` and `PKG_CONFIG_PATH`) and forwards the returned compiler and linker
flags; it does not download or build these packages.
## `dcr tree`

View File

@@ -8,6 +8,7 @@ sidebar_label: Contributing
```bash
git clone https://github.com/dexoron/dcr
# or a source mirror: GitVerse / SourceCraft / SourceForge / GitLab (see README)
cd dcr
cargo build
```

View File

@@ -61,6 +61,15 @@ Use `kind = "flat-bin"` (NASM `-f bin`) and optional `[archive]` for a FAT disk
See [OS-dev recipe](/docs/recipes/os-flat-bin-archive).
## How do I choose a download mirror?
`dcrup install` uses `--mirror auto` by default and tries **cf → gh → sf → ya**.
```bash
dcrup install stable --mirror cf
export DCRUP_MIRROR=ya
```
## Lock file
`dcr.lock` is created during builds (`dcr build`) when registry dependencies are present. To force an update — delete `dcr.lock` and run `dcr build`.

View File

@@ -11,7 +11,7 @@ Install and switch DCR versions (stable / dev / night, pin `0.8.2`, optional bui
### Linux / macOS / BSD / Windows (bash)
```sh
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable
```
@@ -19,36 +19,29 @@ dcrup install stable
Non-interactive install of DCR in one shot:
```sh
curl -fsSL https://sh.dcr-tool.ru | sh -s -- install stable
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- install stable
export PATH="$HOME/.dcr/bin:$PATH"
```
### Windows (PowerShell)
```powershell
irm https://ps1.dcr-tool.ru | iex
# or download then run:
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
# powershell -File .\dcrup.ps1 self-install
& ([scriptblock]::Create((irm https://ps1.dcr-tool.ru))) self-install
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
dcrup install stable
```
Optional cmd bootstrap (if you prefer `curl` of the cmd shim):
```bat
curl -fsSL -o dcrup.cmd https://cmd.dcr-tool.ru
```
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows `dcrup.cmd`).
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`) and is installed in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows).
### Common dcrup commands
```sh
dcrup install stable # latest stable prebuilt
dcrup install stable # latest stable prebuilt (auto: cf → gh → sf → ya)
dcrup install 0.8.2 # pin → 0.8.2@stable
dcrup install 0.8.2@dev
dcrup install stable --libc musl # Linux: musl asset (default: gnu)
dcrup install stable --mirror cf # CloudFlare CDN
dcrup install stable --mirror ya # Yandex CDN (RU fallback)
dcrup install stable --build # cargo build --features archive
dcrup install night # always build from branch dev HEAD
dcrup default 0.8.2
@@ -56,10 +49,28 @@ dcrup update
dcrup list
dcrup show
dcrup which
dcrup uninstall 0.8.2 # remove one installed version
dcrup uninstall --all # remove all installed versions
```
Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version.
### Download mirrors
Prebuilt installs try **cf → gh → sf → ya** unless you pin a host.
| Code | URL | Notes |
|------|-----|--------|
| `cf` | `https://cdn-global.dcr-tool.ru/<tag>/<asset>` | CloudFlare CDN (worldwide) |
| `gh` | `https://github.com/dexoron/dcr/releases/download/<tag>/<asset>` | GitHub Releases |
| `sf` | `https://sourceforge.net/projects/dcr-tool/files/<tag>/<asset>/download` | SourceForge |
| `ya` | `https://cdn-ru.dcr-tool.ru/<tag>/<asset>` | Yandex CDN (RU fallback) |
```sh
dcrup install stable --mirror auto # default
export DCRUP_MIRROR=cf # persist for this shell
```
---
## Arch Linux (AUR)

View File

@@ -6,6 +6,13 @@ sidebar_label: IDE Integration
## VS Code
Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr). Do not install Microsoft C/C++ extensions for DCR projects: DCR uses `clangd`. MS C/C++ is acceptable only when you need its debugger.
Recommended companion extensions:
- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml)
- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only
```bash
dcr gen vscode
```

View File

@@ -111,7 +111,11 @@ Never fails the command — warnings only. Pre-release suffixes (`1.0.0-dev`) ar
| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) |
Settings from raw config (not in typed struct):
- `pkg_config` — list of pkg-config packages
- `pkg_config` — list of packages queried for build-wide compiler/linker flags
Dependencies can also declare a system package directly, for example
`fmt = { pkg-config = "fmt" }`. This records the package in `dcr.lock` and applies
the flags returned by `pkg-config --cflags` and `pkg-config --libs`.
- `ldscript` — linker script path
- `build.steps` / `build.post_steps` — codegen steps

View File

@@ -6,7 +6,7 @@ sidebar_label: Dependencies
## Formats
The `[dependencies]` section supports three formats:
The `[dependencies]` section supports registry, path, Git, and `pkg-config` dependencies:
### String (registry)
@@ -23,10 +23,10 @@ The version string is used as-is for registry lookup.
```toml
[dependencies]
fmt = { git = "https://github.com/fmtlib/fmt", tag = "10.1.1" }
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0" }
```
Fields: `git`, `branch`, `tag`, `rev`.
Fields: `git`, `branch`, `tag`, `rev`, `mode`, `include`, `lib`, `libs`.
### Table (path)
@@ -35,6 +35,32 @@ Fields: `git`, `branch`, `tag`, `rev`.
mylib = { path = "../mylib" }
```
For a prebuilt external library, specify its layout explicitly. The three
arrays are paths relative to the dependency root (`include`, `lib`) and linker
names (`libs`):
```toml
[dependencies]
dcr-math = { path = "../deps/dcr-math", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
```
### Table (`pkg-config`)
Use an installed `.pc` package when the library is provided by the operating system
or another system package manager. DCR invokes `pkg-config --cflags` and
`pkg-config --libs`, preserving include, library, linker, and other flags:
```toml
[dependencies]
fmt = { pkg-config = "fmt" }
openssl = { pkg-config = ["openssl", "libcrypto"] }
```
The `pkg-config` executable must be available in `PATH`. The named `.pc` files must
be available through pkg-config's standard search paths or via `PKG_CONFIG_PATH`.
These dependencies are not downloaded or built by DCR; they are recorded in
`dcr.lock` with a `pkg-config+...` source.
## Registry
DCR uses a package registry for dependency lookup by name.
@@ -42,7 +68,7 @@ DCR uses a package registry for dependency lookup by name.
```toml
# ~/.dcr/config.toml
[registry.default]
url = "https://index.dcr.pm"
url = "https://index.dcr.dexoron.tech"
priority = 1
```
@@ -50,7 +76,23 @@ Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrid
## Git dependencies
Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying branch/tag/rev for git sources:
Git dependencies are cloned into `~/.dcr/git/`; their exact checked-out commit is written to `dcr.lock`.
Both Git and path dependencies have two modes:
- **DCR package:** the source has `dcr.toml`. DCR builds it before the consumer and uses the package name from its manifest for linking.
- **Prebuilt library:** it provides existing headers and binaries through `include`, `lib`, and `libs`. DCR passes them to the compiler/linker without building the dependency.
The mode is normally detected automatically. Use `mode = "build"` or `mode = "prebuild"` when a source contains both a manifest and prebuilt artifacts and you need to choose explicitly.
`dcr-math-lib` publishes both forms. Select its prebuilt library explicitly:
```toml
[dependencies]
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0", mode = "prebuild", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
```
DCR supports specifying branch/tag/rev for git sources:
- `branch` — switch to a branch
- `tag` — switch to a tag
@@ -59,7 +101,7 @@ Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying
## Path dependencies
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.
Local paths may point at a DCR package or a prebuilt external library. A path DCR package is built before its consumer, including its own path/git dependencies. DCR detects dependency cycles. A prebuilt library is never built by DCR; it is linked from the directories declared in its dependency entry.
```toml
[dependencies]
@@ -67,18 +109,51 @@ mylib = { path = "/abs/path/to/lib" }
mylib = { path = "../relative/path" }
```
*Note: Header resolution automatically checks the packaged `target/include` directory of path dependencies, ensuring consumer packages can find headers from compiled static or dynamic libraries.*
For DCR packages, the package name from `dcr.toml` is used for linking (so the dependency key may differ from the library name). Header resolution includes the packaged `target/include` directory and link search includes `target/lib`.
### Recommended library layouts
Buildable DCR library:
```text
dcr-math-lib/
├── dcr.toml
└── src/
├── dcr_math.c
└── dcr_math.h
```
Prebuilt library:
```text
dcr-math-lib/
├── include/
│ └── dcr_math.h
└── lib/
└── libdcr-math.a # or .so / .lib
```
`libs` contains linker names without the `lib` prefix and file extension: use
`["dcr-math"]` for `libdcr-math.a`.
If one repository contains both forms, keep the prebuilt files in `include/` and `lib/` and select the mode explicitly:
```toml
[dependencies]
dcr-math = { git = "https://github.com/dexoron/dcr-math-lib.git", tag = "v0.1.0", mode = "prebuild", include = ["include"], lib = ["lib"], libs = ["dcr-math"] }
```
For a local copy, replace `git` with `path = "../dcr-math-lib"`. In `prebuild` mode DCR does not run the dependency build, even when `dcr.toml` is present.
## dcr.lock
Dependency lock file. Contains package names and sources.
Created during `dcr build` when registry dependencies are present. Not updated during `dcr add` — only on the next `dcr build`.
Created during `dcr build` for resolved registry, path, and Git dependencies. Not updated during `dcr add` — only on the next `dcr build`.
## Resolution process
1. Load all dependencies (registry → git → path)
2. For path deps: recursively read their `dcr.toml`
3. For registry deps: search `index.json`
4. For git deps: clone to cache
5. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler
1. Resolve dependency sources (registry → git → path)
2. Fetch git dependencies into `~/.dcr/git/` and check out the requested ref
3. Build DCR package dependencies recursively
4. Collect `include_dirs`, `lib_dirs`, `libs` for the compiler

View File

@@ -57,3 +57,27 @@ export DCR_INDEX_PATH=/custom/path/index.json
```
Default: `~/.dcr/index.json`.
## DCRUP_MIRROR
Download mirror for `dcrup` (prebuilt binaries only).
```bash
export DCRUP_MIRROR=auto # cf → gh → sf → ya (default)
export DCRUP_MIRROR=cf # CloudFlare CDN
export DCRUP_MIRROR=gh # GitHub Releases
export DCRUP_MIRROR=sf # SourceForge
export DCRUP_MIRROR=ya # Yandex CDN (RU fallback)
```
CLI override: `dcrup install stable --mirror cf`.
## DCRUP_HOME / DCRUP_BIN / DCRUP_REPO
Installer layout and source clone URL:
```bash
export DCRUP_HOME=~/.dcr
export DCRUP_BIN=~/.dcr/bin
export DCRUP_REPO=https://github.com/dexoron/dcr
```

View File

@@ -1,160 +0,0 @@
$ErrorActionPreference = 'Stop'
$RepoUrl = 'https://github.com/dexoron/dcr'
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
$InstallPath = Join-Path $env:LOCALAPPDATA 'dcr'
$BinPath = Join-Path $env:USERPROFILE '.local\bin'
$BinaryPath = Join-Path $InstallPath 'dcr.exe'
function Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Fail($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Require-Cmd($name) {
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
Fail "Not found: $name"
}
}
function Select-Channel {
Write-Host "Choose channel:"
Write-Host " 1) Latest stable release (default)"
Write-Host " 2) Latest dev (pre-release)"
$choice = Read-Host "Enter 1 or 2 [1]"
if ([string]::IsNullOrWhiteSpace($choice)) { $choice = '1' }
switch ($choice) {
'1' { return 'stable' }
'2' { return 'dev' }
default { Fail 'Unknown option' }
}
}
function Select-Mode {
Write-Host "Choose installation mode:"
Write-Host " 1) Download prebuilt binary from GitHub Release (recommended)"
Write-Host " 2) Build from git"
$choice = Read-Host "Enter 1 or 2 [1]"
if ([string]::IsNullOrWhiteSpace($choice)) { $choice = '1' }
switch ($choice) {
'1' { return 'release' }
'2' { return 'build' }
default { Fail 'Unknown option' }
}
}
function Get-Target {
$arch = $env:PROCESSOR_ARCHITECTURE
if ($env:PROCESSOR_ARCHITEW6432) {
$arch = $env:PROCESSOR_ARCHITEW6432
}
switch ($arch) {
'AMD64' { return 'x86_64-pc-windows-msvc' }
'ARM64' { return 'aarch64-pc-windows-msvc' }
'x86' { return 'i686-pc-windows-msvc' }
default { Fail "Unsupported Windows architecture: $arch" }
}
}
function Fetch-ReleaseJson($channel) {
if ($channel -eq 'dev') {
Info "Looking for latest dev (pre-release)..."
$releases = Invoke-RestMethod -Method Get -Uri $ApiAll
$pre = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1
if (-not $pre) { Fail "No dev (pre-release) found on GitHub" }
return $pre
} else {
return Invoke-RestMethod -Method Get -Uri $ApiLatest
}
}
function Install-FromRelease {
param([string]$target, [string]$channel)
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
$release = Fetch-ReleaseJson $channel
$tag = $release.tag_name
$version = $tag.TrimStart('v')
# Имя бинарника: dcr-<triple>-<version>.exe
$assetName = "dcr-$target-$version.exe"
Info "Fetching release $tag (channel: $channel)..."
$asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
if (-not $asset) {
Fail "Asset $assetName not found in release $tag"
}
Info "Downloading $assetName..."
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $BinaryPath
Ok "Binary downloaded: $assetName"
}
function Install-FromSource {
param([string]$target, [string]$channel)
Require-Cmd 'git'
Require-Cmd 'cargo'
$tmp = Join-Path $env:TEMP 'dcr-install'
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
Info 'Cloning repository...'
if ($channel -eq 'dev') {
# Пробуем ветку dev
$result = git clone --depth 1 --branch dev $RepoUrl $tmp 2>&1
if ($LASTEXITCODE -ne 0) {
git clone --depth 1 $RepoUrl $tmp | Out-Null
}
} else {
git clone --depth 1 $RepoUrl $tmp | Out-Null
}
Info 'Building release binary...'
Push-Location $tmp
cargo build --release --target $target
Pop-Location
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
Copy-Item (Join-Path $tmp "target\$target\release\dcr.exe") $BinaryPath -Force
Ok 'Binary installed from source'
Remove-Item -Recurse -Force $tmp
}
function Setup-Path {
New-Item -ItemType Directory -Path $BinPath -Force | Out-Null
$linkPath = Join-Path $BinPath 'dcr.exe'
Copy-Item $BinaryPath $linkPath -Force
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not $userPath) { $userPath = '' }
if (-not ($userPath -split ';' | Where-Object { $_ -eq $BinPath })) {
$newPath = if ($userPath) { "$userPath;$BinPath" } else { $BinPath }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
$env:Path = "$env:Path;$BinPath"
Warn "Added path to PATH: $BinPath"
}
Ok "dcr command is available via $linkPath"
}
Info 'Starting DCR installation'
$channel = Select-Channel
$mode = Select-Mode
$target = Get-Target
if ($mode -eq 'build') {
Install-FromSource -target $target -channel $channel
} else {
Install-FromRelease -target $target -channel $channel
}
Setup-Path
Ok 'Installation completed successfully'

View File

@@ -1,253 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
TMPDIR="/tmp/dcr-install"
INSTALL_PATH="$HOME/.local/share/dcr"
BINPATH="$HOME/.local/bin"
MANPATH="${XDG_DATA_HOME:-$HOME/.local/share}/man"
LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr"
GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
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")"
exec > >(tee -a "$LOGFILE") 2>&1
log() { echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"; }
success() { echo -e "${GREEN}✔ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
error() { echo -e "${RED}✖ $1${NC}"; }
trap 'error "Error on line $LINENO"; exit 1' ERR
INSTALL_MODE=""
CHANNEL=""
check_os() {
case "$(uname -s)" in
Linux|Darwin|FreeBSD|OpenBSD|NetBSD) ;;
*) error "Only Linux, macOS, and BSD systems are supported"; exit 1 ;;
esac
}
detect_target() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os:$arch" in
Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;;
Linux:aarch64|Linux:arm64) TARGET_TRIPLE="aarch64-unknown-linux-gnu" ;;
Linux:i686|Linux:i386) TARGET_TRIPLE="i686-unknown-linux-gnu" ;;
Linux:armv7l|Linux:armv7) TARGET_TRIPLE="armv7-unknown-linux-gnueabihf" ;;
Linux:riscv64) TARGET_TRIPLE="riscv64gc-unknown-linux-gnu" ;;
Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;;
Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;;
FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;;
FreeBSD:aarch64|FreeBSD:arm64) TARGET_TRIPLE="aarch64-unknown-freebsd" ;;
FreeBSD:i686|FreeBSD:i386) TARGET_TRIPLE="i686-unknown-freebsd" ;;
OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;;
OpenBSD:aarch64|OpenBSD:arm64) TARGET_TRIPLE="aarch64-unknown-openbsd" ;;
OpenBSD:i686|OpenBSD:i386) TARGET_TRIPLE="i686-unknown-openbsd" ;;
NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;;
NetBSD:aarch64|NetBSD:arm64) TARGET_TRIPLE="aarch64-unknown-netbsd" ;;
NetBSD:i686|NetBSD:i386) TARGET_TRIPLE="i686-unknown-netbsd" ;;
*) error "Unsupported platform: $os/$arch"; exit 1 ;;
esac
}
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)"
read -r -p "Enter 1 or 2 [1]: " choice < /dev/tty
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"
read -r -p "Enter 1 or 2 [1]: " choice < /dev/tty
case "${choice:-1}" in
1) INSTALL_MODE="release" ;;
2) INSTALL_MODE="build" ;;
*) error "Unknown option"; exit 1 ;;
esac
}
# Returns JSON of the requested release to stdout
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 without leading 'v'
version="${tag#v}"
# Binary name: dcr-<triple>-<version>
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
# Clone dev branch if it exists
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"
}
install_man() {
log "Installing man pages..."
local man_src="$TMPDIR/man/man1"
if [[ -d "$man_src" ]]; then
mkdir -p "$MANPATH/man1"
cp "$man_src"/*.1 "$MANPATH/man1/"
success "Man pages installed to $MANPATH/man1"
fi
}
check_path() {
if ! echo "$PATH" | grep -q "$BINPATH"; then
warn "Directory $BINPATH not found in PATH"
echo "Add this to ~/.bashrc or ~/.zshrc:"
echo "export PATH=\"$BINPATH:\$PATH\""
fi
}
cleanup() {
rm -rf "$TMPDIR" 2>/dev/null || true
}
main() {
log "Starting DCR installation"
check_os
detect_target
select_channel
check_common_dependencies
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
install_man
check_path
cleanup
success "Installation completed successfully"
log "Log saved to $LOGFILE"
}
main "$@"

View File

@@ -1,241 +0,0 @@
#!/bin/sh
set -eu
TMPDIR="/tmp/dcr-install"
INSTALL_PATH="$HOME/.local/share/dcr"
BINPATH="$HOME/.local/bin"
MANPATH="${XDG_DATA_HOME:-$HOME/.local/share}/man"
LOGFILE="$HOME/.cache/dcr-install.log"
REPO_URL="https://github.com/dexoron/dcr"
GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
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" ;;
FreeBSD:aarch64|FreeBSD:arm64) TARGET_TRIPLE="aarch64-unknown-freebsd" ;;
FreeBSD:i686|FreeBSD:i386) TARGET_TRIPLE="i686-unknown-freebsd" ;;
OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;;
OpenBSD:aarch64|OpenBSD:arm64) TARGET_TRIPLE="aarch64-unknown-openbsd" ;;
OpenBSD:i686|OpenBSD:i386) TARGET_TRIPLE="i686-unknown-openbsd" ;;
NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;;
NetBSD:aarch64|NetBSD:arm64) TARGET_TRIPLE="aarch64-unknown-netbsd" ;;
NetBSD:i686|NetBSD:i386) TARGET_TRIPLE="i686-unknown-netbsd" ;;
DragonFly:x86_64|DragonFly:amd64) TARGET_TRIPLE="x86_64-unknown-dragonfly" ;;
*) error "Unsupported BSD platform: $os/$arch"; exit 1 ;;
esac
}
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"
}
install_man() {
log "Installing man pages..."
man_src="$TMPDIR/man/man1"
if [ -d "$man_src" ]; then
mkdir -p "$MANPATH/man1"
cp "$man_src"/*.1 "$MANPATH/man1/"
success "Man pages installed to $MANPATH/man1"
fi
}
check_path() {
if ! echo "$PATH" | grep -q "$BINPATH"; then
warn "Directory $BINPATH not found in PATH"
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
install_man
check_path
cleanup
success "Installation completed successfully"
log "Log saved to $LOGFILE"
}
main "$@"

View File

@@ -25,6 +25,9 @@ Generic git repository.
.B path:./path/to/lib
Local path dependency.
.TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:\fR or comma-separated packages).
.TP
.B <version>
Version from registry.
.SH OPTIONS

View File

@@ -1,4 +1,4 @@
.TH DCR 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
.TH DCR 1 "2026-08-31" "dcr 0.9.1" "DCR Manual"
.SH NAME
dcr \- Cargo\-like build tool and package manager for C/C++ projects
.SH SYNOPSIS
@@ -120,6 +120,9 @@ Generic git repository.
.B path:./path/to/lib
Local path dependency.
.TP
.B pkg-config:name
System pkg-config dependency (also accepts \fBpkg:name1,name2\fR).
.TP
.B \fI<version>\fR
Version from registry (e.g. \fB1.2.3\fR).
.SH GEN SUBCOMMANDS

View File

@@ -1,192 +0,0 @@
#!/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

@@ -15,17 +15,17 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build_config::Config;
use crate::core::deps::register;
use crate::prelude::*;
use crate::core::{build_config::Config, deps::register};
use crate::utils::cli_styles::{Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use toml::Value;
use toml::map::Map;
use crate::utils::log::sprintln;
use toml::{Value, map::Map};
/// Parsed arguments for the `dcr add` command.
///
/// Exactly one of `path`, `git`, or `version_from_registry` is expected to be set,
/// Exactly one of `path`, `git`, `pkg_config`, or `version_from_registry` is expected to be set,
/// depending on how the dependency source was specified.
pub struct AddArgs {
/// Dependency name as it will appear under `[dependencies]` in `dcr.toml`.
@@ -42,6 +42,8 @@ pub struct AddArgs {
pub rev: Option<String>,
/// Version string resolved from the package registry when no explicit source is given.
pub version_from_registry: Option<String>,
/// Package names resolved through pkg-config.
pub pkg_config: Option<Vec<String>>,
}
/// Adds a dependency to the current project's `dcr.toml`.
@@ -57,23 +59,24 @@ pub struct AddArgs {
/// Process exit code: `0` on success, non-zero on usage or I/O errors.
pub fn add(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
BOLD_CYAN,
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(
HELP_EXAMPLES_ST,
" dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]"
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Adds a dependency to the project.");
println!();
printc("SOURCES:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "SOURCES:");
println!(" github:user/repo GitHub repository");
println!(" gitlab:user/repo GitLab repository");
println!(" git:host.com/user/repo Generic git repository");
println!(" path:./path/to/lib Local path dependency");
println!(" pkg-config:fmt System pkg-config dependency");
println!(" <version> Version from registry");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --branch <b> Use a specific branch");
println!(" --tag <t> Use a specific tag");
println!(" --rev <r> Use a specific commit hash");
@@ -88,7 +91,7 @@ pub fn add(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
@@ -96,11 +99,11 @@ pub fn add(args: &[String]) -> i32 {
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -108,7 +111,7 @@ pub fn add(args: &[String]) -> i32 {
let mut config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(cfg) => cfg,
Err(err) => {
error(&err.to_string());
error!("{err}");
return 1;
}
};
@@ -133,6 +136,17 @@ pub fn add(args: &[String]) -> i32 {
}
Value::Table(table)
}
} else if let Some(packages) = add_args.pkg_config {
let mut table = Map::new();
if packages.len() == 1 {
table.insert("pkg-config".to_string(), Value::String(packages[0].clone()));
} else {
table.insert(
"pkg-config".to_string(),
Value::Array(packages.into_iter().map(Value::String).collect()),
);
}
Value::Table(table)
} else if let Some(path) = add_args.path {
let mut table = Map::new();
table.insert("path".to_string(), Value::String(path));
@@ -140,19 +154,19 @@ pub fn add(args: &[String]) -> i32 {
} else if let Some(version) = add_args.version_from_registry {
Value::String(version)
} else {
error("Dependency source (path or git) must be provided");
error!("Dependency source (path, git, or pkg-config) must be provided");
return 1;
};
let key = format!("dependencies.{}", add_args.name);
if let Err(err) = config.edit(&key, dep_value) {
error(&format!("Failed to update dcr.toml: {}", err));
error!("Failed to update dcr.toml: {err}");
return 1;
}
println!(
" {} dependency `{}` to dcr.toml",
colored("Added", BOLD_GREEN),
"Added".style(SUCCESS_ST),
add_args.name
);
0
@@ -165,12 +179,13 @@ pub fn add(args: &[String]) -> i32 {
/// `path:`, `github:`, `gitlab:`, or `git:` prefixes, or a full git URL.
fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if args.is_empty() {
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error("Sources:");
error(" github:user/repo -> github.com/user/repo");
error(" gitlab:user/repo -> gitlab.com/user/repo");
error(" git:host.com/user/repo -> host.com/user/repo");
error(" path:./path/to/lib -> local path");
error!("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
error!("Sources:");
error!(" github:user/repo -> github.com/user/repo");
error!(" gitlab:user/repo -> gitlab.com/user/repo");
error!(" git:host.com/user/repo -> host.com/user/repo");
error!(" path:./path/to/lib -> local path");
error!(" pkg-config:fmt -> system pkg-config package");
return Err(1);
}
@@ -183,7 +198,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
let mut iter = args.iter();
if let Some(n) = iter.next() {
if n.starts_with("--") {
error("First argument must be the dependency name");
error!("First argument must be the dependency name");
return Err(1);
}
name = n.clone();
@@ -191,7 +206,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
if let Some(s) = iter.next() {
if s.starts_with("--") {
error("Second argument must be the dependency source");
error!("Second argument must be the dependency source");
return Err(1);
}
source_spec = s.clone();
@@ -210,7 +225,7 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
)
})
.map_err(|(msg, code)| {
error(&msg);
error!("{msg}");
code
})?;
return Ok(AddArgs {
@@ -221,10 +236,11 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag: None,
rev: None,
version_from_registry: Some(version.to_string()),
pkg_config: None,
});
}
Err(e) => {
error(&format!("Cannot add `{}` without source: {}", name, e));
error!("Cannot add `{}` without source: {}", name, e);
return Err(1);
}
}
@@ -235,26 +251,26 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
"--branch" => {
branch = iter.next().cloned();
if branch.is_none() {
error("--branch requires a value");
error!("--branch requires a value");
return Err(1);
}
}
"--tag" => {
tag = iter.next().cloned();
if tag.is_none() {
error("--tag requires a value");
error!("--tag requires a value");
return Err(1);
}
}
"--rev" => {
rev = iter.next().cloned();
if rev.is_none() {
error("--rev requires a value");
error!("--rev requires a value");
return Err(1);
}
}
_ => {
error(&format!("Unknown argument: {}", arg));
error!("Unknown argument: {arg}");
return Err(1);
}
}
@@ -262,9 +278,25 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
let mut path = None;
let mut git = None;
let mut pkg_config = None;
// Map source prefixes / bare URLs to path or git fields
if let Some(p) = source_spec.strip_prefix("path:") {
if let Some(p) = source_spec
.strip_prefix("pkg-config:")
.or_else(|| source_spec.strip_prefix("pkg:"))
{
let packages = p
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string)
.collect::<Vec<_>>();
if packages.is_empty() {
error!("pkg-config source must name at least one package");
return Err(1);
}
pkg_config = Some(packages);
} else if let Some(p) = source_spec.strip_prefix("path:") {
path = Some(p.to_string());
} else if let Some(g) = source_spec.strip_prefix("github:") {
git = Some(format!("https://github.com/{}", g));
@@ -288,8 +320,15 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|| source_spec.starts_with("git@")
{
git = Some(source_spec);
} else if source_spec.starts_with("./")
|| source_spec.starts_with("../")
|| std::path::Path::new(&source_spec).is_absolute()
{
path = Some(source_spec);
} else {
error("Source must have a prefix (path:, git:, github:, gitlab:) or be a full URL");
error!(
"Source must be a path, pkg-config:, a prefixed source (path:, git:, github:, gitlab:), or a full URL",
);
return Err(1);
}
@@ -301,5 +340,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
tag,
rev,
version_from_registry: None,
pkg_config,
})
}

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::cli::clean::clean;
use crate::cli::flags::parse_build_run_flags;
use crate::cli::r#gen::{ProjectInfo, write_dcr_metadata};
@@ -26,9 +28,11 @@ use crate::utils::build::{
get_config_opt, get_config_str, get_language_with_profile_or_default, get_string_with_profile,
normalize_kind, resolve_artifact_target_dir, resolve_compiler,
};
use crate::utils::cli_styles::{
BOLD_CYAN, BOLD_YELLOW, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST, Style,
};
use crate::utils::fs::{canonicalize_path, find_project_root};
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use crate::utils::log::sprintln;
use std::io::IsTerminal;
use std::path::Path;
use std::sync::{Arc, atomic::AtomicBool};
@@ -36,8 +40,8 @@ use std::sync::{Arc, atomic::AtomicBool};
pub use crate::core::build::get_build_string_with_profile;
/// Formats a left-aligned status verb with the given style for CLI output.
fn status(verb: &str, style: &str) -> String {
colored(&format!("{verb:<9}"), style)
fn status(verb: &str, style: Style) -> String {
format!("{verb:<9}").style(style).to_string()
}
/// CLI build reporter that prints human-readable progress to stderr/stdout.
@@ -50,7 +54,7 @@ impl CliReporter {
/// - `verb`: Short status label (e.g. `"target"`, `"dep"`).
/// - `style`: Color/style constant for the verb.
/// - `rest`: Remaining message text after the verb.
fn line(&self, verb: &str, style: &str, rest: &str) {
fn line(&self, verb: &str, style: Style, rest: &str) {
eprintln!(" {} {}", status(verb, style), rest);
}
}
@@ -94,12 +98,12 @@ impl BuildReporter for CliReporter {
common::finish_progress_line();
// Only announce deps that were actually rebuilt this run.
if rebuilt {
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
self.line("ready", SUCCESS_ST, &format!("{name} v{version}"));
}
}
BuildEvent::Compiling { name, version } => {
common::finish_progress_line();
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
let label = format!(" {} {} v{}", status("compile", SUCCESS_ST), name, version);
// Terminals use an in-place progress line; non-TTY gets a plain log line.
common::set_progress_label(Some(label.clone()));
if !std::io::stderr().is_terminal() {
@@ -112,7 +116,7 @@ impl BuildReporter for CliReporter {
}
BuildEvent::Finished { secs } => {
common::finish_progress_line();
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
self.line("done", SUCCESS_ST, &format!("in {secs}s"));
}
BuildEvent::CompilerOutput { stream, text } => {
// Pause the progress spinner so compiler text is not overwritten.
@@ -140,16 +144,16 @@ impl BuildReporter for CliReporter {
/// Process exit code: `0` on success or help, non-zero on failure.
pub fn build(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [--print-artifact-path]",
BOLD_CYAN,
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(
HELP_EXAMPLES_ST,
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [--print-artifact-path]"
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Compiles the project. Default profile is --debug.");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --debug Build with debug profile (default)");
println!(" --release Build with release profile");
println!(" --target <triple> Cross-compile for the given target");
@@ -173,18 +177,18 @@ pub fn build(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => canonicalize_path(&dir),
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -242,7 +246,7 @@ pub fn build(args: &[String]) -> i32 {
}
Err(err) => {
common::finish_progress_line();
error(&err.message);
error!("{}", err.message);
1
}
}

View File

@@ -15,15 +15,18 @@
// 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::prelude::*;
use crate::core::build_config::Config;
use crate::core::workspace::parse_workspace;
use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
use crate::utils::cli_styles::{
BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
};
use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use crate::utils::log::sprintln;
use glob::glob;
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Entry point for the `dcr clean` command.
///
@@ -36,16 +39,16 @@ use std::path::Path;
/// Process exit code: `0` on success, `1` on failure.
pub fn clean(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr clean [--debug | --release] [--target <triple>] [--all]",
BOLD_CYAN,
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(
HELP_EXAMPLES_ST,
" dcr clean [--debug | --release] [--target <triple>] [--all]"
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Removes build artifacts from the target directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --debug Clean debug artifacts (default)");
println!(" --release Clean release artifacts");
println!(" --target <triple> Clean artifacts for a specific target");
@@ -56,25 +59,25 @@ pub fn clean(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
let flags = match parse_clean_flags(args) {
Ok(v) => v,
Err(msg) => {
error(&msg);
error!("{msg}");
return 1;
}
};
@@ -83,7 +86,7 @@ pub fn clean(args: &[String]) -> i32 {
match with_dir(&root, || clean_from_root(&root, &flags)) {
Ok(()) => 0,
Err(msg) => {
error(&msg);
error!("{msg}");
1
}
}
@@ -210,10 +213,7 @@ fn clean_project_at(
if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string());
}
println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
println!(" Cleaning project `{}`", project_name.style(BOLD_GREEN));
if let Some(profile) = profile {
let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}")
@@ -229,16 +229,16 @@ fn clean_project_at(
target_items.contains(&profile.to_string())
};
if !dir_exists {
warn(&format!("Directory target/{} not found", target_dir));
warn!("Directory target/{target_dir} not found");
} else {
println!(" Profile: {}", colored(profile, BOLD_GREEN));
println!(" Profile: {}", profile.style(BOLD_GREEN));
if let Some(t) = target {
println!(" Target: {}", colored(t, BOLD_GREEN));
println!(" Target: {}", t.style(BOLD_GREEN));
}
let _ = fs::remove_dir_all(&target_dir);
println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
"\n ✔".style(SUCCESS_ST),
target_dir
);
}
@@ -248,12 +248,9 @@ fn clean_project_at(
if items.contains(&"target".to_string()) {
let _ = fs::remove_dir_all("target");
println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
println!("{} Removed directory target", "\n ✔".style(SUCCESS_ST));
} else {
warn("Directory target not found");
warn!("Directory target not found");
}
clean_custom_paths(&config, "debug")?;
clean_custom_paths(&config, "release")?;

View File

@@ -15,8 +15,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use crate::prelude::*;
use crate::utils::cli_styles::{HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
use crate::utils::log::sprintln;
use serde::Deserialize;
use std::fs;
use std::io::Read;
@@ -55,30 +57,30 @@ struct ReleaseAsset {
/// `0` on success / already up to date / pacman-managed refuse; `1` on error.
pub fn flag_update(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr --update", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr --update");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Updates DCR to the latest version from GitHub Releases.");
return 0;
}
if !args.is_empty() {
warn("Command does not support additional arguments");
warn!("Command does not support additional arguments");
return 1;
}
let current_exe = match std::env::current_exe() {
Ok(path) => path,
Err(_) => {
error("Failed to resolve current binary path");
error!("Failed to resolve current binary path");
return 1;
}
};
// Refuse self-update when the binary is owned by pacman/AUR.
if let Some(package_name) = pacman_owned_package(&current_exe) {
warn("This dcr binary is managed by pacman/AUR");
warn!("This dcr binary is managed by pacman/AUR");
println!(
"Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
);
@@ -91,7 +93,7 @@ pub fn flag_update(args: &[String]) -> i32 {
let release = match fetch_latest_release() {
Ok(release) => release,
Err(err) => {
error(&format!("Failed to check for updates: {err}"));
error!("Failed to check for updates: {err}");
return 1;
}
};
@@ -109,14 +111,14 @@ pub fn flag_update(args: &[String]) -> i32 {
.iter()
.find(|asset| candidate_names.iter().any(|name| name == &asset.name))
else {
error(&format!("Binary for target {target} not found"));
error!("Binary for target {target} not found");
return 1;
};
let bytes = match download_asset(&asset.browser_download_url) {
Ok(bytes) => bytes,
Err(err) => {
error(&format!("Failed to download update: {err}"));
error!("Failed to download update: {err}");
return 1;
}
};
@@ -124,7 +126,7 @@ pub fn flag_update(args: &[String]) -> i32 {
let temp_path = temp_binary_path(&current_exe);
if fs::write(&temp_path, &bytes).is_err() {
error("Failed to write temporary binary");
error!("Failed to write temporary binary");
return 1;
}
set_executable_permissions(&temp_path);
@@ -132,7 +134,7 @@ pub fn flag_update(args: &[String]) -> i32 {
// Replace the running executable in place, then clean up the temp file.
if self_replace::self_replace(&temp_path).is_err() {
let _ = fs::remove_file(&temp_path);
error("Failed to replace current binary");
error!("Failed to replace current binary");
return 1;
}

View File

@@ -15,8 +15,9 @@
// 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::prelude::*;
use crate::config::{PROFILE, flags};
use crate::utils::log::warn;
/// Flags shared by the `build` and `run` CLI commands.
pub struct BuildRunFlags {
@@ -82,7 +83,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
while let Some(arg) = iter.next() {
// DCR build/run options are long flags only (`--name`).
if !arg.starts_with("--") {
warn("Unknown argument");
warn!("Unknown argument");
return Err(1);
}
let candidate = arg.trim_start_matches("--");
@@ -106,7 +107,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
if let Some(w) = iter.next() {
workspace = Some(w.clone());
} else {
warn("--workspace requires a value");
warn!("--workspace requires a value");
return Err(1);
}
continue;
@@ -115,7 +116,7 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
if let Some(t) = iter.next() {
target = Some(t.clone());
} else {
warn("--target requires a value");
warn!("--target requires a value");
return Err(1);
}
continue;
@@ -123,13 +124,13 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
// Profile names come from config::flags; only one profile may be set.
if flags(candidate).is_some() {
if profile != PROFILE {
warn("Duplicate profile flag");
warn!("Duplicate profile flag");
return Err(1);
}
profile = candidate.to_string();
continue;
}
warn("Unknown build flag");
warn!("Unknown build flag");
return Err(1);
}

View File

@@ -15,8 +15,12 @@
// 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_CYAN, BOLD_GREEN, colored, printc};
use crate::prelude::*;
use crate::utils::cli_styles::{
BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
};
use crate::utils::log::sprintln;
use glob::glob;
use std::process::Command;
@@ -28,10 +32,10 @@ use std::process::Command;
/// If the first argument is `--help`, prints usage information and returns 0.
pub fn fmt(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr fmt", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr fmt");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Formats all C/C++ source files using clang-format.");
println!(" Scans src/ and tests/ directories recursively.");
return 0;
@@ -69,14 +73,14 @@ pub fn fmt(args: &[String]) -> i32 {
}
if files.is_empty() {
println!(" {} No files to format", colored("Format", BOLD_GREEN));
println!(" {} No files to format", "Format".style(BOLD_GREEN));
return 0;
}
// Format the collected files
println!(
" {} {} files",
colored("Formatting", BOLD_GREEN),
"Formatting".style(BOLD_GREEN),
files.len()
);
@@ -84,15 +88,15 @@ pub fn fmt(args: &[String]) -> i32 {
match status {
Ok(s) if s.success() => {
println!(" {} successful", colored("Format", BOLD_GREEN));
println!(" {} successful", "Format".style(SUCCESS_ST));
0
}
Ok(s) => {
error(&format!("clang-format failed with status: {}", s));
error!("clang-format failed with status: {s}");
1
}
Err(e) => {
error(&format!("Failed to execute clang-format: {}", e));
error!("Failed to execute clang-format: {e}");
1
}
}

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact::{absolute_artifact_path, resolve_artifact_path};
use crate::core::build::builder::collect_sources;
@@ -26,11 +28,11 @@ use crate::utils::build::{
get_list_with_profile, get_string_with_profile, normalize_kind, normalize_platform,
resolve_artifact_target_dir, resolve_compiler, resolve_pkg_config_flags_lossy,
};
use crate::utils::cli_styles::{HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
use crate::utils::fs::{
absolute_join, atomic_write, canonicalize_path, ensure_dcr_dir, find_project_root,
};
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use crate::utils::log::sprintln;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -42,11 +44,13 @@ struct GenDeps {
include_dirs: Vec<String>,
lib_dirs: Vec<String>,
libs: Vec<String>,
cflags: Vec<String>,
ldflags: Vec<String>,
}
/// Collects path-dependency include/lib/libs from `dcr.toml` for code generation.
/// Collects dependency metadata from `dcr.toml` for code generation.
///
/// Skips `system` deps and non-path entries. Falls back to common include/lib
/// Resolves path layouts and pkg-config flags while skipping system deps. Falls back to common include/lib
/// directory names only when those directories exist.
fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps {
let deps_val = match config.get("dependencies") {
@@ -56,6 +60,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs: vec![],
lib_dirs: vec![],
libs: vec![],
cflags: vec![],
ldflags: vec![],
};
}
};
@@ -66,6 +72,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs: vec![],
lib_dirs: vec![],
libs: vec![],
cflags: vec![],
ldflags: vec![],
};
}
};
@@ -73,12 +81,37 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
let mut include_dirs = Vec::new();
let mut lib_dirs = Vec::new();
let mut libs = Vec::new();
let mut cflags = Vec::new();
let mut ldflags = Vec::new();
for (name, value) in deps_table {
let tbl = match value.as_table() {
Some(t) => t,
None => continue,
};
let pkg_names = tbl
.get("pkg-config")
.or_else(|| tbl.get("pkg_config"))
.map(|raw| {
raw.as_str()
.map(|s| vec![s.to_string()])
.or_else(|| {
raw.as_array().map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
})
.unwrap_or_default()
})
.unwrap_or_default();
if !pkg_names.is_empty() {
let (flags, link_flags) =
crate::utils::build::resolve_pkg_config_flags_lossy(&pkg_names, &[], &[]);
cflags.extend(flags);
ldflags.extend(link_flags);
continue;
}
// skip system deps
if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) {
continue;
@@ -169,6 +202,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
include_dirs,
lib_dirs,
libs,
cflags,
ldflags,
}
}
@@ -218,29 +253,29 @@ pub struct ProjectInfo {
pub fn r#gen(args: &[String]) -> i32 {
let subcommand = match args.first() {
Some(s) if s == "--help" => {
printc("USAGE:", BOLD_GREEN);
printc(" dcr gen <subcommand>", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr gen <subcommand>");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Generates IDE and tooling integration files.");
println!();
printc("SUBCOMMANDS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "SUBCOMMANDS:");
println!(" project-info Print project metadata as JSON");
println!(" compile-commands Generate .dcr/compile_commands.json");
println!(" vscode Generate .vscode/ integration files");
println!(" clion Generate .idea/ integration files");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --debug | --release Profile (default: debug)");
println!(" --quiet | -q Suppress success messages on stdout");
return 0;
}
Some(s) => s.as_str(),
None => {
printc("USAGE:", BOLD_GREEN);
printc(" dcr gen <subcommand>", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr gen <subcommand>");
println!();
printc("SUBCOMMANDS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "SUBCOMMANDS:");
println!(" project-info Print project metadata as JSON");
println!(" compile-commands Generate .dcr/compile_commands.json");
println!(" vscode Generate .vscode/ integration files");
@@ -257,7 +292,7 @@ pub fn r#gen(args: &[String]) -> i32 {
"vscode" => gen_vscode(rest),
"clion" => gen_clion(rest),
_ => {
error(&format!("Unknown gen subcommand: {subcommand}"));
error!("Unknown gen subcommand: {subcommand}");
1
}
}
@@ -350,8 +385,10 @@ fn collect_project_info_inner(
});
let resolved = resolve_deps_for_gen(&config, profile, &root);
let (resolved_cflags, resolved_ldflags) =
let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags);
resolved_cflags.extend(resolved.cflags.clone());
resolved_ldflags.extend(resolved.ldflags.clone());
// Build exclude/include pattern lists (same logic as cli::build)
let mut combined_excludes: Vec<PathBuf> = Vec::new();
@@ -818,7 +855,7 @@ fn gen_project_info(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -940,7 +977,7 @@ fn gen_compile_commands(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -953,7 +990,7 @@ fn gen_compile_commands(args: &[String]) -> i32 {
/// Used by `compile-commands`, `vscode`, and `clion`.
fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], quiet: bool) -> i32 {
if let Err(e) = ensure_dcr_dir(root) {
error(&format!("Failed to create .dcr/: {e}"));
error!("Failed to create .dcr/: {e}");
return 1;
}
@@ -974,7 +1011,7 @@ fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], q
0
}
Err(e) => {
error(&format!("Failed to write compile_commands.json: {e}"));
error!("Failed to write compile_commands.json: {e}");
1
}
}
@@ -1128,7 +1165,7 @@ fn gen_vscode(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -1141,13 +1178,13 @@ fn gen_vscode(args: &[String]) -> i32 {
let vscode_dir = root.join(".vscode");
if let Err(e) = std::fs::create_dir_all(&vscode_dir) {
error(&format!("Failed to create .vscode/: {e}"));
error!("Failed to create .vscode/: {e}");
return 1;
}
// tasks.json
if let Err(e) = std::fs::write(vscode_dir.join("tasks.json"), gen_tasks_json()) {
error(&format!("Failed to write tasks.json: {e}"));
error!("Failed to write tasks.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("tasks.json").display());
@@ -1155,7 +1192,7 @@ fn gen_vscode(args: &[String]) -> i32 {
// launch.json — one entry per binary target
let launch = gen_launch_json(&all, &root);
if let Err(e) = std::fs::write(vscode_dir.join("launch.json"), launch) {
error(&format!("Failed to write launch.json: {e}"));
error!("Failed to write launch.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("launch.json").display());
@@ -1163,14 +1200,14 @@ fn gen_vscode(args: &[String]) -> i32 {
// settings.json (clangd compile-commands-dir)
let settings = gen_settings_json(&root);
if let Err(e) = std::fs::write(vscode_dir.join("settings.json"), settings) {
error(&format!("Failed to write settings.json: {e}"));
error!("Failed to write settings.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("settings.json").display());
// extensions.json — disable cpptools, recommend clangd
if let Err(e) = std::fs::write(vscode_dir.join("extensions.json"), gen_extensions_json()) {
error(&format!("Failed to write extensions.json: {e}"));
error!("Failed to write extensions.json: {e}");
return 1;
}
println!("Generated {}", vscode_dir.join("extensions.json").display());
@@ -1418,7 +1455,7 @@ fn gen_clion(args: &[String]) -> i32 {
let all = match collect_all(&root, &profile) {
Ok(v) => v,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -1431,19 +1468,19 @@ fn gen_clion(args: &[String]) -> i32 {
let idea_dir = root.join(".idea");
if let Err(e) = std::fs::create_dir_all(&idea_dir) {
error(&format!("Failed to create .idea/: {e}"));
error!("Failed to create .idea/: {e}");
return 1;
}
let run_configs_dir = idea_dir.join("runConfigurations");
if let Err(e) = std::fs::create_dir_all(&run_configs_dir) {
error(&format!("Failed to create .idea/runConfigurations/: {e}"));
error!("Failed to create .idea/runConfigurations/: {e}");
return 1;
}
// externalTools.xml
let ext_tools = gen_clion_external_tools();
if let Err(e) = std::fs::write(idea_dir.join("externalTools.xml"), ext_tools) {
error(&format!("Failed to write externalTools.xml: {e}"));
error!("Failed to write externalTools.xml: {e}");
return 1;
}
println!("Generated {}", idea_dir.join("externalTools.xml").display());
@@ -1451,7 +1488,7 @@ fn gen_clion(args: &[String]) -> i32 {
// customTargets.xml
let targets = gen_clion_custom_targets();
if let Err(e) = std::fs::write(idea_dir.join("customTargets.xml"), targets) {
error(&format!("Failed to write customTargets.xml: {e}"));
error!("Failed to write customTargets.xml: {e}");
return 1;
}
println!("Generated {}", idea_dir.join("customTargets.xml").display());
@@ -1459,7 +1496,7 @@ fn gen_clion(args: &[String]) -> i32 {
// misc.xml — point CLion at compile_commands.json
let misc = gen_clion_misc_xml(&root);
if let Err(e) = std::fs::write(idea_dir.join("misc.xml"), misc) {
error(&format!("Failed to write misc.xml: {e}"));
error!("Failed to write misc.xml: {e}");
return 1;
}
println!("Generated {}", idea_dir.join("misc.xml").display());
@@ -1467,7 +1504,7 @@ fn gen_clion(args: &[String]) -> i32 {
// .idea/.gitignore
let gitignore = "# CLion generated files\nworkspace.xml\n*.iml\n";
if let Err(e) = std::fs::write(idea_dir.join(".gitignore"), gitignore) {
error(&format!("Failed to write .idea/.gitignore: {e}"));
error!("Failed to write .idea/.gitignore: {e}");
return 1;
}
println!("Generated {}", idea_dir.join(".gitignore").display());
@@ -1481,7 +1518,7 @@ fn gen_clion(args: &[String]) -> i32 {
let fname = format!("{}.xml", sanitize_filename(&info.name));
let path = run_configs_dir.join(&fname);
if let Err(e) = std::fs::write(&path, xml) {
error(&format!("Failed to write runConfigurations/{fname}: {e}"));
error!("Failed to write runConfigurations/{fname}: {e}");
return 1;
}
println!("Generated {}", path.display());
@@ -1685,18 +1722,18 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String, bool), i32> {
}
let start = std::env::current_dir().map_err(|_| {
error("Failed to determine current directory");
error!("Failed to determine current directory");
1i32
})?;
let root = match find_project_root(&start) {
Ok(Some(r)) => canonicalize_path(&r),
Ok(None) => {
error("dcr.toml not found");
error!("dcr.toml not found");
return Err(1);
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return Err(1);
}
};

View File

@@ -15,7 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use crate::utils::cli_styles::{HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
use crate::utils::log::sprintln;
/// Displays the help information for the DCR CLI tool.
///
@@ -27,10 +28,10 @@ pub fn help() -> i32 {
println!("DCR (Dexoron Cargo Realization)");
println!("C project manager inspired by Cargo.");
println!();
printc("USAGE:", BOLD_GREEN);
printc(" dcr <command> [options]", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr <command> [options]");
println!();
printc("COMMANDS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "COMMANDS:");
println!(" new <name> Create a new project");
println!(" init Initialize the current directory as a project");
println!(" build [--profile] Build the project (default: --debug)");
@@ -43,24 +44,24 @@ pub fn help() -> i32 {
println!(" setup Show configured package registries");
println!(" clean Remove the target directory");
println!(" gen <subcommand> Generate IDE integration files");
printc("FLAGS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "FLAGS:");
println!(" --help Show command help");
println!(" --update Update dcr to the latest version");
println!(" --version Show dcr version");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --debug Use debug profile");
println!(" --release Use release profile");
println!(" --force Force rebuild (build/run)");
println!(" --clean Clean before build (build/run)");
println!(" --all Clean all workspace members (clean)");
println!();
printc("EXAMPLES:", BOLD_GREEN);
printc(" dcr new hello", BOLD_CYAN);
printc(" dcr build --release", BOLD_CYAN);
printc(" dcr run --debug", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "EXAMPLES:");
sprintln!(HELP_EXAMPLES_ST, " dcr new hello");
sprintln!(HELP_EXAMPLES_ST, " dcr build --release");
sprintln!(HELP_EXAMPLES_ST, " dcr run --debug");
println!();
printc("TIP:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "TIP:");
println!(" Run 'dcr <command> --help' for command-specific help.");
0
}

View File

@@ -15,15 +15,17 @@
// 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::prelude::*;
use crate::config::FILE_MAIN_C;
use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::cli_styles::{
BOLD_CYAN, BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
};
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs;
use std::io::Write;
use toml::Value;
use crate::utils::log::sprintln;
use std::{fs, io::Write};
/// Initializes the current working directory as a new DCR project.
///
@@ -31,10 +33,10 @@ use toml::Value;
/// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable.
pub fn init(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr init [--vcs <git|none>]");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Initializes the current directory as a DCR project.");
println!(" The directory must be empty.");
return 0;
@@ -49,7 +51,7 @@ pub fn init(args: &[String]) -> i32 {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
error!("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
@@ -60,7 +62,7 @@ pub fn init(args: &[String]) -> i32 {
}
if !clean_args.is_empty() {
warn("Command does not support additional arguments");
warn!("Command does not support additional arguments");
return 1;
}
@@ -72,16 +74,15 @@ pub fn init(args: &[String]) -> i32 {
.unwrap_or_else(|| "project".to_string());
if !items.is_empty() {
error("Directory not empty");
error!("Directory not empty");
return 1;
}
if let Err(e) = validate_package_name(&project_name) {
error(&format!(
"Invalid project name `{}`: {}",
colored(&project_name, BOLD_CYAN),
e
));
error!(
"Invalid project name `{}`: {e}",
project_name.style(BOLD_CYAN),
);
return 1;
}
@@ -91,46 +92,36 @@ pub fn init(args: &[String]) -> i32 {
println!("Initializing the project in {cwd}");
// Create and initialize default manifest (dcr.toml).
let mut config = match Config::new("./dcr.toml") {
Ok(cfg) => cfg,
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.clone()))
.is_err()
{
error("Failed to write dcr.toml");
if Config::create("./dcr.toml", Some(&project_name)).is_err() {
error!("Failed to create dcr.toml");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
"✔".style(SUCCESS_ST),
"dcr.toml".style(BOLD_CYAN)
);
// Create src directory and write template main.c file.
if fs::create_dir("src").is_err() {
error("Failed to create src/");
error!("Failed to create src/");
return 1;
}
let mut main_c = match fs::File::create("./src/main.c") {
Ok(file) => file,
Err(_) => {
error("Failed to create src/main.c");
error!("Failed to create src/main.c");
return 1;
}
};
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
error("Failed to write src/main.c");
error!("Failed to write src/main.c");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
"✔".style(SUCCESS_ST),
"src/main.c".style(BOLD_CYAN)
);
// Resolve target VCS provider or detect existing repositories.
@@ -139,7 +130,7 @@ pub fn init(args: &[String]) -> i32 {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
}
@@ -153,29 +144,26 @@ pub fn init(args: &[String]) -> i32 {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(".");
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
warn!("Failed to initialize git repository: {e}");
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
println!(" {} Initialized git repository", "✔".style(SUCCESS_ST));
}
} else {
warn(
warn!(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(std::path::Path::new(".")) {
warn(&format!("Failed to update .gitignore: {e}"));
warn!("Failed to update .gitignore: {e}");
}
println!(
"Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)
project_name.style(SUCCESS_ST)
);
printc("Next step:", BOLD_GREEN);
printc(" dcr run", BOLD_CYAN);
sprintln!(BOLD_GREEN, "Next step:");
sprintln!(BOLD_CYAN, " dcr run");
0
}

View File

@@ -15,12 +15,16 @@
// 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::prelude::*;
use crate::core::build::common;
use crate::core::build_config::Config;
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
use crate::utils::cli_styles::{
ALERT_ST, BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
};
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use crate::utils::log::sprintln;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -29,14 +33,14 @@ use std::process::Command;
/// Scans source roots (configurable via build.roots) and tests/ directory.
pub fn lint(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr lint [--fix]", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr lint [--fix]");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Runs clang-tidy on all C/C++ source files.");
println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --fix Apply clang-tidy suggestions automatically");
return 0;
}
@@ -46,7 +50,7 @@ pub fn lint(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
@@ -54,11 +58,11 @@ pub fn lint(args: &[String]) -> i32 {
let root = match find_project_root(&start_dir) {
Ok(Some(r)) => r,
Ok(None) => {
error("dcr.toml not found");
error!("dcr.toml not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -66,7 +70,7 @@ pub fn lint(args: &[String]) -> i32 {
let config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
Ok(c) => c,
Err(e) => {
error(&format!("Failed to load dcr.toml: {e}"));
error!("Failed to load dcr.toml: {e}");
return 1;
}
};
@@ -98,7 +102,7 @@ pub fn lint(args: &[String]) -> i32 {
match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) {
Ok(f) => f,
Err(e) => {
error(&format!("Failed to collect source files: {e}"));
error!("Failed to collect source files: {e}");
return 1;
}
};
@@ -116,13 +120,13 @@ pub fn lint(args: &[String]) -> i32 {
files.sort();
if files.is_empty() {
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
println!(" {} No files to lint", "Lint".style(BOLD_GREEN));
return 0;
}
println!(
" {} {} files {}",
colored("Linting", BOLD_GREEN),
"Linting".style(BOLD_GREEN),
files.len(),
if do_fix { "(with --fix)" } else { "" }
);
@@ -142,7 +146,7 @@ pub fn lint(args: &[String]) -> i32 {
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
error(&format!("Failed to execute clang-tidy: {}", e));
error!("Failed to execute clang-tidy: {e}");
return 1;
}
};
@@ -160,13 +164,13 @@ pub fn lint(args: &[String]) -> i32 {
if issue_count > 0 {
total_issues += issue_count as u32;
failed += 1;
print!(" {} ", colored("×", BOLD_YELLOW));
print!(" {} ", "×".style(ALERT_ST));
println!("{} ({} issues)", file, issue_count);
for issue in &issues {
println!(" {}", issue);
}
} else {
print!(" {} ", colored("✓", BOLD_GREEN));
print!(" {} ", "✓".style(SUCCESS_ST));
println!("{}", file);
}
}
@@ -177,13 +181,13 @@ pub fn lint(args: &[String]) -> i32 {
if failed == 0 {
println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
"Lint".style(SUCCESS_ST),
total
);
} else {
println!(
" {} {}/{} files passed, {} issues found",
colored("Lint", BOLD_YELLOW),
"Lint".style(ALERT_ST),
passed,
total,
total_issues

View File

@@ -15,15 +15,17 @@
// 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::prelude::*;
use crate::config::FILE_MAIN_C;
use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::cli_styles::{
BOLD_CYAN, BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SUCCESS_ST,
};
use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::fs;
use std::io::Write;
use toml::Value;
use crate::utils::log::sprintln;
use std::{fs, io::Write};
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
///
@@ -31,10 +33,10 @@ use toml::Value;
/// template source file generation (`src/main.c`), and initial VCS setup.
pub fn new(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr new <name> [--vcs <git|none>]");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Creates a new C/C++ project with the given name.");
println!(" The name may only contain ASCII letters, digits, '_' and '-'.");
return 0;
@@ -51,7 +53,7 @@ pub fn new(args: &[String]) -> i32 {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
error!("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
@@ -62,95 +64,81 @@ pub fn new(args: &[String]) -> i32 {
}
if clean_args.is_empty() {
error("Project name not specified");
error!("Project name not specified");
return 1;
}
if clean_args.len() > 1 {
warn("Command does not support additional arguments");
warn!("Command does not support additional arguments");
return 1;
}
let project_name = &clean_args[0];
println!(
"Creating a Project `{}`...",
colored(project_name, BOLD_CYAN)
);
println!("Creating a Project `{}`...", project_name.style(BOLD_CYAN));
if let Err(e) = validate_package_name(project_name) {
error(&format!(
"Invalid project name `{}`: {}",
colored(project_name, BOLD_CYAN),
e
));
error!(
"Invalid project name `{}`: {e}",
project_name.style(BOLD_CYAN),
);
return 1;
}
if items.contains(project_name) {
error(&format!(
error!(
"Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN)
));
printc("Hint:", BOLD_CYAN);
project_name.style(BOLD_CYAN)
);
sprintln!(BOLD_CYAN, "Hint:");
println!(
" Use `{}` to initialize an existing project\n or specify a different project name",
colored("dcr init", BOLD_CYAN)
"dcr init".style(BOLD_CYAN)
);
return 1;
}
if fs::create_dir(project_name).is_err() {
error("Failed to create directory");
error!("Failed to create directory");
return 1;
}
println!(
" {} Directory created {}",
colored("✔", BOLD_GREEN),
"✔".style(SUCCESS_ST),
project_name
);
// Initialize and write project configuration manifest (dcr.toml).
let toml_path = format!("./{project_name}/dcr.toml");
let mut config = match Config::new(&toml_path) {
Ok(cfg) => cfg,
Err(_) => {
error("Failed to create dcr.toml");
return 1;
}
};
if config
.edit("package.name", Value::String(project_name.to_string()))
.is_err()
{
error("Failed to write dcr.toml");
if Config::create(&toml_path, Some(project_name)).is_err() {
error!("Failed to create dcr.toml");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
"✔".style(SUCCESS_ST),
"dcr.toml".style(BOLD_CYAN)
);
// Generate src directory and populate initial main.c file.
if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
error("Failed to create src/");
error!("Failed to create src/");
return 1;
}
let main_c_path = format!("./{project_name}/src/main.c");
let mut main_c = match fs::File::create(&main_c_path) {
Ok(file) => file,
Err(_) => {
error("Failed to create src/main.c");
error!("Failed to create src/main.c");
return 1;
}
};
if main_c.write_all(FILE_MAIN_C.as_bytes()).is_err() {
error("Failed to write src/main.c");
error!("Failed to write src/main.c");
return 1;
}
println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
"✔".style(SUCCESS_ST),
"src/main.c".style(BOLD_CYAN)
);
// Resolve target VCS mode based on CLI flags or parent directory state.
@@ -159,7 +147,8 @@ pub fn new(args: &[String]) -> i32 {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
}
@@ -173,15 +162,12 @@ pub fn new(args: &[String]) -> i32 {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
warn!("Failed to initialize git repository: {e}");
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
println!(" {} Initialized git repository", "✔".style(SUCCESS_ST));
}
} else {
warn(
warn!(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
@@ -189,14 +175,14 @@ pub fn new(args: &[String]) -> i32 {
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(project_path) {
warn(&format!("Failed to update .gitignore: {e}"));
warn!("Failed to update .gitignore: {e}");
}
println!(
"Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN)
project_name.style(SUCCESS_ST)
);
printc("Next step:", BOLD_GREEN);
printc(&format!(" cd {}\n dcr run", project_name), BOLD_CYAN);
sprintln!(BOLD_GREEN, "Next step:");
sprintln!(BOLD_CYAN, " cd {}\n dcr run", project_name);
0
}

View File

@@ -15,17 +15,17 @@
// 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::prelude::*;
use crate::cli::build::build;
use crate::cli::flags::parse_build_run_flags;
use crate::core::build_config::Config;
use crate::core::runner::run_binary;
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
use crate::utils::fs::find_project_root;
use crate::utils::fs::with_dir;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::path::Path;
use std::process::Command;
use crate::utils::cli_styles::{BOLD_GREEN, Colorize, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::log::sprintln;
use std::{path::Path, process::Command};
/// Retrieves the run command from the config, preferring target-specific,
/// then profile-specific, then the base `run.cmd`.
@@ -67,17 +67,17 @@ fn get_run_cmd(
/// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure.
pub fn run(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
BOLD_CYAN,
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(
HELP_EXAMPLES_ST,
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]"
);
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Builds and runs the project. Only available for kind = \"bin\".");
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --debug Run with debug profile (default)");
println!(" --release Run with release profile");
println!(" --target <triple> Cross-compile for the given target");
@@ -91,18 +91,18 @@ pub fn run(args: &[String]) -> i32 {
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -111,7 +111,8 @@ pub fn run(args: &[String]) -> i32 {
}) {
Ok(cfg) => cfg,
Err(err) => {
error(&err);
error!("{err}");
return 1;
}
};
@@ -127,7 +128,7 @@ pub fn run(args: &[String]) -> i32 {
let display = display_run_cmd(&cmd, &flags.bin_args);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
format!("{:<9}", "run").style(BOLD_GREEN),
display
);
return run_shell_with_args(&cmd, &flags.bin_args);
@@ -143,11 +144,12 @@ pub fn run(args: &[String]) -> i32 {
) {
Ok(Some(ws)) => ws,
Ok(None) => {
error("Workspace root has no members defined");
error!("Workspace root has no members defined");
return 1;
}
Err(e) => {
error(&e);
error!("{e}");
return 1;
}
};
@@ -159,9 +161,9 @@ pub fn run(args: &[String]) -> i32 {
Some(m) => m,
None => {
if let Some(name) = &flags.workspace {
error(&format!("Workspace member '{name}' not found"));
error!("Workspace member '{name}' not found");
} else {
error("No workspace member to run (set `main = true` on one member)");
error!("No workspace member to run (set `main = true` on one member)");
}
return 1;
}
@@ -172,7 +174,8 @@ pub fn run(args: &[String]) -> i32 {
}) {
Ok(code) => code,
Err(e) => {
error(&e);
error!("{e}");
1
}
};
@@ -181,7 +184,7 @@ pub fn run(args: &[String]) -> i32 {
match run_project(&root, &flags, None) {
Ok(code) => code,
Err(e) => {
error(&e);
error!("{e}");
1
}
}
@@ -265,7 +268,7 @@ fn run_project(
let display = display_run_cmd(&cmd, &flags.bin_args);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
format!("{:<9}", "run").style(BOLD_GREEN),
display
);
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
@@ -273,7 +276,7 @@ fn run_project(
let display = display_bin_run(&bin_path, &flags.bin_args);
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
format!("{:<9}", "run").style(BOLD_GREEN),
display
);
return Ok(run_binary(

View File

@@ -15,15 +15,16 @@
// 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::text::{BOLD_CYAN, BOLD_GREEN, printc};
use crate::utils::cli_styles::{HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
use crate::utils::log::sprintln;
/// Sets up DCR registries by loading the registry manager and printing the list of registries.
pub fn setup(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr setup", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr setup");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Sets up DCR registries. Downloads and indexes");
println!(" package registries for dependency resolution.");
return 0;

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::cli::build;
use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags};
use crate::core::build_config::Config;
@@ -22,31 +24,35 @@ use crate::utils::build::{
get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile,
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
};
use crate::utils::cli_styles::{
BOLD_GREEN, Colorize, ERROR_ST, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST, SKIP_ST, SUCCESS_ST,
Style,
};
use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_RED, RESET, colored, printc};
use crate::utils::log::sprintln;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
/// Runs project tests and prints a unified testsuite report.
pub fn test(args: &[String]) -> i32 {
let mut init_header = false;
let mut profile = "debug";
for arg in args {
if arg == "--help" {
printc("USAGE:", BOLD_GREEN);
printc(" dcr test [--init] [--debug | --release]", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(
HELP_EXAMPLES_ST,
" dcr test [--init] [--debug | --release]"
);
println!();
printc("ALIASES:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "ALIASES:");
println!(" dcr tests");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Runs project tests and prints a unified testsuite report.");
println!();
printc("OPTIONS:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "OPTIONS:");
println!(" --init Create tests/dcr_test.h in current project");
println!(" --debug Build and run tests with debug profile (default)");
println!(" --release Build and run tests with release profile");
@@ -60,25 +66,25 @@ pub fn test(args: &[String]) -> i32 {
profile = arg.trim_start_matches("--");
continue;
}
error("Unknown argument");
error!("Unknown argument");
return 1;
}
let start_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => {
error("Failed to determine current directory");
error!("Failed to determine current directory");
return 1;
}
};
let root = match find_project_root(&start_dir) {
Ok(Some(dir)) => dir,
Ok(None) => {
error("dcr.toml file not found");
error!("dcr.toml file not found");
return 1;
}
Err(_) => {
error("Failed to find project root");
error!("Failed to find project root");
return 1;
}
};
@@ -87,7 +93,8 @@ pub fn test(args: &[String]) -> i32 {
match with_dir(&root, ensure_test_header) {
Ok(()) => {}
Err(msg) => {
error(&msg);
error!("{msg}");
return 1;
}
}
@@ -96,7 +103,7 @@ pub fn test(args: &[String]) -> i32 {
match with_dir(&root, || run_testsuite(profile)) {
Ok(code) => code,
Err(msg) => {
error(&msg);
error!("{msg}");
1
}
}
@@ -134,8 +141,12 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
cflags.extend(get_list_with_profile(&config, "cflags", profile));
let ldflags = get_list_with_profile(&config, "ldflags", profile);
let pkg_configs = get_list_with_profile(&config, "pkg_config", profile);
let (cflags, ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
let includes = test_include_dirs(&config, profile);
let (mut cflags, mut ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?;
let deps = crate::core::deps::resolve_deps(&config, profile, None, Path::new("."))?;
cflags.extend(deps.cflags);
ldflags.extend(deps.ldflags);
let mut includes = test_include_dirs(&config, profile);
includes.extend(deps.include_dirs);
let test_sources = collect_test_sources()?;
if test_sources.is_empty() {
return Err("tests/test.c not found; run 'dcr test --init' first".to_string());
@@ -189,19 +200,19 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
for line in stdout.lines() {
let line = line.trim();
if let Some(name) = line.strip_prefix("[PASS] ") {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
println!("{} {}", "[PASS]".style(SUCCESS_ST), name);
pass += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[SKIP] ") {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
println!("{} {}", "[SKIP]".style(SKIP_ST), name);
skip += 1;
parsed_any = true;
continue;
}
if let Some(name) = line.strip_prefix("[FAIL] ") {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
println!("{} {}", "[FAIL]".style(ERROR_ST), name);
fail += 1;
parsed_any = true;
continue;
@@ -209,17 +220,17 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if let Some((status, name)) = line.split_once('\t') {
match status {
"PASS" => {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
println!("{} {}", "[PASS]".style(SUCCESS_ST), name);
pass += 1;
parsed_any = true;
}
"SKIP" => {
println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
println!("{} {}", "[SKIP]".style(SKIP_ST), name);
skip += 1;
parsed_any = true;
}
"FAIL" => {
println!("{} {}", colored("[FAIL]", BOLD_RED), name);
println!("{} {}", "[FAIL]".style(ERROR_ST), name);
fail += 1;
parsed_any = true;
}
@@ -231,11 +242,11 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if !parsed_any {
if suite_success {
for name in &declared {
println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
println!("{} {}", "[PASS]".style(SUCCESS_ST), name);
}
pass = declared.len() as i32;
} else {
println!("{} testsuite", colored("[FAIL]", BOLD_RED));
println!("{} testsuite", "[FAIL]".style(ERROR_ST));
fail = 1;
}
}
@@ -247,14 +258,15 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
};
println!();
println!("{}", colored("=====================", BOLD_GREEN));
println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
println!("{}", colored("=====================", BOLD_GREEN));
sprintln!(BOLD_GREEN, "=====================");
sprintln!(BOLD_GREEN, " Testsuite summary ");
sprintln!(BOLD_GREEN, "=====================");
println!("TOTAL: {}", total);
print_field("PASS", pass, BOLD_GREEN);
print_field("SKIP", skip, BOLD_BLUE);
print_field("FAIL", fail, BOLD_RED);
println!("{}", colored("=====================", BOLD_GREEN));
print_field("PASS", pass, SUCCESS_ST);
print_field("SKIP", skip, SKIP_ST);
print_field("FAIL", fail, ERROR_ST);
sprintln!(BOLD_GREEN, "=====================");
sprintln!(BOLD_GREEN, "=====================");
if fail > 0 || !suite_success {
return Ok(1);
@@ -389,10 +401,10 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
}
/// Prints a test result field with color coding.
fn print_field(label: &str, value: i32, color: &str) {
fn print_field(label: &str, value: i32, style: Style) {
if value == 0 {
println!("{}: {}", label, value);
} else {
println!("{}{}: {}{}", color, label, value, RESET);
sprintln!(style, "{}: {}", label, value);
}
}

View File

@@ -15,9 +15,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::prelude::*;
use crate::core::build_config::Config;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use crate::utils::cli_styles::{BOLD_CYAN, HELP_EXAMPLES_ST, HELP_SECTION_TITLE_ST};
use crate::utils::log::sprintln;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use toml::Value;
@@ -28,10 +30,10 @@ use toml::Value;
/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree.
pub fn tree(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN);
printc(" dcr tree", BOLD_CYAN);
sprintln!(HELP_SECTION_TITLE_ST, "USAGE:");
sprintln!(HELP_EXAMPLES_ST, " dcr tree");
println!();
printc("DESCRIPTION:", BOLD_GREEN);
sprintln!(HELP_SECTION_TITLE_ST, "DESCRIPTION:");
println!(" Displays the dependency tree of the current project.");
return 0;
}
@@ -40,7 +42,7 @@ pub fn tree(args: &[String]) -> i32 {
let config = match Config::open("dcr.toml") {
Ok(c) => c,
Err(_) => {
error("dcr.toml not found in current directory");
error!("dcr.toml not found in current directory");
return 1;
}
};
@@ -54,7 +56,7 @@ pub fn tree(args: &[String]) -> i32 {
.and_then(|v| v.as_str())
.unwrap_or("0.0.0");
printc(&format!("{} v{}", name, version), BOLD_CYAN);
sprintln!(BOLD_CYAN, "{} v{}", name, version);
let mut seen = HashSet::new();
seen.insert(name.to_string());
@@ -115,6 +117,19 @@ fn print_deps(
format!(" v{}", v)
} else if let Some(git) = t.get("git").and_then(|v| v.as_str()) {
format!(" ({})", git)
} else if let Some(pkg) = t.get("pkg-config").or_else(|| t.get("pkg_config")) {
if let Some(s) = pkg.as_str() {
format!(" (pkg-config: {})", s)
} else if let Some(arr) = pkg.as_array() {
let names = arr
.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(", ");
format!(" (pkg-config: {})", names)
} else {
"".to_string()
}
} else {
"".to_string()
}

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use crate::platform;
use crate::utils::build::is_bare_metal_target;
use std::path::{Path, PathBuf};

View File

@@ -15,9 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::utils::build::{is_bare_metal_target, is_compile_only, is_flat_bin};
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -20,15 +20,10 @@
/// This module provides the MSVC-specific build implementation for C and C++
/// projects, including object compilation and linking for executables, static
/// libraries, shared libraries, and flat binaries.
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::platform;
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
use std::{fs, path::Path, process::Command, time::Instant};
/// Builds the project target using the MSVC compiler.
pub fn build(ctx: &BuildContext) -> Result<f64, String> {

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, common};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -35,7 +35,7 @@ use crate::core::build::steps::{
get_build_steps_with_profile, run_build_steps, verify_expectations,
};
use crate::core::build_config::Config;
use crate::core::deps::{register, resolve_deps};
use crate::core::deps::resolve_deps;
use crate::core::workspace::parse_workspace;
use crate::utils::build::{
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
@@ -335,6 +335,81 @@ fn build_project_at(
workspace_root: Option<&Path>,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
) -> Result<(), String> {
let mut dependency_stack = Vec::new();
build_project_at_with_stack(
project_root,
profile,
target,
explicit_target,
exclude_dirs,
force,
verbose,
workspace_root,
cancel,
rep,
&mut dependency_stack,
)
}
/// Builds a package while tracking the dependency chain to reject cycles.
#[allow(clippy::too_many_arguments)]
fn build_project_at_with_stack(
project_root: &Path,
profile: &str,
target: Option<&str>,
explicit_target: bool,
exclude_dirs: &[std::path::PathBuf],
force: bool,
verbose: bool,
workspace_root: Option<&Path>,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
dependency_stack: &mut Vec<PathBuf>,
) -> Result<(), String> {
let root = project_root
.canonicalize()
.unwrap_or_else(|_| project_root.to_path_buf());
if let Some(index) = dependency_stack.iter().position(|item| item == &root) {
let mut chain: Vec<String> = dependency_stack[index..]
.iter()
.map(|item| item.display().to_string())
.collect();
chain.push(root.display().to_string());
return Err(format!("Dependency cycle detected: {}", chain.join(" -> ")));
}
dependency_stack.push(root);
let result = build_project_at_inner(
project_root,
profile,
target,
explicit_target,
exclude_dirs,
force,
verbose,
workspace_root,
cancel,
rep,
dependency_stack,
);
dependency_stack.pop();
result
}
/// Builds one DCR package after the dependency-cycle guard has been installed.
#[allow(clippy::too_many_arguments)]
fn build_project_at_inner(
project_root: &Path,
profile: &str,
target: Option<&str>,
explicit_target: bool,
exclude_dirs: &[std::path::PathBuf],
force: bool,
verbose: bool,
workspace_root: Option<&Path>,
cancel: &Arc<AtomicBool>,
rep: &mut dyn BuildReporter,
dependency_stack: &mut Vec<PathBuf>,
) -> Result<(), String> {
if cancel.load(Ordering::SeqCst) {
return Err("Build interrupted".to_string());
@@ -641,38 +716,34 @@ fn build_project_at(
}
}
let deps_table = config.get("dependencies").and_then(|v| v.as_table());
let mut resolved = resolve_deps(&config, profile, build_target, project_root)?;
resolved.include_dirs.extend(ws_include_dirs);
resolved.lib_dirs.extend(ws_lib_dirs);
resolved.libs.extend(ws_libs);
// Registry dependencies are cached under the DCR registry root. Build
// them as normal DCR projects before the current project is linked.
if let Some(deps) = deps_table {
for (name, value) in deps {
if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let version = pkg_info
.get("latest_version")
.or_else(|| pkg_info.get("version"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
let include_dir = dep_root.join("target").join("include");
let lib_dir = dep_root.join("target").join("lib");
if !include_dir.exists() || !lib_dir.exists() {
rep.on_event(BuildEvent::DepBuilding { name, version });
if !dep_root.join("dcr.toml").is_file() {
return Err(format!(
"Registry dependency `{}` is missing dcr.toml at {}",
name,
dep_root.display()
));
let mut built_roots = std::collections::HashSet::new();
let mut transitive_roots = std::collections::HashSet::new();
let direct_package_roots = resolved.package_roots.clone();
for dep_root in &direct_package_roots {
if !built_roots.insert(dep_root.clone()) {
continue;
}
build_project_at(
&dep_root,
let dep_root = Path::new(dep_root);
let dep_config =
Config::open(&dep_root.join("dcr.toml").to_string_lossy()).map_err(|_| {
format!(
"DCR dependency is missing dcr.toml at {}",
dep_root.display()
)
})?;
let name = get_config_str(&dep_config, "package.name");
let version = get_config_str(&dep_config, "package.version");
rep.on_event(BuildEvent::DepBuilding {
name: &name,
version: &version,
});
build_project_at_with_stack(
dep_root,
profile,
build_target,
has_explicit_target,
@@ -682,25 +753,26 @@ fn build_project_at(
None,
cancel,
&mut *rep,
dependency_stack,
)?;
rep.on_event(BuildEvent::DepReady {
name,
version,
name: &name,
version: &version,
rebuilt: true,
});
} else {
rep.on_event(BuildEvent::DepReady {
name,
version,
rebuilt: false,
});
}
}
}
extend_transitive_link_deps(
dep_root,
profile,
build_target,
&mut resolved,
&mut transitive_roots,
)?;
}
let (resolved_cflags, resolved_ldflags) =
let (mut resolved_cflags, mut resolved_ldflags) =
resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?;
resolved_cflags.extend(resolved.cflags.clone());
resolved_ldflags.extend(resolved.ldflags.clone());
let mut combined_excludes = Vec::new();
for dir in exclude_dirs {
combined_excludes.push(dir.clone());
@@ -919,6 +991,42 @@ fn build_project_at(
Ok(())
}
/// Adds link flags needed by dependencies of a static DCR library.
/// Static archives do not contain their own dependency objects, so the final
/// consumer must link every transitive package in dependency order.
fn extend_transitive_link_deps(
package_root: &Path,
profile: &str,
target: Option<&str>,
consumer_deps: &mut crate::core::deps::common::ResolvedDeps,
visited: &mut std::collections::HashSet<PathBuf>,
) -> Result<(), String> {
let root = package_root
.canonicalize()
.unwrap_or_else(|_| package_root.to_path_buf());
if !visited.insert(root.clone()) {
return Ok(());
}
let config =
Config::open(&root.join("dcr.toml").to_string_lossy()).map_err(|err| err.to_string())?;
let deps = resolve_deps(&config, profile, target, &root)?;
consumer_deps.include_dirs.extend(deps.include_dirs);
consumer_deps.lib_dirs.extend(deps.lib_dirs);
consumer_deps.libs.extend(deps.libs);
consumer_deps.cflags.extend(deps.cflags);
consumer_deps.ldflags.extend(deps.ldflags);
for child_root in deps.package_roots {
extend_transitive_link_deps(
Path::new(&child_root),
profile,
target,
consumer_deps,
visited,
)?;
}
Ok(())
}
/// Converts relative paths in flags to absolute paths based on project root for include and link flags.
fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String {
for prefix in prefixes {

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::builder::artifact;
use crate::core::build::common;
use crate::core::build::{builder::BuildContext, builder::artifact, common};
use crate::utils::build::{is_compile_only, is_flat_bin};
use std::path::Path;
use std::time::Instant;
use std::{path::Path, time::Instant};
/// Keeps only assembler-relevant flags from a cflags list.
///

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds an assembly file using the FASM assembler.
///

View File

@@ -15,12 +15,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds GAS (`.s`) sources via the GNU Assembler.
///

View File

@@ -1,9 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds MASM (`.asm`) sources via `ml`.
///

View File

@@ -16,12 +16,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// Support for NASM assembly language in the DCR build system.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common};
use std::{fs, path::Path, process::Command};
/// Builds assembly sources using the NASM assembler.
///

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::{build::builder::BuildContext, build::common};
use crate::utils::build::run_pkg_config;
use std::fs;
use std::path::{Path, PathBuf};

View File

@@ -1,10 +1,6 @@
use crate::core::build::builder::BuildContext;
use crate::core::build::common;
use crate::core::build::language::Language;
use crate::core::build::language::asm::common as asm;
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::core::build::{builder::BuildContext, common, language::Language};
use std::{fs, path::Path, process::Command};
/// Represents the LLVM IR language implementation for the build system.
pub struct LlvmIr;

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::core::build::engine::ToolchainExecs;
use crate::core::build_config::Config;
use crate::core::{build::engine::ToolchainExecs, build_config::Config};
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
use glob::glob;
use std::fs;

View File

@@ -15,12 +15,13 @@
// 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::prelude::*;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
use toml::map::Map;
use toml::{Value, map::Map};
use toml_edit::{DocumentMut, Item, Table};
const DEFAULT_VERSION: &str = "0.1.0";
@@ -244,13 +245,19 @@ pub struct WorkspaceMemberConfig {
}
impl Config {
#[allow(dead_code)]
/// Creates a new Config instance from a TOML file path. If the file does not exist, it creates a default one.
pub fn new(path: &str) -> Result<Self, ConfigError> {
Self::create(path, None)
}
/// Creates a new Config instance with an explicit package name for the default template when creating a new file.
pub fn create(path: &str, package_name: Option<&str>) -> Result<Self, ConfigError> {
let path = PathBuf::from(path);
let (data, doc) = if path.exists() {
load_parts(&path)?
} else {
let content = default_toml_text();
let content = default_toml_text(package_name)?;
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
fs::write(&path, doc.to_string())?;
let data: Value = toml::from_str(&content)?;
@@ -328,35 +335,35 @@ impl Config {
.map(str::trim)
.filter(|s| !s.is_empty());
let Some(required) = required else {
crate::utils::log::warn(&format!(
warn!(
"package.dcr-version is missing in {}. Add e.g. dcr-version = \"{tool}\" \
so collaborators know which dcr this project targets \
(written automatically by `dcr new` / `dcr init`).",
self.path.display()
));
self.path.display(),
);
return;
};
match crate::utils::build::compare_semver(tool, required) {
Some(std::cmp::Ordering::Less) => {
crate::utils::log::warn(&format!(
warn!(
"this project requires dcr {required} (package.dcr-version), \
but you are running dcr {tool}. Some features may be missing or misbehave. \
Upgrade dcr (e.g. `dcr --update`) or lower package.dcr-version if intentional."
));
);
}
Some(std::cmp::Ordering::Greater) => {
crate::utils::log::warn(&format!(
warn!(
"this project pins dcr {required} (package.dcr-version), \
but you are running newer dcr {tool}. Config/defaults may be outdated. \
Review the changelog and bump package.dcr-version when the project is verified."
));
Review the changelog and bump package.dcr-version when the project is verified.",
);
}
Some(std::cmp::Ordering::Equal) => {}
None => {
crate::utils::log::warn(&format!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring"
));
warn!(
"package.dcr-version = \"{required}\" is not a valid semver (expected X.Y.Z); ignoring",
);
}
}
}
@@ -830,14 +837,20 @@ fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
Ok((data, doc))
}
fn default_toml_text() -> String {
let name = std::env::current_dir()
fn default_toml_text(package_name: Option<&str>) -> Result<String, ConfigError> {
let name = package_name
.map(|s| s.to_string())
.or_else(|| {
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
})
.unwrap_or_else(|| "project".to_string());
validate_non_empty_string(&name, "package.name")?;
validate_package_name(&name)?;
let dcr_version = env!("CARGO_PKG_VERSION");
format!(
Ok(format!(
"[package]\n\
name = \"{name}\"\n\
version = \"{DEFAULT_VERSION}\"\n\
@@ -854,7 +867,7 @@ fn default_toml_text() -> String {
kind = \"{DEFAULT_KIND}\"\n\
\n\
[dependencies]\n"
)
))
}
fn dcr_version_warn_once(path_key: &str) -> bool {
@@ -1285,6 +1298,22 @@ mod tests {
);
}
#[test]
fn create_rejects_invalid_name_without_writing_manifest() {
let dir = temp_dir("invalid_create_name");
let path = dir.join("dcr.toml");
for name in ["", "bad name", "../bad", "bad\"name", " bad", "bad."] {
let result = Config::create(&path.to_string_lossy(), Some(name));
assert!(matches!(result, Err(ConfigError::Invalid(_))), "{name:?}");
assert!(!path.exists(), "invalid name must not create a manifest");
}
let config = Config::create(&path.to_string_lossy(), Some("valid-name")).unwrap();
assert_eq!(
config.get("package.name").and_then(Value::as_str),
Some("valid-name")
);
}
#[test]
fn new_creates_default_config() {
let dir = temp_dir("new_default");

View File

@@ -4,4 +4,7 @@ pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
pub cflags: Vec<String>,
pub ldflags: Vec<String>,
pub package_roots: Vec<String>,
}

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Represents a locked dependency entry containing its name, version, source, and checksum.
#[derive(Debug, Clone)]

View File

@@ -23,6 +23,7 @@ pub mod register;
use crate::core::build_config::Config;
use crate::core::deps::common::ResolvedDeps;
use crate::core::deps::lock::{DepLock, write_lock};
use crate::utils::build::{resolve_pkg_config_flags, run_pkg_config};
use std::path::Path;
/// Returns the version string from a dependency's dcr.toml file, or an empty string if missing.
@@ -39,8 +40,8 @@ fn dep_version(path: &Path) -> String {
/// Resolves dependencies from config into include/lib dirs and lib names, and writes the lock file.
///
/// Registry and path deps contribute to `ResolvedDeps`. Git deps are recorded in the lock only
/// (no fetch or include/lib resolution).
/// Registry, path, and git deps contribute to `ResolvedDeps`. Path and git sources support both
/// DCR packages (built from `dcr.toml`) and prebuilt libraries (described with include/lib/libs).
///
/// # Parameters
/// - `config`: Loaded `dcr.toml`.
@@ -73,7 +74,22 @@ pub fn resolve_deps(
if let Some(deps) = deps_table {
// Process each dependency declaration in the TOML table
for (name, value) in deps {
if register::is_registry_dep(value) {
if let Some(packages) = pkg_config_dep(value, name)? {
let (cflags, ldflags) = resolve_pkg_config_flags(&packages, &[], &[])?;
resolved.cflags.extend(cflags);
resolved.ldflags.extend(ldflags);
let version = packages
.first()
.and_then(|pkg| run_pkg_config(pkg, "--modversion").ok())
.map(|v| v.trim().to_string())
.unwrap_or_default();
lock_packages.push(DepLock {
name: name.clone(),
version,
checksum: String::new(),
source: format!("pkg-config+{}", packages.join(",")),
});
} else if register::is_registry_dep(value) {
let pkg_info = register::resolve_package_from_registry(name)?;
let dep_root = register::package_root_from_registry_info(&pkg_info)?;
@@ -88,6 +104,9 @@ pub fn resolve_deps(
.to_string(),
);
resolved.libs.push(name.clone());
resolved
.package_roots
.push(dep_root.to_string_lossy().to_string());
lock_packages.push(DepLock {
name: name.clone(),
@@ -107,6 +126,8 @@ pub fn resolve_deps(
});
} else if let Some(path) = path_dep_path(value) {
let dep_root = project_root.join(path);
let dcr_package_name = package_name(&dep_root);
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
if let Some(table) = value.as_table() {
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
for inc in includes {
@@ -118,10 +139,7 @@ pub fn resolve_deps(
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists(
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
push_package_output_dirs(&mut resolved, &dep_root);
}
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
@@ -143,16 +161,29 @@ pub fn resolve_deps(
}
}
} else {
resolved.libs.push(name.clone());
resolved
.libs
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
}
} else {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
push_if_exists(
&mut resolved.include_dirs,
&dep_root.join("target").join("include"),
);
push_package_output_dirs(&mut resolved, &dep_root);
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
resolved.libs.push(name.clone());
resolved
.libs
.push(dcr_package_name.clone().unwrap_or_else(|| name.clone()));
}
if use_dcr_package {
resolved
.package_roots
.push(dep_root.to_string_lossy().to_string());
push_package_output_dirs(&mut resolved, &dep_root);
} else if !has_complete_prebuilt_layout(value) {
return Err(format!(
"Path dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
name
));
}
lock_packages.push(DepLock {
@@ -162,11 +193,46 @@ pub fn resolve_deps(
source: format!("path+{}", dep_root.display()),
});
} else if let Some(git_info) = git_dep(value) {
let dep_root = register::fetch_git_dependency(
git_info.url,
git_info.branch.as_deref(),
git_info.tag.as_deref(),
git_info.rev.as_deref(),
)?;
let dcr_package_name = package_name(&dep_root);
let use_dcr_package = use_dcr_package(value, dcr_package_name.is_some(), name)?;
let (has_include, has_lib, has_libs) = value
.as_table()
.map(|table| push_explicit_layout(&mut resolved, &dep_root, table))
.unwrap_or((false, false, false));
if !has_include {
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
if use_dcr_package {
push_package_output_dirs(&mut resolved, &dep_root);
}
}
if !has_lib {
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
}
if !has_libs && let Some(package_name) = dcr_package_name.as_deref() {
resolved.libs.push(package_name.to_string());
}
if use_dcr_package {
resolved
.package_roots
.push(dep_root.to_string_lossy().to_string());
} else if !(has_include && has_lib && has_libs) {
return Err(format!(
"Git dependency `{}` has no dcr.toml; specify include, lib, and libs for a prebuilt library",
name
));
}
let commit = register::git_commit(&dep_root)?;
lock_packages.push(DepLock {
name: name.clone(),
version: git_info.version.unwrap_or_default(),
checksum: String::new(),
source: format!("git+{}", git_info.url),
version: git_info.version.unwrap_or(commit.clone()),
checksum: commit.clone(),
source: format!("git+{}#{}", git_info.url, commit),
});
}
}
@@ -187,6 +253,9 @@ pub fn resolve_deps(
struct GitDep<'a> {
url: &'a str,
version: Option<String>,
branch: Option<String>,
tag: Option<String>,
rev: Option<String>,
}
/// Parses a git dependency from a TOML value table.
@@ -198,11 +267,71 @@ fn git_dep(value: &toml::Value) -> Option<GitDep<'_>> {
.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
return Some(GitDep { url, version });
let branch = table
.get("branch")
.and_then(|v| v.as_str())
.map(str::to_string);
let tag = table
.get("tag")
.and_then(|v| v.as_str())
.map(str::to_string);
let rev = table
.get("rev")
.and_then(|v| v.as_str())
.map(str::to_string);
return Some(GitDep {
url,
version,
branch,
tag,
rev,
});
}
None
}
/// Extracts one or more pkg-config package names from a dependency table.
/// Both `pkg-config` and the legacy-style `pkg_config` spelling are accepted.
fn pkg_config_dep(
value: &toml::Value,
dependency_name: &str,
) -> Result<Option<Vec<String>>, String> {
let Some(table) = value.as_table() else {
return Ok(None);
};
let raw = table.get("pkg-config").or_else(|| table.get("pkg_config"));
let Some(raw) = raw else {
return Ok(None);
};
let packages = if let Some(pkg) = raw.as_str() {
vec![pkg.to_string()]
} else {
raw.as_array()
.ok_or_else(|| {
format!(
"Dependency `{dependency_name}` field `pkg-config` must be a string or array of strings"
)
})?
.iter()
.map(|value| {
value.as_str().map(str::to_string).ok_or_else(|| {
format!(
"Dependency `{dependency_name}` field `pkg-config` must contain only strings"
)
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.collect()
};
if packages.is_empty() || packages.iter().any(|pkg| pkg.trim().is_empty()) {
return Err(format!(
"Dependency `{dependency_name}` field `pkg-config` must not be empty"
));
}
Ok(Some(packages))
}
/// Extracts the path for a path-based dependency, supporting both table format and legacy string format.
fn path_dep_path(value: &toml::Value) -> Option<&str> {
if let Some(table) = value.as_table() {
@@ -226,6 +355,117 @@ fn push_default_lib_dirs(paths: &mut Vec<String>, dep_root: &Path) {
push_if_exists(paths, &dep_root.join("target").join("lib"));
}
/// Adds explicitly configured prebuilt include/lib directories and linker names.
fn push_explicit_layout(
resolved: &mut ResolvedDeps,
dep_root: &Path,
table: &toml::map::Map<String, toml::Value>,
) -> (bool, bool, bool) {
let mut has_include = false;
let mut has_lib = false;
let mut has_libs = false;
if let Some(includes) = table.get("include").and_then(|v| v.as_array()) {
has_include = true;
for inc in includes {
if let Some(path) = inc.as_str() {
resolved
.include_dirs
.push(dep_root.join(path).to_string_lossy().to_string());
}
}
}
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
has_lib = true;
for path in lib_dirs {
if let Some(path) = path.as_str() {
resolved
.lib_dirs
.push(dep_root.join(path).to_string_lossy().to_string());
}
}
}
if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) {
has_libs = true;
for lib in libs {
if let Some(name) = lib.as_str() {
resolved.libs.push(name.to_string());
}
}
}
(has_include, has_lib, has_libs)
}
/// Reports whether a source without a DCR manifest fully describes its prebuilt layout.
fn has_complete_prebuilt_layout(value: &toml::Value) -> bool {
value.as_table().is_some_and(|table| {
["include", "lib", "libs"].iter().all(|field| {
table
.get(*field)
.and_then(|value| value.as_array())
.is_some_and(|values| !values.is_empty())
})
})
}
/// Selects automatic, forced-build, or forced-prebuilt dependency resolution.
fn use_dcr_package(
value: &toml::Value,
has_manifest: bool,
dependency_name: &str,
) -> Result<bool, String> {
let mode = value
.as_table()
.and_then(|table| table.get("mode"))
.map(|value| {
value.as_str().ok_or_else(|| {
format!(
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
)
})
})
.transpose()?;
match mode {
None => Ok(has_manifest),
Some("build") if has_manifest => Ok(true),
Some("build") => Err(format!(
"Dependency `{dependency_name}` uses mode = \"build\" but has no dcr.toml"
)),
Some("prebuild") => Ok(false),
Some(_) => Err(format!(
"Dependency `{dependency_name}` field `mode` must be \"build\" or \"prebuild\""
)),
}
}
/// Adds DCR's packaged output directories even before the dependency is built.
fn push_package_output_dirs(resolved: &mut ResolvedDeps, dep_root: &Path) {
push_unique_path(
&mut resolved.include_dirs,
dep_root.join("target").join("include"),
);
push_unique_path(&mut resolved.lib_dirs, dep_root.join("target").join("lib"));
}
/// Adds a path once, preserving the linker and include-search order.
fn push_unique_path(paths: &mut Vec<String>, path: std::path::PathBuf) {
let path = path.to_string_lossy().to_string();
if !paths.contains(&path) {
paths.push(path);
}
}
/// Reads a DCR package name when the dependency has a manifest.
fn package_name(path: &Path) -> Option<String> {
Config::open(&path.join("dcr.toml").to_string_lossy())
.ok()
.and_then(|config| {
config
.get("package.name")
.and_then(|value| value.as_str())
.map(str::to_string)
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -254,6 +494,20 @@ mod tests {
assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None);
}
#[test]
fn pkg_config_dep_accepts_string_and_array() {
let single: Value = toml::from_str("pkg-config = \"fmt\"").unwrap();
assert_eq!(
pkg_config_dep(&single, "fmt"),
Ok(Some(vec!["fmt".to_string()]))
);
let many: Value = toml::from_str("pkg_config = [\"fmt\", \"zlib\"]").unwrap();
assert_eq!(
pkg_config_dep(&many, "base"),
Ok(Some(vec!["fmt".to_string(), "zlib".to_string()]))
);
}
/// Verifies that push_default_lib_dirs includes the target/lib directory when it exists.
#[test]
fn default_lib_dirs_include_packaged_library_output() {

View File

@@ -17,8 +17,10 @@
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use toml::Value as TomlValue;
/// Represents a registry configuration entry with its URL and priority.
@@ -149,6 +151,95 @@ pub fn registry_lib_dir(dep_root: &Path) -> PathBuf {
dep_root.join("target").join("lib")
}
/// Fetches a git dependency into the DCR cache and returns its working tree.
/// The URL and selected ref determine the cache key; an existing checkout is reused.
pub fn fetch_git_dependency(
url: &str,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<PathBuf, String> {
if !crate::utils::git::is_git_available() {
return Err("git executable not found in PATH".to_string());
}
let mut hasher = Sha256::new();
hasher.update(url.as_bytes());
hasher.update(b"\0");
hasher.update(branch.unwrap_or("").as_bytes());
hasher.update(b"\0");
hasher.update(tag.unwrap_or("").as_bytes());
hasher.update(b"\0");
hasher.update(rev.unwrap_or("").as_bytes());
let key = format!("{:x}", hasher.finalize());
let root = dcr_config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("git")
.join(&key);
if root.join(".git").is_dir() {
let status = Command::new("git")
.args(["fetch", "--all", "--tags", "--prune"])
.current_dir(&root)
.status()
.map_err(|e| format!("failed to execute git fetch: {e}"))?;
if !status.success() {
eprintln!("Warning: git fetch failed for {url}; using the cached checkout");
}
return checkout_git_ref(&root, branch, tag, rev);
}
if root.exists() {
return Err(format!(
"git cache path is not a repository: {}",
root.display()
));
}
if let Some(parent) = root.parent() {
fs::create_dir_all(parent).map_err(|e| format!("failed to create git cache: {e}"))?;
}
let status = Command::new("git")
.args(["clone", "--", url])
.arg(&root)
.status()
.map_err(|e| format!("failed to execute git clone: {e}"))?;
if !status.success() {
return Err(format!("git clone failed for {url}"));
}
checkout_git_ref(&root, branch, tag, rev)
}
/// Returns the exact commit checked out in a cached git dependency.
pub fn git_commit(root: &Path) -> Result<String, String> {
let output = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(root)
.output()
.map_err(|e| format!("failed to execute git rev-parse: {e}"))?;
if !output.status.success() {
return Err(format!("failed to read git revision at {}", root.display()));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn checkout_git_ref(
root: &Path,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<PathBuf, String> {
let selected = rev.or(tag).or(branch);
let Some(reference) = selected else {
return Ok(root.to_path_buf());
};
let status = Command::new("git")
.args(["checkout", "--force", reference])
.current_dir(root)
.status()
.map_err(|e| format!("failed to execute git checkout: {e}"))?;
if !status.success() {
return Err(format!("git checkout failed for ref `{reference}`"));
}
Ok(root.to_path_buf())
}
/// Looks up `name` in the local package index (`DCR_INDEX_PATH` / `~/.dcr/index.json`).
///
/// Configured registries are sorted by priority, but the lookup currently always reads the

View File

@@ -17,9 +17,7 @@
/// Registry configuration management for DCR.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::{collections::HashMap, fs, path::PathBuf};
/// Represents a registry configuration entry with its URL and priority.
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -16,8 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::platform;
use std::path::Path;
use std::process::Command;
use std::{path::Path, process::Command};
/// Runs the built binary for a project, forwarding `bin_args`.
///

View File

@@ -15,8 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
use std::{fs, path::Path};
/// Initializes a Git repository in the target directory and configures its `.gitignore` file.
///

View File

@@ -14,19 +14,23 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::log::error;
mod cli;
mod config;
mod core;
mod platform;
mod prelude;
mod utils;
use prelude::*;
/// Main entry point for DCR, a Cargo-like C/C++ project manager.
///
/// Collects command-line arguments via `std::env::args()` and dispatches to the
/// appropriate subcommand handler. With fewer than two arguments, shows help and
/// exits with code 0; otherwise exits with the handler's exit code.
fn main() {
utils::log::init();
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
@@ -61,7 +65,7 @@ fn main() {
"--help" => cli::help::help(),
"--update" => cli::flag_update::flag_update(rest),
_ => {
error("Unknown command or argument");
error!("Unknown command or argument");
1
}
};

1
src/prelude.rs Normal file
View File

@@ -0,0 +1 @@
pub use log::{error, warn};

View File

@@ -15,8 +15,9 @@
// 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::prelude::*;
use crate::core::build_config::Config;
use crate::utils::log::warn;
use std::path::{Path, PathBuf};
/// Version information parsed from a version string.
@@ -251,10 +252,9 @@ pub fn normalize_target_os(target: &str) -> &str {
"windows" => "x86_64-pc-windows-msvc",
_ if target.contains('-') => target,
_ => {
warn(&format!(
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
target
));
warn!(
"Unknown target '{target}', using as-is. Supported short names: linux, macos, windows"
);
target
}
}

View File

@@ -15,39 +15,33 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[allow(dead_code)]
pub const RESET: &str = "\x1b[0m";
pub use owo_colors::{OwoColorize as Colorize, Style};
// Сli theme styles
// Main
#[allow(dead_code)]
pub const BOLD: &str = "\x1b[1m";
pub const BOLD_RED: Style = Style::new().bright_red().bold();
#[allow(dead_code)]
pub const BOLD_GREEN: Style = Style::new().bright_green().bold();
#[allow(dead_code)]
pub const BOLD_YELLOW: Style = Style::new().bright_yellow().bold();
#[allow(dead_code)]
pub const BOLD_CYAN: Style = Style::new().bright_cyan().bold();
#[allow(dead_code)]
pub const BOLD_BLUE: Style = Style::new().bright_blue().bold();
// Help
#[allow(dead_code)]
pub const BRIGHT_RED: &str = "\x1b[91m";
pub const HELP_SECTION_TITLE_ST: Style = BOLD_GREEN;
#[allow(dead_code)]
pub const BRIGHT_GREEN: &str = "\x1b[92m";
#[allow(dead_code)]
pub const BRIGHT_YELLOW: &str = "\x1b[93m";
#[allow(dead_code)]
pub const BRIGHT_CYAN: &str = "\x1b[96m";
#[allow(dead_code)]
pub const BOLD_RED: &str = "\x1b[1m\x1b[91m";
#[allow(dead_code)]
pub const BOLD_GREEN: &str = "\x1b[1m\x1b[92m";
#[allow(dead_code)]
pub const BOLD_YELLOW: &str = "\x1b[1m\x1b[93m";
#[allow(dead_code)]
pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m";
pub const HELP_EXAMPLES_ST: Style = BOLD_CYAN;
/// Applies ANSI escape codes to format the message with the given style.
///
/// This is a utility for colored output in terminal applications.
// Other
#[allow(dead_code)]
pub fn colored(msg: &str, style: &str) -> String {
format!("{style}{msg}{RESET}")
}
/// Prints `msg` to stdout with the given ANSI `style`.
pub const ERROR_ST: Style = BOLD_RED;
#[allow(dead_code)]
pub fn printc(msg: &str, style: &str) {
println!("{style}{msg}{RESET}");
}
pub const SUCCESS_ST: Style = BOLD_GREEN;
#[allow(dead_code)]
pub const ALERT_ST: Style = BOLD_YELLOW;
#[allow(dead_code)]
pub const SKIP_ST: Style = BOLD_BLUE;

View File

@@ -1,5 +1,4 @@
use std::path::Path;
use std::process::Command;
use std::{path::Path, process::Command};
/// Checks if the `git` executable is available in the system's PATH.
///

View File

@@ -15,17 +15,55 @@
// 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::text::{BOLD_RED, BOLD_YELLOW, colored};
use env_logger::{Builder, WriteStyle};
use log::{Level, LevelFilter};
use owo_colors::OwoColorize;
use std::io::Write;
/// Logs an error message to the standard error stream.
///
/// The message is prefixed with a red "error" label for visibility.
#[allow(dead_code)]
pub fn error(msg: &str) {
eprintln!("{}: {msg}", colored("error", BOLD_RED));
// A function for initializing the logging lib with basic settings.
pub fn init() {
Builder::new()
.filter_level(LevelFilter::Warn)
.write_style(WriteStyle::Auto)
.format(|buf, record| {
let colored_lv = match record.level() {
Level::Error => record.level().red().to_string(),
Level::Warn => record.level().yellow().to_string(),
Level::Info => record.level().default_color().to_string(),
_ => record.level().purple().italic().to_string(),
};
writeln!(
buf,
"{}: {}",
colored_lv.to_lowercase().bold(),
record.args(),
)
})
.init();
}
#[allow(dead_code)]
pub fn warn(msg: &str) {
eprintln!("{}: {msg}", colored("warn", BOLD_YELLOW));
/// Prints message to stdout with the given `Style`. Copy of println! macro
#[allow(unused_macros)]
macro_rules! sprintln {
($style:expr, $($arg:tt)*) => {
{
use ::owo_colors::OwoColorize as _;
println!("{}", format!($($arg)*).style($style));
}
};
}
/// Prints message to stdout with the given `Style`. Copy of print! macro
#[allow(unused_macros)]
macro_rules! sprint {
($style:expr, $($arg:tt)*) => {
{
use ::owo_colors::OwoColorize as _;
print!("{}", format!($($arg)*).style($style));
}
};
}
#[allow(unused_imports)]
pub(crate) use {sprint, sprintln};

View File

@@ -29,4 +29,4 @@ pub mod git;
pub mod log;
/// Text processing and manipulation helpers.
pub mod text;
pub mod cli_styles;

View File

@@ -16,6 +16,15 @@ fn dcr_add_dependencies() {
"path dep not found in toml"
);
// A bare filesystem path is equivalent to the explicit path: form.
let out = run_dcr(&["add", "otherlib", "../otherlib"], &dir);
assert!(out.status.success(), "dcr add bare path should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("otherlib = { path = \"../otherlib\" }"),
"bare path dep not found in toml"
);
// Test github: prefix
let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir);
assert!(out.status.success(), "dcr add github should succeed");
@@ -43,6 +52,24 @@ fn dcr_add_dependencies() {
"git short dep not found in toml"
);
// Test pkg-config prefix.
let out = run_dcr(&["add", "fmt", "pkg-config:fmt"], &dir);
assert!(out.status.success(), "dcr add pkg-config should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("fmt = { pkg-config = \"fmt\" }"),
"pkg-config dep not found in toml"
);
// Test multiple pkg-config packages via pkg: prefix.
let out = run_dcr(&["add", "openssl", "pkg:openssl,libcrypto"], &dir);
assert!(out.status.success(), "dcr add multiple pkg should succeed");
let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap();
assert!(
toml.contains("openssl = { pkg-config = [\"openssl\", \"libcrypto\"] }"),
"multi pkg-config dep not found in toml"
);
// Test flags (branch)
let out = run_dcr(
&["add", "branch_lib", "github:user/repo", "--branch", "dev"],
@@ -96,6 +123,419 @@ fn dcr_builds_lib_package() {
assert!(target_dir.join("lib").exists(), "lib directory missing");
}
#[test]
fn path_prebuilt_dependency_without_manifest_links() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping prebuilt path dependency test");
return;
};
if !is_ar_in_path() {
eprintln!("ar unavailable; skipping prebuilt path dependency test");
return;
}
let root = unique_sandbox_dir("path_prebuilt_dep");
let dep = root.join("dep");
let app = root.join("app");
write_prebuilt_library(
&dep,
compiler,
"prebuilt",
"int prebuilt_answer(void);\n",
"int prebuilt_answer(void) { return 42; }\n",
);
std::fs::create_dir_all(app.join("src")).unwrap();
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nprebuilt = { path = \"../dep\", include = [\"include\"], lib = [\"lib\"], libs = [\"prebuilt\"] }\n",
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include \"prebuilt.h\"\nint main(void) { return prebuilt_answer() == 42 ? 0 : 1; }\n",
)
.unwrap();
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
assert!(
out.status.success(),
"prebuilt path dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn pkg_config_dependency_resolves_system_library() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping pkg-config dependency test");
return;
};
if !std::process::Command::new("pkg-config")
.args(["--exists", "zlib"])
.status()
.map(|status| status.success())
.unwrap_or(false)
{
eprintln!("pkg-config zlib unavailable; skipping pkg-config dependency test");
return;
}
let root = unique_sandbox_dir("pkg_config_dep");
let app = root.join("app");
std::fs::create_dir_all(app.join("src")).unwrap();
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nzlib = { pkg-config = \"zlib\" }\n",
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include <zlib.h>\nint main(void) { return zlibVersion() == 0 ? 1 : 0; }\n",
)
.unwrap();
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
assert!(
out.status.success(),
"pkg-config dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
assert!(lock.contains("source = \"pkg-config+zlib\""));
// Check dcr tree displays pkg-config info
let tree_out = run_dcr(&["tree"], &app);
assert!(tree_out.status.success(), "dcr tree should succeed");
let tree_str = String::from_utf8_lossy(&tree_out.stdout);
assert!(
tree_str.contains("zlib (pkg-config: zlib)"),
"dcr tree should format pkg-config dependency, got: {}",
tree_str
);
}
#[test]
fn path_dcr_dependency_is_built_before_consumer() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping path dependency build test");
return;
};
let root = unique_sandbox_dir("path_dcr_dep");
let dep = root.join("dep");
let app = root.join("app");
std::fs::create_dir_all(dep.join("src")).unwrap();
std::fs::create_dir_all(app.join("src")).unwrap();
std::fs::write(
dep.join("dcr.toml"),
"[package]\nname = \"mathlib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
)
.unwrap();
std::fs::write(dep.join("src/mathlib.h"), "int answer(void);\n").unwrap();
std::fs::write(
dep.join("src/mathlib.c"),
"int answer(void) { return 42; }\n",
)
.unwrap();
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nrenamed = { path = \"../dep\" }\n",
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include \"mathlib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n",
)
.unwrap();
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
assert!(
out.status.success(),
"path dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(dep.join("target/include/mathlib.h").is_file());
assert!(dep.join("target/lib").is_dir());
}
#[test]
fn path_dcr_dependencies_build_transitively() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping transitive path dependency test");
return;
};
let root = unique_sandbox_dir("path_dcr_transitive");
let leaf = root.join("leaf");
let middle = root.join("middle");
let app = root.join("app");
for project in [&leaf, &middle, &app] {
std::fs::create_dir_all(project.join("src")).unwrap();
}
std::fs::write(
leaf.join("dcr.toml"),
"[package]\nname = \"leaf\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
)
.unwrap();
std::fs::write(leaf.join("src/leaf.h"), "int leaf_value(void);\n").unwrap();
std::fs::write(
leaf.join("src/leaf.c"),
"int leaf_value(void) { return 40; }\n",
)
.unwrap();
std::fs::write(
middle.join("dcr.toml"),
"[package]\nname = \"middle\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\nleaf = { path = \"../leaf\" }\n",
)
.unwrap();
std::fs::write(middle.join("src/middle.h"), "int middle_value(void);\n").unwrap();
std::fs::write(
middle.join("src/middle.c"),
"#include \"leaf.h\"\nint middle_value(void) { return leaf_value() + 2; }\n",
)
.unwrap();
std::fs::write(
app.join("dcr.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmiddle = { path = \"../middle\" }\n",
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include \"middle.h\"\nint main(void) { return middle_value() == 42 ? 0 : 1; }\n",
)
.unwrap();
let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]);
assert!(
out.status.success(),
"transitive path dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(leaf.join("target/lib").is_dir());
assert!(middle.join("target/lib").is_dir());
}
#[test]
fn path_dependency_cycle_reports_the_full_chain() {
let root = unique_sandbox_dir("path_dcr_cycle");
let first = root.join("first");
let second = root.join("second");
for project in [&first, &second] {
std::fs::create_dir_all(project.join("src")).unwrap();
}
std::fs::write(
first.join("dcr.toml"),
"[package]\nname = \"first\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nkind = \"staticlib\"\n\n[dependencies]\nsecond = { path = \"../second\" }\n",
)
.unwrap();
std::fs::write(
second.join("dcr.toml"),
"[package]\nname = \"second\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nkind = \"staticlib\"\n\n[dependencies]\nfirst = { path = \"../first\" }\n",
)
.unwrap();
let out = run_dcr(&["build"], &first);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success());
assert!(stderr.contains("Dependency cycle detected"), "{stderr}");
assert!(
stderr.contains("first") && stderr.contains("second"),
"{stderr}"
);
}
#[test]
fn git_dcr_dependency_is_cloned_built_and_locked() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping git dependency build test");
return;
};
if !is_git_in_path() {
eprintln!("git unavailable; skipping git dependency build test");
return;
}
let root = unique_sandbox_dir("git_dcr_dep");
let home = root.join("home");
let origin = root.join("origin");
let app = root.join("app");
std::fs::create_dir_all(origin.join("src")).unwrap();
std::fs::create_dir_all(app.join("src")).unwrap();
std::fs::write(
origin.join("dcr.toml"),
"[package]\nname = \"gitmath\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n",
)
.unwrap();
std::fs::write(origin.join("src/gitmath.h"), "int git_answer(void);\n").unwrap();
std::fs::write(
origin.join("src/gitmath.c"),
"int git_answer(void) { return 7; }\n",
)
.unwrap();
for args in [
vec!["init"],
vec!["add", "."],
vec![
"-c",
"user.email=dcr@example.test",
"-c",
"user.name=DCR",
"commit",
"-m",
"init",
],
vec!["tag", "v0.1.0"],
] {
let status = std::process::Command::new("git")
.args(args)
.current_dir(&origin)
.status()
.unwrap();
assert!(status.success());
}
// TOML basic strings treat backslashes as escapes. Git for Windows emits
// paths with `\\`, so normalize the local repository path before embedding
// it into dcr.toml (the slash form is accepted by Git on every platform).
let origin_url = origin.to_string_lossy().replace('\\', "/");
std::fs::write(
app.join("dcr.toml"),
format!("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\ngitmath = {{ git = \"{origin_url}\", tag = \"v0.1.0\" }}\n"),
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include \"gitmath.h\"\nint main(void) { return git_answer() == 7 ? 0 : 1; }\n",
)
.unwrap();
let home_s = home.to_string_lossy().to_string();
let out = run_dcr_env(
&["build"],
&app,
&[("DCR_COMPILER", compiler), ("HOME", home_s.as_str())],
);
assert!(
out.status.success(),
"git dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
assert!(lock.contains("git+"));
assert!(!lock.contains("checksum = \"\""));
}
#[test]
fn git_prebuilt_dependency_without_manifest_links() {
let Some(compiler) = available_compiler() else {
eprintln!("no compiler found; skipping prebuilt git dependency test");
return;
};
if !is_git_in_path() || !is_ar_in_path() {
eprintln!("git or ar unavailable; skipping prebuilt git dependency test");
return;
}
let root = unique_sandbox_dir("git_prebuilt_dep");
let home = root.join("home");
let origin = root.join("origin");
let app = root.join("app");
write_prebuilt_library(
&origin,
compiler,
"gitprebuilt",
"int git_prebuilt_answer(void);\n",
"int git_prebuilt_answer(void) { return 9; }\n",
);
std::fs::create_dir_all(app.join("src")).unwrap();
for args in [
vec!["init"],
vec!["add", "."],
vec![
"-c",
"user.email=dcr@example.test",
"-c",
"user.name=DCR",
"commit",
"-m",
"init",
],
vec!["tag", "v0.1.0"],
] {
let status = std::process::Command::new("git")
.args(args)
.current_dir(&origin)
.status()
.unwrap();
assert!(status.success());
}
// Keep the Git source TOML-safe on Windows (`C:/...`, not `C:\\...`).
let origin_url = origin.to_string_lossy().replace('\\', "/");
std::fs::write(
app.join("dcr.toml"),
format!("[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\ngitprebuilt = {{ git = \"{origin_url}\", tag = \"v0.1.0\", include = [\"include\"], lib = [\"lib\"], libs = [\"gitprebuilt\"] }}\n"),
)
.unwrap();
std::fs::write(
app.join("src/main.c"),
"#include \"gitprebuilt.h\"\nint main(void) { return git_prebuilt_answer() == 9 ? 0 : 1; }\n",
)
.unwrap();
let home_s = home.to_string_lossy().to_string();
let out = run_dcr_env(
&["build"],
&app,
&[("DCR_COMPILER", compiler), ("HOME", home_s.as_str())],
);
assert!(
out.status.success(),
"prebuilt git dependency build failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap();
assert!(lock.contains("git+"));
assert!(!lock.contains("checksum = \"\""));
}
fn write_prebuilt_library(
root: &std::path::Path,
compiler: &str,
name: &str,
header: &str,
source: &str,
) {
let include_dir = root.join("include");
let lib_dir = root.join("lib");
let src_dir = root.join("src");
std::fs::create_dir_all(&include_dir).unwrap();
std::fs::create_dir_all(&lib_dir).unwrap();
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(include_dir.join(format!("{name}.h")), header).unwrap();
let source_path = src_dir.join(format!("{name}.c"));
let object_path = lib_dir.join(format!("{name}.o"));
let archive_path = lib_dir.join(format!("lib{name}.a"));
std::fs::write(&source_path, source).unwrap();
let status = std::process::Command::new(compiler)
.args(["-c"])
.arg(&source_path)
.args(["-o"])
.arg(&object_path)
.status()
.unwrap();
assert!(status.success(), "failed to compile prebuilt library");
let status = std::process::Command::new("ar")
.args(["rcs"])
.arg(&archive_path)
.arg(&object_path)
.status()
.unwrap();
assert!(status.success(), "failed to archive prebuilt library");
}
fn is_ar_in_path() -> bool {
std::process::Command::new("ar")
.arg("--version")
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
#[test]
fn registry_dependency_is_built_from_cache() {
let Some(compiler) = available_compiler() else {

View File

@@ -79,3 +79,24 @@ fn init_vcs_options_work() {
assert!(gitignore.contains(".dcr/"));
}
}
#[test]
fn new_in_non_ascii_parent_directory() {
let parent_dir = unique_sandbox_dir("Проекты с пробелами");
let out = run_dcr(&["new", "my_app"], &parent_dir);
assert!(
out.status.success(),
"dcr new should succeed inside non-ascii parent dir: {}",
String::from_utf8_lossy(&out.stderr)
);
let project_dir = parent_dir.join("my_app");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
let toml_content = std::fs::read_to_string(project_dir.join("dcr.toml")).unwrap();
assert!(
toml_content.contains("name = \"my_app\""),
"package name should be my_app in dcr.toml"
);
}